Ayden's journal

Github Action과 AWS CodeDeploy를 사용한 CI/CD 구축

workflows

  1. 프로젝트의 main 브랜치로 PR이 머지되면 github action이 동작한다
  2. github action은 github secrets로 관리되고 있는 환경 변수를 모아 .env 파일을 만든다
  3. github action은 main branch를 checkout하고, .env 파일과 함께 zip으로 압축한다
  4. github action은 압축한 파일을 S3로 업로드하고, CodeDeploy로 하여금 배포 사실을 알린다
  5. CodeDeploy는 EC2의 CodeDeploy-Agent에게 배포 사실을 알린다
  6. EC2의 CodeDeploy-Agent는 S3에 업로드된 zip 파일을 가져와 압축을 푼다
  7. EC2의 CodeDeploy-Agent가 프로젝트 내부에 있는 appspec.yml 파일을 읽고 수행한다

전체적인 동작 흐름은 위와 같다. 이를 위해서는 ─ 당연하게도 ─ 프로젝트를 github으로 관리하고 있어야 하고, 이미 동작하고 있는 EC2 인스턴스가 있어야 하며, S3 버킷이 하나 이상 있어야 한다.

 

 

AWS 세팅

CI/CD를 구축하기 위해서는 우선 S3 버킷이 하나 필요하다. 또한 S3와 데이터를 주고 받기 위해 CodeDeploy와 EC2를 위한 IAM 역할이 필요하며, 또한 github action이 AWS에 인가받기 위한 IAM 사용자가 필요하다. EC2에 생성한 역할을 연결해주고, AWS-CLI를 설치한 뒤 CodeDeploy-Agent 설정을 해주어야 한다. (나는 ELB를 통해 EC2를 관리하고 있기 때문에) CodeDeploy 어플리케이션을 생성한 뒤, 배포 그룹을 만들 때 ELB을 연결해야 한다.

정확히 이 순서대로 진행할 것이며, S3 버킷은 이미 존재한다고 가정하겠다.

 

AWS IAM

CI/CD를 위해 EC2와 CodeDeploy를 위한 역할을 각각 생성해주어야 한다. EC2 역할은 S3와 CodeDeploy에 대한 FullAccess 정책을 각각 연결해주고, CodeDeploy 역할은 생성할 때 사용 사례에서 CodeDeploy를 선택하면 자동으로 따라오는 AWSCodeDeployRole 정책만 있으면 된다.


이 모든 CI/CD 정책을 관리할 IAM 사용자의 경우 EC2와 같이 S3와 CodeDeploy에 대한 FullAccess 정책 부여해주면 된다.

 

IAM 사용자에 대한 액세스 키를 발급해주어야 한다. 이 액세스 키는 github action에서 사용할 것이기 때문에 액세스 키를 발급하는 과정에서 사용 사례 - [ AWS 외부에서 실행되는 애플리케이션 ] 을 선택해주어야 한다.

이렇게 발급한 키는 csv 파일로 고이 간직하고 있자.

 

AWS EC2

특정 EC2 인스턴스에 IAM 역할을 부여하기 위해서는 역할을 부여하고자 하는 인스턴스를 체크한 후 작업 > 보안 > IAM 역할 수정에 들어가서 역할을 업데이트해주면 된다.

이후 터미널에서 SSH 포트로 EC2에 접속해 AWS-CLI와 CodeDeploy-Agent를 설치해주어야 한다. 

