Blue/Green 배포 (3): 클라우드 & CI/CD 통합
AWS, GCP, Azure에서의 Blue/Green 구현과 GitHub Actions, GitLab CI 자동화 파이프라인
Contents
클라우드별 Blue/Green은 로드밸런서 가중치 전환이 핵심이고, CI/CD는 검증 -> 승인 -> 전환 -> 모니터링 흐름 설계가 자동화보다 중요하다.
핵심 요약
- 클라우드별 Blue/Green 핵심은 "로드밸런서에서 트래픽 가중치 전환"이다.
- CI/CD 파이프라인은 배포 자동화보다
검증 -> 승인 -> 전환 -> 모니터링흐름 설계가 더 중요하다. - GitHub Actions/GitLab CI 모두 구현 가능하며, 운영 표준화가 선택 기준이다.
클라우드별 전환 메커니즘
세 클라우드 모두 "로드밸런서에서 트래픽을 한 번에 넘긴다"는 원리는 같지만, 전환을 거는 지점이 다르다. 아래 표로 먼저 정리한 뒤, 각 절에서 구현을 본다.
| 클라우드 | 주 전환 메커니즘 | 매니지드 옵션 |
|---|---|---|
| AWS | ALB Target Group weight 조정 | CodeDeploy (ECS/EC2) |
| GCP | Cloud Run 태그 트래픽 분할 / URL Map | Cloud Run revisions |
| Azure | App Service Deployment Slot 스왑 | Traffic Manager weight |
AWS에서의 Blue/Green 배포
AWS에서는 ALB(Application Load Balancer) Target Group 가중치 제어와 CodeDeploy를 조합한다. 인프라를 IaC(Infrastructure as Code)로 고정하고, 전환 명령을 스크립트화해 반복 가능하게 만든다.
ALB + Target Group 아키텍처
Terraform으로 인프라 구성
# alb.tf
resource "aws_lb" "main" {
name = "my-app-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnets
tags = {
Name = "my-app-alb"
}
}
# Blue/Green Target Group - 설정이 동일하므로 for_each로 한 번에 정의
resource "aws_lb_target_group" "app" {
for_each = toset(["blue", "green"])
name = "my-app-${each.key}"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 3
}
tags = {
Name = "my-app-${each.key}"
Color = each.key
}
}
# Listener with weighted routing (초기: Blue 100, Green 0)
resource "aws_lb_listener" "main" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.app["blue"].arn
weight = 100
}
target_group {
arn = aws_lb_target_group.app["green"].arn
weight = 0
}
}
}
}Target Group 두 개가 health_check까지 같으므로 for_each로 묶어 중복을 없앴다. 리스너에서 app["blue"]/app["green"]의 weight만 바꾸면 트래픽이 전환된다.
AWS CLI로 트래픽 전환
#!/bin/bash
# aws-switch-traffic.sh
ALB_LISTENER_ARN="arn:aws:elasticloadbalancing:..."
BLUE_TG_ARN="arn:aws:elasticloadbalancing:...blue"
GREEN_TG_ARN="arn:aws:elasticloadbalancing:...green"
TARGET_COLOR=${1:-green}
if [ "$TARGET_COLOR" == "green" ]; then
BLUE_WEIGHT=0
GREEN_WEIGHT=100
else
BLUE_WEIGHT=100
GREEN_WEIGHT=0
fi
echo "Switching traffic: Blue=$BLUE_WEIGHT%, Green=$GREEN_WEIGHT%"
aws elbv2 modify-listener --listener-arn $ALB_LISTENER_ARN \
--default-actions '[
{
"Type": "forward",
"ForwardConfig": {
"TargetGroups": [
{"TargetGroupArn": "'$BLUE_TG_ARN'", "Weight": '$BLUE_WEIGHT'},
{"TargetGroupArn": "'$GREEN_TG_ARN'", "Weight": '$GREEN_WEIGHT'}
]
}
}
]'
echo "Traffic switch completed!"첫 인자 $1로 전환 방향을 받아 Blue/Green 가중치를 0/100으로 정한다. modify-listener의 ForwardConfig.TargetGroups에 두 Target Group의 Weight를 넘기면 ALB가 트래픽 비중을 바꾼다.
AWS CodeDeploy (ECS Blue/Green)
# appspec.yml (ECS)
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:region:account:task-definition/my-app:2"
LoadBalancerInfo:
ContainerName: "my-app"
ContainerPort: 8080
PlatformVersion: "LATEST"
Hooks:
- BeforeInstall: "LambdaFunctionToValidateBeforeInstall"
- AfterInstall: "LambdaFunctionToValidateAfterInstall"
- AfterAllowTestTraffic: "LambdaFunctionToValidateAfterTestTraffic"
- BeforeAllowTraffic: "LambdaFunctionToValidateBeforeTraffic"
- AfterAllowTraffic: "LambdaFunctionToValidateAfterTraffic"각 Hook의 Lambda는 단계별 검증을 수행하고, 실패를 반환하면 CodeDeploy가 배포를 중단한다. 여기에 더해 CodeDeploy 배포 그룹에 CloudWatch 알람을 연결하면, 전환 후 에러율·레이턴시가 임계값을 넘었을 때 자동으로 Blue로 롤백한다. 앞의 aws elbv2 modify-listener 수동 전환보다 운영에서 더 안전한 선택지다.
GCP에서의 Blue/Green 배포
GCP는 Cloud Run 태그 트래픽 분할과 GKE(Google Kubernetes Engine)+Istio 조합으로 Serverless부터 Kubernetes까지 일관된 전략을 적용할 수 있다. 서비스 특성에 따라 "간단한 트래픽 분할"과 "세밀한 메쉬 라우팅" 중 하나를 선택하면 된다.
Cloud Load Balancing 아키텍처
Cloud Run (Serverless Blue/Green)
# Cloud Run에서 트래픽 분할
# 현재 운영 리비전에는 직전 배포 시 --tag blue가 달려 있다고 가정한다.
# 새 버전 배포 (트래픽 없이)
gcloud run deploy my-app \
--image gcr.io/project/my-app:v1.1.0 \
--region us-central1 \
--no-traffic \
--tag green
# 트래픽 전환 (100% Green)
gcloud run services update-traffic my-app \
--region us-central1 \
--to-tags green=100
# 롤백 (100% Blue)
# --to-latest는 방금 띄운 green(=최신 리비전)을 가리키므로 롤백에 쓰면 안 된다.
gcloud run services update-traffic my-app \
--region us-central1 \
--to-tags blue=100GKE Ingress + ManagedCertificate
Ingress가 my-app-active 서비스로 트래픽을 보내고, 이 서비스의 셀렉터를 Blue/Green 사이로 바꾸는 지점이 전환점이다.
# gcp-traffic-split.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: my-app-cert
spec:
domains:
- my-app.example.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
kubernetes.io/ingress.class: "gce"
networking.gke.io/managed-certificates: "my-app-cert"
spec:
rules:
- host: my-app.example.com
http:
paths:
- path: /*
pathType: ImplementationSpecific
backend:
service:
name: my-app-active
port:
number: 80Azure에서의 Blue/Green 배포
Azure는 App Service Deployment Slots를 중심으로 전환 안정성을 확보하기 쉽다. 트래픽 단위 전환이 필요하면 Traffic Manager/Application Gateway와 결합해 점진 배포를 구성할 수 있다.
Application Gateway + Deployment Slots
App Service Deployment Slots
# Staging 슬롯에 배포
az webapp deployment source config-zip \
--resource-group my-rg \
--name my-app \
--slot staging \
--src app.zip
# 슬롯 스왑 (Blue/Green 전환)
az webapp deployment slot swap \
--resource-group my-rg \
--name my-app \
--slot staging \
--target-slot production
# 롤백 (다시 스왑)
az webapp deployment slot swap \
--resource-group my-rg \
--name my-app \
--slot staging \
--target-slot productionAzure Traffic Manager
// traffic-manager.json (ARM Template)
{
"type": "Microsoft.Network/trafficManagerProfiles",
"apiVersion": "2018-08-01",
"name": "my-app-tm",
"location": "global",
"properties": {
"trafficRoutingMethod": "Weighted",
"endpoints": [
{
"name": "blue-endpoint",
"type": "Microsoft.Network/trafficManagerProfiles/externalEndpoints",
"properties": {
"target": "my-app-blue.azurewebsites.net",
"endpointStatus": "Enabled",
"weight": 100
}
},
{
"name": "green-endpoint",
"type": "Microsoft.Network/trafficManagerProfiles/externalEndpoints",
"properties": {
"target": "my-app-green.azurewebsites.net",
"endpointStatus": "Enabled",
"weight": 0
}
}
]
}
}각 엔드포인트의 weight 값(0/100)이 곧 트래픽 비중이고, endpointStatus로 엔드포인트를 켜고 끈다. Blue를 100, Green을 0으로 두었다가 둘을 맞바꿔 전환한다.
GitHub Actions 자동화
파이프라인을 헬스체크, 승인, 전환 후 모니터링 단계로 나눠 배포 안전성을 확보한다. 빌드 성공만으로 전환하지 않고, 각 단계가 실패하면 다음 잡으로 넘어가지 않게 막는다.
완전 자동화 파이프라인
다섯 개 잡이 build → deploy-green → test-green → promote로 이어지고, rollback은 workflow_dispatch 수동 트리거로 분리된다.
Configure AWS credentials 스텝은 잡마다 동일하므로, 아래에서는 첫 잡(build)에만 전체로 두고 나머지는 주석으로 줄였다.
# .github/workflows/blue-green-deploy.yml
name: Blue/Green Deployment
on:
push:
branches: [main]
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: true
default: 'deploy'
type: choice
options:
- deploy
- promote
- rollback
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-app
ECS_CLUSTER: my-cluster
ECS_SERVICE: my-app-service
jobs:
build:
runs-on: ubuntu-latest
if: github.event.inputs.action != 'rollback'
outputs:
image: ${{ steps.build-image.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
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: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
deploy-green:
needs: build
runs-on: ubuntu-latest
if: github.event.inputs.action != 'rollback'
steps:
- uses: actions/checkout@v4
# - name: Configure AWS credentials (build 잡과 동일, 생략)
- name: Deploy to Green environment
run: |
# Update task definition with new image
NEW_TASK_DEF=$(aws ecs describe-task-definition \
--task-definition my-app-green \
--query 'taskDefinition' | \
jq --arg IMAGE "${{ needs.build.outputs.image }}" \
'.containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')
# Register new task definition
NEW_TASK_ARN=$(aws ecs register-task-definition \
--cli-input-json "$NEW_TASK_DEF" \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
# Update Green service
aws ecs update-service \
--cluster $ECS_CLUSTER \
--service my-app-green \
--task-definition $NEW_TASK_ARN
- name: Wait for Green deployment
run: |
aws ecs wait services-stable \
--cluster $ECS_CLUSTER \
--services my-app-green
test-green:
needs: deploy-green
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run smoke tests on Green
run: |
GREEN_URL="${{ secrets.GREEN_INTERNAL_URL }}"
# Health check
for i in {1..10}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $GREEN_URL/health)
if [ "$STATUS" == "200" ]; then
echo "Health check passed"
break
fi
echo "Attempt $i: Status $STATUS, retrying..."
sleep 5
done
# API tests
./scripts/smoke-tests.sh $GREEN_URL
promote:
needs: test-green
runs-on: ubuntu-latest
if: github.event.inputs.action != 'rollback'
environment: production
steps:
# - name: Configure AWS credentials (build 잡과 동일, 생략)
- name: Switch traffic to Green
run: |
aws elbv2 modify-listener --listener-arn ${{ secrets.ALB_LISTENER_ARN }} \
--default-actions '[
{
"Type": "forward",
"ForwardConfig": {
"TargetGroups": [
{"TargetGroupArn": "${{ secrets.BLUE_TG_ARN }}", "Weight": 0},
{"TargetGroupArn": "${{ secrets.GREEN_TG_ARN }}", "Weight": 100}
]
}
}
]'
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployed ${{ github.sha }} to production",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Blue/Green Deployment Complete*\nCommit: `${{ github.sha }}`\nStatus: :white_check_mark: Success"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
rollback:
runs-on: ubuntu-latest
if: github.event.inputs.action == 'rollback'
environment: production
# promote 잡과 구조가 같다. 차이는 두 가지뿐:
# 1) Switch traffic의 weight를 Blue=100, Green=0으로 반대 설정
# 2) Slack 메시지를 롤백 알림으로 교체
steps:
# - Configure AWS credentials (build 잡과 동일)
# - Rollback to Blue: modify-listener로 Blue=100 / Green=0
# - Notify Slack (Rollback)Kubernetes + Argo Rollouts 파이프라인
# .github/workflows/k8s-blue-green.yml
name: K8s Blue/Green Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
- name: Setup kubectl
uses: azure/setup-kubectl@v3
- name: Configure kubeconfig
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Update Rollout image
run: |
kubectl argo rollouts set image my-app \
app=ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Wait for rollout
run: |
kubectl argo rollouts status my-app --timeout=10m
- name: Auto-promote (if tests pass)
run: |
# Wait for analysis to complete
sleep 60
# Check rollout status
STATUS=$(kubectl argo rollouts status my-app -o json | jq -r '.status')
if [ "$STATUS" == "Paused" ]; then
kubectl argo rollouts promote my-app
fiGitLab CI/CD 통합
GitLab CI는 build → deploy-green → test → promote → cleanup 스테이지로 같은 흐름을 구성한다.
promote와 rollback을 when: manual로 두어 수동 승인 후에만 트래픽을 전환한다.
GitLab CI 파이프라인
# .gitlab-ci.yml
stages:
- build
- deploy-green
- test
- promote
- cleanup
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
KUBE_CONTEXT: production
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
deploy-green:
stage: deploy-green
image: bitnami/kubectl:latest
script:
- kubectl config use-context $KUBE_CONTEXT
- |
kubectl set image deployment/my-app-green \
app=$DOCKER_IMAGE
- kubectl rollout status deployment/my-app-green --timeout=300s
environment:
name: green
url: https://green.my-app.example.com
test-green:
stage: test
image: curlimages/curl:latest
script:
- |
for i in $(seq 1 10); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://green.my-app.example.com/health)
if [ "$STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i failed with status $STATUS"
sleep 5
done
exit 1
needs:
- deploy-green
promote:
stage: promote
image: bitnami/kubectl:latest
script:
- kubectl config use-context $KUBE_CONTEXT
- |
kubectl patch service my-app -p '{"spec":{"selector":{"color":"green"}}}'
- echo "Traffic switched to Green"
environment:
name: production
url: https://my-app.example.com
when: manual
needs:
- test-green
rollback:
stage: promote
image: bitnami/kubectl:latest
# promote와 동일하고, selector만 green -> blue로 되돌린다.
script:
- kubectl config use-context $KUBE_CONTEXT
- kubectl patch service my-app -p '{"spec":{"selector":{"color":"blue"}}}'
when: manual
needs:
- test-green
cleanup-old-blue:
stage: cleanup
image: bitnami/kubectl:latest
script:
- kubectl config use-context $KUBE_CONTEXT
# 이전 Blue를 새 버전으로 갱신해 다음 배포의 대기 환경으로 만든다.
# 주의: 이 시점부터 구버전이 사라져 롤백 대상이 없어지므로,
# 전환 후 모니터링 기간이 끝난 뒤에만 실행한다.
- |
kubectl set image deployment/my-app-blue \
app=$DOCKER_IMAGE
when: manual
needs:
- promoteGitLab 환경 변수 설정
# GitLab CI/CD Variables 설정
KUBECONFIG # Kubernetes 설정 (base64 encoded)
CI_REGISTRY # Container Registry URL
AWS_ACCESS_KEY_ID # AWS 인증 (선택)
AWS_SECRET_ACCESS_KEY
SLACK_WEBHOOK_URL # 알림용배포 파이프라인 비교
세 도구의 기능 차이는 아래 표처럼 크지 않으므로, 이미 쓰는 VCS/권한 체계와 통합이 쉬운 쪽을 고른다. GitHub Actions/GitLab CI는 설정이 단순하고 환경 승인이 기본 내장이며, Jenkins는 플러그인으로 같은 기능을 채운다.
| 기능 | GitHub Actions | GitLab CI | Jenkins |
|---|---|---|---|
| 설정 복잡도 | 낮음 | 낮음 | 높음 |
| 병렬 실행 | 기본 지원 | 기본 지원 | 플러그인 |
| 환경 승인 | Environments | Environments | 플러그인 |
| 비밀 관리 | Secrets | Variables | Credentials |
| Self-hosted | 지원 | 지원 | 기본 |
| 비용 | 무료 티어 있음 | 무료 티어 있음 | 무료 (인프라 비용) |
정리
세 클라우드의 전환 메커니즘은 다르지만 원리는 같다. AWS는 ALB Target Group 가중치, GCP는 Cloud Run 태그·URL Map, Azure는 Deployment Slot 스왑으로 트래픽을 넘긴다. CI/CD 파이프라인의 핵심은 배포 자동화 자체가 아니라 검증, 승인, 전환, 모니터링을 별도 단계로 분리하고 롤백 경로를 항상 열어 두는 데 있다. CodeDeploy의 CloudWatch 알람 자동 롤백이나 promote/rollback의 수동 승인처럼, 전환 후 문제를 빠르게 되돌릴 수 있는 구조를 먼저 설계한다. 앞선 두 편에서 다룬 Blue/Green의 개념과 무중단 전환 절차를 클라우드와 CI/CD로 옮긴 것이 이 글의 내용이다.