Gom3rye

개인 프로젝트) CQRS Todo App CI/CD 프로젝트 with Kafka, Jenkins 본문

현대 오토에버 클라우드 스쿨

개인 프로젝트) CQRS Todo App CI/CD 프로젝트 with Kafka, Jenkins

Gom3rye 2025. 10. 20. 17:35
728x90
반응형

Infra 설정

먼저 클러스터 설치하기

1. vm 3개 만들기 (master, worker1, worker2)
2. 각각의 vm에 sudo vi /etc/netplan/50-cloud-init.yaml 에 다음 내용으로 수정하기 (이미 있는 거 써도 되어서 netplan까지 하고 탭 치면 자동완성)
network:
  version: 2
  ethernets:
    enp0s3:
      dhcp4: no
      addresses:
        - 192.168.56.100/24
      gateway4: 192.168.56.1
      nameservers:
        addresses:
          - 8.8.8.8
          - 8.8.4.4
          
3. sudo netplan apply
4. 클러스터의 네트워크에 가서 이미 만든 포트포워딩 규칙을 사용하고 있는 nat network를 써주기 위해 네트워크 할당
5. 이제 ssh로 접속해서 편하게 작업
6. master와 worker1, worker2 노드들에 각자 맞는 init.sh를 작성하고 chmod 777 init.sh 로 실행권한 주고 ./init.sh 로 실행하기

 

master용 init.sh

이때 나온 join 명령어에 sudo <명령어> --cri-socket=unix:///run/containerd/containerd.sock
로 각 워커노드마다 해주기

#!/bin/bash

set -euo pipefail

echo "1. 시스템 업데이트 및 업그레이드 중..."
sudo apt update && sudo apt upgrade -y
echo "1단계 완료: 시스템 업데이트 및 업그레이드"

echo "2. 스왑 비활성화 및 fstab에서 스왑 주석 처리..."
sudo swapoff -a
sudo sed -i '/\bswap\b/ s/^/#/' /etc/fstab
echo "2단계 완료: 스왑 비활성화 완료"

echo "3. 커널 모듈 로드 및 설정 적용 중..."
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf >/dev/null
overlay
br_netfilter
EOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf >/dev/null
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system
echo "3단계 완료: 커널 모듈 및 sysctl 설정 적용"

echo "4. containerd 설치 및 설정 중..."
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo systemctl enable containerd
sudo systemctl restart containerd
echo "4단계 완료: containerd 설치 및 실행 완료"

echo "5. 쿠버네티스 apt 저장소 설정 및 설치 중..."
sudo mkdir -p /etc/apt/keyrings
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list >/dev/null
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
sudo apt update
sudo apt install -y kubeadm kubectl kubelet
sudo apt-mark hold kubeadm kubectl kubelet
echo "5단계 완료: kubeadm, kubectl, kubelet 설치 완료"

echo "6. kubeadm 초기화 시작..."
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --cri-socket=unix:///run/containerd/containerd.sock
echo "6단계 완료: kubeadm 초기화 완료"

echo "7. kubeconfig 설정..."
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
echo "7단계 완료: kubeconfig 설정 완료"

echo "8. Flannel 네트워크 플러그인 적용 중..."
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
echo "8단계 완료: Flannel 네트워크 플러그인 적용 완료"

echo "9. UFW 방화벽 설정 중..."
sudo ufw status verbose
for port in 6443 2379 2380 10250 10251 10252 8285 8472; do
  sudo ufw allow $port
  sudo ufw allow $port/udp
done
sudo ufw status verbose
echo "9단계 완료: 방화벽 포트 설정 완료"

echo "10. 노드 상태 확인..."
kubectl get nodes

echo "10단계 완료: 노드 상태 확인 완료"

# kubeadm join 커맨드 저장
JOIN_COMMAND=$(sudo kubeadm token create --print-join-command)

echo
echo "===== 워커 노드에서 사용할 join 명령어 ====="
echo "$JOIN_COMMAND"
echo "========================================="


worker용 init.sh

#!/bin/bash

set -euo pipefail

echo "1. 시스템 업데이트 및 업그레이드 중..."
sudo apt update && sudo apt upgrade -y
echo "1단계 완료: 시스템 업데이트 및 업그레이드"

