Github Actions 와 AWS CodeDeploy를 활용해 프로젝트의 CI/CD 환경을 구축하고자 했습니다.
계획한 CI/CD Flow는 다음과 같습니다.

EC2 인스턴스는 Amazon linux ubuntu 22.04 버전을 사용했고, Spring boot 애플리케이션을 배포하였습니다.
일반적인 포스팅들과 약간 다른 부분에 대해 먼저 설명하겠습니다.
한 단계씩 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 추천합니다.)
Github Actions 빌드 결과로 생성되는 Zip 을 저장하기 위해 필요합니다.
모든 퍼블릭 액세스 차단을 체크하고 나머지는 모두 기본값으로 버킷을 생성합시다.
앞서 살펴본 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 인스턴스는 이미 실행 중이신 분들로 가정합니다.
만약 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
CodeDeploy 애플리케이션 생성 전 필요한 사용자를 만들어봅시다.
이번에는 다음과 같이 설정하면 됩니다.
애플리케이션 생성
배포 그룹 생성
이제 AWS 서비스에 대한 작업은 모두 끝났습니다. CI/CD에 필요한 스크립트들을 작성해봅시다.
이름은 자유롭게 지으면 됩니다. .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 은 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개의 에러를 해결하고 자동 배포를 할 수 있었습니다.
유익한 글이었습니다.