
댕댕워크 프로젝트의 CI/CD 배포 방법을 분석해보자. GitHub Actions를 활용하여 AWS를 기반으로 구성된 CI/CD 배포 과정을 코드 단위로 분석하고 각 단계에서 어떤 작업이 이루어지는지 살펴보자. 먼저 Backend부터~
backend_deploy.yaml
on:
push:
branches: [main]
paths:
- "backend/server/**"
main 브랜치의 backend/server 디렉토리 내 파일 변경 사항이 있을 때 이 workflow가 실행된다. 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자리를 태그로 사용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 명령이 이 경로에서 실행됨. - name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
actions/checkout@v4: GitHub Actions 체크아웃 액션을 사용해서 전체 리포지토리 코드를 앞서 생성된 Ubuntu 가상 머신으로 가져오기.fetch-depth: 2: 최근 2개의 커밋 내역만 가져옴으로써 속도와 효율성 향상. - name: Setup Node.js
uses: actions/setup-node@v3
actions/setup-node@v3: Node.js 환경 설정을 위해 사용. 기본적으로 최신 LTS 버전을 자동 설치. - 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: 캐시가 존재하지 않을 경우 복구를 위해 기본 키를 재사용. - 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 명령으로 서버를 빌드. - 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 로그인. - 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
IMAGE_TAG=$(aws ecr describe-images ...):jq를 사용해 JSON 데이터에서 첫 번째 태그를 추출.if [[ -z "$IMAGE_TAG" ]]; then:aws ecr batch-delete-image ...:docker build --platform linux/amd64 -t ${{ env.IMAGE }} --target prod -f server/Dockerfile .
docker build: Docker 이미지를 빌드.--platform linux/amd64:-t ${{ env.IMAGE }}:dangdangwalk).--target prod:-f server/Dockerfile:.: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 }}
aws ecr get-login-password | docker login:docker tag:REPO/IMAGE:TAG).docker push: 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에서 실행되도록 지정.
- 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에 추가된 환경 변수 값을 가져온다. - 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 }}
/home/ubuntu/run-backend.sh) 실행.backend.sh(/home/ubuntu/run-backend.sh와 비슷한 코드)

#!/bin/bash
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_PATH 및 FILE_PATH: 환경 변수 파일의 절대 경로를 설정.aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO"
get-login-password: 인증 토큰을 생성.docker login: Docker CLI를 사용해 ECR에 인증.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: 실행 중인 상태인지 필터링.if [ -z "$TAG" ]; then
echo "ERROR: Image tag argument is missing."
exit 1
fi
TAG 값이 비어 있는지 확인.if [ -n "$IS_BLUE_RUNNING" ]; then
echo "Green 배포를 시작합니다."
IS_BLUE_RUNNING 값이 비어 있지 않다면 dangdang-api-blue 컨테이너가 실행 중임을 의미.docker stop dangdang-api-green && docker rm dangdang-api-green
docker pull "$REPO"/"$IMAGE":"$TAG"
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"
-d: 백그라운드 실행.--log-driver=fluentd: Fluentd를 사용해 로그 중앙화.--restart always: 컨테이너를 항상 재시작.-p 3031:3031: 포트 매핑.--env-file "$FILE_PATH": 환경 변수 파일을 컨테이너에 전달.-v logs:/app/log: 로그를 호스트 디렉토리와 연결.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;
curl로 Health Check 엔드포인트 요청.echo "Nginx를 재시작합니다."
sudo nginx -s reload
reload 명령은 Nginx를 완전히 재시작하지 않고, 활성화된 프로세스를 교체하여 무중단으로 동작을 유지/etc/nginx/sites-enabled/default를 확인해보면 다음과 같이 upstream 블록이 설정되어 있다.upstream api {
server localhost:3031; # Green
server localhost:3032; # Blue
}localhost:3031은 Green 컨테이너, localhost:3032는 Blue 컨테이너를 나타낸다.echo "Blue Container를 종료합니다."
docker stop dangdang-api-blue
else
echo "Blue 배포를 시작합니다."
...
IS_BLUE_RUNNING이 비어 있는 경우 Blue 컨테이너를 배포.sleep 3
echo "이전 이미지를 삭제합니다."
docker image prune -af
이 글을 작성하면서 코드를 공부·분석하며 살펴보니 개선할 점이 보여서
run_backend.sh를 리팩토링해보았다!