echo "2. 스왑 비활성화 및 fstab에서 스왑 주석 처리..."
sudo swapoff -a
sudo sed -i '/\bswap\b/ s/^/#/' /etc/fstab
echo "2단계 완료: 스왑 비활성화 완료"

echo "3. 커널 모듈 로드 및 설정 적용 중..."
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf >/dev/null
overlay
br_netfilter
EOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf >/dev/null
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system
echo "3단계 완료: 커널 모듈 및 sysctl 설정 적용"

echo "4. containerd 설치 및 설정 중..."
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo systemctl enable containerd
sudo systemctl restart containerd
echo "4단계 완료: containerd 설치 및 실행 완료"

echo "5. 쿠버네티스 apt 저장소 설정 및 kubeadm, kubelet 설치 중..."
sudo mkdir -p /etc/apt/keyrings
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list >/dev/null
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
sudo apt update
sudo apt install -y kubeadm kubelet
sudo apt-mark hold kubeadm kubelet
echo "5단계 완료: kubeadm, kubelet 설치 완료"

echo "6. UFW 방화벽 설정 중..."
sudo ufw status verbose
for port in 6443 26443 8285 8472; do
  sudo ufw allow $port
  sudo ufw allow $port/udp
done
sudo ufw status verbose
echo "6단계 완료: 방화벽 포트 설정 완료"

echo
echo "===== 워커 노드에서 마스터 노드에서 받은 kubeadm join 명령어를 입력하세요 ====="
echo "예시: kubeadm join <MASTER_IP>:6443 --token <TOKEN> --discovery-token-ca-cert-hash sha256:<HASH>"
echo "※ 마스터 노드에서 'kubeadm token create --print-join-command' 명령어로 join 커맨드를 생성할 수 있습니다."
echo "==============================================================================="

 

nfs storageclass를 사용하기 위해 master, worker1, worker2 (모든 노드)에 설치하기

sudo apt update
sudo apt install -y nfs-common

 

마스터에 nfs 서버 패키지 설치

sudo apt update
sudo apt install -y nfs-kernel-server

# 공유 디렉토리 생성
sudo mkdir -p /srv/nfs/kubedata
sudo chown -R nobody:nogroup /srv/nfs/kubedata
sudo chmod 777 /srv/nfs/kubedata

# /etc/exports 설정 추가
sudo vi /etc/exports

/srv/nfs/kubedata *(rw,sync,no_subtree_check,no_root_squash)

# 설정 반영 및 nfs 서버 재시작
sudo exportfs -ra
sudo systemctl restart nfs-kernel-server
sudo systemctl enable nfs-kernel-server

sudo exportfs -v 로 확인해보기

 

mysql을 위한 secret.yaml 파일 작성

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
type: Opaque
stringData:
  password: 원하는 비밀번호

 

 

CQRS Todo app 구현

CommandController          QueryController
    │                           │
CommandService             QueryService
    │                           │
EventPublisher (Kafka)     Read-Only DB (Mongo DB)

 

Kafka에 데이터가 잘 들어오는 지 확인하기

kyla@master:~/cqrs-todo-app$ kubectl exec -it -n prod kafka-deployment-67b6fff7b5-4d4sm -- /bin/bash
[appuser@kafka-deployment-67b6fff7b5-4d4sm ~]$ kafka-topics --list --bootstrap-server localhost:9092
todo-events
[appuser@kafka-deployment-67b6fff7b5-4d4sm ~]$ kafka-console-consumer --bootstrap-server localhost:9092 -
-topic todo-events --from-beginning
{"type":"CREATED","id":1,"task":"개인 프로젝트 완성하기","done":false}
{"type":"UPDATED","id":1,"task":"개인 프로젝트 완성하기","done":true}
{"type":"CREATED","id":2,"task":"코테 공부하기","done":false}
{"type":"CREATED","id":3,"task":"항상 감사하는 마음 가지기","done":false}
{"type":"UPDATED","id":3,"task":"항상 감사하는 마음 가지기","done":true}

 

 

Jenkins 초기 비밀번호 확인하기

kubectl exec -it deploy/jenkins -- cat /var/jenkins_home/secrets/initialAdminPassword

