목차
24 장

CI/CD 파이프라인

23장까지 갖춰진 myshop-api는 새 버전이 들어오는 과정에 여전히 사람이 많이 개입합니다. 이번 챕터는 그 과정을 자동화합니다. GitHub Actions에서 OIDC 신뢰로 정적 키 없이 AWS ECR에 컨테이너 이미지를 푸시하고, 매니페스트 repo의 Helm values를 자동 commit 하고, 20장에서 다룬 ArgoCD가 그 변경을 감지해 클러스터로 동기화하는 한 사이클을 정리합니다. PR 승인 게이트, dev / prod 분기, Argo Rollouts 카나리 배포, 이미지 태그 immutability까지 함께 다룹니다.

23장 DB 연동까지 거쳐 myshop-api는 EKS · RDS · Secrets · 커넥션 풀까지 갖춘 완전한 서비스이지만, 새 버전이 들어오는 과정에 여전히 사람이 많이 개입합니다. 누가 컨테이너를 빌드해 push 하고, 누가 매니페스트의 이미지 태그를 바꾸고, 누가 helm upgrade를 돌립니다. 이번 챕터는 이 흐름 전체를 코드로 자동화합니다. GitHub Actions가 OIDC 신뢰로 정적 키 없이 ECR에 이미지를 push 하고, 매니페스트 repo의 Helm values를 자동 commit 하고, 20장 GitOps에서 다룬 ArgoCD가 그 변경을 감지해 클러스터로 동기화하는 한 사이클입니다.

이번 챕터의 목표는 코드 push 한 번에 dev에 자동 배포되고, git tag 한 번에 prod 배포가 큐에 들어가는 상태입니다. 운영 표준의 PR 승인 게이트와 카나리 자동 promote / rollback까지 함께 다룹니다.

두 repo 모델 — 코드와 매니페스트의 분리 #

GitOps의 가장 흔한 패턴은 repo 두 개의 분리입니다. 20장 GitOps의 §“한 repo vs 두 repo"에서 짚었던 모델이 본격적인 운영 파이프라인으로 다뤄집니다.

repo역할
myshop-api (애플리케이션 repo)소스 코드, Dockerfile, GitHub Actions workflow
myshop-manifests (매니페스트 repo)Helm values, ArgoCD Application 매니페스트, 환경별 설정

이 분리의 이점이 셋입니다.

  • 권한의 분리 — 코드 변경과 인프라 / 배포 변경의 리뷰어가 달라질 수 있습니다.
  • 변경의 명확성 — git log를 보면 “이 시점에 어느 버전이 prod에 떠 있었는가"가 명확합니다.
  • ArgoCD가 한 곳만 보면 됨 — 매니페스트 repo만 watch 하면 모든 환경의 desired state가 잡힙니다.

코드 push의 흐름이 다음 한 줄로 표현됩니다.

GitOps의 한 사이클
[개발자 push] -> [GitHub Actions: 빌드 / 테스트 / ECR push]
              -> [매니페스트 repo 의 image tag 자동 commit]
              -> [ArgoCD 가 변경 감지]
              -> [클러스터에 새 버전 배포]

각 단계를 한 절씩 풀어 갑니다.

GitHub Actions — OIDC로 AWS 자격 증명을 동적으로 #

GitHub Actions에서 AWS API를 부르는 옛 방법은 IAM 사용자의 access key / secret key를 GitHub Secrets에 저장하는 것이었습니다. 이 방식의 문제는 명백합니다 — 키가 정적이라 회전이 어렵고, 한 번 새면 영향이 큽니다.

새 표준은 OIDC trust입니다. GitHub Actions가 JWT 토큰을 발급하고, AWS IAM이 그 토큰을 검증해 임시 자격 증명을 발급해 주는 모델 — 16장 RBAC / ServiceAccount 깊이의 IRSA와 같은 구조입니다. ServiceAccount의 projected token이 GitHub Actions의 JWT로 자리만 바뀐 모양입니다.

OIDC provider 등록 (Terraform) #

