Github Actions - CodeDeploy를 이용한 CI/CD 구축 과정

Sangwon·2023년 8월 1일

CI/CD 계획

Github Actions 와 AWS CodeDeploy를 활용해 프로젝트의 CI/CD 환경을 구축하고자 했습니다.

계획한 CI/CD Flow는 다음과 같습니다.

시작하기 앞서

EC2 인스턴스는 Amazon linux ubuntu 22.04 버전을 사용했고, Spring boot 애플리케이션을 배포하였습니다.

일반적인 포스팅들과 약간 다른 부분에 대해 먼저 설명하겠습니다.

  • 그림에는 표현돼있지 않으나, Pull Request 일때에는 자동 빌드만 수행합니다.
    - PR이 Approve 될 때 Merge하는 방식을 사용하고 있어 PR이 생길 때마다 배포되는 것을 방지하기 위함입니다.
  • Merge될 때 자동 빌드 , 자동 배포 (4~10번) 모두 수행합니다.
    - Approve 받았기 때문에 자동 배포 과정을 수행합니다.

AWS 서비스 설정

한 단계씩 AWS 서비스에 접근해 CI/CD 를 위한 설정을 해봅시다.
(각 과정에서 사진을 참조하고 싶다면 https://rachel0115.tistory.com/entry/Github-Actions%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-EC2-S3-CodeDeploy 추천합니다.)

S3 버킷 생성

Github Actions 빌드 결과로 생성되는 Zip 을 저장하기 위해 필요합니다.
모든 퍼블릭 액세스 차단을 체크하고 나머지는 모두 기본값으로 버킷을 생성합시다.

IAM 사용자 생성

앞서 살펴본 CI/CD Flow에서는 Github Actions 의 빌드 결과로 생성된 Zip 파일을 S3에 업로드해야합니다. 또한 Github Actions에서 CodeDeploy에 배포 관련 명령을 전달할 수 있어야 합니다.

따라서, S3FullAccess, CodeDeployFullAccess 정책이 부여된 사용자를 생성합니다.
그리고 AWS 서비스에 접근할 수 있는 방법 중 하나인 Access Key / Secret Access Key를 활용해 접근하기 위해서
보안 자격 증명 > 액세스 키 만들기 버튼을 눌러서 발급 받습니다.

최종적으로, 이 Access Key와 Secret Access Key를 Github Repository secrets 기능을 활용해 저장해둡시다.

EC2 인스턴스에 CodeDeploy Agent 설치

이 포스팅은 각자 EC2 인스턴스는 이미 실행 중이신 분들로 가정합니다.
만약 EC2 인스턴스가 실행 중이 아닌 경우 EC2 인스턴스를 생성하고 실행을 먼저 해야합니다.

EC2 인스턴스의 OS는 ubuntu-22.04를 사용하고 있어 해당 공식 문서를 참고한 뒤,
차례로 제시된 코드들을 실행해줍시다.
https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html

문제가 없다면 공식 문서의 마지막에 있는 다음 코드로 codedeploy-agent 실행이 잘 되어있는지 확인할 수 있습니다!

sudo service codedeploy-agent status

IAM 사용자 생성

CodeDeploy 애플리케이션 생성 전 필요한 사용자를 만들어봅시다.

이번에는 다음과 같이 설정하면 됩니다.

  • 신뢰할 수 있는 유형 : AWS 서비스
  • 사용 사례 : 다른 AWS 서비스의 사용 사례 : CodeDeploy
  • 정책 : AWSCodeDeployRole

CodeDeploy Application 생성

애플리케이션 생성

  • 이름 : 자유롭게 입력
  • 컴퓨팅 플랫폼 : EC2

배포 그룹 생성

  • 배포 그룹 명 : 자유롭게 입력
  • 서비스 역할 : 방금 생성한 IAM
  • 배포 유형 : 현재 위치
  • 환경 : EC2 인스턴스를 선택하고 현재 실행중인 EC2 인스턴스 선택
  • 배포 구성 : CodeDeployDefault.AllAtOnce
  • 로드 밸런싱 활성화 : 체크 (만약 로드밸런서를 쓰지 않으면 체크를 안하면 됩니다!)
    • 이후 로드 밸런싱 타겟 그룹 설정을 해줍니다.

배포를 위한 스크립트 추가

이제 AWS 서비스에 대한 작업은 모두 끝났습니다. CI/CD에 필요한 스크립트들을 작성해봅시다.

Build-deploy.yml

이름은 자유롭게 지으면 됩니다. .github/workflow 밑에 위치해 Github Action이 수행할 동작을 지정할 파일입니다.

다음은 제가 프로젝트에서 활용한 예시 코드입니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Build and Deploy Spring Boot to AWS EC2

on:
  push:
    branches: [ "dev" ]
  pull_request:
    branches: [ "dev" ]

permissions:
  contents: read

env:
  PROJECT_NAME: 프로젝트 명 (S3에 ZIP파일 위치에 필요할 뿐 자유롭게 설정)
  BUCKET_NAME: 앞서 설정한 S3 버킷 명
  CODE_DEPLOY_APP_NAME: 앞서 만든 CodeDeploy 애플리케이션 이름
  DEPLOYMENT_GROUP_NAME: 앞서 만든 CodeDeploy 배포 그룹 이름


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
    - name: Build with Gradle
      uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
      with:
        arguments: build

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && github.event.head_commit != null

    steps:
      - uses: actions/checkout@v2

      - name: Make Zip File
        run: zip -qq -r ./$GITHUB_SHA.zip .
        shell: bash

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

      - name: Code Deploy to EC2 instance
        run: aws deploy create-deployment
          --application-name $CODE_DEPLOY_APP_NAME
          --deployment-config-name CodeDeployDefault.AllAtOnce
          --deployment-group-name $DEPLOYMENT_GROUP_NAME
          --s3-location bucket=$BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip

제 프로젝트에서는 Dev 브랜치로 팀원과 코드를 관리하고 있고,
Pull Request에는 빌드만, Merge 시에는 빌드 + 배포를 위해 위와 같이 코드를 구성하였습니다.

참고로 On에서 Merge가 아니라 Push 인 이유는 Merge를 따로 지원하고 있지 않으며 Merge 시 Push Event가 발생하기 때문입니다.

또한 Deploy job은 if: 를 활용해 Merge(Push) 시에만 동작하도록 해주었습니다.

appspec.yml 과 scripts/deploy.sh

두 파일 모두 스프링부트 프로젝트 "최상단"에 위치해야 함에 주의합시다.

appspec.yml 은 CodeDeploy가 배포 과정에서 참고하는 파일이며,
scripts/deploy.sh는 배포 과정 단계 중 AfterInstall 시 실행할 명령이 있는 파일입니다.

코드를 보며 정리해봅시다.

appspec.yml
해당 파일은 CodeDeploy가 배포 과정에서 참조하는 파일입니다.
앞서 설명한 AfterInstall 배포 단계에서 특정 sh 가 실행되도록 하였습니다.
(이외에도 다양한 배포 단계가 있고, 이에 맞추어 다양한 sh이 실행되도록 변경할 수도 있습니다!)

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/배포를 원하는 경로 지정
file_exists_behavior: OVERWRITE
permissions:
  - object: /home/ubuntu/배포를 원하는 경로 지정/
    owner: ubuntu
    group: ubuntu
hooks:
  AfterInstall:
    - location: scripts/deploy.sh
      timeout: 60
      runas: ubuntu

scripts/deploy.sh

REPOSITORY=/home/ubuntu/배포를 원하는 경로
cd $REPOSITORY

APP_NAME=자유롭게 이름 설정
JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep 'SNAPSHOT.jar' | tail -n 1)
JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME

