目次
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