1. CICD 파이프라인 시작

pipeline {
    agent {
        kubernetes {
            namespace 'jenkins'
            yaml '''
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: jenkins-agent
  restartPolicy: Never
  
  volumes:
  - name: gradle-cache
    persistentVolumeClaim:
      claimName: gradle-cache-pvc
  - name: kaniko-cache
    persistentVolumeClaim:
      claimName: kaniko-cache-pvc
  - name: docker-config
    secret:
      secretName: dockerhub-secret
      items:
      - key: .dockerconfigjson
        path: config.json
  - name: workspace-volume
    emptyDir: {}

  containers:
  - name: gradle
    image: gradle:8.5.0-jdk21
    command: ["sleep"]
    args: ["infinity"]
    volumeMounts:
    - name: gradle-cache
      mountPath: /home/jenkins/.gradle
    - name: workspace-volume
      mountPath: /home/jenkins/agent

  - name: kaniko-command
    image: gcr.io/kaniko-project/executor:v1.23.2-debug
    command: ["/busybox/cat"]
    tty: true
    volumeMounts:
    - name: docker-config
      mountPath: /kaniko/.docker
    - name: kaniko-cache
      mountPath: /cache
    - name: workspace-volume
      mountPath: /home/jenkins/agent

  - name: kaniko-query
    image: gcr.io/kaniko-project/executor:v1.23.2-debug
    command: ["/busybox/cat"]
    tty: true
    volumeMounts:
    - name: docker-config
      mountPath: /kaniko/.docker
    - name: kaniko-cache
      mountPath: /cache
    - name: workspace-volume
      mountPath: /home/jenkins/agent

  - name: kaniko-frontend
    image: gcr.io/kaniko-project/executor:v1.23.2-debug
    command: ["/busybox/cat"]
    tty: true
    volumeMounts:
    - name: docker-config
      mountPath: /kaniko/.docker
    - name: kaniko-cache
      mountPath: /cache
    - name: workspace-volume
      mountPath: /home/jenkins/agent

  - name: kubectl
    image: dtzar/helm-kubectl:3.15.0
    command: ["sleep"]
    args: ["infinity"]
    volumeMounts:
    - name: workspace-volume
      mountPath: /home/jenkins/agent
'''
        }
    }
  • agent kubernetes: Jenkins가 쿠버네티스에서 파드를 띄워 작업을 수행한다는 선언.
  • namespace 'jenkins': 작업할 네임스페이스를 지정.
  • yaml: 쿠버네티스 Pod 정의를 직접 선언.
  • serviceAccountName 'jenkins-agent': Pod에서 사용할 서비스 계정.
  • volumes:
    • gradle-cache: Gradle 빌드 캐시를 위한 PVC(영구 저장소).
    • kaniko-cache: Kaniko (도커 이미지 빌드 툴) 캐시용 PVC.
    • docker-config: 도커 허브 로그인 정보가 담긴 시크릿 마운트.
    • workspace-volume: 여러 컨테이너가 공유할 작업 공간(빈 디렉터리).
  • containers:
    • gradle: Gradle 빌드용, 무한 대기 상태로 대기(sleep infinity)하여 명령 대기.
    • kaniko-command, kaniko-query, kaniko-frontend: Kaniko executor 이미지 3개(각각 command-service, query-service, todo-frontend용) - 빌드에 독립적 사용.
    • kubectl: 쿠버네티스와 헬름 명령어 실행용 컨테이너.

2. 옵션 설정

options {
    timeout(time: 45, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '10'))
}
  • 파이프라인 실행 최대 시간 45분 제한.
  • 최근 10개 빌드 기록만 보관하고 나머지는 삭제.

3. 환경 변수

environment {
    DOCKERHUB_REPO = 'kyla333'
    DEPLOY_NAMESPACE = 'prod'
}
  • DOCKERHUB_REPO: 도커 허브 사용자명 혹은 리포지토리명.
  • DEPLOY_NAMESPACE: 쿠버네티스 배포 대상 네임스페이스(production).

4. Stages


Stage: Initialize