CURRENT_PID=$(pgrep -f $APP_NAME)

if [ -z $CURRENT_PID ]
then
  echo "> 종료할 애플리케이션이 없습니다."
else
  echo "> kill -9 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

echo "> Deploy - $JAR_PATH "
nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null &

이 shell 파일은 배포 후(AfterInstall 단계) EC2에서 jar 파일을 실행하여 실제 배포를 하는 명령어입니다.

정상적으로 이 과정을 마쳤다면,
Github Repository에 Pull Request 를 Merge하면 EC2 인스턴스에 배포가 완료될 것입니다.

트러블슈팅

배포 과정에서 크게 3가지 오류를 겪었고 해결 과정을 공유합니다.
(참고로 위에 적은 build-deploy.yml, appspec.yml, sripts/deploy.sh 는 이 오류 해결을 위한
내용들이 반영된 코드입니다!)

물론 해당 에러 메시지가 반드시 제가 겪은 오류에만 나타나는 것은 아니므로, 참고용으로 사용해주세요!

에러 1 : BeforeInstall 단계

원인 : Github Actions에서 Zip 파일을 생성할 때 빈 Zip 파일이 생성되었기 때문
에러 메시지 : CodeDeploy agent was not able to receive the lifecycle event.
해결 : build-deploy.yml에 zip 파일 만들기 전에 uses:actions/checkout@v2 를 추가했습니다.
(build job에서 해당 코드가 있더라도, 반드시 deploy job에서도 있어야 합니다.)

에러 2 : Install 단계

원인 : destination에 이미 배포하려는 파일과 동일한 이름의 파일이 존재
에러 메시지 : The Deployment failed because a specified file already exists at a location.
해결 : appspec.yml 에 file_exists_behavior : OVERWRITE 추가

에러 3 : AllowTraffic 단계

원인 : 로드밸런서를 활용하는데 타겟 그룹의 인스턴스에서 health status가 unhealthy인 경우
에러 메시지 : 무한으로 진행중이 되기 때문에 별도의 에러 메시지는 없었습니다.
해결 : 인스턴스의 상태 검사 경로를 유효한 (White label error가 나지 않는!) 경로로 변경하면 됩니다.

이렇게 3개의 에러를 해결하고 자동 배포를 할 수 있었습니다.

profile
컴퓨터공학을 전공하였고, 현재는 금융업에 종사하며 투자에 관심이 많습니다.

1개의 댓글

comment-user-thumbnail
2023년 8월 1일

유익한 글이었습니다.

답글 달기