[CICD] git-jenkins-ECR로 자동배포 구축하기

개요

  • 개발자 배포 시 자동으로 서버에 배포될 수 있도록 jenkins-ECR 파이프라인 구축

Before

image

step1: 개발자가 git push를 통해 개발 내용을 업로드한다.
step2: 개발자가 merge request를 요청한다.
step3: 개발책임자가 merge request를 승인한다.
step4: 개발자는 이후 어플리케이션이 구동중인 EC2 서버에 접속한다.
step5: 개발자는 EC2에 위치한 배포스크립트를 수동으로 실행한다.

  • 매번 git clone, build, 서버 분기를 위해 shutdown이 잦다
  • 보안 문제 : 모든 개발자가 접근 가능하고, root 계정으로 모든 배포를 진행

Now

image

step1. 개발자가 git push를 통해 개발 내용을 업로드한다.
step2. 개발자가 merge request를 요청한다
step3. 개발책임자가 merge request를 승인한다.
step4. git 설정) merge request가 승인되면 jenkins로 webhook을 송신한다
step5. jenkins에서 git에서의 webhook이 트리거되면
step6. jenkins 파이프라인을 실행한다.

  • git clone
  • 이미지 build
  • ECR cli 접속하여 이미지 업데이트 후, jenkins 로컬 이미지 삭제 (ECR 접속 시, 기존 이미지의 네이밍 변경 후 최신 이미지로 교체)
  • EC2 cli 접속(ssh)하여 배포 스크립트 실행 (IAM을 통해 jenkins 계정을 만들어, root가 아닌 jenkins 계정 사용하도록 함)

step7.EC2는 배포 스크립트를 실행한다.

  • docker로 ECR을 접속한다
  • ECR에서 6번에서 업데이트된 최신 이미지를 다운로드한다.
  • 기존 실행 중인 컨테이너를 중지 후 삭제한다.
  • 최신 이미지로 컨테이너를 실행한다.

물리 구성 엔드포인트

image

구축 방법

사전 준비 사항

  • AWS EC2(Linux 2) 기준으로 작성. (2024-03-19)

    • aws cli v2 설치
    • AWS IAM 자격증명 생성
    • ECR 레포지토리 생성
    • Docker 설치 및 실행
    • sshd 서버 설치 및 실행
    • 배포용 계정 생성 및 pem키 등록

aws cli v2 설치

  • 기존에 aws cli가 설치되어 있지 않은 경우 (기본 설치 방법)
    # Linux (AWS CLI v2)
    curl "<https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip>" -o "awscliv2.zip"
    unzip awscliv2.zip
    sudo ./aws/install
    # brew
    brew install awscli
    # 설치확인
    aws --version
    
  • 기존에 aws cli(v1)가 설치되어 있는 경우
    sudo yum remove awscli
    #### 설치 생략 기본 설치방법 참고 ####
    
    # 현재 버전 aws cli 위치 확인
    which aws
    # 위에서 나온 경로
    {설치경로}/aws --version
    
  • aws cli v1,v2 둘다 설치되어 있을 때 심볼릭 링크 변경방법
    # 1 링크 삭제
    sudo rm /usr/bin/aws
    # 2 링크 추가
    sudo ln -s /usr/local/bin/aws /usr/bin/aws
    

AWS IAM 자격증명

  • root가 아닌 ‘jenkins’ 계정을 따로 IAM에서 생성 후 필요 권한만 부여
    • IAM 권한 : AmazonEC2ContainerRegistryFullAccess AmazonElasticContainerRegistryPublicFullAccess
  • cli 설치 후 자격증명
     # AWS 자격증명
     $ aws configure
     AWS Access Key ID [None]: {발급한 jenkins계정 Key id}
     AWS Secret Access Key [None]: {발급한 Secret Access Key}
     Default region name [None]: 
     Default output format [None]:
       
     # 자격증명 활성화 확인
     $ aws sts get-caller-identity
    

AWS ECR 레포지토리 생성

  • Amazon ECR > Private registry > Repositories에서 리포지토리 생성 image

Docker 설치 및 실행

# 도커 설치
sudo yum install docker -y

# 도커 실행
sudo service docker start

# 도커 권한 부여 (username)
sudo usermod -aG docker {username}

# EC2 인스턴스 재접속 후 확인
exit
sudo service docker status 

sshd 서버 실행

# ssh 서버 실행
sudo service sshd start

# 상태 확인
sudo service sshd status

배포용 계정 생성 및 pem 키 등록

  • 배포용 유저 jenkins 생성(root대신 배포하게 함)
    sudo adduser jenkins
    
  • pem 키 발급받은 후 /home/{username}/.ssh/authorized_keys에 저장 및 권한 축소
    # /home/{username}/.ssh/authorized_keys에 pem키 정보 등록 이후
    sudo chmod 700 /home/jenkins/.ssh
    sudo chmod 600 /home/jenkins/.ssh/authorized_keys
    
  • 유저 디렉토리 권한 부여
    sudo chown -R jenkins:jenkins /home/jenkins
    
  • docker 권한 부여
    sudo usermod -aG docker jenkins
    
    # docker restart (리스타트 후에 컨테이너 다시 실행시켜야함)
    sudo service docker restart
    
  • docker 배포 스크립트 작성 및 실행 권한 부여
    # 생성된 유저로 로그인한 후 실행
    cd ~
    vim docker-deploy.sh
    chmod +x docker-deploy.sh
    

