이전 글에서는 아래 과정을 직접 수동으로 진행했다.
Docker 이미지 빌드 -> ECR Push -> EC2 Pull -> docker run
하지만 매번 직접 명령어를 입력하는 것은 번거로운 일이다.
이번 글에서는 GitHub Actions를 사용해서 Node 서버를 자동 배포하는 과정을 정리한다.
지난 글
https://geonbbang.tistory.com/74
Docker를 이용하여 EC2에 Node 서버 배포하기 (1)
Node 서버를 운영하다 보면 다음과 같은 문제들이 생길 수 있다.배포를 했더니 개발 환경과 운영 환경이 같지 않아서 문제가 발생한다.다른 개발자와의 개발 환경이 달라 충돌이 발생한다.가끔
geonbbang.tistory.com
전체 흐름은 아래와 같다.
- GitHub Actions 실행
- Docker 이미지 빌드
- AWS ECR Push
- EC2 스크립트 실행
- EC2 스크립트 작성
- docker-compose.yml 작성
GitHub Actions 실행
GitHub Actions 실행을 위해 아래 경로에 workflow 파일을 작성한다.
.github/workflows/deploy.yml
name: Build + Deploy
on:
push:
branches: [dev]
jobs:
build-analyze-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
checks: write
steps:
# 코드 체크아웃
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Node 세팅
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
# 의존성 설치
- name: Install deps
run: npm install
# AWS 인증 (OIDC)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-2
# ECR 로그인
- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# Docker build & push
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:latest
${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
retries: 2
# EC2 배포
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
bash ~/app/deploy.sh
이 workflow가 실행되면 전체 흐름은 아래처럼 동작한다.
GitHub Push
→ GitHub Actions 실행
→ Docker 이미지 Build
→ ECR Push
→ EC2 접속
→ deploy.sh 실행
→ Blue/Green 배포
deploy.sh 작성
GitHub Actions의 마지막 단계에서는 아래 명령이 실행된다.
script: |
bash ~/app/deploy.sh
즉, EC2 내부에서 실제 배포를 수행하는 역할을 하는 것이다.
deploy.sh
#!/bin/bash
cd ~/app
STATE_FILE=~/app/deploy_state
CURRENT=$(cat $STATE_FILE)
if [ "$CURRENT" = "blue" ]; then
NEW=green
NEW_PORT=3001
OLD=blue
OLD_PORT=3000
else
NEW=blue
NEW_PORT=3000
OLD=green
OLD_PORT=3001
fi
echo "Deploying $NEW..."
echo "Logging in to ECR..."
aws ecr get-login-password \
--region ap-northeast-2 \
| docker login \
--username AWS \
--password-stdin {ECR_URL}
# 최신 이미지 pull
docker compose pull
# 새 컨테이너 실행
docker compose up -d app-$NEW
# =========================
# HEALTH CHECK
# =========================
echo "Health checking..."
for i in {1..10}; do
if curl -f http://localhost:$NEW_PORT/health; then
echo "Healthy!"
break
fi
sleep 2
done
# 실패 시 롤백
if ! curl -f http://localhost:$NEW_PORT/health; then
echo "Deploy failed → rollback"
docker compose stop app-$NEW
exit 1
fi
# =========================
# NGINX SWITCH
# =========================
echo "Switching nginx → $NEW_PORT"
sudo sed -i "s/$OLD_PORT/$NEW_PORT/g" /etc/nginx/sites-available/sample-app
sudo nginx -s reload
# 상태 저장
echo "$NEW" > $STATE_FILE
# 기존 컨테이너 종료
docker compose stop app-$OLD
echo "Deploy complete"
이 스크립트의 역할은 다음과 같다.
현재 서비스 중인 컨테이너는 그대로 둔 상태에서, 반대쪽 컨테이너에 새 버전 앱을 먼저 실행한다.
새 버전이 정상인지 헬스체크로 확인한 뒤, Nginx 설정을 바꿔 트래픽을 새 버전으로 전환한다.
전환이 끝나면 이전 컨테이너를 종료하고, 문제가 있으면 기존 서비스는 유지한 채 배포만 실패 처리 한다.
deploy_state 작성
다음은 현재 어떤 컨테이너가 운영 중인지 저장하는 파일이다.
deploy_state
blue
이 파일은 굉장히 단순하지만 Blue/Green 배포에서 핵심 역할을 한다.
배포 스크립트는 이 값을 기준으로:
- 현재 blue 운영 중 → green 배포
- 현재 green 운영 중 → blue 배포
를 결정한다.
docker-compose.yml 작성
다음은 실제 컨테이너를 정의하는 파일이다.
services:
app-blue:
image: {ECR_URL}/geonbbang/sample-app:latest
container_name: app-blue
command: node dist/main
ports:
- "3000:3000"
env_file:
- .env
restart: always
app-green:
image: {ECR_URL}/geonbbang/sample-app:latest
container_name: app-green
command: node dist/main
ports:
- "3001:3000"
env_file:
- .env
restart: always
왜 컨테이너를 2개 사용하는가?
Blue/Green 배포의 핵심은 현재 운영 중인 서버를 유지한 상태로 새 버전을 띄우는 것
즉 현재 app-blue 가 운영 중이라면 app-green 에 새 버전을 먼저 배포한다.
새 서버가 정상이라면 nginx 트래픽을 전환하고, 이후 기존 서버를 종료한다.
마무리
이렇게 구성함으로써
- 수동 배포 제거
- 무중단 배포
- 자동 롤백 기반 확보
- 운영 안정성 향상
의 효과를 챙길 수 있게 되었다.
'DevOps' 카테고리의 다른 글
| Docker를 이용하여 EC2에 Node 서버 배포하기 (1) (0) | 2026.04.28 |
|---|---|
| CloudWatch Logs와 Slack 통합: 에러 메시지 실시간 전송 설정 (0) | 2024.12.25 |
| Nest 프로젝트에 Pinpoint 적용 (0) | 2024.12.05 |
| CloudWatch를 이용해 알람 받기 (0) | 2024.01.28 |
| 로그 남기기 (0) | 2024.01.21 |