GitHub Actions, AWS CodeDeploy 를 활용한 CI/CD 적용

Aiden·2023년 6월 18일
12

사내에서는 EC2 에 직접 접속하여 Git 을 통해 새로운 소스 코드를 pull 받고, Docker Image 로 컨테이너를 띄워 배포하는 방식을 사용하고 있었습니다.
하지만 매번 EC2 에 접속해 직접 Git, Docker 명령을 타이핑하여야 한다는 점은 굉장히 번거로웠고, 이는 배포에 대한 부담감으로 이어졌습니다.

따라서 몇 달 간, 번거로운 배포 과정을 간소화하기 위해 CI/CD 태스크를 담당해 진행하였고 얼마 전, 프로젝트에 성공적으로 적용하였습니다.

이번 포스팅에서는 GitHub Actions, AWS CodeDeploy 를 활용하여 사내 프로젝트에 CI/CD 를 적용했던 과정에 대해 소개하고, 적용된 CI/CD 시스템의 플로우에 대해 정리해보도록 하겠습니다.


Task 소개

먼저, 이번 CI/CD 태스크에 대해 간단히 소개하도록 하겠습니다.
기존의 배포 프로세스에 CI/CD 를 적용하고, 이를 파이프라인화하여 목표 배포 프로세스로 개선하여야 합니다.

🚀 기존 배포 프로세스

CI/CD 적용 전, 기존의 배포 프로세스는 아래와 같습니다.

  • Local 소스 코드 작업
  • GitHub PR & Merge
  • AWS EC2 접속
  • GitHub Pull
  • Build
  • Deployment

소스 코드 작업이 끝나면, GitHub Pull Request 를 통해 Merge 합니다. 이 때, 코드 병합 후 어떤 테스트도 수행되지 않습니다.
이후 EC2 에 직접 접속하여 병합된 소스 코드를 Pull 하고, Docker Image 파일을 빌드하여 컨테이너로 배포합니다.
이 모든 과정은 수동으로 이루어지며, 각각의 환경 별로 반복해서 작업해주어야 합니다.

🚀 목표 배포 프로세스

아래는 CI/CD 적용 후, 개선된 배포 프로세스입니다.

  • Local 소스 코드 작업 <- 기존과 동일
  • GitHub PR & Merge <- 기존과 동일
  • CI/CD Pipeline

GitHub Pull Request 를 통해 Merge 하는 과정까지는 동일합니다.
하지만 이후 AWS EC2 접속부터 Deployment 까지의 네 단계가 CI/CD Pipeline 이라는 하나의 단계로 대체되었습니다.
그 이유는 소스 코드 Merge 이후 수동으로 작업하던 배포 과정이 모두 자동화되었기 때문입니다.

다시 말해 CI/CD 가 적용된 이후에는 더 이상 EC2 에 접속하여 Git 과 Docker 명령을 터미널에 직접 입력할 필요가 없습니다.
Merge 이벤트 혹은 Git Push 이벤트가 발생하면, 새로운 소스 코드는 자동으로 EC2 에서 배포될 것입니다.

✨ 기대 효과

CI/CD 적용을 통한 기대 효과는 크게 아래와 같습니다.

  • 대부분의 과정을 자동화하여 번거로운 배포 과정 간소화
  • 작은 단위의 잦고 빠른 배포를 위한 인프라 구축
  • 배포 작업의 일관성 확보 및 휴먼 에러 예방

먼저 기존 배포 프로세스의 대부분의 과정을 자동화하여 번거로운 배포 과정을 간소화하는 것이 가장 큰 목적입니다.
이렇게 되면 배포에 대한 부담감이 줄어들고, 따라서 작은 단위의 소스 코드 변경사항들이 더욱 잦은 빈도로 배포될 수 있을 것입니다.

또한 기존에 수동으로 이루어지던 배포 프로세스를 자동화하여 휴먼 에러를 방지할 수 있습니다.
잘못된 브랜치의 소스 코드를 배포하거나, 다른 서버에 소스 코드가 배포되는 등 배포 과정에서 발생할 수 있는 다양한 휴먼 에러들을 예방하고, 배포 작업의 일관성을 보장할 수 있습니다.


Branch 전략

본격적인 CI/CD 구축 과정을 살펴보기 전에, 현재 채택하여 사용하고 있는 Git 브랜치 전략에 대해 간단히 정리해보겠습니다.

사내에서는 각각의 브랜치를 기준으로 환경을 구분하고 있고, 이는 아래와 같습니다.

  • main (On Service)
  • release (Staged)
  • develop

🔥 main

main 은 현재 서비스 중인 버전의 브랜치Service 환경이라고도 하며,
검수가 완료된 후 release 브랜치로부터 Merge 되어 배포됩니다.

🚧 release

release 는 현재 검수 중인 브랜치Staging 환경이라고도 하며, 바로 다음 배포에 main 브랜치로 Merge 됩니다.
Merge 후 제거되는 일회성 브랜치이며, 브랜치 이름에는 release/v* 의 형식으로 배포되는 소스코드의 버전이 항상 포함되어 있습니다.

🔨 develop

현재 개발 중인 Feature 들이 상시로 Merge 되는 브랜치입니다.
한 배포 단위의 Feature 들이 모두 Merge 되면, release 브랜치로 분기됩니다.


CI/CD 설계

위처럼, 사내에서 사용하는 환경은 Development 환경(develop), Staging 환경(release), Service 환경(main) 이렇게 세 가지이며, 이 중 배포가 필요한 환경은 Staging 환경(release)Service 환경(main) 두 가지입니다.

