본문 바로가기

인프라

Blue-Green Deployment (자동화)

[아직 배포용 클라우드 계정이 준비되지 않아 GitHub Actions 검증은 되지 않은 상태입니다. 다만, AWS CLI의 경우 문제 없음을 확인하였습니다.]

이 글에서는 AWS ALB와 두 타겟그룹(Blue/Green)에서 무중단 배포를 자동화하는 방법에 대해 다룬다.

CI/CD 자동화는 GitHub Actions를 사용하며, 각 타켓그룹에는 인스턴스가 1개씩만 있다고 가정한다.

전체 흐름

  1. 빌드 & 푸시
  2. Github Actions로 prod 브랜치 push 시 도커로 애플리케이션을 빌드해 이미지를 DockerHub에 푸시한다.
  3. 현재 라이브 색상 확인
  4. ALB 리스너가 Blue/Green 중 어디로 트래픽을 보내는지 조회한다.
  5. 비활성 타겟그룹에 배포 진행→ 로컬 스모크 체크(curl -fsS http://localhost:8080/health)
  6. SSH 접속 → docker pull → docker run -d
  7. ALB 헬스체크 대기배포한 TG가 healthy 될 때 까지 대기한다.
  8. 배포한 TG를 ALB에 가중치 0으로 연결한다.
  9. 스위칭
  10. 배포한 TG로 트래픽을 전환한다.(기존 TG는 제외)
  11. 실패시 롤백
  12. 리스너 기본 대상을 즉시 이전 TG로 되돌린다.
  13. 문제없다면, 기존 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