前書き
私の個人開発で採用している CI/CD を参考に、CI/CD の全体像を理解することが目的です。
私は個人開発でやってますが、どの場面においても複数人, 中・大規模での開発を想定しているので、その前提で読んでください。例えば、小規模開発であればステート分割などをせずに全体に対して terraform plan(apply) を実行すればいいですが、複数人, 中・大規模での開発を想定するのであれば、適切にステート分割を行い差分のあるディレクトリのみを terraform plan(apply) するということが必要になってきます。
本プロジェクトは モノレポ で管理しています。terraform-project/(インフラ)、k8s-project/(k8s マニフェスト)、backend-project/(Go バックエンド)など、全てのプロジェクトを単一リポジトリで管理しています。
terraform の CI/CD
用語
-
messiは integration 環境の名前です -
messi,prodディレクトリのようなディレクトリを 環境ディレクトリ と呼びます -
aws/,google/のようなディレクトリを プロバイダーディレクトリ と呼びます -
ap-northeast-1/のようなディレクトリを リージョンディレクトリ と呼びます -
eks,app1,networkなどのtfstate単位のディレクトリをステートディレクトリと呼ぶことにします
各ツールの特徴
-
tfaction← 本プロジェクトでは使用していない-
terraformの自動化のフレームワークであり、これを使うか使わないかは大きな分かれ道だと思います。私は以下の理由でtfactionを使わずにシェル芸をメインにterraformCI/CD を構築する選択をしました- フレームワークだけあって一回使ってしまうと抜け出せなさそう、メンテが終了してしまったときに対処するのが大変そうという印象を受けたからです
- 自分のやりたいことが明確に見えていたので、フレームワークに縛られたくなかったからです
-
pushイベントを利用して、apply に失敗したら PR が自動で作成される仕組みにして、待ち時間をなくしている点は素晴らしいです
-
-
terragrunt← 使用しています-
terraformの薄いラッパーです - コードの自動生成で環境間などをDRYにできます
- 私の場合、
_provider.tf,_backend.tfを一番上位のroot.hclでまとめて設定しています -
terragruntはprovider.tfやbackend.tfを自動生成してくれます。本プロジェクトでは、これだけのためにterragruntを使っています。これによって、tfstate のkeyやallowed_account_idsを機械的に生成することができるので、ミスがなくなります
- 私の場合、
-
dependencyブロックを使用すると、単一のディレクトリでterragrunt init,validate,plan,applyを実行する際に、依存先のディレクトリの初期化も自動で行われてしまいます。大規模プロジェクトでは、この初期化処理により実行時間が大幅に増加するため、本プロジェクトではdependencyの使用を一切禁止しています。絶対に使ってはいけません -
dependencyを使わずに、terraform ネイティブのterraform_remote_stateを使います
-
-
tfmigrate← 本プロジェクトでは使用しなくなりました-
import,mv,rm操作をするときに、適用後にplan差分がないかどうかをチェックできます -
import,moved,removedブロックの登場により必須級ではなくなったが、適用後にplan差分がある場合にエラーを吐いてくれるので、import,moved,removedブロックには無い性能を持っています - 保守性の観点から依存するツールはできる限り減らしたいと思っているので、tfmigrate を使わない構成に変更しました
-
-
tfcmt(github-comment) ← 使用しています-
terraform planとterraform applyの結果を PR のコメントに出してくれます - 以前コメントが長くてスクロールが面倒だと思っていましたが、
github-commentの hide 機能で古いコメントを折りたたむことで解消できています - PR を見ながら plan/apply の結果を確認できるメリットの方が大きいと判断して採用しました
-
-
aqua← 本プロジェクトでは使用していない-
asdfみたいにツールのバージョンを管理をできます - 私は
tfenv,asdfのような管理は逆に複雑だと思ってしまうので、バイナリを直接インストールして/usr/local/bin/に配置しています-
asdfなどの抽象化が入るとその前提が崩れてしまい、トラブルシューティングを難しくするときがあります。私は、asdfのような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまいます
-
-
ディレクトリ構成
ディレクトリ構造は envs/{環境}/{プロバイダー}/{リージョン}/{ステートディレクトリ}/ の階層になっています。
ステート分割の方針:
- 共有インフラ(
network,eks,alb,waf等)はそれぞれ独立したステートディレクトリにします - アプリケーションサービス(
app1,app2等)は サービス単位で一つのステートディレクトリ にします。サービス内の DB・KVS・メール等のリソースは{feature}--{resource_type}.tf形式のファイルで同一ディレクトリに置きます - リージョンは
aws/ap-northeast-1,aws/us-east-1のようにディレクトリを分けます。リージョンを増やしたければそのときにディレクトリを追加します
環境間・リージョン間の同期:
環境間のインフラ状態は基本的に一致させます(messi でバグが見つかれば prod でも見つかるという状態)。同期したいステートディレクトリに sync.yaml を置くことで、CI が差分を検知して sync PR を自動作成します。_provider.tf, _backend.tf(自動生成ファイル)と locals.tf(環境固有の値)は同期対象から外します。VPN のように特定環境にしか存在しないリソースは sync.yaml を置かないだけです。
環境変数の一元管理:
VPC CIDR や DB スペック等の環境ごとの値は root.hcl に集約し、terragrunt init で _backend.tf / _provider.tf として各ディレクトリに自動生成します。
ディレクトリ構成(モノレポ内 terraform-project/)
terraform-project/
├── .github/ # terraform-project 専用ではなく、モノレポ .github/ に統合
├── envs/
│ ├── management/
│ │ ├── aws/
│ │ │ ├── ap-northeast-1/
│ │ │ │ └── sso/
│ │ │ └── us-east-1/
│ │ └── google/
│ ├── messi/
│ │ └── aws/
│ │ └── ap-northeast-1/
│ │ ├── adminhome/
│ │ ├── alb/
│ │ ├── argocd-alb/
│ │ ├── cognito/
│ │ ├── eks/
│ │ ├── inhouse-domain/
│ │ ├── network/
│ │ ├── app2/
│ │ ├── app1/ # 一つのサービス = 一つのディレクトリ
│ │ │ ├── auth--aws_cognito_user_pool.tf
│ │ │ ├── db--aws_db_instance.tf
│ │ │ ├── kvs--aws_elasticache_replication_group.tf
│ │ │ ├── cdn--aws_cloudfront_distribution.tf
│ │ │ ├── locals.tf
│ │ │ ├── outputs.tf
│ │ │ └── terragrunt.hcl
│ │ ├── statichome/
│ │ └── waf/
│ └── prod/
│ ├── aws/
│ │ └── ap-northeast-1/
│ │ ├── app1/
│ │ ├── statichome/
│ │ └── vpn/
│ ├── github/
│ └── google/
├── static_analysis/
│ ├── sync_preview.sh
│ └── ...
└── root.hcl
GitHub Actions ワークフローはモノレポ直下の .github/workflows/terraform-project--*.yml に配置されています。
terragrunt との付き合い方
本プロジェクトでは terragrunt を最小限の機能のみで使用しています。
自動生成機能の活用
下記のように一番トップの root.hcl に値を埋め込んで、terragrunt init で _backend.tf と _provider.tf を自動生成します。
_*.tf という形式のファイルは terragrunt によって自動生成されたものです。
dependency を使わない設計
dependency ブロックを使用すると、単一のディレクトリで terragrunt init, validate, plan, apply を実行する際に、依存先のディレクトリの初期化も自動で行われてしまいます。大規模プロジェクトでは、この初期化処理により実行時間が大幅に増加するため、本プロジェクトでは dependency の使用を一切禁止しています。
代わりに、terraform ネイティブの terraform_remote_state を使用することで、依存関係を管理しながらも、各ディレクトリを独立して操作できるようにしています。これにより、terraform コマンドでも全ての操作を完結でき、terragrunt に依存しない設計を実現しています。
# root.hcl(抜粋)
locals {
regex_list = regex("^envs/(management|messi|prod|dev)/(aws|google|github)/(ap-northeast-1|...)/(.*)$", path_relative_to_include())
environment = local.regex_list[0]
provider = local.regex_list[1]
region = local.regex_list[2]
state_dir = local.regex_list[3]
aws_account_id = local.environment == "messi" ? "xxxxxxxxxxxx" : "xxxxxxxxxxxx"
tfstate_bucket_name = "xxx-tfstate-bucket" # 全環境の tfstate を prod バケットに集約
}
generate "backend" {
path = "_backend.tf"
if_exists = "overwrite"
contents = <<EOF
terraform {
backend "s3" {
bucket = "${local.tfstate_bucket_name}"
key = "terraform-infra/${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-1"
}
}
EOF
}
generate "provider" {
path = "_provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.region}"
allowed_account_ids = ["${local.aws_account_id}"]
}
EOF
}
環境間・リージョン間の DRY にモジュールを使用しない
環境間のリソース定義の同期はシェルスクリプトを用意することで対応しています。余計な抽象化は可読性を下げるだけです。
それぞれの環境で頻繁に使う設定があれば、モジュールを使うことは許容されますが、非推奨です。基本はべた書きにしています。
ブランチ戦略: 廃止
以前は環境ごとのブランチ戦略を採用していました。messi 環境は messi ブランチ、prod 環境は prod ブランチ、management 環境は management ブランチと対応させていました。
この戦略を廃止して シングルブランチ(main) に統一した理由:
- PR のブランチ間違い: PR 作成時に向けるブランチを間違えることがあった(messi ブランチに向けるべき PR を prod ブランチに向けてしまうなど)
- 認知負荷: 複数のブランチを意識しながら作業するのは地味に認知負荷が高かった
- モノレポとの相性の悪さ: モノレポでは複数のプロジェクトが同居しており、プロジェクトごとにブランチ戦略が異なると管理が複雑になる
代わりに merge gate と sync PR の仕組みを導入しています。
merge gate
terraform apply は merge_group イベントではなく、merge gate(workflow_dispatch ベース)で実行します。
モノレポでは merge_group イベントの使用を禁止しています。あるプロジェクトの CI 失敗が、全く無関係なプロジェクトのマージをブロックしてしまうためです。
merge gate の仕組み (merge_gate_management.yml):
- PR イベントで
{project}--merge_gate_{name}という commit status が pending 状態で作成される - PR の全チェックが完了・成功すると、
/mergeコメントを促すコメントが投稿される - 開発者が
/mergeコメントを投稿すると、terraform-project--merge_gate_terraform_apply.ymlがworkflow_dispatchでキックされる - apply 成功後、commit status が pass に更新され、auto-merge が有効化される
sync PR
main にマージされると terraform-project--sync_pr_create.yml が起動し、sync.yaml に基づいて同期が必要なステートディレクトリへの sync PR を自動作成します。開発者はその PR を確認してマージするだけです。
この仕組みに乗っかりたいディレクトリにだけ sync.yaml を置きます(オプション)。sync.yaml がなければ sync PR は作成されません。sync_from で同期元のパスを、files でコピー対象のファイルを指定します。
# 例: envs/prod/aws/ap-northeast-1/app1/sync.yaml
sync_from: envs/messi/aws/ap-northeast-1/app1
files:
- auth--aws_cognito_user_pool.tf
- db--aws_db_instance.tf
- terragrunt.hcl
sync.yaml を持つディレクトリが「同期先」です。main へのマージで sync_from 先のディレクトリが変更されていた場合、同期先への sync PR が自動作成されます。locals.tf(環境固有の値)や _backend.tf/_provider.tf(自動生成ファイル)は files に含めません。
CI
-
terraform plan- dependency を使用しない設計により、
terraformコマンドのみで実行可能(terragrunt planは使わない) - terraform ネイティブなコマンドを使用することで、dependency が使われていないことのチェックも行える
-
pull_requestイベント - 流れ
- (1)
terraform planの対象となるステートディレクトリは下記を計算したものです- ( + ) git 差分のあるステートディレクトリ
- ( + ) モジュール(
modulesディレクトリ)に変更が入っている場合、それを使っているリソースのステートディレクトリ - ( check ) リソースが残っていたのにディレクトリごと削除された場合にエラーを吐く。リソースが残っているのに
terraform管理外になるという最悪の事態を避ける必要があります
- (2) plan ファイルを GitHub Actions アーティファクトに保存(apply 時に使用)
- (3) 上記で取得したステートディレクトリに対して、
terraform planを実施し、tfcmtで結果を PR コメントに投稿
- (1)
- dependency を使用しない設計により、
-
terraform apply- dependency を使用しない設計により、
terraformコマンドのみで実行可能 - merge gate(
workflow_dispatch) - 流れ
- (1) plan と同様に apply 対象ディレクトリを特定する
- (2) plan 時に保存したアーティファクトから plan ファイルを取得(Terraform が stale 検出を自動実行、state が変更されていれば "Saved plan is stale" エラー)
- (3) 各ステートディレクトリに
terraform apply tfplanを実施し、tfcmtで結果を PR コメントに投稿 - (4) apply 成功後、sync PR を自動作成
- dependency を使用しない設計により、
-
terragrunt init&terraform validate-
pull_requestイベント - 流れ
- (1) git差分があるディレクトリを特定します
- (2) 特定されたディレクトリに対して
terragrunt initを実行し、_backend.tf/_provider.tfを自動生成。差分があれば 自動コミットします(手動でterragrunt initを実行し忘れても CI が補完します) - (3)
terraform validateを実行します
-
-
terraform fmt&tflint- フォーマット・静的解析
- 環境間, リージョン間のコードが適切に同期されているかどうかをチェックする
-
terraform drift check- ベースブランチの実インフラとtfstateの差分チェック
-
pull_requestイベント - 流れ
- (1) git差分があるステートディレクトリを特定
- (2) ベースブランチに切り替えて、対象ディレクトリで
terraform plan -detailed-exitcodeを実行 - (3) ベースブランチで既にドリフトが存在する場合、PRコメントで警告
matrix ジョブの利用
init、plan、apply を実施するステートディレクトリを特定したら、matrix ジョブを使います。
各ステートディレクトリを matrix でパラレル実行し、plan_check_state_dirs_matrix ジョブで全マトリクスの成否をまとめてチェックします。
シークレット管理
以下に記載してあります。
GitHub Apps Token
PAT もしくは Fine-grained personal access token を半年おきとかに交換する手間が嫌だったり、セキュリティを高くしたりしたいのであれば、 GitHub Apps Token を使いましょう。GitHub Apps Token は毎回一時トークンが発行されるので、セキュリティが高いです。
以下は、PAT もしくは Fine-grained personal access token を使わないという前提で話を進めます。
CI において GitHub Apps Token は以下の理由で必須です。
- Go の Private モジュール(レポジトリ) のダウンロードには
secrets.GITHUB_TOKENでは無理です - 別レポジトリへの
pushや PR 作成は、secrets.GITHUB_TOKENでは無理です - PR を作成したときに
secrets.GITHUB_TOKENを使うと、自動テストが流れません - GitHub の IaC を GitHub Actions で apply するためにも必要です
- PR を作成して承認したいとき、別ユーザでないと承認できません。なので、作成を GitHub Apps Token で行い、承認を
secrets.GITHUB_TOKENで行います。その後の、Enable auto-merge は、どちらのトークンが行っても構いません- 自動テストを待たなくてもいい場合は、GitHub Apps Token で PR作成を行い、PR承認をスキップして、再び GitHub Apps Token で
gh pr merge --admin --merge "$PR_URL"を実行します
- 自動テストを待たなくてもいい場合は、GitHub Apps Token で PR作成を行い、PR承認をスキップして、再び GitHub Apps Token で
マルチアカウントによる権限管理
AWS OrganizationsとAWS SSOを使用したマルチアカウント環境での権限管理について説明します。
本プロジェクトでは、prod環境のS3バケットで全環境のtfstateを集約管理しています。
許可セット
2つの許可セットを作成します:
- admin: 管理者権限(tfstateを管理しているS3バケットの自分の環境のキー配下への書き込み権限と、全てのキーに対する読み取り権限を含む)
- readonly: 読み取り専用権限(tfstateを管理しているS3バケットの全てのキーに対する読み取り権限を含む)
グループと権限割り当て
2つのグループを定義し、それぞれに異なる権限を割り当てます:
インフラチーム(強い権限)
- management × admin
- management × readonly
- prod × admin
- prod × readonly
- messi × admin
- messi × readonly
プロダクトチーム(制限された権限)
- management × readonly
- prod × readonly
- messi × admin
- messi × readonly
terraform plan の実行
- 全てのロールは全アカウントのtfstate用バケットを読み取れるため、
terraform plan時のremote参照が可能です - 書き込み権限がない環境に対する
planでは-lock=falseオプションを使用します- これによりプロダクトチームでもprod環境に対して
planを実行できます
- これによりプロダクトチームでもprod環境に対して
- 制限事項: KMSによる復号化などはreadonly権限に含まれないため、これを必須とする
planはプロダクトチームでは実行できません(CI でもローカルでも)
セキュリティ・ネットワーク制限
VPN
固定 IP を持つためにVPNを構築します。
WAF による IP 制限
ステージング環境は一般公開しないため、WAF でアクセスを VPN の IP のみに絞ります。本番環境は VPN の IP に加えて GitHub Actions Runner の固定 IP(e2e テスト用)も許可します。
GitHub Actions から ArgoCD を操作したり e2e テストを実行するには、ALB に設定された WAF を通過できる固定 IP が必要です。GitHub Actions に固定 IP を持たせる方法は以下の 2 つです:
- Self-hosted Runner: 自前でRunnerをホストし固定IPを設定
- GitHub Hosted Runner(有料): Enterprise Planに加えてさらに課金し、Larger Runnerを使用して固定IPを取得
EKS
EKS Auto Mode
aws-load-balancer-controller や Pod Identity Agent が予めインストールされており、自らインストールする必要がないので便利です。
EC2のスペックを考える必要もないです。
アクセスエントリー
従来は IAM ロール に EKS へのアクセス権限を付与するために、aws-auth という ConfigMap をいじっていたかと思いますが、アクセスエントリーを使うことで ConfigMap をいじらないで済みます。
aws-auth という ConfigMap を terraform で設定する場合、k8s プロバイダーを定義する必要があったので面倒でしたが、EKS API(アクセスエントリー) で設定できるようになり k8s プロバイダーを定義する必要がなくなりました。
Pod Identity
サービスアカウントに IAM ロール を付与するために、従来なら terraform 側で完結せずに、Manifest ファイルでの設定も必要でしたが、Pod Identity の登場により terraform 側で完結するようになりました。
マルチテナント vs シングルテナント
私は費用と運用コストを考えて、マルチテナントを採用しています。
CI/CD の構築や terraform のコードの複雑さを考えると、マルチテナントの方が圧倒的に見通しが良くなります。
また、EKS のバージョンアップ作業が大幅に楽になります。シングルクラスタ(マルチテナント)であれば、クラスタを一度アップグレードするだけで全サービスに適用できます。マルチクラスタ(シングルテナント)の場合、サービスの数だけクラスタがあるので、その数だけアップグレード作業が発生します。
デメリットとして、耐障害性が低いことや、それぞれのマイクロサービスのコストを把握しづらいことが挙げられます。
ECRレプリケーション
DockerイメージはCIで ap-northeast-1 のECRにpushし、ECR Replicationで他のリージョンや環境に自動複製します。これによりイメージpull時のコストと時間を削減できます。
k8s の CI/CD
各ツールの特徴
-
argocd-vault-plugin← 廃止(ESO に移行済み)- 以前は AWS Secrets Manager からシークレットを取得し、ArgoCD 上でマニフェストに埋め込むために使用していました
- 廃止理由(ESO に移行):
- AVP はマニフェスト生成時にプレースホルダーを実際の値に置換しますが、置換後の平文マニフェストが ArgoCD の Redis キャッシュに保存されます。Redis が侵害されると全シークレットが漏洩します
- ArgoCD 公式ドキュメントでも AVP はセキュリティアンチパターンとして記載されています
- repo-server に AWS Secrets Manager へのアクセス権限が必要で、最小権限の原則に反します
- AVP バイナリを initContainer で手動ダウンロードし、CMP としてサイドカー構成にする必要があります
-
ESO(External Secrets Operator)← 使用しています(AVP の後継)-
ExternalSecretリソースから K8sSecretを直接生成します - ArgoCD が Secret に触れないため、Redis に平文 Secret が保存されません
-
refreshIntervalを設定することで、Secrets Manager 側の値が更新された際に K8s Secret も自動更新されます - sync-wave による順序制御が必要です(ExternalSecret を Deployment より先に apply)
-
-
helm← 使用しています(ArgoCD 関連のみ)- アプリケーションのマニフェスト(backend-project 等)は直接 YAML ファイルで管理しており、helm chart は使用していません
- ArgoCD 自身・ESO 等のインフラ系コンポーネントは helmfile +
*-auto-generated.yamlパターンで管理しています
-
helmfile← 使用しています(ArgoCD 関連のみ)- helm と同様、ArgoCD・ESO 等のインフラ系コンポーネントに限定して使用しています
- アプリケーション系は直接 YAML ファイルで管理しているため、helmfile の使用範囲は限定的です
-
sops← 使用しています- ArgoCD 管理外のシークレット(ArgoCD 自身の GitHub OAuth シークレット、SSH 鍵等)を暗号化してから GitHub に push しています
- CI 上で復号してから
kubectl applyされます
- AWS Secrets Manager ← 使用します
- AWS Secrets Manager にセットしておけば、
terraform側でも使えるし、k8s 側(ESO 経由)でも使えるのが便利です
- AWS Secrets Manager にセットしておけば、
ディレクトリ構成
k8s-project はモノレポ内の k8s-project/ ディレクトリで管理しています。以前は独立したリポジトリでしたが、インフラ変更(terraform-project)とk8s変更(k8s-project)を同一PRで扱えるようにするためモノレポに統合しました。
ディレクトリ構成
k8s-project/
├── envs/
│ ├── messi/
│ │ └── aws/
│ │ └── ap-northeast-1/
│ │ ├── argocd/
│ │ │ ├── applicationset/
│ │ │ │ └── applicationset.yaml # ArgoCD ApplicationSet (main ブランチ監視)
│ │ │ ├── appproject/
│ │ │ │ ├── messi-appproject.yaml
│ │ │ │ └── prod-appproject.yaml
│ │ │ ├── argocd/
│ │ │ │ ├── argocd-helmfile.yaml # helmfile (auto-generated の元)
│ │ │ │ ├── argocd-auto-generated.yaml # helmfile template の出力
│ │ │ │ ├── argocd-namespace.yaml
│ │ │ │ ├── argocd-values.yaml
│ │ │ │ ├── github-oauth-secret.yaml # sops 暗号化
│ │ │ │ └── k8s-project-ssh-private-key-secret.yaml # sops 暗号化
│ │ │ ├── external-secrets/
│ │ │ │ ├── external-secrets-helmfile.yaml
│ │ │ │ ├── external-secrets-auto-generated.yaml
│ │ │ │ ├── external-secrets-namespace.yaml
│ │ │ │ ├── external-secrets-values.yaml
│ │ │ │ └── cluster-secret-store.yaml # ClusterSecretStore (ESO)
│ │ │ └── ingressclass/
│ │ │ └── alb.yaml
│ │ ├── ingressclass/
│ │ │ ├── alb/
│ │ │ │ └── alb.yaml
│ │ │ └── alb-internal/
│ │ │ └── alb-internal.yaml
│ │ ├── customnodepool/
│ │ │ └── customnodepool/
│ │ │ └── small-instance-nodepool.yaml
│ │ ├── backend-project-a/ # サービスごとにディレクトリを分ける
│ │ │ └── backend-project-a/
│ │ │ ├── backend-project-a-webprimary.yaml # SA + ExternalSecret + Deployment + HPA + Ingress
│ │ │ ├── backend-project-a-migrateprimary.yaml # Job + ExternalSecret
│ │ │ ├── backend-project-a-webemailsender.yaml
│ │ │ └── ...
│ │ ├── backend-project-b/
│ │ │ └── backend-project-b/
│ │ │ └── ...
│ │ └── my-nginx/
│ │ └── my-nginx-kai/
│ │ └── my-nginx.yaml
│ └── prod/
│ └── aws/
│ └── ap-northeast-1/
│ ├── argocd/
│ │ └── ...
│ ├── backend-project-a/
│ │ └── backend-project-a/
│ │ └── ...
│ └── ...
└── static_analysis/
├── helmfile_template.sh
└── compare_realms.sh
アーキテクチャの要点:
- アプリケーションマニフェスト(backend-project 等):
helm chartを使わず直接 YAML ファイルで管理。ServiceAccount + ExternalSecret + Deployment + HPA + Ingressをひとつの YAML ファイルにまとめています - インフラコンポーネント(ArgoCD, ESO):
helmfile→helmfile template→*-auto-generated.yamlパターンで管理。CI でhelmfile templateを実行し、差分があれば auto-generated.yaml を自動コミット します - ArgoCD 管理外のリソース (
argocd/ディレクトリ配下): merge gate (kubectl apply) で適用します
ArgoCD や k8s 上での名前の規則は以下になります:
- 最下部のディレクトリまでハイフンでつなげた名前が ArgoCD 上の
applicationの名前となります。例:k8s-project/envs/messi/aws/ap-northeast-1/backend-project-a/backend-project-a→messi-aws-ap-northeast-1-backend-project-a-backend-project-a - その一つ上のディレクトリまでハイフンでつなげた名前が k8s 上の
namespaceとなります。applicationSetの設定で、k8s 上のnamespaceを自動で作るようにできます - k8s 上のリソースの名前は自由です
argoCD の監視
applicationSet リソースの Git Generator で argoCD の監視対象を設定します。
ArgoCD は main ブランチを監視しています(revision: main)。
以前はブランチ戦略を採用しており、messi ArgoCD は messi ブランチ、prod ArgoCD は prod ブランチを監視していました。現在はシングルブランチ(main)に統一しているため、全環境の ArgoCD が main ブランチを監視します。
# applicationset.yaml (messi 環境)
generators:
- git:
repoURL: git@github.com:your-org/your-monorepo.git
revision: main # main ブランチを監視
directories:
- path: k8s-project/envs/messi/aws/ap-northeast-1/*/*
- path: k8s-project/envs/messi/aws/ap-northeast-1/argocd/*
exclude: true
ArgoCD 自身に関わるリソース(argocd/ ディレクトリ配下)は監視対象から除外しています。
ブランチ戦略: 廃止
以前は messi 環境の ArgoCD は messi ブランチを、prod 環境は prod ブランチを監視する構成でした。prod へリリースしたいときは messi ブランチを prod ブランチにマージするという運用でした。
廃止の理由:
- terraform-project と同様の理由(PR ブランチ間違い、認知負荷)
- ArgoCD の監視ブランチ設定自体も管理が複雑になる
現在は全環境が main ブランチを監視しており、k8s-project/envs/{env}/... のディレクトリパスで環境を区別しています。main へのマージで直接各環境に反映されます。
Secret 管理(ESO)
アプリケーションの Secret は ESO(External Secrets Operator)で管理しています。
# ExternalSecret の例(backend-project-webprimary.yaml より)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: "backend-project-webprimary-external-secret"
namespace: "backend-project"
annotations:
argocd.argoproj.io/sync-wave: "-1" # Deployment より先に適用
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: "backend-project-webprimary-secret"
creationPolicy: Owner
data:
- secretKey: POSTGRES_PASSWORD
remoteRef:
key: "my-project-messi-aws-ap-northeast-1"
property: POSTGRES_PASSWORD
sync-wave による順序制御が重要です。 AVP はマニフェスト生成時(apply 前)に Secret 値が確定しましたが、ESO は apply 後に非同期で Secret が生成されます。argocd.argoproj.io/sync-wave: "-1" で ExternalSecret を Deployment より先に apply することで、Secret が存在しないまま Pod が起動するのを防いでいます。
ClusterSecretStore は k8s-project/envs/{env}/aws/{region}/argocd/external-secrets/cluster-secret-store.yaml に定義されています。
ingress リソースのグループ化
alb のコストを削減するために、一つのalbを複数サービスで使えるように ingress group を作成しましょう。EKS Auto Mode では ingressClassParams リソースを使って、グループを設定します。
ただし、パブリックな alb とインターナルな alb は scheme が違うので、別々の alb にする必要があります。
CI
-
argocd app diff-
pull_requestイベント - 差分が生じた yaml ファイルを持つ
applicationに対して、argocd app diffを実行します -
argocd app diffは、差分があるときはエラーステータス 1 なので、このときは成功判定にします - messi 環境は WAF がないため、通常の
ubuntu-latestrunner で実行できます - prod 環境は固定IPを持ったrunnerを使用します
-
-
helmfile template-
pull_requestイベント - 全ての helmfile ファイルに対して
helmfile templateを実行し、*-auto-generated.yamlに差分があれば 自動コミットします(手動でhelmfile templateを実行し忘れても CI が補完します) - helmfile ファイルが
*helmfile.yamlの形式であることもチェックします
-
-
kubectl diff-
pull_requestイベント - ArgoCD 管理外の Manifest(
argocd/ディレクトリ配下)を CI でdiffします
-
-
kubectl apply- merge gate(
workflow_dispatch)← 旧:merge_groupイベント -
argocd/ディレクトリに変更がある場合のみ実行されます - ファイルを依存関係順に apply します:
- namespace(他の全リソースが依存)
- Secret(sops 暗号化を復号してから apply)
- auto-generated(Deployment, CRD, Webhook を含む)
- CRD カスタムリソース・設定ファイル(ClusterSecretStore 等)
- Webhook 依存の解決のため、1st pass → Deployment Ready 待機 → 2nd pass(失敗ファイルのリトライ)の 2 パス方式を採用しています
- merge gate(
共通アクション用のレポジトリ
コンポジットアクションはモノレポ内の .github/actions/ で管理しています。
汎用的なアクション(generate_github_apps_token, revoke_github_apps_token, tag_release 等)はプロジェクトプレフィックスなしのディレクトリに配置し、プロジェクト固有のアクション(terraform-project--get_state_dirs_for_terraform_init_plan_apply 等)はプレフィックス付きで配置しています。
workflow_callを余計に使うとジョブが増えて見にくくなるので、コンポジットアクションで済むのなら、workflow_callを気軽に使わずにコンポジットアクションを使うことが重要です。
アプリのディレクトリ(backend-project/)
モノレポ内の backend-project/ ディレクトリでアプリケーションコードを管理しています。
Docker
- イメージサイズが大きすぎるとPULLに時間がかかりますし、ECR の金額も上がってしまうので、積極的にマルチステージビルドを使いましょう
- CI で使う場合は
build-push-actionアクションを使用しましょう
デプロイ構成(k8s マニフェスト)
- migrate イメージは
jobで使います - web イメージは Manifest の普通のコンテナに配置します
CD(リリースフロー)
- ビルド & ECRへの push & タグ付け & version.txt の更新 & k8s-project への PR 作成
-
dispatch_workflowイベント - モノレポ内の
.github/actions/tag_release_and_build_push_and_create_pr_to_k8s-project/アクションを利用します。これによりボタン一つで k8s-project に PR が作成されます。それをマージすれば ArgoCD が自動デプロイします
-