Staging 환경 배포는 개발된 버전의 애플리케이션이 사용자에게 전달되기 이전에 내부 테스트를 위해 필요하며,
Service 환경 배포는 내부 테스트, 즉 검수가 완료된 애플리케이션을 사용자에게 전달하기 위한 배포를 의미합니다.

따라서 CI/CD 또한 아래와 같이 두 가지 환경에 모두 적용되어야 합니다.

🔥 main

검수가 완료된 소스 코드가 사용자에게 배포되는 프로세스입니다.
release 브랜치에서 main 브랜치로 Merge 이벤트가 발생하면, 지정된 CI/CD 프로세스가 수행되어 자동으로 배포되어야 합니다.

  • release — Merge Event! 🎉 —> main
  • CI/CD Pipeline

🚧 release

내부 검수를 위해 소스 코드가 Staging 환경에 배포되는 프로세스입니다.
release 브랜치에서 Push 이벤트가 발생하면, 지정된 CI/CD 프로세스가 수행되어 자동으로 배포되어야 합니다.

  • release Push Event! 🎉
  • CI/CD Pipeline

이제 CI/CD Pipeline 에서 수행되어야 할 작업들을 정의해주어야 합니다.

기본적으로 두 환경 모두 CI/CD Pipeline 에서 수행하여야 할 작업은 동일합니다.

CI 에서는 주로 소스 코드 빌드와 같은 각종 테스트를 거치게 되며, 이 모든 과정이 통과되었을 경우, 코드를 Merge 합니다.
이후, Merge 된 소스 코드는 CD 프로세스를 통해 Repository 로 릴리즈되며, 서비스 환경에 배포됩니다.

위와 같은 CI/CD 프로세스를 사내 프로젝트 환경에 맞추어 설계해보면, 아래와 같습니다.

🟢 CI Steps

  • 빌드 테스트
  • Merge

📦 CD Steps

  • 각각의 EC2 서버로 소스 코드 전달 (Service | Staging)
  • 프로젝트 도커 이미지 배포

CI 프로세스에서는 빌드 테스트를 통해 Merge 될 소스 코드 버전이 정상적으로 빌드되는지를 확인합니다.

🚨 참고로 사내에서는 현재 테스트 코드를 작성하고 있지 않기 때문에 빌드 테스트만을 수행하지만,
단위 테스트와 통합 테스트가 포함되지 않은 CI 는 CI 로서의 의미가 떨어진다고 생각합니다. 🚨

이후, 빌드 테스트를 통과한 소스 코드는 Merge 되고, CD 프로세스가 수행됩니다.
CD 프로세스의 첫 단계에서는 Merge 된 소스 코드가 각각의 배포 환경, 즉 각각의 EC2 서버로 전달되고, 마지막으로 전달된 소스 코드의 Docker Image 가 자동으로 컨테이너화되면서 배포되어 CI/CD Pipeline 은 종료됩니다.

이 과정은 GitHub ActionsAWS CodeDeploy 로 간단하게 구현이 가능하며, 구체적인 흐름은 아래와 같습니다.

🟢 CI Steps with GitHub Actions

  • 빌드 테스트 (GitHub Actions)
  • Merge

📦 CD Steps with GitHub Actions & AWS CodeDeploy

  • AWS S3 로 Merge 된 소스 코드 업로드 (GitHub Actions)
  • AWS CodeDeploy 에 배포 요청 전달 (GitHub Actions)
  • AWS S3 에 업로드된 소스 코드를 EC2 로 가져와 배포 (AWS CodeDeploy)

먼저 GitHub Actions 를 활용해 사전에 정의된 이벤트 발생 시 빌드 테스트를 수행하고, 테스트를 통과한 경우에 코드가 Merge 될 수 있도록 구현할 수 있습니다.
또한, Merge 된 소스 코드를 AWS S3 에 업로드하고, AWS CodeDeploy 에 배포 요청을 전달하도록 구현합니다.

배포 요청을 전달받은 AWS CodeDeployAWS S3 에 업로드된 소스 코드를 사전에 정의된 EC2 로 가져오며, 작성된 스크립트에 따라 해당 파일을 배포하고 CI/CD Pipeline 은 종료됩니다.


CI/CD 구축

아래에서는 앞서 설계한 CI/CD 프로세스를 기반으로 직접 GitHub Actions 스크립트를 작성하고, AWS CodeDeploy 서비스를 구성해보도록 하겠습니다.

1. GitHub Actions 작성 (1)

먼저, 프로젝트 루트 경로에 /.github/workflows/ 디렉터리를 생성하고 두 개의 GitHub Actions 파일을 작성하여야 합니다.

  • /.github/workflows/ci.cd.prod.yml
  • /.github/workflows/ci.cd.test.yml

📌 두 개의 파일로 분리한 이유 📌

이는 각 브랜치를 기준으로 서로 다른 GitHub Actions 파일을 사용하기 위함입니다.
main 브랜치는 실서비스 배포 목적의 브랜치이고, release 브랜치는 검수 목적의 브랜치이기 때문에 추후 두 브랜치 간 Workflow 에 차이가 벌어질 가능성이 높다고 판단했기 때문입니다.


예를 들어, 검수 목적의 CD 프로세스에서는 실서비스와는 다른 환경 변수를 참조해야하거나, S3 혹은 EC2 인스턴스 및 세부적인 환경이 변경되어야 하는 경우가 있을 수 있습니다.
이 때, 실서비스 배포와 검수용 서비스의 배포 프로세스가 하나의 파일에 함께 작성되어 있다면 수정이나 확장에서 불리하고, 자칫 하나의 파일에 너무 많은 내용의 코드가 작성될 우려가 있어 이와 같이 분리하여 작성하였습니다.

