- Published on
GitHub Actions로 EKS 무중단 배포 - Blue-Green CI/CD
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버를 멈추지 않고 배포하는 건 “배포 스크립트를 잘 짠다” 수준을 넘어, 트래픽 전환(라우팅), 헬스체크, 관측(모니터링), 롤백이 한 덩어리로 설계되어야 합니다. 특히 EKS에서는 Deployment rollingUpdate만으로도 무중단에 가까워질 수 있지만, 장애 격리/검증/즉시 롤백까지 포함한 운영 품질을 만들려면 Blue-Green이 훨씬 명확합니다.
이 글에서는 GitHub Actions + ECR + EKS 조합으로, 다음을 만족하는 실전 CI/CD를 구축합니다.
- PR/메인 브랜치 머지 시 자동 빌드/푸시
- EKS에 green 환경을 먼저 올리고 헬스 검증
- 검증 통과 시 Service/Ingress 전환으로 트래픽 스위치
- 실패 시 자동 롤백
- 운영 중 자주 터지는 권한/ALB 오류 포인트까지 포함
> EKS에서 AWS 권한 문제로 배포가 막히는 경우가 많습니다. Pod/컨트롤러가 STS를 못 받아 403이 나는 케이스는 아래 글이 진짜 자주 도움됩니다: EKS Pod에서 STS 403 AccessDenied 원인 8가지
아키텍처: Blue-Green을 EKS에서 구현하는 2가지 방식
EKS에서 Blue-Green은 보통 둘 중 하나로 구현합니다.
1) Service Selector 스위칭(가장 단순)
myapp-blueDeployment,myapp-greenDeployment 두 세트를 유지Service myapp의 selector를version=blue↔version=green으로 변경- ALB Ingress는 Service만 바라보므로, Service만 바꾸면 트래픽이 이동
장점: 단순, 쿠버네티스 표준 오브젝트만으로 가능 단점: 전환 순간의 연결 드레인/세션/리드니스 설계가 중요
2) ALB 가중치(카나리/점진 전환)
- AWS Load Balancer Controller의 advanced routing(가중치/조건)로 점진 전환
장점: 10%→50%→100% 같은 점진 전환 단점: 설정 복잡, 컨트롤러/리소스 의존도 증가
이 글은 1) Service Selector 스위칭으로 “가장 재현 가능한” 파이프라인을 먼저 완성합니다.
사전 준비: ECR, OIDC, kubectl 접근
필수 구성요소
- EKS 클러스터 + kubectl 접근 가능
- ECR 리포지토리
- GitHub Actions에서 AWS에 접근할 OIDC(권장) 또는 액세스키
- 클러스터에 배포할 Namespace (예:
prod)
GitHub Actions → AWS OIDC 설정(권장)
액세스키를 GitHub Secrets에 넣는 방식은 운영에서 회전/유출 리스크가 큽니다. OIDC로 AssumeRoleWithWebIdentity를 쓰면 훨씬 안전합니다.
- AWS IAM Role 생성
- Trust policy에 GitHub OIDC provider 추가
- Role 정책에 ECR push, EKS describe, (필요 시) SSM, CloudWatch 권한 부여
권한이 부족하면 배포는 “성공”인데 실제로는 ALB/ExternalDNS/Pod가 AWS API를 못 쳐서 장애가 납니다. ExternalDNS가 Route53 레코드를 못 만드는 문제는 IRSA 점검이 핵심입니다: EKS ExternalDNS가 Route53 생성 실패할 때 IRSA 점검
쿠버네티스 매니페스트: Blue/Green 분리
아래는 가장 단순한 형태입니다.
1) Deployment (blue)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
namespace: prod
labels:
app: myapp
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:PLACEHOLDER
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
2) Deployment (green)
blue와 동일하되 name/version만 green으로.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
namespace: prod
labels:
app: myapp
version: green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:PLACEHOLDER
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
3) Service: selector만 바꿔서 트래픽 전환
초기에는 blue를 바라보게 합니다.
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: prod
spec:
selector:
app: myapp
version: blue
ports:
- name: http
port: 80
targetPort: 8080
4) Ingress(ALB): Service만 바라보게 유지
AWS Load Balancer Controller를 쓴다는 가정입니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
namespace: prod
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /actuator/health/readiness
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
> ALB에서 502/504가 간헐적으로 뜨면 “애플리케이션 문제”로 오해하기 쉽지만, 타겟그룹 헬스체크/리드니스/드레인/보안그룹/서브넷 라우팅이 원인인 경우가 많습니다. 운영에서 자주 보는 케이스는 아래 글을 참고하세요. > - EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지 > - EKS ALB Ingress 504(5xx) 간헐 발생 원인·해결
GitHub Actions 워크플로우: 빌드 → 배포 → 검증 → 스위치
핵심은 “green에 먼저 올리고, readiness가 OK면 Service selector를 바꾼다”입니다.
아래 예시는:
- Docker 이미지 빌드 후 ECR 푸시
- 현재 Service가 바라보는 색(blue/green)을 읽고 반대색을
TARGET_COLOR로 결정 - TARGET Deployment 이미지 업데이트
- rollout 완료 대기
- (선택) 내부 헬스 엔드포인트를 포트포워딩으로 확인
- Service selector 전환
- 실패 시 Service를 원래 색으로 복구
.github/workflows/deploy-blue-green.yml
name: deploy-blue-green
on:
push:
branches: [ "main" ]
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-northeast-2
EKS_CLUSTER: my-eks
NAMESPACE: prod
APP_NAME: myapp
ECR_REPO: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-role
aws-region: ${{ env.AWS_REGION }}
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build & Push
id: build
run: |
set -euo pipefail
IMAGE_TAG=${GITHUB_SHA::12}
docker build -t $ECR_REPO:$IMAGE_TAG .
docker push $ECR_REPO:$IMAGE_TAG
echo "image=$ECR_REPO:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Setup kubectl
run: |
set -euo pipefail
aws eks update-kubeconfig --name "$EKS_CLUSTER" --region "$AWS_REGION"
kubectl version --client
- name: Decide target color
id: color
run: |
set -euo pipefail
CURRENT=$(kubectl -n "$NAMESPACE" get svc "$APP_NAME" -o jsonpath='{.spec.selector.version}')
if [ "$CURRENT" = "blue" ]; then
TARGET="green"
else
TARGET="blue"
fi
echo "current=$CURRENT" >> $GITHUB_OUTPUT
echo "target=$TARGET" >> $GITHUB_OUTPUT
- name: Deploy to target (no traffic)
run: |
set -euo pipefail
TARGET=${{ steps.color.outputs.target }}
IMAGE=${{ steps.build.outputs.image }}
kubectl -n "$NAMESPACE" set image deploy/${APP_NAME}-${TARGET} ${APP_NAME}=$IMAGE
kubectl -n "$NAMESPACE" rollout status deploy/${APP_NAME}-${TARGET} --timeout=300s
- name: Smoke test target (port-forward)
run: |
set -euo pipefail
TARGET=${{ steps.color.outputs.target }}
# target color의 Pod 하나를 골라 포트포워딩 후 readiness 확인
POD=$(kubectl -n "$NAMESPACE" get pod -l app=$APP_NAME,version=$TARGET -o jsonpath='{.items[0].metadata.name}')
kubectl -n "$NAMESPACE" port-forward pod/$POD 18080:8080 >/tmp/pf.log 2>&1 &
PF_PID=$!
sleep 2
curl -fsS http://127.0.0.1:18080/actuator/health/readiness | cat
kill $PF_PID
- name: Switch traffic (Service selector)
run: |
set -euo pipefail
TARGET=${{ steps.color.outputs.target }}
kubectl -n "$NAMESPACE" patch svc "$APP_NAME" \
-p '{"spec":{"selector":{"app":"'"$APP_NAME"'","version":"'"$TARGET"'"}}}'
- name: Verify after switch
run: |
set -euo pipefail
# 전환 직후 간단 검증(실서비스라면 synthetic check/외부 모니터링 연동 권장)
kubectl -n "$NAMESPACE" get endpoints "$APP_NAME" -o wide
- name: Rollback on failure
if: failure()
run: |
set -euo pipefail
CURRENT=${{ steps.color.outputs.current }}
kubectl -n "$NAMESPACE" patch svc "$APP_NAME" \
-p '{"spec":{"selector":{"app":"'"$APP_NAME"'","version":"'"$CURRENT"'"}}}'
echo "Rolled back service selector to $CURRENT"
운영 품질을 결정하는 디테일 7가지
1) readinessProbe는 “트래픽 받아도 되는가”를 정확히 표현해야 함
Blue-Green에서 전환은 빠르지만, readiness가 부정확하면 전환 순간부터 5xx가 납니다.
- DB migration 중이면 readiness는 실패해야 함
- 외부 의존(캐시/메시지브로커) 연결이 준비되지 않으면 실패해야 함
- 단순히 프로세스가 살아있음(liveness)과 다름
2) terminationGracePeriod + preStop으로 커넥션 드레인
Service selector가 바뀌면 기존 Pod로의 신규 연결은 줄지만, 이미 맺어진 연결은 남습니다.
terminationGracePeriodSeconds를 충분히preStop훅에서 서버가 graceful shutdown 하도록
예: Spring Boot라면 server.shutdown=graceful, spring.lifecycle.timeout-per-shutdown-phase 등을 고려합니다.
3) HPA/리소스는 “green 배포 순간”을 고려해 여유를 둬야 함
Blue와 Green을 동시에 띄우는 동안 일시적으로 리소스가 2배 필요합니다.
- 노드 오토스케일러(Cluster Autoscaler/Karpenter)와 함께 설계
- PDB(PodDisruptionBudget)로 가용성 하한 설정
4) 데이터 스키마 변경은 ‘호환성’이 핵심
무중단 배포에서 가장 흔한 사고는 앱은 green으로 갔는데 DB 스키마가 아직/이미 바뀐 케이스입니다.
- Expand/Contract(확장/수축) 방식
- 컬럼 추가 → 양쪽 버전 호환 → 트래픽 전환 → 구버전 제거 → 컬럼 제거
5) 관측(Observability) 없이는 자동 롤백이 오히려 위험
워크플로우의 smoke test만으로는 부족합니다.
- 전환 후 1~5분 동안 5xx, latency, 주요 비즈니스 지표를 관찰
- CloudWatch Alarm + GitHub Actions(또는 Argo Rollouts)로 자동 롤백 연동 고려
6) ALB 헬스체크와 readiness를 일치시키기
ALB healthcheck path가 앱의 readiness와 다르면 “쿠버네티스는 준비됨”인데 “ALB는 비정상”이 됩니다.
- Ingress annotation의
alb.ingress.kubernetes.io/healthcheck-path - Security Group inbound(노드/파드 대상)
- target-type ip/instance에 따른 네트워크 경로
7) IAM/IRSA는 CI/CD와 런타임을 분리해서 설계
- GitHub Actions Role: ECR push,
eks:DescribeCluster, (필요 시)sts:AssumeRole - Pod(ServiceAccount) Role(IRSA): S3/SQS/SecretsManager/Route53 등 런타임 권한
이걸 섞으면 “배포는 되는데 런타임이 터지는” 혹은 그 반대가 됩니다.
자주 겪는 장애 시나리오와 빠른 체크
배포 후 ALB 502/504가 발생
- readiness는 OK인데 ALB는 unhealthy: healthcheck path/port 불일치
- target group deregistration delay/드레인 부족
- Security Group, NACL, 서브넷 라우팅
위 케이스는 실제로 매우 빈번합니다: EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지
GitHub Actions에서 kubectl이 클러스터에 못 붙음
- OIDC role trust 정책의
sub/aud조건 불일치 aws eks update-kubeconfig는 되지만kubectl이 Unauthorized:aws-authConfigMap 매핑 누락- 네트워크(프라이빗 엔드포인트)로 인해 러너에서 접근 불가
Pod가 AWS API 호출하다 STS 403
- IRSA 설정/ServiceAccount annotation 누락
- OIDC provider, trust policy 조건 오류
- 토큰 마운트/환경변수 충돌
이 경우는 아래 체크리스트로 빨리 좁힐 수 있습니다: EKS Pod에서 STS 403 AccessDenied 원인 8가지
마무리: “전환”을 자동화하면 배포는 운영이 된다
Blue-Green의 본질은 새 버전을 격리된 상태로 충분히 준비시킨 뒤, 라우팅만 바꿔서 즉시 전환하는 것입니다. GitHub Actions로 이를 자동화하면 배포는 더 이상 이벤트성 작업이 아니라, 재현 가능한 운영 프로세스가 됩니다.
다음 단계로는 아래 확장을 추천합니다.
- 전환 후 SLO 기반 자동 롤백(CloudWatch Alarm/Prometheus)
- 점진 전환(가중치 기반)으로 카나리 + 블루그린 혼합
- Argo Rollouts/Flagger 도입으로 배포 전략 표준화
원하시면 현재 사용 중인 구성(예: ALB Ingress 여부, target-type, 프라이빗 클러스터, HPA/Karpenter 사용)을 기준으로 위 워크플로우를 그대로 적용 가능한 형태로 커스터마이징해드릴게요.