EKS 기반 spring, vuejs 웹 어플리케이션 배포 및 CI/CD 구축하기 #4

2024. 11. 12. 02:28개발 플랫폼/AWS

Jenkins를 통한 CI

EKS안에 jenkins서버를 구축해서 CI작업을 하려고 하였으나 리소스 용량 문제로 따로 EC2 인스턴스를 통해 젠킨스 서버를 구축하게 되었다.

 

간단한 CI 파이프라인 FLOW

  1. 개발자가 코드를 GitHub의 main 브랜치에 Push를 하면 웹훅을 통해 CI 파이프라인을 트리거
  2. 코드 변경 사항을 감지하여 애플리케이션을 빌드
  3. Docker 이미지를 AWS ECR에 푸시
  4. ArgoCD 배포 파일을 업데이트하여 자동으로 GitHub에 푸시

1. Jenkins를 위한 IAM 역할 생성

jenkins-eks-ecr-role에 AmazonEC2ContainerRegistryFullAccess, AmazonEKSClusterPolicy 권한 정책 부여

ECR에 이미지를 푸시, EKS클러스터에 접근하기 위한 정책 부여

 

2. Jenkins를 운영할 EC2 인스턴스 생성

Jenkins-CI-Server라는 EC2 인스턴스 생성 및 생성한 역할 설정

 

생성한 인스턴스에 접속하여 배포된 어플리케이션과 같은 버전의 Java를 설치하고 Docker, Jenkins를 설치한다.

 

3. Jenkins 설정

모든 설치가 끝났다면 8080포트로 젠킨스 접속이 가능하다

우선 Route 53을 통해 Jenkins에 대한 도메인을 등록했었기 때문에 라우팅 처리를 한다.

 

shell에 해당 명령어를 통해 초기 비밀번호 찾아 젠킨스 로그인

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

젠킨스 접속 후 Dashboard -> Jenkins 관리 -> Plugins에서 아래 플러그인 설치

  • GitHub plugin
  • Gradle Plugin
  • Docker Pipeline Plugin
  • Amazon ECR Plugin
  • Kubernetes CLI Plugin
  • Discord Notifier

다른 플러그인을 사용하려면 추가로 설치해도 됨

 

Github Access token 설정

아래 Settings -> Developer settings -> Personal access tokens 에서 jenkins에서 사용할 토큰 생성

 

repo에 대한 권한 설정을 해준다.

 

Credentials 저장

 

username/password로 username은 깃허브 아이디에서 이메일을 제외하고 입력, password는 access token값으로 저장

 

Webhook 설정

젠킨스 주소/github-webhook/으로 웹훅 설정

 

파이프라인 생성

 

New Item을 클릭하여 Pipeline 선택 후 생성(OK)

 

구성

구성 정보를 입력 후 저장

branch를 */main으로 입력 -> main 브랜치에 푸시가 발생하면 CI 파이프라인 트리거

 

4. Jenkinsfile 작성

1. 환경 변수 설정 (Environment)

Jenkins 파이프라인에서 사용할 환경 변수들을 설정합니다:

  • AWS_REGION: AWS 리전 설정 (ap-northeast-2)
  • ECR_REGISTRY: ECR 레지스트리 주소
  • FRONTEND_REPOSITORY: 프론트엔드 Docker 이미지 레포지토리
  • BACKEND_REPOSITORY: 백엔드 Docker 이미지 레포지토리
  • FRONTEND_IMAGE_TAG: 프론트엔드 이미지 태그
  • BACKEND_IMAGE_TAG: 백엔드 이미지 태그

2. Checkout Stage (코드 체크아웃)

GitHub에서 main 브랜치를 체크아웃합니다. GitHub 인증 정보(github-https-credentials)를 사용합니다.

git branch: 'main', url: 'https://github.com/beyond-sw-camp/be08-fin-HQ-Heroes.git', credentialsId: 'github-https-credentials'

 

3. Determine Changes Stage (변경 사항 확인)

  • git diff를 사용하여 마지막 커밋과 현재 커밋 간의 파일 변경 사항을 확인합니다.
  • Frontend/와 Backend/Heroes/ 디렉토리 내 파일이 변경된 경우, 해당 영역에 대한 빌드를 설정합니다.
def changedFiles = sh(script: 'git diff --name-only HEAD~1', returnStdout: true).trim().split("\n")
env.BUILD_FRONTEND = changedFiles.any { it.startsWith("Frontend/") } ? "true" : "false"
env.BUILD_BACKEND = changedFiles.any { it.startsWith("Backend/Heroes/") } ? "true" : "false"

 

