[아직 배포용 클라우드 계정이 준비되지 않아 GitHub Actions 검증은 되지 않은 상태입니다. 다만, AWS CLI의 경우 문제 없음을 확인하였습니다.]
이 글에서는 AWS ALB와 두 타겟그룹(Blue/Green)에서 무중단 배포를 자동화하는 방법에 대해 다룬다.
CI/CD 자동화는 GitHub Actions를 사용하며, 각 타켓그룹에는 인스턴스가 1개씩만 있다고 가정한다.
전체 흐름
- 빌드 & 푸시
- Github Actions로 prod 브랜치 push 시 도커로 애플리케이션을 빌드해 이미지를 DockerHub에 푸시한다.
- 현재 라이브 색상 확인
- ALB 리스너가 Blue/Green 중 어디로 트래픽을 보내는지 조회한다.
- 비활성 타겟그룹에 배포 진행→ 로컬 스모크 체크(curl -fsS http://localhost:8080/health)
- SSH 접속 → docker pull → docker run -d
- ALB 헬스체크 대기배포한 TG가 healthy 될 때 까지 대기한다.
- 배포한 TG를 ALB에 가중치 0으로 연결한다.
- 스위칭
- 배포한 TG로 트래픽을 전환한다.(기존 TG는 제외)
- 실패시 롤백
- 리스너 기본 대상을 즉시 이전 TG로 되돌린다.
- 문제없다면, 기존 live를 정리한다.
필요한 권한/시크릿
- GitHub Secrets
| 시크릿 이름 | 설명 |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub 계정 ID |
DOCKERHUB_TOKEN |
DockerHub 액세스 토큰 |
AWS_ACCESS_KEY_ID |
배포에 필요한 최소 권한의 액세스 키 |
AWS_SECRET_ACCESS_KEY |
위 키의 시크릿 |
AWS_REGION |
|
LISTENER_ARN |
ALB 리스너의 ARN |
TG_BLUE_ARN |
Blue Target Group의 ARN |
TG_GREEN_ARN |
Green Target Group의 ARN |
BLUE_HOST |
Blue EC2의 접속 주소(IP/도메인) |
GREEN_HOST |
Green EC2의 접속 주소(IP/도메인) |
SSH_USER |
원격 배포 계정 |
SSH_PRIVATE_KEY |
EC2 접속용 키(Blue/Green) 모두 동일한 키 사용 |
ENV_PATH |
인스턴스 내 환경변수 파일 경로 |
HEALTH_PATH |
스모크/헬스체크 경로 |
APP_PORT |
애플리케이션 포트 |
SERVICE_PORT |
호스트머신 포트(APP_PORT와 매핑) |
ALB_DNS |
ALB의 DNS 이름 (배포 후 스모크 테스트용) |
- IAM 권한
GitHub Actions가 AWS API를 호출해서 ALB를 제어하고 EC2에 배포할 수 있도록 하기 위한 IAM 권한
| 권한 | 필요한 이유 | 동작 예시 |
|---|---|---|
DescribeListeners |
ALB의 리스너 상태를 조회해야 함. 현재 어느 Target Group(Blue/Green) 으로 트래픽이 가고 있는지 알아야 하기 때문. | Actions가 aws elbv2 describe-listeners --listener-arn <ARN> 명령으로 현재 리스너에 연결된 TG arn 확인 |
ModifyListener |
새 버전이 정상 작동하면 리스너의 기본 대상(Target Group) 을 Green ↔ Blue로 전환해야 함. 이게 Blue-Green 전환의 핵심 단계. | aws elbv2 modify-listener --listener-arn ... --default-actions ... |
DescribeTargetHealth |
배포 후 트래픽을 전환하기 전 새로운 TG가 healthy 상태인지 확인해야 함. 헬스체크가 통과될 때까지 대기해야 무중단 전환 가능. | aws elbv2 describe-target-health --target-group-arn ... |
DescribeTargetGroups |
태그 활용 시 TG ARN 조회 자동화 가능 |
2025.10.18 - [인프라] - AWS: IAM 사용자 생성
AWS: IAM 사용자 생성
깃헙 액션 배포 시 ALB Listener의 대상그룹 변경을 위한 최소한의 권한을 가진 IAM 사용자를 생성해보려고 한다.내 워크플로우에서 필요한 권한은 다음과 같다:elasticloadbalancing:DescribeListenerselasticload
bumsoft.tistory.com
GitHub Actions
현재 연결된 대상그룹의 arn 구하는 법은 이전 글을 참고하면 좋을 것 같다.
2025.10.18 - [인프라] - AWS CLI: describe-rules --listener-arn
AWS CLI: describe-rules --listener-arn
블루 그린 배포를 위해 GitHub Actions yaml을 작성하던 중 로드밸런서의 현재 대상그룹 ARN 값이 필요하게 되었다.ALB Listener arn은 깃헙 시크릿으로 전달받아 알 수 있기에 describe-rules을 사용하면 될 것
bumsoft.tistory.com
name: Prod Blue-Green Deploy
on:
push:
branches: [ "prod" ]
concurrency:
group: prod-deploy
cancel-in-progress: true
env:
IMAGE_NAME: realconnect/backend
jobs:
build_and_push:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Compute image tag
id: tag
run: echo "tag=prod-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & Push
run: |
TAG="${{ steps.tag.outputs.tag }}"
docker build -t "$IMAGE_NAME":"$TAG" .
docker tag "$IMAGE_NAME":"$TAG" "$IMAGE_NAME":prod-latest
docker push "$IMAGE_NAME":"$TAG"
docker push "$IMAGE_NAME":prod-latest
deploy:
needs: build_and_push
runs-on: ubuntu-latest
steps:
# AWS 접속
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
# 현재 연결된 TG 확인 (Blue / Green)
- name: Figure out live color
id: color
run: |
LISTENER_ARN=${{ secrets.LISTENER_ARN }}
TG_BLUE=${{ secrets.TG_BLUE_ARN }}
TG_GREEN=${{ secrets.TG_GREEN_ARN }}
# 아래 쿼리는 단일 TG일 때만 유효함
TG_ACTIVE=$(aws elbv2 describe-rules \
--listener-arn "$LISTENER_ARN" \
--query "Rules[?IsDefault].Actions[0].TargetGroupArn" \
--output text)
if [ "$TG_ACTIVE" = "$TG_BLUE" ]; then
echo "현재 활성화된 TG는 BLUE입니다."
echo "live=BLUE" >> "$GITHUB_OUTPUT"
echo "live_tg_arn=$TG_BLUE" >> "$GITHUB_OUTPUT"
echo "next=GREEN" >> "$GITHUB_OUTPUT"
echo "next_tg_arn=$TG_GREEN" >> "$GITHUB_OUTPUT"
echo "host=${{ secrets.GREEN_HOST }}" >> "$GITHUB_OUTPUT"
else
echo "현재 활성화된 TG는 GREEN입니다."
echo "live=GREEN" >> "$GITHUB_OUTPUT"
echo "live_tg_arn=$TG_GREEN" >> "$GITHUB_OUTPUT"
echo "next=BLUE" >> "$GITHUB_OUTPUT"
echo "next_tg_arn=$TG_BLUE" >> "$GITHUB_OUTPUT"
echo "host=${{ secrets.BLUE_HOST }}" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to INACTIVE host via SSH
uses: appleboy/ssh-action@v1.2.0
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
SERVICE_PORT: ${{ secrets.SERVICE_PORT }}
APP_PORT: ${{ secrets.APP_PORT }}
HEALTH_PATH: ${{ secrets.HEALTH_PATH }}
ENV_PATH: ${{ secrets.ENV_PATH }}
IMAGE_NAME: ${{ env.IMAGE_NAME }}
TAG: ${{ needs.build_and_push.outputs.tag }}
with:
host: ${{ steps.color.outputs.host }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
envs: DOCKERHUB_USERNAME,DOCKERHUB_TOKEN,SERVICE_PORT,APP_PORT,HEALTH_PATH,ENV_PATH,IMAGE_NAME,TAG
script: |
set -eu
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
docker pull "$IMAGE_NAME":"$TAG"
docker rm -f backend || true
docker run -d --name backend --restart=always \
-p "$SERVICE_PORT":"$APP_PORT" \
--env-file "$ENV_PATH" \
"$IMAGE_NAME":"$TAG"
# 로컬 스모크: 10회, 3초 간격. 실패 시 배포 중단
i=0
while [ $i -lt 10 ]; do
if curl -fsS "http://localhost:${SERVICE_PORT}${HEALTH_PATH}" >/dev/null; then
echo "Local smoke OK"
break
fi
i=$((i+1))
sleep 3
done
[ $i -lt 10 ] || { echo "로컬 스모크 체크 실패"; exit 1; }
- name: Wait until ALB says next TG is healthy
run: |
set -euo pipefail
LISTENER_ARN="${{ secrets.LISTENER_ARN }}"
CURRENT_TG_ARN=${{ steps.color.outputs.live_tg_arn }}
NEXT_TG_ARN=${{ steps.color.outputs.next_tg_arn }}
# ALB 헬스체크를 위해 가중치 0으로 next_tg도 연결
aws elbv2 modify-listener \
--listener-arn "$LISTENER_ARN" \
--default-actions "Type=forward,ForwardConfig={TargetGroups=[{TargetGroupArn=$CURRENT_TG_ARN,Weight=100},{TargetGroupArn=$NEXT_TG_ARN,Weight=0}]}"
# 40회, 3초 간격. 실패 시 배포 중단(맨 아래 step실행)
for i in {1..40}; do
STATE=$(aws elbv2 describe-target-health \
--target-group-arn "$NEXT_TG_ARN" \
--query 'TargetHealthDescriptions[0].TargetHealth.State' --output text)
echo "Target state: $STATE"
[ "$STATE" = "healthy" ] && exit 0
sleep 3
done
echo "헬스체크 타임아웃"; exit 1
# 가중치 변경(단일 next_tg로)
- name: Switch ALB default to NEXT TG
run: |
aws elbv2 modify-listener \
--listener-arn "${{ secrets.LISTENER_ARN }}" \
--default-actions "Type=forward,TargetGroupArn=${{ steps.color.outputs.next_tg_arn }}"
echo "${{ steps.color.outputs.next }}로 전환 완료."
# 마지막 스모크: ALB로 요청 테스트
- name: Final smoke
run: |
set -euo pipefail
curl -fsSI "https://${{ secrets.ALB_DNS }}${{ secrets.HEALTH_PATH }}"
- name: ROLLBACK to previous LIVE TG
if: failure()
run: |
aws elbv2 modify-listener \
--listener-arn "${{ secrets.LISTENER_ARN }}" \
--default-actions "Type=forward,TargetGroupArn=${{ steps.color.outputs.live_tg_arn }}"
echo "${{ steps.color.outputs.live }}로 롤백."'인프라' 카테고리의 다른 글
| AWS S3: 보안관점에서 Presigned PUT vs POST (0) | 2026.01.20 |
|---|---|
| AWS: IAM Billing 읽기 권한 (0) | 2025.10.21 |
| AWS: IAM 사용자 생성 (0) | 2025.10.18 |
| AWS CLI: describe-rules --listener-arn (0) | 2025.10.18 |
| Blue-Green Deployment (기본) (0) | 2025.10.17 |