phonebill/deployment/cicd/Jenkinsfile
hiondal 892f30ba44 Jenkins CI/CD 파이프라인 구축 완료
- Kustomize 기반 환경별 배포 구조 구축
  • Base 매니페스트: deployment/cicd/kustomize/base/
  • 환경별 오버레이: overlays/{dev,staging,prod}
  • 기존 k8s 매니페스트를 Kustomize 구조로 마이그레이션

- Jenkins 파이프라인 설정
  • Jenkinsfile: Pod Template, SonarQube, 배포 자동화
  • 환경별 설정 파일: config/deploy_env_vars_{env}
  • 수동 배포 스크립트: scripts/deploy.sh

- Azure 연동 설정
  • ACR (acrdigitalgarage01) 및 AKS (aks-digitalgarage-01)
  • 환경별 리소스 분리 및 보안 설정

- 완전한 구축 가이드 문서
  • deployment/cicd/jenkins-pipeline-guide.md
  • Jenkins 플러그인, RBAC, 트러블슈팅 가이드 포함

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 10:46:05 +09:00

426 lines
15 KiB
Groovy

#!/usr/bin/env groovy
/**
* Jenkins Pipeline for phonebill Microservices
* Supports multi-environment deployment (dev, staging, prod)
* Services: api-gateway, user-service, bill-service, product-service, kos-mock
*/
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
serviceAccountName: jenkins-agent
containers:
- name: gradle
image: gradle:8.5-jdk17
command:
- cat
tty: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1"
volumeMounts:
- name: gradle-cache
mountPath: /home/gradle/.gradle
- name: podman
image: quay.io/podman/stable:latest
command:
- cat
tty: true
securityContext:
privileged: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1"
- name: azure-cli
image: mcr.microsoft.com/azure-cli:latest
command:
- cat
tty: true
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "250m"
volumes:
- name: gradle-cache
persistentVolumeClaim:
claimName: jenkins-gradle-cache
'''
}
}
parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'staging', 'prod'],
description: 'Target deployment environment'
)
choice(
name: 'SERVICES_TO_BUILD',
choices: ['all', 'api-gateway', 'user-service', 'bill-service', 'product-service', 'kos-mock'],
description: 'Services to build and deploy'
)
booleanParam(
name: 'SKIP_TESTS',
defaultValue: false,
description: 'Skip unit tests during build'
)
booleanParam(
name: 'SKIP_SONAR',
defaultValue: false,
description: 'Skip SonarQube analysis'
)
booleanParam(
name: 'FORCE_DEPLOY',
defaultValue: false,
description: 'Force deployment even if no changes detected'
)
}
environment {
// Load environment-specific variables
CONFIG_FILE = "deployment/cicd/config/deploy_env_vars_${params.ENVIRONMENT}"
// Build configuration
GRADLE_USER_HOME = '/home/gradle/.gradle'
GRADLE_OPTS = '-Xmx2048m -XX:MaxPermSize=512m'
// Azure credentials
AZURE_CREDENTIALS = credentials('azure-service-principal')
ACR_CREDENTIALS = credentials('acr-credentials')
// SonarQube
SONAR_TOKEN = credentials('sonarqube-token')
// Slack notification
SLACK_TOKEN = credentials('slack-token')
}
stages {
stage('Initialize') {
steps {
script {
echo "🚀 Starting phonebill pipeline for ${params.ENVIRONMENT} environment"
// Load environment-specific configuration
if (fileExists(env.CONFIG_FILE)) {
def props = readProperties file: env.CONFIG_FILE
props.each { key, value ->
env[key] = value
}
echo "✅ Loaded configuration from ${env.CONFIG_FILE}"
} else {
error "❌ Configuration file not found: ${env.CONFIG_FILE}"
}
// Set services to build
if (params.SERVICES_TO_BUILD == 'all') {
env.SERVICES_LIST = env.SERVICES
} else {
env.SERVICES_LIST = params.SERVICES_TO_BUILD
}
echo "🎯 Services to build: ${env.SERVICES_LIST}"
echo "📦 Target namespace: ${env.AKS_NAMESPACE}"
}
}
}
stage('Checkout & Prepare') {
steps {
checkout scm
script {
env.BUILD_TIMESTAMP = sh(
script: 'date +%Y%m%d-%H%M%S',
returnStdout: true
).trim()
env.IMAGE_TAG = "${env.BUILD_NUMBER}-${params.ENVIRONMENT}-${env.BUILD_TIMESTAMP}"
echo "🏷️ Image tag: ${env.IMAGE_TAG}"
}
}
}
stage('Build & Test') {
parallel {
stage('Gradle Build') {
steps {
container('gradle') {
script {
def services = env.SERVICES_LIST.split(',')
for (service in services) {
echo "🔨 Building ${service}..."
if (!params.SKIP_TESTS) {
sh """
./gradlew ${service}:clean ${service}:test ${service}:build \
--no-daemon \
--parallel \
--build-cache
"""
} else {
sh """
./gradlew ${service}:clean ${service}:build -x test \
--no-daemon \
--parallel \
--build-cache
"""
}
}
}
}
}
post {
always {
// Publish test results
script {
def services = env.SERVICES_LIST.split(',')
for (service in services) {
if (fileExists("${service}/build/test-results/test/*.xml")) {
publishTestResults(
testResultsPattern: "${service}/build/test-results/test/*.xml",
allowEmptyResults: true
)
}
}
}
}
}
}
}
}
stage('SonarQube Analysis') {
when {
not { params.SKIP_SONAR }
}
steps {
container('gradle') {
withSonarQubeEnv('SonarQube') {
sh '''
./gradlew sonarqube \
-Dsonar.projectKey=${SONAR_PROJECT_KEY} \
-Dsonar.sources=${SONAR_SOURCES} \
-Dsonar.exclusions="${SONAR_EXCLUSIONS}" \
--no-daemon
'''
}
}
}
}
stage('Quality Gate') {
when {
not { params.SKIP_SONAR }
}
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Container Build & Push') {
parallel {
stage('Build Images') {
steps {
container('podman') {
script {
// Login to ACR
sh """
echo "${ACR_CREDENTIALS_PSW}" | podman login \
--username "${ACR_CREDENTIALS_USR}" \
--password-stdin \
${REGISTRY_URL}
"""
def services = env.SERVICES_LIST.split(',')
for (service in services) {
echo "🐳 Building container image for ${service}..."
sh """
cd ${service}
podman build \
--tag ${REGISTRY_URL}/phonebill/${service}:${IMAGE_TAG} \
--tag ${REGISTRY_URL}/phonebill/${service}:latest-${params.ENVIRONMENT} \
--file Dockerfile \
.
podman push ${REGISTRY_URL}/phonebill/${service}:${IMAGE_TAG}
podman push ${REGISTRY_URL}/phonebill/${service}:latest-${params.ENVIRONMENT}
"""
}
}
}
}
}
}
}
stage('Deploy to Kubernetes') {
steps {
container('kubectl') {
script {
// Login to Azure and get AKS credentials
sh """
az login --service-principal \
--username "${AZURE_CREDENTIALS_USR}" \
--password "${AZURE_CREDENTIALS_PSW}" \
--tenant "${AZURE_CREDENTIALS_TENANT_ID}"
az aks get-credentials \
--resource-group ${AZURE_RESOURCE_GROUP} \
--name ${AKS_CLUSTER_NAME} \
--overwrite-existing
"""
// Deploy services using kustomize
def services = env.SERVICES_LIST.split(',')
for (service in services) {
echo "🚀 Deploying ${service} to ${params.ENVIRONMENT}..."
sh """
cd ${KUSTOMIZE_BASE}/${service}
# Update image tag in kustomization.yaml
sed -i 's|newTag:.*|newTag: ${IMAGE_TAG}|' ${KUSTOMIZE_OVERLAY}/kustomization.yaml
# Apply deployment
kubectl apply -k ${KUSTOMIZE_OVERLAY} -n ${AKS_NAMESPACE}
# Wait for rollout
kubectl rollout status deployment/${service} -n ${AKS_NAMESPACE} --timeout=300s
"""
}
}
}
}
}
stage('Health Check') {
steps {
container('kubectl') {
script {
def services = env.SERVICES_LIST.split(',')
for (service in services) {
echo "🏥 Health checking ${service}..."
retry(count: env.HEALTH_CHECK_RETRY as Integer) {
sh """
kubectl get deployment ${service} -n ${AKS_NAMESPACE} -o json | \
jq -e '.status.readyReplicas == .status.replicas and .status.replicas > 0'
"""
sleep time: 30, unit: 'SECONDS'
}
echo "✅ ${service} is healthy"
}
}
}
}
}
}
post {
always {
script {
// Archive build artifacts
archiveArtifacts(
artifacts: '**/build/libs/*.jar',
allowEmptyArchive: true,
fingerprint: true
)
// Clean workspace
cleanWs()
}
}
success {
script {
def message = """
✅ *phonebill Deployment Successful*
• Environment: `${params.ENVIRONMENT}`
• Services: `${env.SERVICES_LIST}`
• Image Tag: `${env.IMAGE_TAG}`
• Build: `${env.BUILD_NUMBER}`
• Duration: `${currentBuild.durationString}`
""".stripIndent()
// Send Slack notification
slackSend(
channel: env.SLACK_CHANNEL,
color: 'good',
message: message,
token: env.SLACK_TOKEN
)
// Send email notification
emailext(
subject: "✅ phonebill Deployment Success - ${params.ENVIRONMENT}",
body: message,
to: env.EMAIL_RECIPIENTS
)
}
}
failure {
script {
def message = """
❌ *phonebill Deployment Failed*
• Environment: `${params.ENVIRONMENT}`
• Services: `${env.SERVICES_LIST}`
• Build: `${env.BUILD_NUMBER}`
• Error: `${currentBuild.result}`
• Console: ${env.BUILD_URL}console
""".stripIndent()
// Send Slack notification
slackSend(
channel: env.SLACK_CHANNEL,
color: 'danger',
message: message,
token: env.SLACK_TOKEN
)
// Send email notification
emailext(
subject: "❌ phonebill Deployment Failed - ${params.ENVIRONMENT}",
body: message,
to: env.EMAIL_RECIPIENTS
)
}
}
}
}