사이드 프로젝트에 블루/그린 배포 붙여보기 — 9초의 벽

2026. 6. 3. 18:06프로젝트

지난 주에 GitHub Actions로 EC2 배포 자동화를 끝냈는데, 컨테이너 교체할 때 매번 5초쯤 끊김이 있었다.

 

main에 푸시하면 1m 40초 만에 자동 배포되는 건 좋은데, 그 사이에 누가 접속하면 5xx 상태코드를 받는다.

사이드 프로젝트라 사용자는 사실 나뿐이지만, 인프라 직무 전환을 준비 중이라 블루/그린 패턴은 꼭 한번 만들어봐야 했다.

 

이번 주에 시도했고, 결론부터 말하면 9초로 끝났다.

0초가 목표였는데. 어디까지 갔고 어디서 막혔는지 적어둔다.

 

큰 그림

같은 EC2 안에 컨테이너를 두 벌 띄울 자리를 만든다.

  • blue = 포트 8080
  • green = 포트 8081

ALB는 둘 중 한쪽에만 트래픽을 보낸다. 배포할 땐 노는 쪽에 새 버전을 올리고, 헬스체크가 통과하면 ALB 리스너를 그쪽으로 돌린다. 다음 배포는 반대로. 매번 토글된다.

[평소: blue 가동]
사용자 → ALB → blue(8080) v1

[배포 중]
사용자 → ALB → blue(8080) v1      <- 트래픽 그대로
               green(8081) v2     <- 새 버전 (트래픽 안 받음)

[전환]
사용자 → ALB → green(8081) v2     <- 여기로 돌림
               blue(8080) v1      <- 곧 정리됨
 
ALB는 어느 쪽이 가동 중인지 상태로 알고 있으니, 워크플로우가 ALB한테 물어보면 "이번 배포는 어느 색인지" 자동으로 정해진다. 사람이 안 정해줘도 된다.
 

워크플로우 구조

GitHub Actions job을 3개로 나눴다.

 

1. build-and-push — Docker 이미지 빌드해서 ECR에 push. Week 1에서 만든 그대로.

2. detect-active-color — ALB 리스너에게 "지금 어느 TG로 보내는 중?"을 물어봄. blue면 다음 배포 대상은 green, 반대면 반대. 이 결정을 다음 job에 넘긴다.

3. deploy — 진짜 본체. step이 순서대로:

① SSM으로 EC2에 새 색 컨테이너 띄움 + 로컬 헬스 체크
② 새 색 TG에 타겟 등록
③ 새 색 TG가 ALB 기준 healthy 되길 대기
④ ALB 리스너를 새 색으로 전환
⑤ 옛 색 컨테이너 정리
⑥ SES 메일 알림
 

핵심은 ④다. aws elbv2 modify-listener로 default action을 바꾸는 한 줄. 이게 사용자 트래픽 방향을 돌리는 순간이다.

 

만났던 오류들

docker-compose의 서비스 1개 구조 — 처음엔 flicker-app 서비스 하나에 환경변수로 CONTAINER_NAME, HOST_PORT만 바꾸는 식으로 짰다. 그랬더니 compose가 "같은 서비스의 새 컨테이너"로 인식해서 기존 걸 재생성했다. blue/green이 같은 슬롯을 다툰 셈. 18초 동안 502가 났다. 두 서비스(flicker-app-blue, flicker-app-green)로 분리하고 compose profiles로 격리해서 해결.

GitHub Actions의 secret 마스킹 — detect job에서 "다음 배포 대상 TG ARN"을 계산해서 outputs로 넘기는 구조였는데, 다음 job에선 빈 값으로 들어왔다. ARN이 secret 값에서 파생됐기 때문이다. GitHub이 secret을 다른 job으로 그대로 흘려보내는 걸 마스킹으로 차단한다. 각 step에서 target-color만 받고, ARN은 그 step 안에서 secrets.TG_BLUE_ARN/TG_GREEN_ARN을 직접 참조하는 방식으로 바꿨다.