terraform/modules/github-oidc/main.tf
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions_ecr_push" {
  name = "github-actions-myshop-api-ecr-push"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:myshop/myshop-api:ref:refs/heads/main"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ecr_push" {
  role       = aws_iam_role.github_actions_ecr_push.name
  policy_arn = aws_iam_policy.ecr_push.arn
}

Conditionsub가 핵심입니다 — myshop/myshop-api repo의 main 브랜치에서 트리거된 워크플로만 이 Role을 가져갈 수 있습니다. 다른 repo, 다른 브랜치, 다른 fork는 모두 거부됩니다. 16장의 IRSA trust policy가 namespace + ServiceAccount 이름으로 격리했던 방식이, GitHub Actions에서는 repo + branch로 바뀝니다.

Workflow — 빌드와 push #

.github/workflows/build.yml
name: Build and push

on:
  push:
    branches: [main]
    tags: ['v*']

permissions:
  id-token: write    # OIDC 토큰 발급에 필요
  contents: read

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: myshop-api

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set image tag
        id: meta
        run: |
          if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
            echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
          else
            echo "tag=main-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
          fi

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-myshop-api-ecr-push
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ steps.meta.outputs.tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Update manifest repo
        env:
          GH_TOKEN: ${{ secrets.MANIFESTS_REPO_TOKEN }}
        run: |
          gh api repos/myshop/myshop-manifests/dispatches \
            -f event_type=update-image \
            -F client_payload[app]=myshop-api \
            -F client_payload[tag]=${{ steps.meta.outputs.tag }} \
            -F client_payload[env]=dev

핵심 단계 셋을 짚습니다.

  • Configure AWS credentials (OIDC) — 위에서 만든 IAM Role을 OIDC로 AssumeRoleWithWebIdentity. 이 한 단계로 정적 키 없이 임시 자격 증명을 받습니다.
  • Build and push — Docker buildx로 멀티 플랫폼 빌드 + ECR push. GHA cache로 layer 캐싱이 자동입니다.
  • Update manifest reporepository_dispatch 이벤트로 매니페스트 repo의 다른 워크플로를 트리거합니다. 이 워크플로가 Helm values를 자동 commit 합니다.

매니페스트 repo의 자동 commit #

매니페스트 repo에는 위 dispatch를 받아 values 파일을 갱신하는 워크플로를 둡니다.

myshop-manifests/.github/workflows/update-image.yml
name: Update image tag

on:
  repository_dispatch:
    types: [update-image]

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Update values
        run: |
          APP=${{ github.event.client_payload.app }}
          TAG=${{ github.event.client_payload.tag }}
          ENV=${{ github.event.client_payload.env }}

          yq -i ".image.tag = \"$TAG\"" charts/$APP/values-$ENV.yaml

      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add charts/
          git commit -m "chore: bump ${{ github.event.client_payload.app }} to ${{ github.event.client_payload.tag }} (${{ github.event.client_payload.env }})"
          git push

이 commit이 매니페스트 repo의 main 브랜치에 들어가면, ArgoCD가 그 변경을 watch 하고 있다가 클러스터에 자동 동기화합니다. 22장values-dev.yaml / values-prod.yaml의 두 파일이 본 챕터의 자동 commit의 대상이 되는 모양입니다.

ArgoCD — 매니페스트 repo의 watcher #

20장 GitOps에서 다룬 ArgoCD의 모델을 그대로 씁니다. Application CRD 매니페스트 한 장이 myshop-api 한 환경의 배포를 담당합니다.

ArgoCD 설치 #

ArgoCD Helm 설치
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
  -n argocd --create-namespace \
  --values argocd-values.yaml
argocd-values.yaml — 일부
server:
  ingress:
    enabled: true
    ingressClassName: alb
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
    hosts:
      - argocd.myshop.example.com

configs:
  cm:
    timeout.reconciliation: 30s

ArgoCD UI는 argocd.myshop.example.com으로 노출됩니다. 22장에서 만든 AWS Load Balancer Controller가 이 Ingress도 ALB로 풀어 주는 모양입니다. 운영 환경에서는 SSO (GitHub, Google)와 묶어 두는 게 표준이고, 14장 RBAC / NetworkPolicy / ResourceQuota에서 본 RBAC 모델이 ArgoCD UI의 권한 모델로도 그대로 옮겨 옵니다.