// AWS CLI 설치
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
// AWS configure 인증 정보 입력
sudo aws configure
// configure를 실행하면 아래의 내용을 하나씩 입력하게 된다
// 이미 존재하는 Admin IAM User의 access key를 입력했다
AWS Access Key ID [None]: AWS_ACCESS_KEY_ID 입력
AWS Secret Access Key [None]: AWS_SECRET_ACCESS_KEY 입력
Default region name [None]: ap-northeast-2
Default output format [None]: json
// codedeploy-agent는 Ruby로 작성되었기 때문에
// 이를 실행하기 위해 Ruby 패키지를 설치해준다
sudo apt update
sudo apt install ruby-full
sudo apt install wget
// CodeDeploy Agent 설치 파일 다운로드
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
// CodeDeploy Agent 설치
sudo ./install auto > /tmp/logfile
// CodeDeploy 데몬 실행 확인
sudo service codedeploy-agent status
// 인스턴스 부팅 시 CodeDeploy 자동 실행 쉘스크립트 파일 작성
sudo vim /etc/init.d/codedeploy-startup.sh
// 쉘스크립트 파일 내용
#!/bin
sudo service codedeploy-agent restart
// 쉘스크립트 파일에 실행 권한 부여
sudo chmod +x /etc/init.d/codedeploy-startup.sh

 

AWS CodeDeploy

앞서 언급한 것처럼 나는 수동으로 EC2 인스턴스에 Express 어플리케이션을 돌리고 있었고, ELB를 통해 EC2로 가는 트래픽을 관리하고 있었다. 따라서 CodeDeploy의 배포 그룹을 생성할 때, 로드 밸런서 항목에서 로드 밸런싱 활성화를 허용해두었다. 서비스 역할은 앞서 생성한 IAM-Deploy-CodeDeploy를 연결해주고, 환경 구성은 아래와 같이 설정했다.

 

 

Github Action 세팅

github secrets나 github action 자체를 어떻게 사용하는지에 대해서는 여기서 다루지는 않겠다. 다만, CI/CD를 위해 만든 CICD.yml이 어떻게 구성되어있는지 정도만 남겨두겠다.

 

우선 yaml의 이름을 정하고, on을 통해 어느 시점에 이 action이 동작하게 할지 정의한다. 내 경우에는 main 브랜치로 향하는 PR이 닫혔을 때 동작하도록 했다. 다만 PR이 닫힌다는 것이 merge 될 때 뿐만 아니라 수동으로 닫힐 때도 해당되기 때문에, jobs에서 조건을 통해 merge 될 때만 동작할 수 있게 해두었다.

env를 통해 action 안에서만 한정적으로 사용할 수 있는 환경 변수를 지정해줄 수도 있다. 당연하지만 공개되어도 문제 없는 것들만 이렇게 사용해야 한다.

name: CI/CD
on:
pull_request:
types: [closed]
branches:
- main
jobs:
deploy:
if: github.event.pull_request.merged == true
env:
ENV_PATH: .env
environment: production
runs-on: ubuntu-latest

 

steps를 통해 단계별로 진행할 내용을 작성한다. 우선 checkout으로 main의 커밋을 가져오고, github secrets에 있는 내용을 가져다가 .env 파일을 생성한다(실제 secrets에는 DATABASE_URL 뿐만 아니라 다른 변수도 이것저것 있다). 그리고 코드와 .env 파일을 가져다가 zip으로 압축한다. 이렇게 압축한 파일의 이름은 커밋의 해시코드를 가져다 쓰고 있다.

steps:
- name: Checkout branch
uses: actions/checkout@v4
- name: Create .env file
run: |
touch ${{ env.ENV_PATH }}
echo DATABASE_URL=${{ secrets.DATABASE_URL }} >> ${{ env.ENV_PATH }}
- name: Zip project files
run: zip -r ./$GITHUB_SHA.zip .

 

그리고 앞서 생성한 IAM 사용자의 액세스 키와 비밀 액세스 키를 사용하여 AWS에 인가를 요청한다.

- name: Access to AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ap-northeast-2

 

인가를 받고나면 S3에 압축한 파일을 업로드하고, CodeDeploy에게 버킷 주소랑 압축 파일의 이름을 알려줌으로써 EC2의 CodeDeploy-Agent를 동작하게 하는 것이다.

