AWS 기반 CI/CD 배포 과정 분석: Backend

JaeKyung Hwang·2024년 12월 20일
0

TECH

목록 보기
14/16
post-thumbnail

댕댕워크 프로젝트의 CI/CD 배포 방법을 분석해보자. GitHub Actions를 활용하여 AWS를 기반으로 구성된 CI/CD 배포 과정을 코드 단위로 분석하고 각 단계에서 어떤 작업이 이루어지는지 살펴보자. 먼저 Backend부터~

🚀 backend_deploy.yaml

1. Trigger 조건

on:
  push:
    branches: [main]
    paths:
      - "backend/server/**"
  • main 브랜치의 backend/server 디렉토리 내 파일 변경 사항이 있을 때 이 workflow가 실행된다.

2. 환경 변수 설정

env:
  IMAGE: dangdangwalk
  REPO: 533267282498.dkr.ecr.ap-northeast-2.amazonaws.com
  TAG: ${GITHUB_SHA::7}
  • IMAGE: Docker 이미지 이름 (dangdangwalk)
  • REPO: ECR(Elastic Container Registry) 저장소 주소

    AWS ECR: AWS 관리형 컨테이너 이미지 레지스트리 서비스 (AWS에서 관리하는 docker hub와 같은 서비스)

  • TAG: 현재 GitHub SHA 해시의 첫 7자리를 태그로 사용

3. ECR Build Job

a. 작업 정의

jobs:
  build:
    name: ECR Build
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend/
  • name: 작업 이름을 "ECR Build"로 설정.
  • runs-on: GitHub-hosted runner의 ubuntu-latest 이미지를 사용.
  • defaults.run.working-directory: 기본 작업 디렉토리를 ./backend/로 설정하여 모든 run 명령이 이 경로에서 실행됨.

b. 코드 체크아웃

      - name: Check out code
        uses: actions/checkout@v4
        with:
          fetch-depth: 2
  • actions/checkout@v4: GitHub Actions 체크아웃 액션을 사용해서 전체 리포지토리 코드를 앞서 생성된 Ubuntu 가상 머신으로 가져오기.
  • fetch-depth: 2: 최근 2개의 커밋 내역만 가져옴으로써 속도와 효율성 향상.

c. Node.js 설정

      - name: Setup Node.js
        uses: actions/setup-node@v3
  • actions/setup-node@v3: Node.js 환경 설정을 위해 사용. 기본적으로 최신 LTS 버전을 자동 설치.

d. 의존성 캐싱

      - name: Cache dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: backend/server/node_modules
          key: |
            ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('backend/server/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('backend/server/package-lock.json') }}
  • actions/cache@v3: node_modules 캐싱을 통해 반복적인 의존성 설치 시간을 절약.
  • path: 캐싱할 파일 경로를 backend/server/node_modules로 지정.
  • key: 현재 환경(OS)와 package-lock.json 해시를 기준으로 고유한 캐시 키 생성.
  • restore-keys: 캐시가 존재하지 않을 경우 복구를 위해 기본 키를 재사용.

e. 의존성 설치 및 빌드

      - name: Install Dependencies
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        working-directory: ./backend/server
        run: npm ci
      - name: Run build
        working-directory: ./backend/server
        run: npm run build
  • Install Dependencies: 캐시가 없을 경우 npm ci 명령으로 의존성 설치.
  • Run build: npm run build 명령으로 서버를 빌드.

f. AWS 설정 및 Docker 작업

      - 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: ECR Login
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
  • aws-actions/configure-aws-credentials@v1: AWS 인증 정보를 사용해 ECR 액세스 설정.
  • aws-actions/amazon-ecr-login@v1: ECR 로그인.

g. Docker 작업

      - name: ECR Delete
        run: |
          IMAGE_TAG=$(aws ecr describe-images --repository-name ${{ env.IMAGE }} | jq '.imageDetails[].imageTags[0]')
          if [[ -z "$IMAGE_TAG" ]]; then
            echo "No images found in the repository."
          else
            aws ecr batch-delete-image --repository-name ${{ env.IMAGE }} --image-ids imageTag=$IMAGE_TAG
          fi
      - name: Docker build
        run: |
          docker build --platform linux/amd64 -t ${{ env.IMAGE }} --target prod -f server/Dockerfile .
      - name: ECR push
        run: |
          aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ env.REPO }}
          docker tag ${{ env.IMAGE }} ${{ env.REPO }}/${{ env.IMAGE }}:${{ env.TAG }}
          docker push ${{ env.REPO }}/${{ env.IMAGE}}:${{ env.TAG }}
ECR Delete: 이전 Docker 이미지를 삭제
IMAGE_TAG=$(aws ecr describe-images --repository-name ${{ env.IMAGE }} | jq '.imageDetails[].imageTags[0]')
if [[ -z "$IMAGE_TAG" ]]; then
  echo "No images found in the repository."