stage('Initialize') {
    steps {
        script {
            env.IMAGE_TAG = sh(
                script: 'git rev-parse --short=7 HEAD',
                returnStdout: true
            ).trim()
            echo "📦 IMAGE_TAG: ${env.IMAGE_TAG}"
        }
    }
}
  • git rev-parse --short=7 HEAD: 현재 커밋의 7자리 해시를 가져와 IMAGE_TAG 환경 변수에 저장.
  • 이후 도커 이미지 태그로 활용.

Stage: Detect Changes

stage('Detect Changes') {
    steps {
        script {
            env.CHANGED_SERVICES = detectChangedServices()
            if (!env.CHANGED_SERVICES) {
                echo "ℹ️  No service code changes - skipping build"
                currentBuild.result = 'SUCCESS'
                currentBuild.description = "No changes"
                env.SKIP_BUILD = 'true'
            } else {
                echo "🔍 Changed: ${env.CHANGED_SERVICES}"
                currentBuild.description = "Building: ${env.CHANGED_SERVICES}"
            }
        }
    }
}
  • 사용자 정의 함수 detectChangedServices() 호출해서 변경된 서비스 목록을 판단.
  • 변경 없으면 빌드 스킵 플래그(SKIP_BUILD) 설정.
  • 변경 있으면 어떤 서비스가 바뀌었는지 출력 및 빌드 설명 설정.

Stage: Build Backend Services

stage('Build Backend Services') {
    when {
        expression { 
            env.SKIP_BUILD != 'true' && 
            (env.CHANGED_SERVICES.contains('command-service') || 
             env.CHANGED_SERVICES.contains('query-service'))
        }
    }
    steps {
        container('gradle') {
            script {
                def services = env.CHANGED_SERVICES.split(',')
                
                services.each { svc ->
                    if (svc in ['command-service', 'query-service']) {
                        echo "🔨 Building ${svc}..."
                        sh """
                            cd ${svc}
                            chmod +x gradlew
                            ./gradlew clean bootJar -x test --no-daemon
                            echo "✅ ${svc} build completed"
                            ls -lh build/libs/
                        """
                    }
                }
            }
        }
    }
}
  • 조건:
    • 빌드 스킵 플래그가 없고,
    • 변경된 서비스에 백엔드(command-service, query-service)가 포함된 경우.
  • gradle 컨테이너에서 변경된 각 백엔드 서비스별로 Gradle 빌드 실행(bootJar 생성, 테스트 제외).
  • 빌드 결과 확인 및 출력.

Stage: Build & Push Images

stage('Build & Push Images') {
    when {
        expression { env.SKIP_BUILD != 'true' }
    }
    steps {
        script {
            def services = env.CHANGED_SERVICES.split(',')
            def imageTag = env.IMAGE_TAG  // 로컬 변수로 캡처
            def dockerRepo = env.DOCKERHUB_REPO

            def builds = [:]

            if (services.contains('command-service')) {
                builds['Command Service'] = {
                    container('kaniko-command') {
                        sh """
                            /kaniko/executor \\
                              --context=\${WORKSPACE}/command-service \\
                              --dockerfile=\${WORKSPACE}/command-service/Dockerfile \\
                              --destination=${dockerRepo}/command-service:${imageTag} \\
                              --destination=${dockerRepo}/command-service:latest \\
                              --cache=true --cache-ttl=24h --cache-dir=/cache \\
                              --cache-repo=${dockerRepo}/command-service-cache
                        """
                        echo "✅ command-service:${imageTag} pushed"
                    }
                }
            }

            if (services.contains('query-service')) {
                builds['Query Service'] = {
                    container('kaniko-query') {
                        sh """
                            /kaniko/executor \\
                              --context=\${WORKSPACE}/query-service \\
                              --dockerfile=\${WORKSPACE}/query-service/Dockerfile \\
                              --destination=${dockerRepo}/query-service:${imageTag} \\
                              --destination=${dockerRepo}/query-service:latest \\
                              --cache=true --cache-ttl=24h --cache-dir=/cache \\
                              --cache-repo=${dockerRepo}/query-service-cache
                        """
                        echo "✅ query-service:${imageTag} pushed"
                    }
                }
            }

            if (services.contains('todo-frontend')) {
                builds['Frontend'] = {
                    container('kaniko-frontend') {
                        sh """
                            /kaniko/executor \\
                              --context=\${WORKSPACE}/todo-frontend \\
                              --dockerfile=\${WORKSPACE}/todo-frontend/Dockerfile \\
                              --destination=${dockerRepo}/todo-frontend:${imageTag} \\
                              --destination=${dockerRepo}/todo-frontend:latest \\
                              --cache=true --cache-ttl=24h --cache-dir=/cache \\
                              --cache-repo=${dockerRepo}/todo-frontend-cache
                        """
                        echo "✅ todo-frontend:${imageTag} pushed"
                    }
                }
            }

            parallel builds
        }
    }
}
  • 빌드 스킵 안했을 때 실행.
  • 변경된 각 서비스별로 kaniko 컨테이너를 이용해 도커 이미지를 빌드 & 푸시.
  • kaniko는 도커 데몬 없이 이미지 빌드하는 도구 ->  파일 내용의 변경 여부를 해시 값으로 감지하여 지능적으로 이미지를 재빌드
  • parallel builds로 병렬 처리해 빠른 빌드 가능.
  • 캐시, latest 태그 함께 관리.
  • 이미지 태그는 git 커밋 해시 기반.