1-1) Event 지정

각각의 파일 내에는 스크립트가 트리거될 이벤트를 작성합니다.

  • ci.cd.prod.yml
    • release 브랜치에서 main 브랜치로 Merge 이벤트가 발생하면 트리거됩니다.
    • GitHub Actions 에서는 아직까지 Merged 이벤트를 공식적으로 지원하고 있지 않기 때문에 아래와 같이 작성하여 Merged 를 감지할 수 있습니다.
    • main 브랜치의 PR 이 Closed 이고, merged 가 true 일 때를 트리거 조건으로 지정하였습니다.
name: 🚀 Deploy workflow on production environment

on:
  pull_request:
    branches: [main]
      types: [closed]

jobs:
  deploy:
    if: github.event.pull_request.merged == true
  • ci.cd.test.yml
    • release 브랜치는 develop 브랜치에서 분기되어 Push 되며, PR 은 사용하고 있지 않습니다.
    • 브랜치 이름에는 항상 버전명이 명시되어 있기 때문에 Glob Pattern 을 사용해야 하고, 이를 위해서는 브랜치 이름을 따옴표('')로 감싸주어야 합니다.
    • release 브랜치에 Push 이벤트 발생 시 트리거 되도록 작성하였습니다.
name: 🚀 Deploy workflow on test environment

on:
  push:
    branches: ['release/v**']

jobs:
  deploy: 

1-2) GitHub Repository Environment 생성

배포를 위해서는 새로운 버전의 소스 코드 뿐만 아니라, 애플리케이션 로드에 필요한 환경변수 파일을 생성하여 함께 전달해주어야 합니다.

이 때, 환경변수는 코드와 함께 버전 관리될 수 없기 때문에 GitHub Repository Secrets 로부터 환경변수를 읽어와 파일을 생성할 수 있습니다.

중요한 점은 각각의 파일이 배포 환경에 따라 서로 다른 환경변수를 참조하여야 한다는 것입니다.

이를 위해, GitHub Repository 에 productiontest 라는 Environment 를 각각 생성해주었습니다.

  • Repository Settings → Environments → New environment

    • production 은 ci.cd.prod.yml 에서 참조할 환경, test 는 ci.cd.test.yml 에서 참조할 환경입니다.
  • Environment 클릭 → Environment secrets → Add secret

    • Name 에는 환경변수의 키, Value 에는 값을 입력하여 추가합니다.
  • GitHub Actions environment 지정

    • 각각의 workflow 파일에 참조할 환경을 명시해줍니다.
      • ci.cd.prod.yml
        • environment 를 production 으로 지정
      • ci.cd.test.yml
        • environment 를 test 로 지정
jobs:
  deploy:
    if: github.event.pull_request.merged == true
    environment: production
jobs:
  deploy:
    environment: test

2. AWS S3 생성

GitHub Actions 로부터 소스 파일이 업로드될 AWS S3 버킷을 생성합니다.

2-1) S3 버킷 생성

  • Amazon S3 → 버킷 → 버킷 만들기
    • ACL 비활성화 / 퍼블릭 액세스 차단
      • 버킷과 GitHub Actions, 버킷과 EC2 간 연결은 Credentials 와 내부 네트워크의 사용을 위해 별도 권한을 설정해줄 것이기 때문에 문제가 되지 않습니다.

3. AWS EC2 IAM 생성

EC2 는 CodeDeploy 를 통해 S3 버킷에 업로드된 소스 파일을 가져와 배포하게 되는데, 이를 위해 CodeDeploy 와 S3 버킷에 모두 접근할 수 있는 IAM 권한을 생성하여 EC2 에 부여해주어야 합니다.

3-1) EC2 IAM 생성

  • AWS IAM → 역할 → 역할 만들기
  • 아래 두 가지 권한을 역할에 추가
    • AWSCodeDeployFullAccess
    • AmazonS3FullAccess

3-2) EC2 IAM 연결

앞서 생성한 IAM 역할을 EC2 와 연결합니다.

  • AWS EC2 → 인스턴스 → 인스턴스 선택 → 작업 → 보안 → IAM 역할 수정
    • 이제 EC2 인스턴스는 CodeDeploy 와 S3 에 대한 Full Access 권한을 가지게 되었습니다.
    • production 과 test 를 위한 인스턴스가 별도로 구성되어 있기 때문에, 동일한 IAM 권한을 두 인스턴스 모두에 할당해주었습니다.

4. AWS CodeDeploy 생성

이제 CodeDeploy 애플리케이션을 생성하고, IAM 역할을 할당해주어야 합니다.

4-1) CodeDeploy IAM 생성

  • AWS IAM → 역할 → 역할 만들기
    • 다른 AWS 서비스의 사용 사례에서 CodeDeploy 를 선택합니다.
    • 다음 화면에서는 자동으로 권한이 추가되어 있습니다.

4-2) CodeDeploy 애플리케이션 생성

  • 애플리케이션 생성
    • AWS CodeDeploy → 애플리케이션 → 애플리케이션 생성
      • 컴퓨팅 플랫폼은 EC2/온프레미스를 선택합니다.


  • 배포 그룹 생성
    • 애플리케이션 클릭 → 배포 그룹 → 배포 그룹 생성
      • production 과 test 환경을 구분하여 배포하기 위해 각각의 배포 그룹을 모두 생성해주어야 합니다.
      • 서비스 역할에는 앞서 생성한 CodeDeploy IAM 역할을 할당해줍니다.
      • 위 역할을 기반으로 CodeDeploy 애플리케이션은 EC2 내부에서 배포 작업을 수행할 수 있게 됩니다.
      • 환경 구성에서는 Amazon EC2 인스턴스를 선택합니다.
      • 태그 그룹의 키는 Name 으로 선택하고, 값은 배포 대상 EC2 인스턴스를 선택해줍니다.

