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 の流れが次の一行で表現されます。
[開発者 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) #
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
}Condition の sub が核心です — myshop/myshop-api repo の main ブランチでトリガーされたワークフローだけがこの Role を取得できます。別の repo、別のブランチ、別の fork はすべて拒否されます。第16章 の IRSA trust policy が namespace + ServiceAccount 名で隔離していた方式が、GitHub Actions では repo + branch に変わります。
Workflow — ビルドと push #
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 repo—repository_dispatchイベントでマニフェスト repo の別のワークフローをトリガーします。このワークフローが Helm values を自動 commit します。
マニフェスト repo の自動 commit #
マニフェスト repo には、上の dispatch を受けて values ファイルを更新するワークフローを置きます。
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 インストール #
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
-n argocd --create-namespace \
--values argocd-values.yamlserver:
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: 30sArgoCD 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 #
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 を切って手動トリガーで行くパターンがよく使われます。
syncPolicy:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
# automated 節を削除 -> 手動 sync モード配備の流れが次のように分岐します。
[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 するようにするパターンです。
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: trueargocd/applications/ ディレクトリに新しい Application を作ると自動で ArgoCD に登録され、その Application が自分のマニフェストを同期します。クラスタ自体の運用も GitOps の中に入ってきます。第20章 §「App of Apps」で触れたモデルが、本格的なマルチ環境運用の標準セットアップとして定着する段階です。
Image Updater — image tag 更新を ArgoCD で #
上の流れは GitHub Actions がマニフェスト repo に commit して image tag を更新していました。ArgoCD Image Updater はこの段階を ArgoCD へ移すオプションです。
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.yamlArgoCD 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 が解いてくれます。
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 % の順で漸進的に切り替えます。分析段階で失敗が検知されると自動でロールバックされます。
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 の運用標準ゲートも固めておきます。
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 までが一度回った時点で点検する項目です。
aws ecr describe-images \
--repository-name myshop-api \
--region ap-northeast-2 \
--query 'imageDetails[*].[imageTags,imagePushedAt]' \
--output tableargocd app get myshop-api-prod
argocd app sync myshop-api-prod # 手動 sync (prod の場合)
argocd app history myshop-api-prodkubectl 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 セットアップで具体的な形につながります。
練習問題 #
- 本章の GitHub OIDC Terraform マニフェストを適用して、ご自身の GitHub 組織の一つの repo から静的キーなしに ECR push ができるようセットアップします。
Condition.subのパターンをrepo:org/repo:ref:refs/heads/mainとrepo:org/repo:environment:productionの二つに変えながら、それぞれがどのトリガーでだけ動作するかを比較します。環境ベースの隔離がブランチベースの隔離とどう異なるセキュリティの質感を作るかを一段落で整理します。 - dev と prod の二つの ArgoCD Application マニフェストを作り、dev は
automated.prune + selfHeal、prod は手動 sync に分岐してみます。dev でkubectl editで Deployment の replicas を任意に変更したとき、selfHeal が何秒で git の値へ戻すかを計測します。同じ動作が prod ではなぜ危険かを、第26章 の運用の質感に照らして一段落で説明します。 - 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 連携運用セットアップへつながります。