- name: Upload to S3
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ env.S3_BUCKET_NAME }}/$GITHUB_SHA.zip
- name: Deploy to EC2 with CodeDeploy
run: aws deploy create-deployment
--application-name Codeit-Backend-CICD
--deployment-config-name CodeDeployDefault.AllAtOnce
--deployment-group-name ${{ env.DEPLOYMENT_GROUP_NAME }}
--s3-location bucket=${{ env.S3_BUCKET_NAME }},bundleType=zip,key=$GITHUB_SHA.zip

 

yml 파일의 전체 코드를 보면 아래와 같다. 몇 줄의 코드가 빠져있기는 하지만, 보안 상의 문제로 빼놨을 뿐 동작에는 차이가 없다. 

name: CI/CD
on:
pull_request:
types: [closed]
branches:
- main
jobs:
deploy:
if: github.event.pull_request.merged == true
env:
ENV_PATH: .env
environment: production
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v4
- name: Create .env file
run: |
touch ${{ env.ENV_PATH }}
echo DATABASE_URL=${{ secrets.DATABASE_URL }} >> ${{ env.ENV_PATH }}
- name: Zip project files
run: zip -r ./$GITHUB_SHA.zip .
- name: Access to AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ap-northeast-2
- name: Upload to S3
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ env.S3_BUCKET_NAME }}/$GITHUB_SHA.zip
- name: Deploy to EC2 with CodeDeploy
run: aws deploy create-deployment
--application-name Codeit-Backend-CICD
--deployment-config-name CodeDeployDefault.AllAtOnce
--deployment-group-name ${{ env.DEPLOYMENT_GROUP_NAME }}
--s3-location bucket=${{ env.S3_BUCKET_NAME }},bundleType=zip,key=$GITHUB_SHA.zip

 

 

appspec.yml

이렇게 github action을 통해 CodeDeploy로 배포 요청을 보내게 되면, EC2의 CodeDeploy-Agent는 프로젝트 내부에 있는 appsepc.yml 파일을 읽고 수행한다.

version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/server
file_exists_behavior: OVERWRITE
permissions:
- object: /
pattern: '**'
owner: ec2-user
group: ec2-user
hooks:
AfterInstall:
- location: after-deploy.sh
timeout: 2000
runas: root

 

 

files의 source는 CodeDeploy-Agent가 가져온 zip 파일을 기준으로 하는 상대 경로, destination은 절대 경로로 작성되어있다. 이는 압축 해제한 파일을 어디다 가져다 둘지에 대한 내용을 정의해두는 것이다. 내 경우는 압축 풀어서 나온 걸 전부 /home/ec2-user/server로 갖다놓겠다는 것이고, 혹시 같은 이름의 파일이 이미 server 폴더에 존재할 경우 덮어쓰겠다(file_exists_behavior: OVERWRITE)는 의미이다.

 

permissions의 경우 권한을 설정할 파일이나 디렉토리를 지정(object)하고, 해당 파일이나 디렉토리로부터 일치하는 파일 패턴을 지정(pattern)한다. 그리고 파일 또는 디렉토리의 소유자(ec2-user)와 그룹(ec2-user)을 지정한다. 내가 사용하는 Amazon Linux의 경우 사용자 계정이 ec2-user인 것 같다.

 

hooks는 AWS CodeDeploy에서 사용되는 라이프사이클 이벤트에 따라 서로 다른 명령을 순서대로 실행할 수 있도록 해준다. 이러한 hooks의 종류에는 AfterInstall 외에도 ApplicationStop이나 DownloadBundle 등 여러가지가 있다. 나는 인스톨이 끝난 후 after-deploy 스크립트를 관리자 권한(runas: root)으로 2000초 안에 실행되도록 했다.

 

after-deploy 스크립트의 경우 자잘한 명령어들로 구성되어있는데, 기본적으로는 npm install과 npm run build, pm2 restart my-app 명령어를 실행하도록 되어있다.

블로그의 프로필 사진

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기