4. Build Backend Docker Image Stage (백엔드 Docker 이미지 빌드)

  • Backend/Heroes 디렉토리로 이동하여 Gradle을 사용해 백엔드 프로젝트를 빌드합니다 (./gradlew clean bootJar).
  • Dockerfile을 사용하여 Docker 이미지를 빌드합니다.
dir('Backend/Heroes') {
    sh 'chmod +x ./gradlew'
    sh './gradlew clean bootJar'
    sh "docker build -t ${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG} -f Dockerfile ."
}

 

5. Push Backend to ECR Stage (백엔드 이미지 ECR 푸시)

  • AWS CLI를 사용하여 ECR에 로그인합니다.
  • 빌드한 백엔드 이미지를 ECR로 푸시합니다.
sh "aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}"
sh "docker tag ${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG} ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}"
sh "docker push ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}"

 

6. Build Frontend Docker Image Stage (프론트엔드 Docker 이미지 빌드)

  • Frontend 디렉토리로 이동하여 프론트엔드 Docker 이미지를 빌드합니다.
dir('Frontend') {
    sh "docker build -t ${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG} -f Dockerfile ."
}

 

7. Push Frontend to ECR Stage (프론트엔드 이미지 ECR 푸시)

  • 빌드한 프론트엔드 이미지를 ECR에 푸시합니다.
sh "docker tag ${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG} ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}"
sh "docker push ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}"

 

8. Update ArgoCD Stage (ArgoCD 업데이트)

  • Kubernetes 배포 파일(heroes-frontend-deploy.yaml, heroes-deploy.yaml)에서 이미지 태그를 업데이트합니다.
  • 변경된 파일들을 Git에 커밋하고 푸시하여 ArgoCD가 자동으로 배포하도록 합니다.
def frontendFilePath = 'k8s/heroes/heroes-frontend-deploy.yaml'
def backendFilePath = 'k8s/heroes/heroes-deploy.yaml'

if (env.BUILD_FRONTEND == "true") {
    sh 'sed -i "s|image:.*frontend-repo:.*|image: ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}|g" ' + frontendFilePath
}
if (env.BUILD_BACKEND == "true") {
    sh 'sed -i "s|image:.*backend-repo:.*|image: ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}|g" ' + backendFilePath
}