5. IAM 사용자 생성

GitHub Actions 는 소스코드를 S3 버킷에 업로드하고, CodeDeploy 의 특정 배포 그룹에 대한 배포 요청을 전달하여야 합니다.

이를 위해서는 권한이 필요하고, 해당 권한을 가진 사용자를 생성하여 GitHub Actions 가 인증해 권한을 사용할 수 있도록 설정해주어야 합니다.

5-1) IAM 사용자 추가

  • AWS IAM → 사용자 → 사용자 추가
  • IAM 사용자 권한 부여
    • 권한 옵션은 직접 정책 연결을 선택합니다.
    • EC2 IAM 설정과 동일하게 아래 두 가지 권한을 할당합니다.
      • AWSCodeDeployFullAccess
      • AmazonS3FullAccess

5-2) IAM 사용자 프로그래밍 방식 액세스 키 생성

GitHub Actions 가 사용자로 인증하기 위해 사용할 액세스 키를 생성합니다.

  • 사용자 클릭 → 보안 자격 증명 → 액세스 키 → 액세스 키 만들기
  • 액세스 키 모범 사례 및 대안
    • AWS 외부에서 실행되는 애플리케이션을 선택합니다.
    • 생성된 액세스 키와 비밀 액세스 키는 아래 화면에서만 발급 가능하기 때문에 .csv 파일 다운로드를 통해 안전하게 보관하여야 합니다.
    • 이렇게 생성된 키는 GitHub Actions 의 사용자 인증 정보로 활용됩니다.

5-3) GitHub Repository Secrets 액세스 키 저장

GitHub Actions 가 CD 프로세스 내에서 앞서 만든 키를 사용해 인증하기 위해서는 GitHub Repository Secrets 에 키를 저장해야 합니다.

  • Repository → Settings → Secrets and variables → Actions → New repository secret
    • 이전의 Environment Secret 을 생성했던 것과는 다르게, production 과 test 환경 모두에서 동일한 액세스 키, 비밀 액세스 키를 사용할 것이므로 Environment Secret 이 아닌 Repository Secret 으로 한 번만 저장해주면 됩니다.

6. EC2 CodeDeploy-Agent 설치

CodeDeploy 가 S3 버킷에 업로드된 코드를 EC2 의 특정 위치에 배포하기 위해서는 배포 대상 EC2 에 codedeploy-agent 가 설치되어 있어야 합니다.

현재 production 과 test 환경은 별도의 EC2 를 사용하고 있기 때문에 두 인스턴스 모두에 codedeploy-agent 를 설치합니다.

codedeploy-agent 설치 전, EC2 에는 npm, yarn 과 같은 패키지 매니저git, nvm, node 가 필수적으로 설치되어 있어야 합니다.

6-1) awscli 설치

아래는 EC2 에 awscli 를 설치하는 과정입니다.

a) AWS EC2 접속

b) AWS CLI 설치

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install

c) AWS configure 인증 정보 입력

  • AWS IAM 사용자의 액세스 키와 비밀 액세스 키를 인증 정보에 입력하여 저장합니다.
$ sudo aws configure
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

6-2) codedeploy-agent 설치

a) Ruby 패키지 설치

codedeploy-agent 는 Ruby 로 작성되었기 때문에 이를 EC2 에서 실행하기 위해 Ruby 패키지를 설치해주어야 합니다.

$ sudo apt update
$ sudo apt install ruby-full
$ sudo apt install wget

b) CodeDeploy Agent 설치 파일 다운로드

Ubuntu 의 경우, /home/ubuntu 로 이동하여 설치 파일을 다운로드합니다.
CentOS 의 경우, /root 에서 설치하여도 문제가 발생하지 않았습니다.

codedeploy-agent 설치 파일을 다운로드하였다면, 설치 파일에 실행 권한을 부여합니다.

$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install

c) CodeDeploy Agent 설치

다운로드한 install 파일을 실행하여 codedeploy-agent 를 설치합니다.

📌 CodeDeploy Agent 설치 이슈
공식문서에 따르면, 현재 Ubuntu 20.04 이상 버전에서 codedeploy-agent 설치 시 이슈가 있어 설치 과정의 출력을 임시 로그 파일에 작성하여 해결한다고 합니다.

$ sudo ./install auto > /tmp/logfile

d) CodeDeploy 데몬 실행 확인

  • codedeploy-agent 가 정상적으로 실행되고 있는지 확인합니다.
$ sudo service codedeploy-agent status
  • 아래와 같은 화면이 출력되면 정상적으로 실행되고 있다는 의미입니다.

e) CodeDeploy 인스턴스 부팅 시 자동 실행 설정

  • 인스턴스가 부팅될 때마다 codedeploy-agent 가 자동 실행되도록 쉘스크립트 파일을 작성합니다.
$ sudo vim /etc/init.d/codedeploy-startup.sh
  • 내용은 아래와 같습니다.
#!/bin
sudo service codedeploy-agent restart
  • 해당 파일에 실행 권한을 부여하고 완료합니다.
$ sudo chmod +x /etc/init.d/codedeploy-startup.sh

7. appspec.yml 작성

이제 프로젝트 루트 경로에 appspec.yml 파일을 작성합니다.

CodeDeploy 가 EC2 에 소스 코드를 배포하는 과정을 이 파일에 정의할 수 있습니다.