else
  aws ecr batch-delete-image --repository-name ${{ env.IMAGE }} --image-ids imageTag=$IMAGE_TAG
fi
  • 목적: 이전 Docker 이미지를 삭제하여 저장소 공간을 확보.
  • 세부 설명:
    1. IMAGE_TAG=$(aws ecr describe-images ...):
      • AWS CLI를 사용하여 ECR에 저장된 이미지 태그를 가져옵니다.
      • jq를 사용해 JSON 데이터에서 첫 번째 태그를 추출.
    2. if [[ -z "$IMAGE_TAG" ]]; then:
      • 이미지 태그가 없으면 "No images found" 메시지 출력.
    3. aws ecr batch-delete-image ...:
      • 이미지 태그가 있으면 해당 태그의 이미지를 삭제.
Docker build: Docker 이미지를 빌드
docker build --platform linux/amd64 -t ${{ env.IMAGE }} --target prod -f server/Dockerfile .
  • 목적: Dockerfile을 기반으로 Docker 이미지를 생성.
  • 세부 설명:
    1. docker build: Docker 이미지를 빌드.
    2. --platform linux/amd64:
      • x86-64 아키텍처용 이미지 생성.
    3. -t ${{ env.IMAGE }}:
      • 생성된 이미지에 태그를 지정 (dangdangwalk).
    4. --target prod:
      • Dockerfile의 특정 단계(target)만 빌드.
    5. -f server/Dockerfile:
    6. .:
      • 현재 디렉토리를 빌드 컨텍스트로 사용.
ECR push: 이미지를 ECR에 푸시
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ env.REPO }}
docker tag ${{ env.IMAGE }} ${{ env.REPO }}/${{ env.IMAGE }}:${{ env.TAG }}
docker push ${{ env.REPO }}/${{ env.IMAGE }}:${{ env.TAG }}
  • 목적: 빌드한 이미지를 ECR에 업로드.
  • 세부 설명:
    1. aws ecr get-login-password | docker login:
      • AWS CLI로 ECR 비밀번호를 받아 Docker CLI로 로그인.
    2. docker tag:
      • 생성된 이미지를 ECR 저장소 형식으로 태그 지정 (REPO/IMAGE:TAG).
    3. docker push:
      • 태그가 지정된 이미지를 ECR 저장소에 업로드.

4. ECR Deploy Job

a. 작업 정의

  deploy:
    name: ECR Deploy
    needs: build
    runs-on: [self-hosted, label-api]
  • name: 작업 이름을 "ECR Deploy"로 설정.
  • needs: build: GitHub Actions에서 작업 간 의존성(Dependency)을 정의하는 키워드로 deploy 작업이 실행되기 전에 build 작업이 완료되어야 함을 나타냄.
  • runs-on: [self-hosted, label-api]: deploy 작업이 Self-hosted Runner에서 실행되며, 태그가 label-api로 설정된 Runner에서 실행되도록 지정.
    • label-api 태그가 있는 Self-hosted Runner, 즉 dangdang-walk runner에서 실행된다.
    • 참고로 GitHub Actions Runner는 GitHub Actions의 workflow 작업을 실행하는 애플리케이션으로 자신이 구성한 환경에 설치하면 된다.

b. .env 파일 생성

      - name: Create .env
        env:
          ENV_FILE: ${{ secrets.ENV_FILE_BACKEND }}
        run: |
          if [ ! -d "backend/server" ]; then 
            mkdir backend/server;  
          fi
          echo "$ENV_FILE" > ./backend/server/.env.prod
  • .env.prod 환경 파일을 생성하고 GitHub Secrets에 추가된 환경 변수 값을 가져온다.

c. EC2 배포

      - name: Deploy to Amazon EC2
        run: |
          chmod 777 /home/ubuntu/run-backend.sh
          /home/ubuntu/run-backend.sh ${{ env.TAG }} .env.prod ${{ env.REPO }} ${{ env.IMAGE }}
  • EC2에서 배포 스크립트(/home/ubuntu/run-backend.sh) 실행.
  • Docker 이미지를 가져와 EC2 인스턴스에 배포.

🔄 backend.sh

(/home/ubuntu/run-backend.sh와 비슷한 코드)

1. 스크립트 시작

#!/bin/bash
  • Bash 스크립트의 시작을 알림. 이 스크립트는 Bash 쉘에서 실행됨.

2. 환경 변수 설정

TAG=$1
ENV_FILE=$2
REPO=$3
IMAGE=$4
ENV_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend"
FILE_PATH="$ENV_PATH/$ENV_FILE"

echo $FILE_PATH
  • TAG: 첫 번째 인자로 전달받은 이미지 태그.
  • ENV_FILE: 두 번째 인자로 전달받은 환경 변수 파일 이름.
  • REPO: 세 번째 인자로 전달받은 Docker 이미지 레포지토리 주소.
  • IMAGE: 네 번째 인자로 전달받은 Docker 이미지 이름.
  • ENV_PATHFILE_PATH: 환경 변수 파일의 절대 경로를 설정.
  • 설정된 환경 변수 파일 경로를 출력하여 확인.