withCredentials([usernamePassword(credentialsId: 'github-https-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
    sh 'git add .'
    sh 'git commit -m "Update image tags for frontend and backend with latest build" || echo "Nothing to commit, images may be up-to-date."'
    sh 'git push https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/beyond-sw-camp/be08-fin-HQ-Heroes.git main'
}

 

9. Post Actions (성공/실패 알림)

  • 성공: Discord 웹훅을 통해 빌드 성공 메시지를 보냅니다.
  • 실패: 빌드 실패 시에도 Discord 웹훅을 통해 실패 메시지를 보냅니다.
discordSend description: "ArgoCD 배포 파이프라인이 성공적으로 완료되었습니다.\n\n" +
             "**Build ID**: ${BUILD_ID}\n" +
             "**Frontend Image**: ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}\n" +
             "**Backend Image**: ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}\n" +
             "**소요 시간**: ${currentBuild.durationString}",
             footer: "빌드 성공: ${currentBuild.displayName}",
             link: env.BUILD_URL,
             result: currentBuild.currentResult,
             title: "Jenkins 빌드 성공",
             webhookURL: "YOUR_DISCORD_WEBHOOK_URL"

 

전체 Jenkinsfile

pipeline {
    agent any
    environment {
        AWS_REGION = 'ap-northeast-2'
        ECR_REGISTRY = '774305581884.dkr.ecr.ap-northeast-2.amazonaws.com'
        FRONTEND_REPOSITORY = 'frontend-repo'
        BACKEND_REPOSITORY = 'backend-repo'
        FRONTEND_IMAGE_TAG = "${BUILD_ID}-frontend"
        BACKEND_IMAGE_TAG = "${BUILD_ID}-backend"
    }
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/beyond-sw-camp/be08-fin-HQ-Heroes.git', credentialsId: 'github-https-credentials'
            }
        }

        stage('Determine Changes') {
            steps {
                script {
                    def changedFiles = sh(script: 'git diff --name-only HEAD~1', returnStdout: true).trim().split("\n")
                    env.BUILD_FRONTEND = changedFiles.any { it.startsWith("Frontend/") } ? "true" : "false"
                    env.BUILD_BACKEND = changedFiles.any { it.startsWith("Backend/Heroes/") } ? "true" : "false"
                }
            }
        }

        stage('Build Backend Docker Image') {
            when {
                expression { env.BUILD_BACKEND == "true" }
            }
            steps {
                dir('Backend/Heroes') {  
                    script {
                        sh 'chmod +x ./gradlew'
                        sh './gradlew clean bootJar'
                        sh "docker build -t ${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG} -f Dockerfile ."
                    }
                }
            }
        }
        
        stage('Push Backend to ECR') {
            when {
                expression { env.BUILD_BACKEND == "true" }
            }
            steps {
                script {
                    sh "aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}"
                    sh "docker tag ${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG} ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}"
                    sh "docker push ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}"
                }
            }
        }

        stage('Build Frontend Docker Image') {
            when {
                expression { env.BUILD_FRONTEND == "true" }
            }
            steps {
                dir('Frontend') {  
                    script {
                        sh "docker build -t ${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG} -f Dockerfile ."
                    }
                }
            }
        }
        
        stage('Push Frontend to ECR') {
            when {
                expression { env.BUILD_FRONTEND == "true" }
            }
            steps {
                script {
                    sh "docker tag ${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG} ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}"
                    sh "docker push ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}"
                }
            }
        }

        stage('Update ArgoCD') {
            when {
                anyOf {
                    expression { env.BUILD_FRONTEND == "true" }
                    expression { env.BUILD_BACKEND == "true" }
                }
            }
            steps {
                script {
                    def frontendFilePath = 'k8s/heroes/heroes-frontend-deploy.yaml'
                    def backendFilePath = 'k8s/heroes/heroes-deploy.yaml'

                    sh 'echo "Before Update:"'
                    sh 'cat ' + frontendFilePath
                    sh 'cat ' + backendFilePath

                    if (env.BUILD_FRONTEND == "true") {
                        sh 'echo "Updating frontend image tag in heroes-frontend-deploy.yaml"'
                        sh 'sed -i "s|image:.*frontend-repo:.*|image: ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}|g" ' + frontendFilePath
                    }
                    if (env.BUILD_BACKEND == "true") {
                        sh 'echo "Updating backend image tag in heroes-deploy.yaml"'
                        sh 'sed -i "s|image:.*backend-repo:.*|image: ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}|g" ' + backendFilePath
                    }

                    sh 'echo "After Update:"'
                    sh 'cat ' + frontendFilePath
                    sh 'cat ' + backendFilePath

                    withCredentials([usernamePassword(credentialsId: 'github-https-credentials', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
                        sh 'git config user.name "growjong8802"'
                        sh 'git config user.email "growjong8802@gmail.com"'
                        sh 'git add .'
                        sh 'git commit -m "Update image tags for frontend and backend with latest build" || echo "Nothing to commit, images may be up-to-date."'
                        sh 'git push https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/beyond-sw-camp/be08-fin-HQ-Heroes.git main'
                    }
                }
            }
        }

    }
    
    post {
        success {
            discordSend description: "ArgoCD 배포 파이프라인이 성공적으로 완료되었습니다.\n\n" +
                        "**Build ID**: ${BUILD_ID}\n" +
                        "**Frontend Image**: ${ECR_REGISTRY}/${FRONTEND_REPOSITORY}:${FRONTEND_IMAGE_TAG}\n" +
                        "**Backend Image**: ${ECR_REGISTRY}/${BACKEND_REPOSITORY}:${BACKEND_IMAGE_TAG}\n" +
                        "**소요 시간**: ${currentBuild.durationString}",
                footer: "빌드 성공: ${currentBuild.displayName}",
                link: env.BUILD_URL,
                result: currentBuild.currentResult,
                title: "Jenkins 빌드 성공",
                webhookURL: "YOUR_DISCORD_WEBHOOK_URL"
        }
        
        failure {
            discordSend description: "ArgoCD 배포 파이프라인이 실패했습니다.\n\n" +
                        "**Build ID**: ${BUILD_ID}\n" +
                        "**소요 시간**: ${currentBuild.durationString}",
                footer: "빌드 실패: ${currentBuild.displayName}",
                link: env.BUILD_URL,
                result: currentBuild.currentResult,
                title: "Jenkins 빌드 실패",
                webhookURL: "YOUR_DISCORD_WEBHOOK_URL"
        }
    }
}

 

이렇게 젠킨스 파일까지 작성이 끝났고 main브랜치에 푸시가 발생하면 자동으로 Jenkins가 CI작업을 시작하는 파이프라인 구축이 끝났다.

CI는 이런식으로 작동을 하게 되고 CD글 작성까지 마무리 되면 영상으로 CI/CD를 같이 남겨야겠다.