K8s 実戦 #4 CI/CD パイプライン — GitHub Actions / ECR / ArgoCD
K8s 実戦シリーズの 4 番目の記事です。#3 まで経て myshop-api は EKS・RDS・Secrets・コネクションプールまで備えた完全なサービスですが、新しいバージョンが入ってくる過程は依然として人の手に縛られています。誰かがコンテナをビルドして push し、誰かがマニフェストの image tag を変え、誰かが helm upgrade を回します。この記事ではこの流れ全体をコードで自動化します。GitHub Actions が OIDC 信頼で静的キーなしに ECR に image を push し、マニフェスト repo の Helm values を自動 commit し、上級 #6 で扱った ArgoCD がその変更を watch してクラスタに同期する 1 サイクルです。
このシリーズは K8s 実戦 6 編です。
- #1 EKS クラスタセットアップ — Terraform / eksctl / IRSA / アドオン
- #2 アプリデプロイ骨格 — Deployment / Service / Ingress / Helm
- #3 DB 連動 — RDS / Secrets Manager / External Secrets / コネクションプール
- #4 CI/CD パイプライン — GitHub Actions / ECR / ArgoCD ← この記事
- #5 モニタリング・アラーム — Prometheus / CloudWatch / Alertmanager
- #6 運用チェックリスト — アップグレード / バックアップ・リカバリ / コスト / セキュリティ
2 つの repo モデル — コードとマニフェストの分離 #
GitOps のもっとも一般的なパターンは repo 2 つの分離です。
| repo | 役割 |
|---|---|
myshop-api (アプリケーション repo) | ソースコード、Dockerfile、GitHub Actions workflow |
myshop-manifests (マニフェスト repo) | Helm values、Application マニフェスト、環境別設定 |
この分離には 3 つの利点があります。
- 権限の分離 — コード変更とインフラ/デプロイ変更のレビュアーが異なりうる
- 変更の明確性 — git log を見れば「この時点でどのバージョンが prod に立っていたか」が明確
- ArgoCD が 1 か所を見ればよい — マニフェスト repo だけ watch すればすべての環境の desired state が押さえられる
コード push の流れが次の 1 行で表現されます。
[開発者 push] → [GitHub Actions: ビルド/テスト/ECR push]
→ [マニフェスト repo の image tag を自動 commit]
→ [ArgoCD が変更検知]
→ [クラスタに新バージョンデプロイ]各ステップを 1 節ずつ解いていきます。
GitHub Actions — OIDC で AWS 資格情報を動的に #
GitHub Actions から AWS API を呼ぶ古い方法は IAM ユーザーの access key / secret key を GitHub Secrets に保存することでした。この方式の問題は明らかです — キーが静的なので回転が難しく、一度漏れると影響が大きいです。
新しい標準は OIDC trust です。GitHub Actions が JWT トークンを発行し、AWS IAM がそのトークンを検証して一時資格情報を発行してくれるモデル — 上級 #2 IRSA と同じ仕組みです。
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 ブランチからトリガされた workflow だけがこの Role を取れます。別の repo、別のブランチ、別の fork はすべて拒否されます。
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主要ステップは 3 つです。
Configure AWS credentials (OIDC)— 上で作った IAM Role を OIDC でAssumeRoleWithWebIdentity。この 1 ステップで静的キーなしに一時資格情報を受け取る。Build and push— Docker buildx でマルチプラットフォームビルド + ECR push。GHA cache で layer キャッシング。Update manifest repo— repository_dispatch イベントでマニフェスト repo の別の workflow をトリガ。この workflow が Helm values を自動 commit します。
マニフェスト repo の自動 commit #
マニフェスト repo には上の dispatch を受けて values ファイルを更新する workflow を置きます。
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 していてクラスタに自動同期します。
ArgoCD — マニフェスト repo の watcher #
上級 #6 で扱った ArgoCD をそのまま使います。Application CRD マニフェスト 1 枚が myshop-api 1 つの環境のデプロイを担当します。
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 で公開されます。運用環境では SSO(GitHub、Google)と組み合わせておくのが標準です。
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: 3mautomated— git の変更が即座にクラスタに反映。dev に適合。selfHeal: true— 誰かがkubectl editで直接修正しても git マニフェストで自動回復。prune: true— git から消えたオブジェクトはクラスタからも削除。
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 でレビューされ、実際の適用は運用者がもう 1 度確認します。
Application 束の標準 — App of Apps #
ArgoCD に Application マニフェストを手で適用せず、1 つのルート 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 の中に入ってきます。
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 していて新しい tag を発見するとマニフェスト repo に自動 commit します。GitHub Actions の commit ステップが必要なくなりますが、polling 周期が 5 分単位なので即時性は落ちます。コード push とマニフェスト commit の順序を git に明確に残したければ GitHub Actions commit モデルがより直感的です。
カナリー・ブルーグリーン — Argo Rollouts #
標準 Deployment の RollingUpdate はもっとも単純な無中断デプロイモデルです。より精緻なパターン(カナリー、ブルーグリーン、自動分析後 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#5 で扱う Prometheus メトリクスがこの段階でカナリーの自動 promote / rollback の決定に直接入ります。Rollouts は #5 で扱ったオブザーバビリティスタックと組み合わさるときに真価を発揮します。
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 を指定すれば、その環境に行く workflow は人の承認なしには始まりません。tag 1 度で prod デプロイが自動で始まることを防ぐ標準パターンです。
最初のサイクルの点検 #
GitHub Actions push → ECR → マニフェスト commit → ArgoCD sync までが 1 度回った時点で点検する項目です。
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}'3 つのコマンドが一貫して新しい tag を指していれば 1 サイクルが正常動作中です。ArgoCD UI では同じ情報が視覚的に表示され、マニフェストとクラスタの間の drift も一目で見えます。
1 つの罠 — コンテナ image tag の mutability #
運用標準は image tag を immutable にすること です。同じ tag が異なるイメージを指すようにすると ArgoCD の drift detection が意味を失います。次のセットアップが必須です。
- ECR repository に immutable tags 有効化 — Terraform で
image_tag_mutability = "IMMUTABLE" latesttag を prod で絶対に使わない — 常に git SHA または semver- image tag = git commit hash または git tag — どの commit がどの環境に立っているかが一目で見える
このセットアップが抜けると「昨日まで動作していたその tag が今日は別のイメージ」という事故が発生します。これが GitOps の source of truth が崩れるポイントです。
締めくくり #
myshop-api の新しいバージョンがクラスタに入ってくる流れをコードで自動化しました。GitHub Actions が OIDC 信頼で静的キーなしに ECR に push し、マニフェスト repo の Helm values を自動 commit し、ArgoCD がその変更を watch してクラスタに同期する 1 サイクルです。dev は自動 sync、prod は PR レビュー + GitHub environments + ArgoCD 手動 sync の 2 重ゲートを経るパターンまで押さえ、Argo Rollouts でカナリー自動 promote / rollback のより精緻な仕組みも確認しました。この時点で myshop-api はコード push 1 度で dev に自動デプロイされ、git tag 1 度で prod デプロイがキューに入る流れが定着しました。次の記事ではこのすべての動作を覗き込む 1 層を載せます — Prometheus + Grafana + Alertmanager + CloudWatch で構成する運用クラスタのオブザーバビリティスタックと核心アラームルールセットを扱います。