mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-01-21 10:06:24 +00:00
- 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>
426 lines
15 KiB
Groovy
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} |