중요한 점은, 프로젝트 당 appspec.yml 은 유일해야 하며, 항상 루트 경로에 위치하여야 한다는 점입니다.
다시 말해 production 과 test 에서 수행될 프로젝트 배포 과정은 모두 동일하게 작성되어야 합니다.

appspec.yml 파일이 위치한 프로젝트 루트를 기준으로, 구조에 맞게 EC2 디렉터리를 생성하고 각각의 파일을 위치시켜줍니다.

완성된 appspec.yml 파일 예시는 아래와 같습니다.

version: 0.0
os: linux
files:
    - source: /
      destination: /home/api/api_back
    - source: /nginx
      destination: /home/api/nginx
    - source: /docker-compose.yml
      destination: /home/api
    - source: /.env
      destination: /home/api
    - source: /config/
      destination: /home/api/config
file_exists_behavior: OVERWRITE

permissions:
    - object: /home/api
      pattern: '**'
      owner: root
      group: root

hooks:
    AfterInstall:
        - location: scripts/after-deploy.sh
          timeout: 2000
          runas: root
  • appspec.yml 작성 시, version 은 0.0 으로 명시합니다. (2023-03-30 기준)
  • os 는 ubuntu 를 사용하고 있으므로 linux 로 지정합니다.
  • files 섹션은 프로젝트 파일들이 배포될 위치를 지정합니다.
    • source 에는 프로젝트 내 특정 파일 혹은 디렉터리를 명시하며, appspec.yml 파일 기준으로 상대 경로를 명시합니다.
    • destination 은 source 에 명시된 파일들이 배포될 위치를 지정합니다. source 와는 달리 절대 경로를 명시하여야 합니다.
    • files_exists_behavior 를 통해 파일이 이미 존재할 경우의 액션을 지정합니다. 위 경우, OVERWRITE 로 설정하여 파일을 덮어쓰도록 하였습니다.
  • permissions 섹션은 특정 디렉터리 혹은 파일들에 대한 권한이 필요할 경우 선택적으로 지정할 수 있습니다.
    • object 에는 권한이 지정될 디렉터리를 명시합니다.
    • glob pattern 을 활용하여 특정 파일 혹은 디렉터리를 명시할 수도 있습니다.
    • 기본 사용자는 /home/ubuntu 이므로, 프로젝트 상황에 맞게 권한을 가진 사용자를 명시해줍니다.
  • hooks 섹션은 CodeDeploy 의 프로세스 진행 도중, hook 을 사용하여 특정 이벤트에 수행될 작업을 지정합니다.
    • AfterInstall 은 CodeDeploy 의 프로젝트 배포가 완료된 시점의 이벤트입니다.
    • 프로젝트 배포가 완료되면, location 으로 지정된 파일을 실행하게 됩니다.
    • timeout 은 2초로 지정해두었으며, runas 를 통해 명령을 실행할 사용자를 지정할 수 있습니다.

  • 환경 변수의 경우 프로젝트와 함께 버전 관리되지는 않지만, GitHub Actions 스크립트에서 파일 생성 후 프로젝트 파일들과 함께 S3 버킷에 전달해줄 것이기 때문에 appspec.yml 에서 환경 변수 파일의 저장 위치를 지정해주어야 합니다.

8. GitHub Actions 작성 (2)

1번에서 작성했던 GitHub Actions 스크립트 파일을 마저 작성하도록 하겠습니다.

8-1) 환경 변수 파일 생성

GitHub Actions 에서 환경 변수 파일을 생성하기 위해서는 GitHub Repository Secret 에서 사전에 추가했던 환경 변수들을 불러와주어야 합니다.

