mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
Jenkins CI/CD 파이프라인 완전 구축 및 가이드 개선
주요 작업: - Kustomize 기반 환경별 배포 구조 완성 (dev/staging/prod) - deployment-patch.yaml 개선: replicas + resources 통합 관리 - Strategic Merge Patch 형식으로 변경하여 가독성 및 유지보수성 향상 - 환경별 차등 리소스 할당 정책 적용 - Jenkins 파이프라인 스크립트 및 수동 배포 스크립트 완성 - 상세한 체크리스트 및 실수 방지 가이드 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vendored
+133
-409
@@ -1,425 +1,149 @@
|
||||
#!/usr/bin/env groovy
|
||||
def PIPELINE_ID = "${env.BUILD_NUMBER}"
|
||||
|
||||
/**
|
||||
* Jenkins Pipeline for phonebill Microservices
|
||||
* Supports multi-environment deployment (dev, staging, prod)
|
||||
* Services: api-gateway, user-service, bill-service, product-service, kos-mock
|
||||
*/
|
||||
def getImageTag() {
|
||||
def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss')
|
||||
def currentDate = new Date()
|
||||
return dateFormat.format(currentDate)
|
||||
}
|
||||
|
||||
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
|
||||
'''
|
||||
podTemplate(
|
||||
label: "${PIPELINE_ID}",
|
||||
serviceAccount: 'jenkins',
|
||||
containers: [
|
||||
containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true),
|
||||
containerTemplate(name: 'gradle', image: 'gradle:jdk17', ttyEnabled: true, command: 'cat'),
|
||||
containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true)
|
||||
]
|
||||
) {
|
||||
node(PIPELINE_ID) {
|
||||
def props
|
||||
def imageTag = getImageTag()
|
||||
def environment = params.ENVIRONMENT ?: 'dev'
|
||||
def services = ['api-gateway', 'user-service', 'bill-service', 'product-service', 'kos-mock']
|
||||
|
||||
stage("Get Source") {
|
||||
checkout scm
|
||||
props = readProperties file: "deployment/cicd/config/deploy_env_vars_${environment}"
|
||||
}
|
||||
}
|
||||
|
||||
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("Setup AKS") {
|
||||
container('azure-cli') {
|
||||
withCredentials([azureServicePrincipal('azure-credentials')]) {
|
||||
sh """
|
||||
az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID
|
||||
az aks get-credentials --resource-group \${props.resource_group} --name \${props.cluster_name} --overwrite-existing
|
||||
kubectl create namespace phonebill-\${environment} --dry-run=client -o yaml | kubectl apply -f -
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
stage('Build & SonarQube Analysis') {
|
||||
container('gradle') {
|
||||
withSonarQubeEnv('SonarQube') {
|
||||
sh """
|
||||
chmod +x gradlew
|
||||
./gradlew build -x test
|
||||
|
||||
// Deploy services using kustomize
|
||||
def services = env.SERVICES_LIST.split(',')
|
||||
for (service in services) {
|
||||
echo "🚀 Deploying ${service} to ${params.ENVIRONMENT}..."
|
||||
# 각 서비스별 테스트 및 분석
|
||||
./gradlew :api-gateway:test :api-gateway:jacocoTestReport :api-gateway:sonar \\
|
||||
-Dsonar.projectKey=phonebill-api-gateway-\${environment} \\
|
||||
-Dsonar.projectName=phonebill-api-gateway
|
||||
|
||||
./gradlew :user-service:test :user-service:jacocoTestReport :user-service:sonar \\
|
||||
-Dsonar.projectKey=phonebill-user-service-\${environment} \\
|
||||
-Dsonar.projectName=phonebill-user-service
|
||||
|
||||
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
|
||||
"""
|
||||
}
|
||||
./gradlew :bill-service:test :bill-service:jacocoTestReport :bill-service:sonar \\
|
||||
-Dsonar.projectKey=phonebill-bill-service-\${environment} \\
|
||||
-Dsonar.projectName=phonebill-bill-service
|
||||
|
||||
./gradlew :product-service:test :product-service:jacocoTestReport :product-service:sonar \\
|
||||
-Dsonar.projectKey=phonebill-product-service-\${environment} \\
|
||||
-Dsonar.projectName=phonebill-product-service
|
||||
|
||||
./gradlew :kos-mock:test :kos-mock:jacocoTestReport :kos-mock:sonar \\
|
||||
-Dsonar.projectKey=phonebill-kos-mock-\${environment} \\
|
||||
-Dsonar.projectName=phonebill-kos-mock
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('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('Build & Push Images') {
|
||||
container('podman') {
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: 'acr-credentials',
|
||||
usernameVariable: 'USERNAME',
|
||||
passwordVariable: 'PASSWORD'
|
||||
)]) {
|
||||
sh "podman login acrdigitalgarage01.azurecr.io --username \$USERNAME --password \$PASSWORD"
|
||||
|
||||
services.each { service ->
|
||||
sh """
|
||||
podman build \\
|
||||
--build-arg BUILD_LIB_DIR="\${service}/build/libs" \\
|
||||
--build-arg ARTIFACTORY_FILE="\${service}.jar" \\
|
||||
-f deployment/container/Dockerfile \\
|
||||
-t acrdigitalgarage01.azurecr.io/phonebill/\${service}:\${environment}-\${imageTag} .
|
||||
|
||||
podman push acrdigitalgarage01.azurecr.io/phonebill/\${service}:\${environment}-\${imageTag}
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Update Kustomize & Deploy') {
|
||||
container('azure-cli') {
|
||||
sh """
|
||||
# Kustomize 설치
|
||||
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||||
sudo mv kustomize /usr/local/bin/
|
||||
|
||||
# 환경별 디렉토리로 이동
|
||||
cd deployment/cicd/kustomize/overlays/\${environment}
|
||||
|
||||
# 이미지 태그 업데이트
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/api-gateway:\${environment}-\${imageTag}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/user-service:\${environment}-\${imageTag}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/bill-service:\${environment}-\${imageTag}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/product-service:\${environment}-\${imageTag}
|
||||
kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/kos-mock:\${environment}-\${imageTag}
|
||||
|
||||
# 매니페스트 적용
|
||||
kubectl apply -k .
|
||||
|
||||
echo "Waiting for deployments to be ready..."
|
||||
kubectl -n phonebill-\${environment} wait --for=condition=available deployment/\${environment}-api-gateway --timeout=300s
|
||||
kubectl -n phonebill-\${environment} wait --for=condition=available deployment/\${environment}-user-service --timeout=300s
|
||||
kubectl -n phonebill-\${environment} wait --for=condition=available deployment/\${environment}-bill-service --timeout=300s
|
||||
kubectl -n phonebill-\${environment} wait --for=condition=available deployment/\${environment}-product-service --timeout=300s
|
||||
kubectl -n phonebill-\${environment} wait --for=condition=available deployment/\${environment}-kos-mock --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
|
||||
)
|
||||
container('azure-cli') {
|
||||
sh """
|
||||
echo "🔍 Health Check starting..."
|
||||
|
||||
# API Gateway Health Check
|
||||
GATEWAY_POD=\$(kubectl get pod -n phonebill-\${environment} -l app=api-gateway -o jsonpath='{.items[0].metadata.name}')
|
||||
kubectl -n phonebill-\${environment} exec \$GATEWAY_POD -- curl -f http://localhost:8080/actuator/health || exit 1
|
||||
|
||||
echo "✅ All services are healthy!"
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user