댕댕워크 프로젝트의 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
를 리팩토링해보았다!