이종관
Back to Posts

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

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

2026년 1월 27일·11 min read·
infra
deployment
blue-green
aws
gcp
azure
github-actions
gitlab-ci
cicd

이 글은 Blue/Green 배포 시리즈의 세 번째 글입니다.

  1. 기초와 전략
  2. Kubernetes 구현
  3. 클라우드 & CI/CD 통합 (현재 글)
  4. 운영 및 최적화

1. AWS에서의 Blue/Green 배포

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

1.1 ALB + Target Group 아키텍처

1.2 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 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
      }
    }
  }
}

1.3 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.4 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"

2. GCP에서의 Blue/Green 배포

2.1 Cloud Load Balancing 아키텍처

2.2 Cloud Run (Serverless Blue/Green)

# 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

2.3 GKE + Istio

# 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

3. Azure에서의 Blue/Green 배포

3.1 Application Gateway + Deployment Slots

3.2 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 production

3.3 Azure 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
        }
      }
    ]
  }
}

4. GitHub Actions 자동화

4.1 완전 자동화 파이프라인

# .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 }}

4.2 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
          fi

5. GitLab CI/CD 통합

5.1 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
  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

5.2 GitLab 환경 변수 설정

# 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   # 알림용

6. 배포 파이프라인 비교

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

7. 다음 단계

이 글에서는 주요 클라우드 환경과 CI/CD 도구를 통한 Blue/Green 배포 자동화를 다루었습니다.

다음 글에서는 운영 및 최적화를 다룹니다:

  • 모니터링 및 알림 설정
  • 비용 최적화 전략
  • 네트워킹 고려사항
  • 트러블슈팅 가이드
  • 실제 사례 연구

이전 글: Blue/Green 배포 (2) - Kubernetes 구현 ← 다음 글: Blue/Green 배포 (4) - 운영 및 최적화 →