Jenkins
CI/CDツール
Jenkins
概要
Jenkinsは最も歴史のあるオープンソースCI/CDツールです。豊富なプラグイン、高いカスタマイズ性、オンプレミス対応が特徴で、Groovy DSLによる柔軟なパイプライン構築を提供します。
詳細
Jenkins(ジェンキンス)は、2004年にHudson(ハドソン)として開始され、2011年に現在の名称に変更されたオープンソースの自動化サーバーです。最も歴史あるCI/CDツールとして、20年以上にわたって進化を続けています。Java/Groovy言語で開発され、Jenkinsfileを通じて「Pipeline as Code」を実現します。宣言型(Declarative)と手続き型(Scripted)の2つのパイプライン記法をサポートし、Groovy DSLによる高度な制御フローを構築できます。1,800以上のプラグインによる拡張性の高さが最大の特徴で、Git、Docker、Kubernetes、AWS、Azure等あらゆるツールとの統合が可能です。ビルド、テスト、デプロイの自動化に加え、Blue Oceanプラグインによる視覚的なパイプライン編集、分散ビルド、並列実行、条件分岐処理など複雑なワークフローを柔軟に構築できます。セルフホスト型のため完全なコントロールが可能で、企業内での独自カスタマイズや機密情報の管理において優位性を持ちます。
メリット・デメリット
メリット
- 完全無料: オープンソースで全機能が無制限利用可能
- 豊富なプラグイン: 1,800以上のプラグインで高い拡張性
- 高いカスタマイズ性: Groovy DSLによる柔軟なパイプライン制御
- 実績と安定性: 20年以上の開発実績と大企業での採用実績
- 完全なコントロール: セルフホスト型による独自カスタマイズ
- 強力なコミュニティ: 活発な開発者コミュニティとサポート
- 分散ビルド: Master-Agentアーキテクチャによるスケーラビリティ
- Pipeline as Code: Jenkinsfileによるバージョン管理対応
デメリット
- 運用コスト: サーバー管理、メンテナンス、アップデートが必要
- 学習コスト: Groovy記法とプラグイン生態系の理解が必要
- 設定の複雑さ: 高機能な分、初期設定と構成管理が複雑
- UI/UXの古さ: 従来的なインターフェースと操作性
- セキュリティ管理: プラグインや設定のセキュリティ脆弱性対応
- パフォーマンス: 大規模環境でのメモリ消費と応答速度
- 依存関係管理: プラグイン間の互換性とバージョン管理
主要リンク
- Jenkins公式サイト
- Jenkins公式ドキュメント
- Jenkins Pipeline Documentation
- Jenkins Plugins Index
- Jenkins Blue Ocean
- Jenkins GitHub Repository
書き方の例
基本的な宣言型パイプライン
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
options {
skipStagesAfterUnstable()
timeout(time: 1, unit: 'HOURS')
retry(3)
}
environment {
NODE_VERSION = '18'
DOCKER_IMAGE = 'myapp'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run test'
sh 'npm run lint'
}
post {
always {
publishTestResults testResultsPattern: 'test-results.xml'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
script {
sh 'npm run deploy'
}
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
emailext (
subject: "Jenkins Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "Build failed. Check console output at ${env.BUILD_URL}",
to: "${env.CHANGE_AUTHOR_EMAIL}"
)
}
}
}
Docker統合パイプライン
// Jenkinsfile (Docker Integration)
pipeline {
agent none
stages {
stage('Build') {
agent {
docker {
image 'node:18-alpine'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
steps {
sh 'npm install'
sh 'npm run build'
stash includes: 'dist/**', name: 'build-artifacts'
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
agent {
docker {
image 'node:18-alpine'
}
}
steps {
unstash 'build-artifacts'
sh 'npm run test:unit'
}
post {
always {
junit 'test-results/unit.xml'
}
}
}
stage('Integration Tests') {
agent {
docker {
image 'node:18-alpine'
}
}
steps {
unstash 'build-artifacts'
sh 'npm run test:integration'
}
post {
always {
junit 'test-results/integration.xml'
}
}
}
}
}
stage('Build Docker Image') {
agent any
steps {
unstash 'build-artifacts'
script {
def image = docker.build("${env.DOCKER_IMAGE}:${env.BUILD_ID}")
docker.withRegistry('https://registry.example.com', 'docker-registry-credentials') {
image.push()
image.push('latest')
}
}
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
agent any
steps {
script {
def environment = env.BRANCH_NAME == 'main' ? 'production' : 'staging'
sh """
docker run -d --name ${environment}-${env.BUILD_ID} \
-p 8080:3000 \
${env.DOCKER_IMAGE}:${env.BUILD_ID}
"""
}
}
}
}
}
手続き型パイプライン(Scripted Pipeline)
// Jenkinsfile (Scripted Pipeline)
node('master') {
def branch = env.BRANCH_NAME
def buildNumber = env.BUILD_NUMBER
try {
stage('Checkout') {
checkout scm
}
stage('Environment Setup') {
def nodeHome = tool 'NodeJS-18'
env.PATH = "${nodeHome}/bin:${env.PATH}"
sh 'node --version'
sh 'npm --version'
}
stage('Install Dependencies') {
sh 'npm ci'
}
stage('Build') {
sh 'npm run build'
archiveArtifacts artifacts: 'dist/**', fingerprint: true
}
stage('Test') {
try {
parallel(
'Unit Tests': {
sh 'npm run test:unit'
},
'Linting': {
sh 'npm run lint'
},
'Security Scan': {
sh 'npm audit'
}
)
} catch (Exception e) {
currentBuild.result = 'UNSTABLE'
echo "Tests failed: ${e.getMessage()}"
} finally {
publishTestResults testResultsPattern: 'test-results/*.xml'
}
}
if (branch == 'main') {
stage('Security Checks') {
sh 'npm run security:check'
// SonarQube analysis
withSonarQubeEnv('SonarQube') {
sh 'npm run sonar'
}
// Quality gate
timeout(time: 10, unit: 'MINUTES') {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
}
stage('Deploy to Production') {
input message: 'Deploy to production?',
ok: 'Deploy',
submitterParameter: 'DEPLOYER'
echo "Deployment approved by: ${env.DEPLOYER}"
withCredentials([
usernamePassword(credentialsId: 'production-server',
usernameVariable: 'DEPLOY_USER',
passwordVariable: 'DEPLOY_PASS')
]) {
sh '''
scp -r dist/ ${DEPLOY_USER}@production-server:/var/www/html/
ssh ${DEPLOY_USER}@production-server "systemctl restart nginx"
'''
}
}
} else if (branch == 'develop') {
stage('Deploy to Staging') {
sh 'npm run deploy:staging'
}
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
throw e
} finally {
stage('Cleanup') {
deleteDir()
}
stage('Notifications') {
if (currentBuild.result == 'FAILURE') {
slackSend channel: '#dev-team',
color: 'danger',
message: "❌ Build Failed: ${env.JOB_NAME} #${buildNumber}"
} else if (currentBuild.result == 'SUCCESS') {
slackSend channel: '#dev-team',
color: 'good',
message: "✅ Build Successful: ${env.JOB_NAME} #${buildNumber}"
}
}
}
}
マルチブランチパイプライン
// Jenkinsfile (Multi-branch Pipeline)
pipeline {
agent any
parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'staging', 'production'],
description: 'Target environment'
)
booleanParam(
name: 'SKIP_TESTS',
defaultValue: false,
description: 'Skip running tests'
)
string(
name: 'DEPLOY_VERSION',
defaultValue: 'latest',
description: 'Version to deploy'
)
}
environment {
REGISTRY = 'docker.io'
IMAGE_NAME = 'mycompany/myapp'
KUBECONFIG = credentials('kubeconfig')
}
stages {
stage('Prepare') {
steps {
script {
env.BRANCH_LOWER = env.BRANCH_NAME.toLowerCase()
env.IMAGE_TAG = "${env.BRANCH_LOWER}-${env.BUILD_NUMBER}"
if (env.BRANCH_NAME == 'main') {
env.TARGET_ENV = 'production'
} else if (env.BRANCH_NAME == 'develop') {
env.TARGET_ENV = 'staging'
} else {
env.TARGET_ENV = 'dev'
}
}
echo "Building branch: ${env.BRANCH_NAME}"
echo "Target environment: ${env.TARGET_ENV}"
echo "Image tag: ${env.IMAGE_TAG}"
}
}
stage('Build') {
steps {
sh 'docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .'
sh 'docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${BRANCH_LOWER}-latest'
}
}
stage('Test') {
when {
not {
params.SKIP_TESTS
}
}
parallel {
stage('Unit Tests') {
steps {
sh 'docker run --rm ${IMAGE_NAME}:${IMAGE_TAG} npm test'
}
}
stage('Security Scan') {
steps {
sh 'trivy image ${IMAGE_NAME}:${IMAGE_TAG}'
}
}
stage('Performance Test') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
steps {
sh 'docker run --rm ${IMAGE_NAME}:${IMAGE_TAG} npm run test:performance'
}
}
}
}
stage('Push to Registry') {
steps {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh 'echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin'
sh 'docker push ${IMAGE_NAME}:${IMAGE_TAG}'
sh 'docker push ${IMAGE_NAME}:${BRANCH_LOWER}-latest'
}
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'develop'
branch 'feature/*'
}
}
steps {
script {
sh """
helm upgrade --install ${env.TARGET_ENV}-myapp ./helm/myapp \
--namespace ${env.TARGET_ENV} \
--set image.tag=${env.IMAGE_TAG} \
--set environment=${env.TARGET_ENV} \
--wait --timeout=300s
"""
}
}
}
}
post {
always {
sh 'docker system prune -f'
}
success {
script {
if (env.BRANCH_NAME == 'main') {
build job: 'create-release',
parameters: [
string(name: 'VERSION', value: env.BUILD_NUMBER),
string(name: 'IMAGE_TAG', value: env.IMAGE_TAG)
]
}
}
}
failure {
emailext (
subject: "❌ Jenkins Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: """
<h2>Build Failed</h2>
<p><strong>Job:</strong> ${env.JOB_NAME}</p>
<p><strong>Build Number:</strong> ${env.BUILD_NUMBER}</p>
<p><strong>Branch:</strong> ${env.BRANCH_NAME}</p>
<p><strong>Console Output:</strong> <a href="${env.BUILD_URL}console">${env.BUILD_URL}console</a></p>
""",
to: "${env.CHANGE_AUTHOR_EMAIL}",
mimeType: 'text/html'
)
}
}
}
共有ライブラリの使用
// vars/deployApp.groovy (Shared Library)
def call(Map config) {
pipeline {
agent any
stages {
stage('Deploy') {
steps {
script {
echo "Deploying ${config.appName} to ${config.environment}"
def helmValues = [
"image.tag=${config.imageTag}",
"environment=${config.environment}",
"replicas=${config.replicas ?: 1}"
]
sh """
helm upgrade --install ${config.appName} ./helm/${config.appName} \
--namespace ${config.environment} \
--set ${helmValues.join(' --set ')} \
--wait --timeout=${config.timeout ?: '300s'}
"""
}
}
}
}
}
}
// Jenkinsfile using Shared Library
@Library('my-shared-library') _
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t myapp:latest .'
}
}
stage('Deploy') {
steps {
deployApp([
appName: 'myapp',
environment: 'production',
imageTag: 'latest',
replicas: 3,
timeout: '600s'
])
}
}
}
}