git

  • jenkins로 webhook 송신
  • 브랜치별(스테이징/운영) 분기

jenkins webhook 송신

  • git>Settings>Webhooks
  • 푸시할 때 jenkins 파이프라인으로 webhook을 송신한다.
  • 여담) merge, 배포 등 이벤트 확인을 위해 dooray 메신저로도 webhook 걸어둠 image

브랜치별 분기

  • webhook settings에서 스테이징 배포면 스테이징 파이프라인으로, 운영 배포면 운영 파이프라인으로 webhook을 송신하도록 한다. image

Jenkins

  • 파이프라인 스크립트 작성
    pipeline{
       // 파이프라인 실행
       agent any
       
       environment{
          AWS_DEFAULT_REGION = "ap-northeast-2"
          AWS_ACCOUNT_ID = "AWS콘솔ID"
          IMAGE_REPO_NAME = "csp-was-prd"
          PROFILE = "prd"
          LATEST = "latest"
          PLATFORM = "linux/amd64"
          MODULE_NAME = "csp-was-prd"
       }
       
       stages{
          // git clone
          stage('git clone') {
              steps {
                  git branch: 'main', credentialsId: {jenkins에등록한git토큰credential}, url: 'http://{git서버IP}/csp/csp-was.git'
              }
          }
          // build 
          stage("Build Gradle") {
              steps {
                  bat "gradlew.bat clean bootJar"
              }
          }
          // 이미지 이름 변경
          stage("Login AWS & Change Previous Image") {
              steps {
                  script {
                      withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'aws-ecr']]) {
                          def currentDateTime = new Date().format('yyyyMMdd.HHmmss', TimeZone.getTimeZone('Asia/Seoul'))
                          def login = "docker login --username AWS -p \$((Get-ECRLoginCommand -Region ap-northeast-2).Password) AWS콘솔ID.dkr.ecr.ap-northeast-2.amazonaws.com;"
                          def changeTagIfPresent = """
                              \$images = Get-ECRImageBatch -imageId @{ imageTag='${LATEST}' } -Region '${AWS_DEFAULT_REGION}' -RepositoryName '${IMAGE_REPO_NAME}';
                              echo \$images;
                              if (\$images.Images.Count -gt 0) {
                                  \$imageManifest = \$images.Images[0].ImageManifest;
                                  Write-ECRImage -RepositoryName '${IMAGE_REPO_NAME}' -ImageManifest \$imageManifest -Region '${AWS_DEFAULT_REGION}' -ImageTag ${currentDateTime};
                                  aws ecr batch-delete-image --repository-name '${IMAGE_REPO_NAME}' --image-ids imageTag='${LATEST}'
                              } else {
                                  Write-Host No images found with tag '${LATEST}' in the repository '${IMAGE_REPO_NAME}';
                              }
                          """ as String
       
                          powershell('Import-Module AWSPowerShell; ' + login + changeTagIfPresent)
                      }
                  }
              }
          }
          // 이미지 빌드  ECR에 업로드
          stage("Build and Push Docker Image"){
             steps{
                script{
                   bat "docker build --platform ${PLATFORM} -t ${IMAGE_REPO_NAME} . --build-arg SPRING_PROFILES_ACTIVE=${PROFILE}"
                   bat "docker tag ${IMAGE_REPO_NAME}:${LATEST} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${LATEST}"
                   bat "docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${LATEST}"
                   bat "docker rmi ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${LATEST} --force"
                   bat "docker rmi ${IMAGE_REPO_NAME}:${LATEST} --force"
                   bat "docker logout public.ecr.aws"
                }
             }
          }
          // EC2 접속해서 배포 스크립트 실행
          stage("Server Deploy"){
             steps{
                script{
                   def sshOptions = "-o StrictHostKeyChecking=no"
                   def bastionHost = "jenkins@배스천호스트IP"
                   def targetHosts = ["jenkins@EC2인스턴스-a-IP", "jenkins@EC2인스턴스-c-IP"]
                   def privateKeyBastion = "~/.ssh/hdcl-csp-key.pem"
                   def privateKeyTarget = "./hdcl-csp-key.pem"
    
                   withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'aws-ecr']]) {
                          targetHosts.each { targetHost ->
                              sh """
                                  ssh -t -t ${sshOptions} -i ${privateKeyBastion} ${bastionHost} ssh ${sshOptions} -i ${privateKeyTarget} ${targetHost} '/home/jenkins/docker-deploy.sh'
                              """
                          }
                      }
                   }
                }
            
         
             }
    
     }
    
  • git webhook 트리거 설정 : 파이프라인 설정 image

EC2 배포 스크립트

  • /home/jenkins/docker-deploy.sh
    #!/bin/bash
    
    # kill current process (docker로 띄우지 않은 프로세스)
    PORT=8081
    PID=$(lsof -t -i:${PORT})
    
    if [ -n "$PID" ]; then
      kill -9 $PID
    fi
    
    aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin AWS콘솔ID.dkr.ecr.ap-northeast-2.amazonaws.com
    
    docker pull --platform linux/amd64 AWS콘솔ID.dkr.ecr.ap-northeast-2.amazonaws.com/csp-was-prd:latest
    
    (docker stop csp-was-prd 2>/dev/null || true) && \
    (docker rm -f csp-was-prd 2>/dev/null || true) && \
    
    docker run --platform linux/amd64 -p 8081:8081 -d --name csp-was-prd AWS콘솔ID.dkr.ecr.ap-northeast-2.amazonaws.com/csp-was-prd:latest