Stage: Deploy to Production

stage('Deploy to Production') {
    when {
        expression { env.SKIP_BUILD != 'true' }
    }
    steps {
        container('kubectl') {
            script {
                def services = env.CHANGED_SERVICES.split(',')
                def imageTag = env.IMAGE_TAG
                def dockerRepo = env.DOCKERHUB_REPO
                def namespace = env.DEPLOY_NAMESPACE
                
                echo "🚀 Deploying to ${namespace}: ${services.join(', ')}"
                
                services.each { svc ->
                    def deploymentName = (svc == 'todo-frontend') ? 'frontend-deployment' : "${svc}-deployment"
                    def containerName = (svc == 'todo-frontend') ? 'frontend' : svc
                    
                    echo "📦 Updating ${deploymentName}..."
                    sh """
                        kubectl set image deployment/${deploymentName} \
                          ${containerName}=${dockerRepo}/${svc}:${imageTag} \
                          -n ${namespace}
                        
                        echo "⏳ Waiting for rollout..."
                        kubectl rollout status deployment/${deploymentName} \
                          -n ${namespace} \
                          --timeout=5m
                        
                        echo "✅ ${deploymentName} deployed"
                    """
                }
            }
        }
    }
}
  • 빌드 스킵 안했으면 실행.
  • kubectl 컨테이너에서 변경된 서비스들의 쿠버네티스 Deployment에 이미지 태그 업데이트 및 롤아웃 대기.
  • todo-frontend 서비스는 배포명과 컨테이너명이 별도로 설정되어 있음.
  • 배포 성공 여부를 확인 후 다음으로 진행.

5. Post 빌드 작업

post {
    success {
        script {
            if (env.SKIP_BUILD == 'true') {
                echo "✅ No changes - pipeline skipped"
            } else {
                echo """
✅ CI/CD Pipeline Completed!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 Image Tag: ${env.IMAGE_TAG}
🚀 Deployed: ${env.CHANGED_SERVICES}
🌐 Namespace: ${env.DEPLOY_NAMESPACE}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                """
            }
        }
    }
    failure {
        echo """
❌ Pipeline Failed
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Check logs above for details.
Commit: ${env.GIT_COMMIT}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        """
    }
    always {
        echo "🏁 Finished at ${new Date()}"
    }
}
  • 성공 시:
    • 빌드 스킵 시 별도 메시지 출력
    • 아니면 배포 정보 출력
  • 실패 시: 에러 메시지와 커밋 정보 출력.
  • 항상: 완료 시간 출력.

6. 사용자 정의 함수: detectChangedServices()