myshop-api Application #

argocd/applications/myshop-api-prod.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myshop-api-prod
  namespace: argocd
spec:
  project: myshop

  source:
    repoURL: https://github.com/myshop/myshop-manifests.git
    targetRevision: main
    path: charts/myshop-api
    helm:
      valueFiles:
        - values.yaml
        - values-prod.yaml

  destination:
    server: https://kubernetes.default.svc
    namespace: myshop

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - ServerSideApply=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        maxDuration: 3m

세 옵션의 결을 짚습니다.

  • automated — git의 변경이 즉시 클러스터에 반영됩니다. dev에 적합한 모드입니다.
  • selfHeal: true — 누가 kubectl edit로 직접 수정해도 git 매니페스트로 자동 복구합니다.
  • prune: true — git에서 사라진 객체는 클러스터에서도 삭제됩니다.

23장의 Helm hook으로 만든 마이그레이션 Job은 ArgoCD에서는 PreSync hook으로 자동 변환됩니다. ArgoCD가 새 매니페스트를 적용하기 전에 마이그레이션 Job을 먼저 돌리고, 그 Job이 성공해야 다음 단계로 넘어가는 흐름이 GitOps 안으로 자연스럽게 흡수됩니다.

dev vs prod — 자동 sync 분기 #

prod는 자동 sync를 끄고 수동 트리거로 가는 패턴이 자주 쓰입니다.

myshop-api-prod.yaml — 수동 sync
syncPolicy:
  syncOptions:
    - CreateNamespace=true
    - ServerSideApply=true
  # automated 절을 제거 -> 수동 sync 모드

배포 흐름이 다음과 같이 분기됩니다.

dev vs prod 배포 흐름
[dev]
git push -> GitHub Actions 빌드 -> ECR push
        -> 매니페스트 repo commit (values-dev.yaml)
        -> ArgoCD 자동 sync -> dev 클러스터 배포

[prod]
git tag v1.5.0 -> GitHub Actions 빌드 -> ECR push
              -> 매니페스트 repo commit (values-prod.yaml)
              -> ArgoCD UI 에서 사람이 "Sync" 클릭
              -> prod 클러스터 배포

prod 배포의 사람 게이트가 안전장치입니다. 매니페스트 자체는 git PR로 리뷰되고, 실제 적용은 운영자가 한 번 더 확인합니다. 26장 운영 체크리스트의 변경 관리 절차에서 이 이중 게이트가 본격적으로 다뤄집니다.

Application 묶음의 표준 — App of Apps #

ArgoCD에 Application 매니페스트를 손으로 적용하지 않고, 한 루트 Application이 다른 Application 들을 watch 하게 만드는 패턴입니다.

argocd/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  source:
    repoURL: https://github.com/myshop/myshop-manifests.git
    targetRevision: main
    path: argocd/applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true

argocd/applications/ 디렉터리에 새 Application을 만들면 자동으로 ArgoCD에 등록되고, 그 Application이 자기 매니페스트를 동기화합니다. 클러스터 자체의 운영도 GitOps 안으로 들어옵니다. 20장 §“App of Apps"에서 짚었던 모델이 본격적인 멀티 환경 운영의 표준 셋업으로 자리 잡는 단계입니다.

Image Updater — image tag 갱신을 ArgoCD로 #

위 흐름은 GitHub Actions가 매니페스트 repo에 commit 해 image tag를 갱신했습니다. ArgoCD Image Updater는 이 단계를 ArgoCD로 옮기는 옵션입니다.

myshop-api-prod.yaml — Image Updater annotation
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: helmvalues:./charts/myshop-api/values-prod.yaml

ArgoCD Image Updater가 ECR을 정기적으로 polling 하다가 새 태그를 발견하면 매니페스트 repo에 자동 commit 합니다. GitHub Actions의 commit 단계가 필요 없어지지만, polling 주기가 5분 단위라 즉시성은 떨어집니다. 코드 push와 매니페스트 commit의 순서를 git에 명확히 남기고 싶다면 GitHub Actions commit 모델이 더 직관적입니다. 본 책의 표준 경로는 GitHub Actions commit이고, Image Updater는 다중 클러스터 환경에서의 옵션 정도로 짚어 둡니다.