3. AWS ECR 로그인

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO"
  • AWS Elastic Container Registry(ECR)에 로그인.
  • get-login-password: 인증 토큰을 생성.
  • docker login: Docker CLI를 사용해 ECR에 인증.

4. Blue 컨테이너 상태 확인

IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue | grep running)
echo "Blue : "
echo "$IS_BLUE_RUNNING"
  • 현재 실행 중인 컨테이너 dangdang-api-blue의 상태를 확인.
  • docker inspect: 컨테이너 정보를 조회.
  • grep running: 실행 중인 상태인지 필터링.

5. 이미지 태그 확인

if [ -z "$TAG" ]; then
  echo "ERROR: Image tag argument is missing."
  exit 1
fi
  • TAG 값이 비어 있는지 확인.
  • 비어 있다면 오류 메시지를 출력하고 스크립트 종료.

6. Blue-Green 배포 로직

a. Green 컨테이너 배포 시작

if [ -n "$IS_BLUE_RUNNING" ]; then
  echo "Green 배포를 시작합니다."
  • IS_BLUE_RUNNING 값이 비어 있지 않다면 dangdang-api-blue 컨테이너가 실행 중임을 의미.
  • 새로운 Green 컨테이너를 배포.

b. Green 컨테이너 배포

docker stop dangdang-api-green && docker rm dangdang-api-green
  • 기존 Green 컨테이너를 중지하고 삭제.
docker pull "$REPO"/"$IMAGE":"$TAG"
  • 새로운 이미지를 ECR에서 Pull.
docker run -d --log-driver=fluentd --name dangdang-api-green --restart always -p 3031:3031 --env-file "$FILE_PATH" -v logs:/app/log "$REPO"/"$IMAGE":"$TAG"
  • 새로운 Green 컨테이너를 실행:
    • -d: 백그라운드 실행.
    • --log-driver=fluentd: Fluentd를 사용해 로그 중앙화.
    • --restart always: 컨테이너를 항상 재시작.
    • -p 3031:3031: 포트 매핑.
    • --env-file "$FILE_PATH": 환경 변수 파일을 컨테이너에 전달.
    • -v logs:/app/log: 로그를 호스트 디렉토리와 연결.

c. Health Check

while [ 1 = 1 ]; do
  echo "Green Health check를 시작합니다."
  sleep 3

  REQUEST=$(curl http://localhost:3031)
  if [ -n "$REQUEST" ]; then
    echo "Green Health check 성공했습니다."
    break;
  fi
done;
  • Green 컨테이너의 상태를 주기적으로 확인:
    • curl로 Health Check 엔드포인트 요청.
    • 성공 시 루프 종료.

d. Nginx 재시작 및 Blue 컨테이너 종료

echo "Nginx를 재시작합니다."
sudo nginx -s reload
  • Nginx를 재시작(설정 반영)하여 트래픽을 Green 컨테이너로 전환.
    • 무중단 업데이트: reload 명령은 Nginx를 완전히 재시작하지 않고, 활성화된 프로세스를 교체하여 무중단으로 동작을 유지
  • ✅ EC2 Instance에 접속해 /etc/nginx/sites-enabled/default를 확인해보면 다음과 같이 upstream 블록이 설정되어 있다.
    upstream api {
        server localhost:3031;    # Green
        server localhost:3032;    # Blue
    }
    • localhost:3031은 Green 컨테이너, localhost:3032는 Blue 컨테이너를 나타낸다.
    • 원래는 여러 백엔드 서버 간 트래픽을 로드 밸런싱하기 위한 설정(default로 round-robin 방식으로 요청을 순차적으로 서버에 전달)으로 사용되지만, 현재 설정에서는 Blue-Green 배포를 위해 단일 활성 컨테이너로 트래픽을 전환하는 방식으로 활용된다.
echo "Blue Container를 종료합니다."
docker stop dangdang-api-blue
  • 기존 Blue 컨테이너를 종료.

e. Blue 컨테이너 배포 (반대 상황)

else
  echo "Blue 배포를 시작합니다."
  ...
  • IS_BLUE_RUNNING이 비어 있는 경우 Blue 컨테이너를 배포.
  • 논리는 Green 배포와 동일하지만 포트 번호를 3032로 설정.

7. 마무리 작업

sleep 3

echo "이전 이미지를 삭제합니다."
docker image prune -af
  • 3초 대기 후 이전 이미지를 삭제해 리소스 정리.

이 글을 작성하면서 코드를 공부·분석하며 살펴보니 개선할 점이 보여서 run_backend.sh리팩토링해보았다!

profile
이것저것 관심 많은 개발자👩‍💻

0개의 댓글