def detectChangedServices() {
    if (!env.GIT_PREVIOUS_SUCCESSFUL_COMMIT) {
        echo "🆕 First build - deploying all services"
        return 'command-service,query-service,todo-frontend'
    }

    def diff = sh(
        script: "git diff --name-only ${env.GIT_PREVIOUS_SUCCESSFUL_COMMIT}..${env.GIT_COMMIT}",
        returnStdout: true
    ).trim()

    if (!diff) {
        return ''
    }

    def changedFiles = diff.split('\n').findAll { it?.trim() }
    def services = ['command-service', 'query-service', 'todo-frontend']
    def changedServices = []

    services.each { svc ->
        if (changedFiles.any { it.startsWith("${svc}/") }) {
            changedServices << svc
        }
    }

    if (changedServices.isEmpty() && changedFiles.any { it == 'Jenkinsfile' }) {
        echo "⚙️  Only Jenkinsfile changed - skipping"
        return ''
    }

    return changedServices.join(',')
}
  • 첫 빌드이면 무조건 모든 서비스 배포.
  • 이전 성공 커밋과 현재 커밋간 차이를 git diff --name-only로 구함.
  • 변경된 파일 경로에 따라 어떤 서비스가 바뀌었는지 판단.
  • command-service, query-service, todo-frontend 3개 서비스 기준.
  • 만약 서비스 소스 코드는 바뀌지 않고 Jenkinsfile만 바뀌었다면 빌드 스킵.
  • 변경된 서비스 리스트를 쉼표로 연결해 반환.

요약

1. 다중 컨테이너 Pod 에이전트 (Multi-Container Pod Agent)

  • 빌드를 위해 하나의 무거운 컨테이너 대신, 각 작업에 최적화된 '전문가 컨테이너'들로 구성된 Pod를 사용
    • gradle: 백엔드 빌드 전문가
    • kaniko: Docker 이미지 생성 전문가
    • kubectl: 쿠버네티스 배포 전문가
  • 이들은 공동 작업 공간 (Volume)을 통해 결과물을 공유하며 협업한다.

2. 영구 캐시를 활용한 빌드 가속 (Persistent Caching)

  • 빌드 속도를 극적으로 향상시키는 '기억 장치'
    • Gradle 캐시: 한번 받은 라이브러리는 다시 다운로드하지 않고 즉시 재사용한다.
    • Kaniko 캐시: 변경되지 않은 Docker 이미지 레이어는 재사용하여 이미지 빌드 시간을 단축한다.

3. 자동화 프로세스 단계별 분석 (1/2)

단계 Stage 주요 활동
1 Initialize - Git 커밋 해시로 고유 이미지 태그(예: a1b2c3d)를 생성한다.
2 Detect Changes - "파이프라인의 두뇌": 이전 빌드와 코드를 비교하여 변경된 서비스 목록을 만든다.
- 변경이 없으면 여기서 파이프라인이 성공적으로 종료된다.
3 Build Backend - (조건부 실행) 백엔드 서비스가 변경 목록에 있을 경우에만 .jar 파일을 빌드한다.

3. 자동화 프로세스 단계별 분석 (2/2)

4 Build & Push - (병렬 실행) 변경된 모든 서비스의 Docker 이미지를 동시에 빌드한다.
- 프론트엔드 빌드(npm build)는 이 과정 안에서 함께 수행된다.
- 빌드된 이미지를 a1b2c3d, latest 두 개의 태그로 Docker Hub에 푸시한다.
5 Deploy - kubectl이 Docker Hub에서 새 이미지를 가져와 쿠버네티스에 배포한다.
- 무중단 롤링 업데이트 방식으로 사용자는 서비스 중단을 느끼지 못한다.
6 Post Actions - 파이프라인의 최종 결과를 요약하여 보고하며 모든 과정을 마무리
  • 쿠버네티스 기반 Jenkins 파이프라인으로 여러 컨테이너에서 병렬 빌드 및 이미지 푸시 수행.
  • Git 변경 사항 기반 빌드 & 배포 결정.
  • Gradle 빌드 → Kaniko 이미지 빌드 & 푸시 → kubectl을 이용한 쿠버네티스 배포.
  • 캐시, 병렬 처리, 자동 배포 상태 확인까지 포함된 효율적인 CI/CD 파이프라인.

4. 핵심 기술과 기대 효과

Key Takeaways

  • 선택적 빌드: 변경된 것만 빌드/배포하여 CI/CD 시간을 평균 60~70% 단축.
  • 빌드 캐싱: 반복 빌드 시 Gradle 빌드 시간을 5분 → 30초 내외로 단축.
  • 프로세스 자동화: 수동 배포에 소요되던 시간을 0으로 만들어 개발자가 핵심 업무에 집중.