카나리 · 블루-그린 — Argo Rollouts #

표준 Deployment의 RollingUpdate는 가장 단순한 무중단 배포 모델입니다. 4장 Deployment / ReplicaSet에서 다룬 그 모델 위에, 더 정교한 패턴 (카나리, 블루-그린, 자동 분석 후 promote)은 Argo Rollouts가 풀어 줍니다.

rollout.yaml — 5% 카나리 -> 분석 -> 100%
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myshop-api
  namespace: myshop
spec:
  replicas: 10
  strategy:
    canary:
      canaryService: myshop-api-canary
      stableService: myshop-api-stable
      trafficRouting:
        alb:
          ingress: myshop-api
          servicePort: 80
      steps:
        - setWeight: 5
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 25
        - pause: { duration: 10m }
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100
  selector:
    matchLabels:
      app.kubernetes.io/name: myshop-api
  template:
    spec:
      containers:
        - name: api
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api:1.5.0
          # ... (Deployment 와 동일한 spec)

새 버전이 5% 트래픽으로 5분 → 자동 분석 (Prometheus 메트릭 쿼리) → 통과하면 25% → 50% → 100% 순서로 점진 전환합니다. 분석 단계에서 실패가 감지되면 자동으로 롤백됩니다.

analysistemplate.yaml — 성공률 분석
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: success-rate
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc:9090
          query: |
            sum(rate(http_requests_total{app="myshop-api",status=~"2.."}[5m]))
              / sum(rate(http_requests_total{app="myshop-api"}[5m]))
      successCondition: result[0] >= 0.99
      failureLimit: 1

25장 모니터링 · 알람에서 다룰 Prometheus 메트릭이 이 단계에서 카나리의 자동 promote / rollback 결정에 직접 들어갑니다. Argo Rollouts는 19장 옵저버빌리티의 메트릭 스택과 묶일 때 진가가 드러납니다 — 사람이 보는 대시보드가 아니라 자동화의 입력 데이터로 메트릭이 쓰이는 모양입니다.

PR 흐름의 표준 — environments + required reviewers #

GitHub Actions의 운영 표준 게이트도 잡아 둡니다.

.github/workflows/build.yml — environment 사용
jobs:
  build-prod:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://api.myshop.example.com
    steps:
      - ...

environment: production을 GitHub Settings에서 만들고 Required reviewers를 지정하면, 그 환경으로 가는 워크플로는 사람의 승인 없이는 시작되지 않습니다. tag 한 번에 prod 배포가 자동 시작되는 것을 막는 표준 패턴입니다. ArgoCD UI의 수동 Sync와 합쳐 빌드 단계 + 배포 단계의 이중 게이트가 만들어집니다.

첫 사이클의 점검 #

GitHub Actions push → ECR → 매니페스트 commit → ArgoCD sync 까지가 한 번 돈 시점에서 점검할 항목들입니다.

ECR 이미지 확인
aws ecr describe-images \
  --repository-name myshop-api \
  --region ap-northeast-2 \
  --query 'imageDetails[*].[imageTags,imagePushedAt]' \
  --output table
ArgoCD Application 상태
argocd app get myshop-api-prod
argocd app sync myshop-api-prod   # 수동 sync (prod 의 경우)
argocd app history myshop-api-prod
배포된 이미지 태그가 맞는가
kubectl get deployment myshop-api -n myshop \
  -o jsonpath='{.spec.template.spec.containers[0].image}'

세 명령이 일관되게 새 태그를 가리키면 한 사이클이 정상 동작 중입니다. ArgoCD UI에서는 같은 정보가 시각적으로 표시되고, 매니페스트와 클러스터 사이의 drift도 한눈에 보입니다. 만약 ArgoCD가 OutOfSync로 멈춰 있다면 27장 kubectl 디버깅 패턴의 GitOps 디버깅 절을 참고합니다 — values 파일의 형식 오류, ECR 이미지 권한 부족 (ImagePullBackOff), 매니페스트 repo의 trust가 가장 흔한 세 가지 원인입니다.