GitHub Actions 에서는 이를 위해 secrets 객체를 제공하고 있으며, 이를 통해 환경 변수들을 불러올 수 있습니다.

  • ci.cd.prod.yml
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@v3

      - name: 🗂️ Make config folder
        run: mkdir -p config

      - name: ⚙️ Create .env file
        run: |
            touch ${{ env.ENV_PATH }}
            echo DOMAIN_FIR=${{ secrets.DOMAIN_FIR }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_PROD=${{ secrets.SOCKET_PORT_PROD }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_TEST=${{ secrets.SOCKET_PORT_TEST }} >> ${{ env.ENV_PATH }}
  • environment 속성을 통해 원하는 환경의 환경 변수를 불러올 수 있습니다.
  • actions/checkout@v3 를 활용해 배포할 프로젝트의 브랜치로 이동해줍니다.
  • env 속성을 통해 job 내에서 쓰일 변수를 생성해 사용할 수 있습니다.
    • ${{env.변수명}} 과 같은 형식으로 사용합니다.
  • secrets 객체에서 각각의 환경 변수 키를 통해 값을 불러오고, 환경 변수 파일을 작성합니다.
    • ${{secrets.환경 변수 키}} 와 같은 형식으로 불러올 수 있습니다.

  • ci.cd.test.yml
jobs:
  deploy:
    env:
      ENV_PATH: .env
    environment: test
    runs-on: ubuntu-latest
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 🗂️ Make config folder
        run: mkdir -p config

      - name: ⚙️ Create .env file
        run: |
            touch ${{ env.ENV_PATH }}
            echo DOMAIN_FIR=${{ secrets.DOMAIN_FIR }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_PROD=${{ secrets.SOCKET_PORT_PROD }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_TEST=${{ secrets.SOCKET_PORT_TEST }} >> ${{ env.ENV_PATH }}
  • test 환경의 스크립트 파일도 동일하게 작성해줍니다.
  • 단, test 환경의 Secret 을 불러와야 하기 때문에 environment 속성은 test 로 지정해줍니다.

8-2) 프로젝트 파일 압축

배포 대상 프로젝트 소스와 앞서 생성한 환경 변수 파일을 한 번에 묶어 압축하는 과정입니다.
S3 버킷으로 업로드할 파일의 크기를 최대한 줄이기 위해 압축하게 됩니다.

- name: 📦 Zip project files
  run: zip -r ./$GITHUB_SHA.zip .
  • $GITHUB_SHA 는 GitHub Actions 에서 기본적으로 제공하는 환경변수로, 해당 workflow 를 트리거한 Commit 의 고유 해시값입니다.
  • S3 에 저장되는 압축 파일들의 이름이 중복되지 않도록 하기 위해 활용하였습니다.
  • production 과 test 모두 동일하게 작성해줍니다.

8-3) AWS 인증

  • GitHub Actions 에서 IAM 사용자 인증을 위해 액세스키와 비밀 액세스키를 사용합니다.
  • 사전에 Repository Secrets 에 추가해주었기 때문에 이를 불러와 사용합니다.
  • production 과 test 모두 동일하게 작성해줍니다.
- name: 🌎 Access to AWS
  uses: aws-actions/configure-aws-credentials@v2
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ap-northeast-2

8-4) S3 에 압축된 파일 업로드

  • 이제 IAM 사용자 인증이 완료되었으므로 S3 버킷의 지정한 디렉터리에 소스코드 압축 파일을 업로드합니다.
  • production 과 test 모두 동일하게 작성해줍니다.
- name: 🚛 Upload to S3
  run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ secrets.S3_BUCKET_NAME }}/${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip

8-5) CodeDeploy 배포

  • 소스코드 압축 파일이 정상적으로 S3 에 업로드되면, CodeDeploy 애플리케이션에 배포를 요청합니다.
  • --deployment-group-name 에는 AWS 콘솔에서 지정한 배포 그룹 이름을 정확하게 기입하여야 합니다.
  • env 에 배포 그룹 이름을 변수로 지정하고 이를 활용하도록 하겠습니다.
  • production 과 test 에도 동일하게 작성하며, 배포 그룹 이름의 경우에는 각각 적절하게 지정해줍니다.