기대 효과

  • 개발 생산성 향상: 개발자는 코드 푸시만 하면 되므로, 개발의 흐름이 끊기지 않는다.
  • 빠른 피드백 루프: 빌드와 배포가 빨라져 코드 변경 결과를 즉시 확인할 수 있다.
  • 안정적인 서비스 운영: 휴먼 에러가 배제된 일관된 배포 프로세스로 서비스 안정성이 크게 향상된다.

Troubleshooting

초반에 기본 자원인 disk 25GB, memory 4GB, cpu 1개씩으로 할당해서 워커1, 워커2을 만들고 마스터 노드는 disk 25GB, memory 4GB, cpu 2개로 할당해서 만들었다.

 

앱을 돌리기에는 괜찮았지만 Jenkins를 깔고 돌리자마자 disk가 부족해서 할당이 안 되고 파드들이 제대로 생성되지 않기 시작했다. 이에 disk, memory 자원들을 모두 늘려주었다.

 

master: disk 40GB, memory 8GB, cpu 2개

worker1: disk 40GB, memory 10GB, cpu 4개

worker2: disk 40GB, memory 10GB, cpu 4개

 

이렇게 하니 Jenkins가 돌아가는 worker2 노드에서 top을 해보니 cpu 사용량이 거의 300%를 찍기도 했지만 자원 때문에 파드들이 죽진 않았다. 또한 디스크 증설 후 master, worker1, worker2는 전체 디스크의 약 60%를 차지하는 상황으로 꽤 안정화되었다.

-> Jenkins가 무겁다는 얘기는 들었지만 이렇게 무거울 줄은 몰랐다.

-> 또한 Jenkins를 쓰면 주기적으로 디스크 관리도 해줘야 한다고 한다.

 

디스크 확장 명령어:

vm 끄고 터미널에서
.\VBoxManage.exe modifyhd "C:\Users\kimmy\VirtualBox VMs\Worker1\Worker1.vdi" --resize 40960

vm 키고 그 안에서
sudo fdisk -l
sudo pvresize /dev/sda3
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv

 

✅ 왜 디스크 관리가 필요할까?

Jenkins는 다음과 같은 항목들로 디스크를 계속 소비한다.

항목 설명
워크스페이스 (workspace) 각 Job이 실행될 때 체크아웃된 코드와 빌드 파일이 저장된다.
빌드 아티팩트 (build artifacts) 빌드 결과물 (예: JAR, WAR, 바이너리, 로그 등)이 저장된다.
이전 빌드 기록 Jenkins는 기본적으로 많은 이전 빌드를 저장한다.
캐시, 플러그인 임시 파일 Git 캐시, Maven/NPM 캐시, 테스트 리포트 등 임시 저장 파일이 계속 쌓인다.
플러그인 데이터 사용 중인 플러그인들도 디스크를 많이 사용한다.

✅ 디스크 정리 자동화 추가 - 정리 Job을 주기적으로 실행

cron 형식의 Jenkins Job 또는 Pipeline을 추가해서 PVC 내부를 주기적으로 정리할 수 있다.

예: Gradle, Kaniko 캐시 정리 Job

pipeline {
    agent {
        kubernetes {
            yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: cleaner
    image: alpine
    command:
    - sh
    - -c
    - |
      echo "🧹 Cleaning Gradle & Kaniko cache..."
      rm -rf /gradle-cache/* /kaniko-cache/*
    volumeMounts:
    - name: gradle-cache
      mountPath: /gradle-cache
    - name: kaniko-cache
      mountPath: /kaniko-cache

  volumes:
  - name: gradle-cache
    persistentVolumeClaim:
      claimName: gradle-cache-pvc
  - name: kaniko-cache
    persistentVolumeClaim:
      claimName: kaniko-cache-pvc
'''
        }
    }

    triggers {
        cron('H H * * 0') // 매주 일요일 1회
    }

    stages {
        stage('Cleanup') {
            steps {
                container('cleaner') {
                    echo "🧹 Caches cleaned!"
                }
            }
        }
    }
}
728x90
반응형