이종관
글 목록으로

Blue/Green 배포 (3): 클라우드 & CI/CD 통합

AWS, GCP, Azure에서의 Blue/Green 구현과 GitHub Actions, GitLab CI 자동화 파이프라인

2026년 1월 27일·12 min read·
infra
deployment
blue-green
cicd
cloud

TL;DR

  • 클라우드별 Blue/Green 핵심은 "로드밸런서에서 트래픽 가중치 전환"이다.
  • CI/CD 파이프라인은 배포 자동화보다 검증 -> 승인 -> 전환 -> 모니터링 흐름 설계가 더 중요하다.
  • GitHub Actions/GitLab CI 모두 구현 가능하며, 운영 표준화가 선택 기준이다.

AWS에서의 Blue/Green 배포

AWS에서는 ALB Target Group 가중치 제어와 CodeDeploy를 조합하면 운영 난이도를 크게 낮출 수 있다. 핵심은 인프라를 IaC로 고정하고, 전환 명령을 스크립트화해 반복 가능하게 만드는 것이다.

AWS는 여러 서비스를 통해 Blue/Green 배포를 지원한다.

ALB + Target Group 아키텍처

Terraform으로 인프라 구성

hcl
# 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 Target Group
resource "aws_lb_target_group" "blue" {
  name        = "my-app-blue"
  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-blue"
    Color = "blue"
  }
}
 
# Green Target Group
resource "aws_lb_target_group" "green" {
  name        = "my-app-green"
  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-green"
    Color = "green"
  }
}
 
# Listener with weighted routing
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.blue.arn
        weight = 100
      }
      target_group {
        arn    = aws_lb_target_group.green.arn
        weight = 0
      }
    }
  }
}

AWS CLI로 트래픽 전환

bash
#!/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!"

AWS CodeDeploy (ECS Blue/Green)

yaml
# 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"

GCP에서의 Blue/Green 배포

GCP는 Cloud Run 태그 트래픽 분할과 GKE+Istio 조합으로 Serverless부터 Kubernetes까지 일관된 전략을 적용할 수 있다. 서비스 특성에 따라 "간단한 트래픽 분할"과 "세밀한 메쉬 라우팅" 중 하나를 선택하면 된다.

Cloud Load Balancing 아키텍처

Cloud Run (Serverless Blue/Green)

bash
# Cloud Run에서 트래픽 분할
# 새 버전 배포 (트래픽 없이)
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)
gcloud run services update-traffic my-app \
  --region us-central1 \
  --to-latest

GKE + Istio

yaml
# 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: 80

Azure에서의 Blue/Green 배포

Azure는 App Service Deployment Slots를 중심으로 전환 안정성을 확보하기 쉽다. 트래픽 단위 전환이 필요하면 Traffic Manager/Application Gateway와 결합해 점진 배포를 구성할 수 있다.

Application Gateway + Deployment Slots

App Service Deployment Slots

bash
# 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 production

Azure Traffic Manager

json
// 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
        }
      }
    ]
  }
}

GitHub Actions 자동화

파이프라인은 빌드 성공보다 "배포 안전성"을 보장하는 게 목적이다. 그래서 헬스체크, 승인 단계, 전환 후 모니터링 단계를 분리해 두는 구성이 유효한다.

완전 자동화 파이프라인

yaml
# .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
    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
        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: 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
        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: 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
 
    steps:
      - 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: Rollback to Blue
        run: |
          aws elbv2 modify-listener --listener-arn ${{ secrets.ALB_LISTENER_ARN }} \
            --default-actions '[
              {
                "Type": "forward",
                "ForwardConfig": {
                  "TargetGroups": [
                    {"TargetGroupArn": "${{ secrets.BLUE_TG_ARN }}", "Weight": 100},
                    {"TargetGroupArn": "${{ secrets.GREEN_TG_ARN }}", "Weight": 0}
                  ]
                }
              }
            ]'
 
      - name: Notify Slack (Rollback)
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": ":warning: Rollback executed",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Rollback Executed*\nTraffic switched back to Blue environment"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Kubernetes + Argo Rollouts 파이프라인

yaml
# .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
          fi

GitLab CI/CD 통합

GitLab CI에서도 동일한 운영 패턴을 적용할 수 있다. 핵심은 환경 변수/시크릿 관리와 수동 승인 정책을 명확히 하여 사고 시 즉시 롤백 가능성을 확보하는 것이다.

GitLab CI 파이프라인

yaml
# .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
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - |
      kubectl patch service my-app -p '{"spec":{"selector":{"color":"blue"}}}'
    - echo "Rolled back to Blue"
  when: manual
  needs:
    - test-green
 
cleanup-old-blue:
  stage: cleanup
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context $KUBE_CONTEXT
    # 이전 Blue를 새로운 Green 대기 환경으로 전환
    - |
      kubectl set image deployment/my-app-blue \
        app=$DOCKER_IMAGE
  when: manual
  needs:
    - promote

GitLab 환경 변수 설정

bash
# 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 ActionsGitLab CIJenkins
설정 복잡도낮음낮음높음
병렬 실행기본 지원기본 지원플러그인
환경 승인EnvironmentsEnvironments플러그인
비밀 관리SecretsVariablesCredentials
Self-hosted지원지원기본
비용무료 티어 있음무료 티어 있음무료 (인프라 비용)