- name: 🚀 Deploy to EC2 with CodeDeploy
  run: aws deploy create-deployment
      --application-name codedeploy-app
      --deployment-config-name CodeDeployDefault.AllAtOnce
      --deployment-group-name ${{ env.DEPLOYMENT_GROUP_NAME }}
      --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},bundleType=zip,key=${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip

9. after-deploy.sh 작성

after-deploy.sh 는 앞서 appspec.yml 에서 걸어놓은 이벤트 Hook 으로, CodeDeploy 가 모든 프로젝트의 배포를 완료한 뒤에 실행되는 파일입니다.

  • appspec.yml 실행 과정에서, 모든 프로젝트 내의 파일들과 GitHub Actions 에서 생성해 함께 전달한 환경 변수 파일들이 알맞은 위치에 저장되었을 것입니다.
  • 따라서 모든 파일들의 저장이 완료되면, 프로젝트 Image 파일을 Docker 로 띄워 배포해주어야 합니다.
#!/bin/bash
REPOSITORY=/home/api/

cd $REPOSITORY/api_back

echo "> 🔵 Stop & Remove docker services."
cd ..
docker compose down

echo "> 🟢 Run new docker services."
docker compose up --build -d

10. GitHub Actions 작성 (3)

지금까지 작성된 GitHub Actions 파일은 아래와 같습니다.

두 스크립트 파일 모두 각각의 브랜치에 지정한 이벤트가 발생할 경우 실행되며, 환경 변수 파일을 생성하고 프로젝트 파일과 함께 압축하여 S3 버킷으로 업로드합니다.
이후, CodeDeploy 에 업로드한 파일과 함께 배포 요청을 전달하게 됩니다.

  • ci.cd.prod.yml
name: 🚀 Deploy workflow on production environment

on:
  pull_request:
    branches: [main]
      types: [closed]

jobs:
  deploy:
    if: github.event.pull_request.merged == true
    env:
      ENV_PATH: .env
      S3_BUCKET_DIR_NAME: production
      DEPLOYMENT_GROUP_NAME: production
    environment: production
    runs-on: ubuntu-latest
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 🗂️ Make config folder
        run: mkdir -p config

      - name: ⚙️ Create .env file
        run: |
            touch ${{ env.ENV_PATH }}
            echo DOMAIN_FIR=${{ secrets.DOMAIN_FIR }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_PROD=${{ secrets.SOCKET_PORT_PROD }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_TEST=${{ secrets.SOCKET_PORT_TEST }} >> ${{ env.ENV_PATH }}
      
      - name: 📦 Zip project files
         run: zip -r ./$GITHUB_SHA.zip .
      
      - name: 🌎 Access to AWS
        uses: aws-actions/configure-aws-credentials@v2
        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://${{ secrets.S3_BUCKET_NAME }}/${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip
         
      - name: 🚀 Deploy to EC2 with CodeDeploy
         run: aws deploy create-deployment
             --application-name codedeploy-app
             --deployment-config-name CodeDeployDefault.AllAtOnce
             --deployment-group-name ${{ env.DEPLOYMENT_GROUP_NAME }}
             --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},bundleType=zip,key=${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip
  • ci.cd.test.yml
name: 🚀 Deploy workflow on test environment

on:
  push:
    branches: ['release/v**']

jobs:
  deploy:
    env:
      ENV_PATH: .env
      S3_BUCKET_DIR_NAME: test
      DEPLOYMENT_GROUP_NAME: test
    environment: test
    runs-on: ubuntu-latest
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 🗂️ Make config folder
        run: mkdir -p config

      - name: ⚙️ Create .env file
        run: |
            touch ${{ env.ENV_PATH }}
            echo DOMAIN_FIR=${{ secrets.DOMAIN_FIR }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_PROD=${{ secrets.SOCKET_PORT_PROD }} >> ${{ env.ENV_PATH }}
            echo SOCKET_PORT_TEST=${{ secrets.SOCKET_PORT_TEST }} >> ${{ env.ENV_PATH }}
      
      - name: 📦 Zip project files
         run: zip -r ./$GITHUB_SHA.zip .

      - name: 🌎 Access to AWS
        uses: aws-actions/configure-aws-credentials@v2
        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://${{ secrets.S3_BUCKET_NAME }}/${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip
         
      - name: 🚀 Deploy to EC2 with CodeDeploy
         run: aws deploy create-deployment
             --application-name codedeploy-app
             --deployment-config-name CodeDeployDefault.AllAtOnce
             --deployment-group-name ${{ env.DEPLOYMENT_GROUP_NAME }}
             --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},bundleType=zip,key=${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip

마지막으로 위 스크립트 파일의 jobs 에 빌드 테스트를 추가해주어야 합니다.

빌드 테스트는 deploy job 보다 먼저 수행되어야 하며, develop 브랜치 또한 빌드 테스트를 수행하여야 합니다.

따라서 각각의 파일을 아래와 같이 수정해주었습니다.

  • 빌드 테스트가 추가된 ci.cd.prod.yml
name: 🚀 Build & Deploy workflow on production environment

on:
  pull_request:
    branches: [main]
    types: [closed]

jobs:
  build:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.15.0]
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 📀 Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: ✨ Install pnpm
        uses: pnpm/action-setup@v2
        id: pnpm-install
        with:
          version: 7.29.3
          run_install: false

      - name: 🚛 Get pnpm cache store directory
        id: pnpm-cache
        shell: bash
        run: |
            echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

      - name: ⚡️ Setup pnpm cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
               ${{ runner.os }}-pnpm-store-

      - name: 📦 Install dependencies
        run: pnpm install

      - name: 🔨 Build Project
        run: pnpm build
  deploy:
     needs: build

     ...생략...

deploy job 이전에 build job 이 추가되어 트리거 이벤트 조건이 build job 의 if 문으로 수정되었습니다.

또한 deploy job 에는 needs 옵션을 주어 build job 이 종료된 이후에 수행될 수 있도록 의존성을 추가해주었습니다.
사실 GitHub Actions 의 모든 job 은 별도의 환경에서 수행됩니다.
다시 말해, bulid job 과 deploy job 은 각각 격리된 환경에서 개별적으로 수행되며 needs 옵션을 주지 않으면 병렬적으로 수행되기 때문에 순서를 보장할 수 없습니다.

이외에 빌드 테스트를 수행할 노드 버전을 matrix 옵션에 추가해주었고, 패키지 매니저로는 pnpm 을 사용하였습니다.

GitHub Actions 작성 방법에 대해 숙지하고 있다면 이해하기 어려운 내용은 아니라고 생각해 test 환경의 스크립트 파일로 넘어가보겠습니다.


  • 빌드 테스트가 추가된 ci.cd.test.yml
name: 🚀 Build & Deploy workflow on test environment

on:
    push:
        branches: [develop, 'release/v**']
    pull_request:
        branches: [develop]

jobs:
    build:
        runs-on: ubuntu-latest
        strategy:
            matrix:
                node-version: [18.15.0]
        steps:
            ...생략...

    deploy:
        if: contains(github.ref_name, 'release/v')
        needs: build
        
        ...생략...

build job 의 steps 는 ci.cd.prod.yml 의 예시와 동일하기 때문에 생략하였습니다.

test 환경의 스크립트 파일에서 눈의 띄는 변경 사항은 develop 브랜치가 추가되었다는 점입니다.
이전의 CI/CD 설계에서는 release 와 main 브랜치에 대해서만 CI/CD 프로세스를 적용하기로 했었지만, 사실 develop 브랜치에 feature 들이 추가될 때마다 빌드 테스트가 자동으로 수행되는 것이 좋습니다.
따라서 스크립트 파일의 on 옵션에는 release 브랜치 뿐 아니라, develop 브랜치 관련 이벤트도 추가해주었습니다.

다만 develop 브랜치는 빌드 테스트만 수행하고 종료되며, 배포 작업을 수행하지는 않습니다.
즉, 새롭게 추가된 feature 들이 애플리케이션 로드와 빌드에 문제를 일으키지 않는지 확인하는 과정이기 때문에 배포 작업에서는 제외해주어야 합니다.

이를 위해, deploy job 의 if 조건에 contains 메서드를 활용해주었습니다.
github.ref_name 은 github 에서 제공하는 컨텍스트로, workflow 를 트리거한 브랜치 혹은 태그의 이름입니다.
다시 말해 workflow 를 트리거한 브랜치 이름에 release/v 가 포함되어 있는 경우에만 deploy job 을 수행하게 됩니다.

이렇게 설정하면, develop 브랜치에서 Push 혹은 PR 이벤트가 발생한 경우 build job 만을 수행하게 되고,
release 브랜치의 Push 이벤트가 발생한 경우 build job 과 deploy job 을 모두 수행할 수 있습니다.


11. AWS Slack 연동

이제 CI/CD 프로세스 적용을 위한 모든 파일이 작성되었습니다.

하지만 CodeDeploy 의 배포가 성공했는지 여부를 확인하기 위해서는 AWS Console 에 접속하여야 한다는 불편함이 남아있습니다.

따라서 CodeDeploy 의 배포 결과를 Slack 알림으로 받아볼 수 있도록 AWS Chatbot 을 연동해주는 것이 좋습니다.

11-1) AWS WorkSpace 생성

  • AWS Chatbot → 구성된 클라이언트 → 새 클라이언트 구성 → Slack → 워크스페이스 액세스 권한 허용

11-2) AWS Chatbot 생성

  • Slack WorkSpace → 새 채널 구성
  • 구성 이름 → 채널 유형 → 채널 ID 지정
  • 권한 설정
    • 알림 권한과 읽기 전용 명령 권한을 선택하여 할당합니다.
    • 채널 가드 레일 정책에서 채널 멤버의 권한을 개별적으로 설정할 수 있습니다.

11-3) AWS Chatbot 인라인 정책 추가