한 가지 함정 — 컨테이너 이미지 태그의 mutability #

운영 표준은 이미지 태그를 immutable로 두는 것입니다. 같은 태그가 다른 이미지를 가리키게 하면 ArgoCD의 drift detection이 의미를 잃습니다. 다음 셋업이 필수입니다.

  • ECR repository에 immutable tags 활성화 — Terraform으로 image_tag_mutability = "IMMUTABLE"을 켭니다.
  • latest 태그를 prod에서 절대 사용하지 않기 — 항상 git SHA 또는 semver입니다.
  • 이미지 태그 = git commit hash 또는 git tag — 어느 commit이 어느 환경에 떠 있는가가 한눈에 보입니다.

이 셋업이 빠지면 “어제까지 동작하던 그 태그가 오늘은 다른 이미지"라는 사고가 발생합니다. GitOps의 source of truth가 깨지는 지점입니다. 20장 GitOps §“git이 단일 소스가 되려면"에서 짚었던 원칙이 본 챕터의 ECR 셋업에서 구체적인 형태로 이어집니다.

연습문제 #

  1. 본 챕터의 GitHub OIDC Terraform 매니페스트를 적용해 본인 GitHub 조직의 한 repo에서 정적 키 없이 ECR push가 되도록 셋업합니다. Condition.sub의 패턴을 repo:org/repo:ref:refs/heads/mainrepo:org/repo:environment:production 두 가지로 바꿔 가며 각각이 어떤 트리거에서만 동작하는지 비교합니다. 환경 기반 격리가 브랜치 기반 격리와 어떻게 다른 보안 결을 만드는지 한 단락으로 정리합니다.
  2. dev와 prod의 두 ArgoCD Application 매니페스트를 만들고, dev는 automated.prune + selfHeal, prod는 수동 sync로 분기해 봅니다. dev에서 kubectl edit으로 Deployment의 replicas를 임의 변경했을 때 selfHeal이 몇 초 만에 git의 값으로 되돌리는지를 측정합니다. 같은 동작이 prod에서는 왜 위험한지, 26장의 운영 결에 비춰 한 단락으로 설명합니다.
  3. Argo Rollouts의 카나리 매니페스트를 적용해 myshop-api의 새 버전을 5% → 25% → 100%로 자동 promote 해 봅니다. 일부러 5xx를 반환하는 버전을 배포해 분석 단계가 실패를 감지하고 자동 롤백되는 모습을 관찰합니다. 이 자동 분석의 입력이 되는 Prometheus 쿼리가 25장의 알람 룰과 어떻게 연결되는지 메모합니다.

한 줄 요약: 운영 클러스터의 CI/CD 표준은 GitHub Actions OIDC + ECR + 매니페스트 repo 자동 commit + ArgoCD watch의 네 단계가 한 사이클로 동작하는 GitOps 파이프라인이다. 두 repo 분리는 권한 · 변경 추적 · ArgoCD의 단일 watch 대상을 한 번에 풀고, dev는 automated sync + selfHeal로, prod는 수동 sync + GitHub environment의 이중 게이트로 분기한다. Argo Rollouts의 카나리는 Prometheus 메트릭을 자동화의 입력으로 활용해 promote / rollback을 사람의 손에서 코드로 옮긴다. 이미지 태그 immutability가 빠지면 GitOps의 source of truth가 깨진다.

다음 챕터 #

이 시점에서 myshop-api는 코드 push 한 번에 dev에 자동 배포되고, git tag 한 번에 prod 배포가 큐에 들어가는 패턴이 자리 잡았습니다. 그러나 그 모든 동작을 들여다보는 한 층이 아직 없습니다.

다음 챕터에서는 그 빈 곳을 채웁니다. 25장 모니터링 · 알람에서는 Prometheus + Grafana + Alertmanager + CloudWatch로 구성하는 운영 클러스터의 옵저버빌리티 스택과 핵심 알람 룰셋을 다룹니다. 19장 옵저버빌리티의 메트릭 · 로그 · 트레이스 모델이 본격적인 AWS 결합 운영 셋업으로 이어집니다.

X