ECR 토큰 12시간 만료 — 도중에 EC2에서 수동으로 컨테이너 띄우려다 authorization token has expired로 막혔다. ECR 인증 토큰은 12시간만 유효하다. aws ecr get-login-password로 매번 새 토큰을 받아 docker login에 파이프하는 게 정석. SSM 스크립트도 이 패턴으로 통일.

Target Group의 unused 상태 — TG에 타겟 등록은 됐는데 그 TG가 리스너에 안 붙어있으면 상태가 unused다. healthy만 기다리면 영원히 대기. unused를 통과로 인정했더니 워크플로우는 지나갔는데, 이게 9초 다운타임의 진짜 원인이 됐다.

9초가 왜 났는가

전환 직전에 green TG는 unused 상태였다. 리스너에 안 붙어있으니 ALB는 green health check를 안 돌리고 있었다는 뜻이다. 그 상태에서 리스너를 green으로 돌리면, ALB는 그제서야 health check를 시작한다. 통과할 때까지 healthy 타겟이 0개라서 503. 그 시간이 약 9초였다.

504 Gateway Timeout이 아니라 503 Service Unavailable이라는 게 단서다. 504는 백엔드가 응답이 없는 경우(타임아웃)고, 503은 ALB 자체가 보낼 타겟이 없는 경우. green 컨테이너는 잘 떠있었지만 ALB 입장에선 "아직 검증 안 됨"이었던 것.

 

시도한 해결책

green을 미리 리스너에 weight 0으로 붙여놓고, ALB health check가 한 번 통과한 뒤 100%로 전환하는 패턴을 시도했다.

원래:
① green 등록 → ② 바로 100% 전환 → 9초 503 → ③ healthy

수정:
① green 등록 → ② weight 0으로 리스너에 부착 (트래픽은 여전히 100% blue)
              → ③ green이 ALB 기준 healthy 될 때까지 대기
              → ④ 100% 전환 (이미 healthy라 공백 없어야 함)

 

같이 health check 설정도 공격적으로 조정했다.

Interval 30s → 5s, Healthy threshold 5 → 2, Timeout 5s → 2s. 그래야 healthy 대기 시간이 줄어든다.

 

aws elbv2 modify-listener의 --default-actions에 ForwardConfig shorthand로 가중치를 넣었다.

Type=forward,ForwardConfig={TargetGroups=[{TargetGroupArn=...,Weight=100},{...,Weight=0}]}

 

그런데 측정값이 안 바뀌었다.

수정 후에도 9초 그대로였다. 워크플로우 모든 step은 초록불이었지만 503 카운트는 똑같다.

 

CLI shorthand가 깨져서 weight 0 attach가 실제론 적용 안 됐을 가능성이 가장 의심된다. 중첩 구조라 ForwardConfig를 JSON 파일로 넘기는 방식으로 바꿔야 할 수 있다. 또는 0.2초 간격 측정으로 503의 정확한 시작/끝 타이밍을 잡고 step별 타임스탬프와 대조해야 한다.

정리

블루/그린의 핵심 명분은 다운타임 0이고, 그걸 만들려면 단순히 "새 컨테이너 띄우고 리스너 돌리기"로는 부족하다.

새 타겟이 ALB 입장에서 진짜 받을 준비가 됐는지 확인하는 게 진짜 어려운 부분이고, 그러려면 새 타겟을 리스너에 미리 노출시켜놓는 패턴이 필요하다.

다운타임이 5초(원래 docker rm → docker run)에서 9초로 늘었으니, 측정값만 보면 후퇴다. 다만 구조는 무중단의 토대가 됐다. 0초 만들기는 디테일 문제고, 컨테이너 두 벌 + ALB 토글 + 자동 색 감지 + 안전한 cleanup 흐름은 다 갖춰졌다.

 

다음 사이클에 0초 도전을 해보는 것으로 마무리하겠다.