이제 새로운 Chatbot 관련 정책을 생성하여 앞서 만든 Chatbot IAM 에 추가해주어야 합니다.

AWS IAM 에서는 Chatbot 역할에 대한 템플릿을 별도로 제공하고 있지 않기 때문에 인라인으로 직접 추가해주어야 합니다.

  • AWS IAM → 역할 → 방금 생성한 IAM 클릭 → 권한 추가 → 인라인 정책 생성
  • 서비스 선택 → Chatbot 검색 & 선택 → 모든 Chatbot 작업 선택 → 모든 리소스 선택 → 정책 생성

11-4) AWS CodeDeploy 알림 생성

Chatbot 과 Slack 연결, Chatbot 의 IAM 역할 설정까지 완료되었습니다.

이제 CodeDeploy 의 배포 결과가 알림을 생성하도록 설정합니다.

  • AWS CodeDeploy → 애플리케이션 → 애플리케이션 선택 → 알림 → 알림 규칙 생성
    • 배포가 성공 혹은 실패했을 때 알림을 트리거하도록 설정해주었습니다.
    • 알림 규칙에 사용할 대상으로, 앞서 생성한 Chatbot 을 선택합니다.

11-5) Slack 채널에 aws bot 초대

등록한 Slack 채널의 채팅방에 아래와 같이 커맨드를 입력하여 Chatbot 을 초대합니다.

이제 아래와 같이 Slack 알림 메시지가 전송됩니다.


CI/CD 플로우

이제 프로젝트에 CI/CD 프로세스가 성공적으로 적용되었습니다.

각 환경 별로 적용된 CI/CD 플로우를 정리해보도록 하겠습니다.

🔨 develop

먼저 develop 브랜치의 CI/CD 플로우입니다.

개발이 완료된 feature 들은 develop 브랜치로 PR 을 통해 Merge 됩니다.
이 때 발생한 PR 이벤트에 따라 GitHub Actions 의 Build job 이 수행됩니다.

만일 Build job 이 성공하지 못했을 경우, 코드는 병합될 수 없습니다.

🚧 release

다음은 release 브랜치의 CI/CD 플로우입니다.

한 배포 단위의 feature 들이 모두 merge 되었다면, develop 브랜치에서 분기되어 Push 됩니다.
이 때 발생한 Push 이벤트에 따라 GitHub Actions 의 Build job 과 Deploy job 이 수행됩니다.

이후, GitHub Actions 로부터 배포 요청을 받은 AWS CodeDeployAWS S3 에 업로드된 소스 코드를 받아 압축을 푼 뒤, 지정된 EC2 에서 appspec.yml 과 after-deploy.sh 파일의 스크립트 명령을 모두 실행하여 배포합니다.

마지막으로 CodeDeploy 는 배포 결과를 AWS Chatbot 에게 전달하며, 최종 배포 결과는 Slack 알림으로 전송됩니다.

🔥 main

마지막으로 main 브랜치의 CI/CD 플로우입니다.

기본적으로 release 브랜치의 플로우와 동일하며, release 브랜치로부터 Pull Request 이벤트에 의해 트리거된다는 점만이 차이점입니다.


이렇게 프로젝트 CI/CD 적용 및 플로우에 대한 정리를 모두 마쳤습니다.

기존의 번거롭던 배포 프로세스는 한 줄의 Git 커맨드만으로 간소화되었고, 배포 성공 여부 또한 실시간으로 Slack 에서 확인할 수 있게 되었습니다.

덕분에 배포에 대한 부담감이 줄어들고, 소요되던 시간도 훨씬 단축되어 이전보다 더욱 잦은 주기로 배포를 진행하고 있습니다.

물론 아쉬운 점도 있습니다.
앞서 짧게 언급했었지만, 이번 CI/CD 적용 과정에서 Unit Test 와 Integration Test 단계가 존재하지 않는 CI 는 CI 라고 볼 수 없다는 생각이 들었습니다.

Build Test 만으로는 새롭게 추가된 feature 들이 애플리케이션 내부에 어떤 영향을 미치는지 전혀 파악할 수 없고, 애플리케이션 로드 혹은 빌드가 문제없이 수행되었다고 해서 모든 로직이 정상 동작하는 것이 아니기 때문입니다.

CI 에서 Unit Test 와 Integration Test 를 수행하고, 테스트 코드를 작성하여야 하는 이유에 대해 확실하게 깨닫게 된 좋은 경험이었습니다.

1개의 댓글

comment-user-thumbnail
2023년 10월 3일

와우! 마지막 slack 연동까지! 너무 정리가 잘되어 있어 도움을 많이 받았습니다.
감사합니다.

답글 달기