最新バージョンを参照するほうがいいかもしれません
私の CI/CD の結論 v2 が出ました。
前書き
私の個人開発で採用している CI/CD を参考に、CI/CD の全体像を理解することが目的です。
私は個人開発でやってますが、どの場面においても複数人, 中・大規模での開発を想定しているので、その前提で読んでください。例えば、小規模開発であればステート分割などをせずに全体に対して terraform plan(apply) を実行すればいいですが、複数人, 中・大規模での開発を想定するのであれば、適切にステート分割を行い差分のあるディレクトリのみを terraform plan(apply) するということが必要になってきます。
自動化のすばらしさを知るために、試しにリリースしてみる ※ これは terraform は関係ない
(1) アプリケーションコードに変更を加える前
(2) アプリのレポジトリで開発する
(3) 「ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成」をボタン一つで完結
GitHub Actions の dispatch_workflow イベントを使って、Dockerイメージのビルド, ECRへの push, タグ付け, k8sレポジトリへのPR作成 を自動化してます。

(4) 自動作成されたk8sプロジェクトへのPRをマージする
k8s レポジトリへのPRは、イメージ名だけ変更したPRとなっています。sed コマンドを使って前回イメージ名を新しいイメージ名に置き換えてます。
自動作成されたPRを確認してマージします。

(5) アプリケーションコードに変更を加えた後
k8sレポジトリにマージされたら、argoCD が監視しているので勝手にデプロイされます。

terraform の CI/CD (myTfProjectレポジトリ)
用語
-
messiは integration 環境の名前です -
messi,stagディレクトリのようなディレクトリを 環境ディレクトリ と呼びます -
aws-ap-northeast-1のようなディレクトリを プロバイダー&リージョンディレクトリ と呼びます -
main,hubなどのtfstate単位のディレクトリをステートディレクトリと呼ぶことにします
各ツールの特徴
-
tfaction← 私は使用していません-
terraformの自動化のフレームワークであり、これを使うか使わないかは大きな分かれ道だと思います。私は以下の理由でtfactionを使わずにシェル芸をメインにterraformCI/CD を構築する選択をしました- フレームワークだけあって一回使ってしまうと抜け出せなさそう、メンテが終了してしまったときに対処するのが大変そうという印象を受けたからです
- 自分のやりたいことが明確に見えていたので、フレームワークに縛られたくなかったからです
-
-
terragrunt← 私は使用しています-
terraformの薄いラッパーです - コードの自動生成で環境間などをDRYにできます
- 私の場合、
_variable.tf,_provider.tf,_backend.tf,tfmigrate.hclを一番上位のterragrunt.hclでまとめて設定しています。非常に便利な機能だと思います
- 私の場合、
-
dependencyブロックを使って他のステートへの依存を簡単に書ける -
run-all(run --all) を使うことで、ステートディレクトリ間の依存関係を考慮して、順番にplan(apply) を実行してくれる- CI では
git差分のあるディレクトリに対してのみplan(apply) を実行する仕組みにしているので基本的にrun-all(run --all) で実行することはないと思います。少なくとも私のプロジェクトでは使ってないです。上位のディレクトリでrun-all planをやったら、なんのために ステート分割をしているのかという話になるので...
- CI では
- 他のステートディレクトリの値に依存していて、その値は
applyで決定するがplanの時点では決定していない時、普通ならplanの時点でエラーになるが、モック値を使うことでplan時のエラーを回避できます- ステート分割して各ディレクトリに
planを実行する場合、つまりrun-all(run --all)を使わない場合、ステートディレクトリの実行順序は決まってないので、planはモック値を使うことでパスしたが、applyの時点でも依存先のステートディレクトリの値が決まっておらずにモック値が使用されて失敗するということが起きえます。ただ、複数ステートディレクトリに渡りコードを書いたとき、依存先のステートディレクトリをapplyしてから、依存元のステートディレクトリをコメントインするというような作業をせずに、複数回merge_groupイベントに突っ込んでapplyすれば済むのはメリットだと思います
- ステート分割して各ディレクトリに
-
-
tfmigrate← 私は使用しています-
import,mv,rm操作をするときに、適用後にplan差分がないかどうかをチェックできます -
import,mv,rm操作を GitHub で管理できます -
import,mv,rmブロックの登場により必須級ではなくなったが、適用後にplan差分がある場合にエラーを吐いてくれるので、import,mv,rmブロックには無い性能を持っています。importブロック等は適用後にplan差分がある場合でもエラーを吐いてくれません
-
-
tfcmt(github-comment) ← 私は使用していません-
terraform planなどのをログをPRのコメントに出してくれます - 私はPRのコメントに長いログが出るのが少し嫌だったので採用しませんでした
- 私は
merge_groupイベント内のterraform apply時は、ログへのリンクとステータスを GitHub API を叩いてPRのコメントに表示しています
-
-
aqua← 私は使用していません-
asdfみたいにツールのバージョンを管理をできます - 私は
tfenv,asdfのような管理は逆に複雑だと思ってしまうので、バイナリを直接インストールして/usr/local/bin/に配置しています- 私たちは、PATHが通っている
/usr/local/bin/や/bin/にバイナリを配置して実行することが多いと思いますが、asdfなどの抽象化が入るとその前提が崩れてしまうときがあります。これはトラブルシューティングを難しくします。私は、asdfのような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまうのです
- 私たちは、PATHが通っている
-
ディレクトリ構成
私が目標とするインフラ状態に対してどのようなディレクトリ構成にするのかを、箇条書きにしました。
- 環境ごとのインフラ状態はできるかぎり同じ状態にしたいです。つまり、stag でバグが見つかれば prod でもバグが見つかるというような状態にしたいです
- messi と stag, stag と prod のディレクトリの状態が一部のファイルを除き、完全に一致することを pre-commit と CI で強制します
- 一致しないファイルとして許容されるのは、
_variable.tfなどのterragrunt initで自動生成された環境特有の値が入っているファイルと、_asymmetryという文字列が入ったファイルです- 例えば、
argocd-vault-pluginが AWS Secrets Manager にアクセスするには、argocd-repo-server のサービスアカウントに IAM Role を付与するための Pod Identity を定義する必要があります。このリソースは一つの環境で定義すれば十分なので(argoCD は一つで全部のEKSを管理するから)、環境間で一致させる必要はありません。なので、ファイル名に_asymmetry含めて特有の環境だけに存在することを許可します
- 例えば、
- 一致しないファイルとして許容されるのは、
- messi と stag, stag と prod のディレクトリの状態が一部のファイルを除き、完全に一致することを pre-commit と CI で強制します
- VPC CIDR や DBのスペック などの環境変数は一元管理したいです
- 一番上位の
terragrunt.hclで一元管理して、terragrunt initで自動生成します
- 一番上位の
- 原神, Apex 等のゲームみたいに、ユーザがどのリージョンのサーバを使うのかを選択できるようにしたいです
-
aws-ap-northeast-1,aws-us-east-1のようにしてディレクトリを切ります - リージョンを増やしたければ、そのときにディレクトリを増やします
-
aws-ap-northeast-1,aws-us-east-1, ...(プロバイダー&リージョンディレクトリ) 間の一致は強制しません。例えば、アークナイツでは日本版と大陸版のリリースタイミングは全然違います
-
- 適切にステート分割を行うことで
plan(apply) の実行時間を短縮したい- 今のところ、hub(ネットワーク系), main(EKS など), db(データベースなど)で切っています。新たにステートディレクトリを切りたいときはここに並べます
- リソースを増やすときにステート操作で整理したいときがあると思うが、気軽にステート操作をできるようにしておきたいです
-
tfmigrateを使います - 以下の理由により、プロバイダー&リージョンディレクトリ 直下に
tfmigrateを配置します- プロバイダー&リージョンディレクトリ 間で
multi-state操作をすることはないので、環境ディレクトリ直下にtfmigrateを配置する必要はないです - 環境ディレクトリ直下に
tfmigrateを配置すると、同じようなマイグレーションファイルがtfmigrateディレクトリに並んでしまいます。異なるプロバイダー&リージョンディレクトリで、同じステート操作をすることが想定されるからです - ステートディレクトリ間で
multi-state操作をすることは良くあります
- プロバイダー&リージョンディレクトリ 間で
-
~/myTfProject$ tree -a -L 10 -I ".git"
.
├── .github
│ ├── CODEOWNERS
│ ├── actions
│ │ ├── check_if_not_deleting_tfstate_with_resources
│ │ │ └── action.yml
│ │ ├── create_pr_to_another_environment
│ │ │ └── action.yml
│ │ ├── install_tools
│ │ │ └── action.yml
│ │ ├── setup_terraform_and_tfmigrate
│ │ │ └── action.yml
│ │ ├── terraform_apply
│ │ │ └── action.yml
│ │ ├── terraform_plan
│ │ │ └── action.yml
│ │ ├── tfmigrate_apply
│ │ │ └── action.yml
│ │ └── tfmigrate_plan_and_return_target_dirs
│ │ └── action.yml
│ └── workflows
│ ├── detect_git_diff_among_envs.yml
│ ├── detect_git_diff_in_common_dir_from_common_branch.yml
│ ├── detect_hardcoding_environment.yml
│ ├── detect_hardcoding_resource_id.yml
│ ├── exec_actionlint.yml
│ ├── pre-commit_pre-push_check.yml
│ ├── reusable_tfmigrate_apply_and_terraform_apply.yml
│ ├── reusable_tfmigrate_plan_and_terraform_plan.yml
│ ├── terraform_init_validate.yml
│ ├── tfmigrate_apply_and_terraform_apply_caller.yml
│ └── tfmigrate_plan_and_terraform_plan_caller.yml
├── .gitignore
├── .lefthook
│ ├── ci
│ │ ├── terraform_init.sh
│ │ └── terraform_validate.sh
│ ├── pre-commit
│ │ ├── static_analysis.sh
│ │ ├── static_analysis_format.sh
│ │ └── static_analysis_sync_envs.sh
│ └── pre-push
│ └── test_pre-push.sh
├── .tflint.hcl
├── README.md
├── envs
│ ├── common
│ │ ├── aws-ap-northeast-1
│ │ │ ├── .tfmigrate.hcl
│ │ │ ├── main
│ │ │ │ ├── .terraform
│ │ │ │ │ ├── providers
│ │ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ │ └── hashicorp
│ │ │ │ │ │ └── aws
│ │ │ │ │ │ ├── 5.89.0
│ │ │ │ │ │ └── 5.90.0
│ │ │ │ │ └── terraform.tfstate
│ │ │ │ ├── .terraform.lock.hcl
│ │ │ │ ├── _backend.tf
│ │ │ │ ├── _provider.tf
│ │ │ │ ├── _variable.tf
│ │ │ │ ├── aws_iam_policy.tf
│ │ │ │ ├── aws_identitystore_group.tf
│ │ │ │ ├── aws_identitystore_group_membership.tf
│ │ │ │ ├── aws_identitystore_user.tf
│ │ │ │ ├── aws_ssoadmin_account_assignment.tf
│ │ │ │ ├── aws_ssoadmin_managed_policy_attachment.tf
│ │ │ │ ├── aws_ssoadmin_permission_set.tf
│ │ │ │ ├── aws_ssoadmin_permission_set_inline_policy.tf
│ │ │ │ ├── data_aws_ssoadmin_instances.tf
│ │ │ │ └── terragrunt.hcl
│ │ │ ├── terragrunt.hcl
│ │ │ └── tfmigrate
│ │ │ └── .gitkeep
│ │ └── aws-global
│ │ ├── .tfmigrate.hcl
│ │ ├── hub
│ │ │ ├── .terraform
│ │ │ │ ├── providers
│ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ └── hashicorp
│ │ │ │ │ └── aws
│ │ │ │ │ ├── 5.89.0
│ │ │ │ │ └── 5.90.0
│ │ │ │ └── terraform.tfstate
│ │ │ ├── .terraform.lock.hcl
│ │ │ ├── _backend.tf
│ │ │ ├── _provider.tf
│ │ │ ├── _variable.tf
│ │ │ ├── aws_route53_zone.tf
│ │ │ ├── aws_route53domains_registered_domain.tf
│ │ │ ├── output.tf
│ │ │ └── terragrunt.hcl
│ │ ├── terragrunt.hcl
│ │ └── tfmigrate
│ │ └── .gitkeep
│ ├── messi
│ │ ├── aws-ap-northeast-1
│ │ │ ├── .tfmigrate.hcl
│ │ │ ├── db
│ │ │ │ ├── .terraform
│ │ │ │ │ ├── providers
│ │ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ │ └── hashicorp
│ │ │ │ │ │ └── aws
│ │ │ │ │ │ ├── 5.89.0
│ │ │ │ │ │ └── 5.90.0
│ │ │ │ │ └── terraform.tfstate
│ │ │ │ ├── .terraform.lock.hcl
│ │ │ │ ├── _backend.tf
│ │ │ │ ├── _provider.tf
│ │ │ │ ├── _variable.tf
│ │ │ │ ├── aws_db_subnet_group.tf
│ │ │ │ ├── aws_rds_cluster.tf
│ │ │ │ ├── aws_rds_cluster_instance.tf
│ │ │ │ ├── aws_security_group.tf
│ │ │ │ ├── aws_security_group_rule.tf
│ │ │ │ ├── data_aws_secretsmanager_secret_version.tf
│ │ │ │ ├── terragrunt.hcl
│ │ │ │ └── variable.tf
│ │ │ ├── hub
│ │ │ │ ├── .terraform
│ │ │ │ │ ├── providers
│ │ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ │ └── hashicorp
│ │ │ │ │ │ └── aws
│ │ │ │ │ │ ├── 5.89.0
│ │ │ │ │ │ └── 5.90.0
│ │ │ │ │ └── terraform.tfstate
│ │ │ │ ├── .terraform.lock.hcl
│ │ │ │ ├── _backend.tf
│ │ │ │ ├── _provider.tf
│ │ │ │ ├── _variable.tf
│ │ │ │ ├── aws_eip.tf
│ │ │ │ ├── aws_internet_gateway.tf
│ │ │ │ ├── aws_internet_gateway_attachment.tf
│ │ │ │ ├── aws_kms_alias.tf
│ │ │ │ ├── aws_kms_key.tf
│ │ │ │ ├── aws_nat_gateway.tf
│ │ │ │ ├── aws_route.tf
│ │ │ │ ├── aws_route_table.tf
│ │ │ │ ├── aws_route_table_association.tf
│ │ │ │ ├── aws_secretsmanager_secret.tf
│ │ │ │ ├── aws_subnet.tf
│ │ │ │ ├── aws_vpc.tf
│ │ │ │ ├── output.tf
│ │ │ │ └── terragrunt.hcl
│ │ │ ├── main
│ │ │ │ ├── .terraform
│ │ │ │ │ ├── providers
│ │ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ │ └── hashicorp
│ │ │ │ │ │ └── aws
│ │ │ │ │ │ ├── 5.89.0
│ │ │ │ │ │ └── 5.90.0
│ │ │ │ │ └── terraform.tfstate
│ │ │ │ ├── .terraform.lock.hcl
│ │ │ │ ├── _backend.tf
│ │ │ │ ├── _provider.tf
│ │ │ │ ├── _variable.tf
│ │ │ │ ├── aws_acm_certificate.tf
│ │ │ │ ├── aws_acm_certificate_validation.tf
│ │ │ │ ├── aws_ecr_repository_asymmetry.tf
│ │ │ │ ├── aws_eks_access_entry.tf
│ │ │ │ ├── aws_eks_access_policy_association.tf
│ │ │ │ ├── aws_eks_cluster.tf
│ │ │ │ ├── aws_eks_pod_identity_association_asymmetry_avp.tf
│ │ │ │ ├── aws_iam_policy_asymmetry_avp.tf
│ │ │ │ ├── aws_iam_role.tf
│ │ │ │ ├── aws_iam_role_asymmetry_avp.tf
│ │ │ │ ├── aws_iam_role_policy_attachment.tf
│ │ │ │ ├── aws_iam_role_policy_attachment_asymmetry_avp.tf
│ │ │ │ ├── aws_route53_record.tf
│ │ │ │ ├── data_aws_iam_policy_document.tf
│ │ │ │ ├── data_aws_iam_policy_document_asymmetry_avp.tf
│ │ │ │ ├── terragrunt.hcl
│ │ │ │ └── variable.tf
│ │ │ ├── terragrunt.hcl
│ │ │ └── tfmigrate
│ │ │ ├── .gitkeep
│ │ │ ├── 20250306145900_rename_test.hcl
│ │ │ └── 20250306194600_rename_test_2.hcl
│ │ └── aws-us-east-1
│ │ ├── .tfmigrate.hcl
│ │ ├── hub
│ │ │ ├── .terraform
│ │ │ │ ├── providers
│ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ └── hashicorp
│ │ │ │ │ └── aws
│ │ │ │ │ ├── 5.89.0
│ │ │ │ │ └── 5.90.0
│ │ │ │ └── terraform.tfstate
│ │ │ ├── .terraform.lock.hcl
│ │ │ ├── _backend.tf
│ │ │ ├── _provider.tf
│ │ │ ├── _variable.tf
│ │ │ ├── aws_vpc.tf
│ │ │ ├── terragrunt.hcl
│ │ │ └── using_test_sg.tf
│ │ ├── main
│ │ │ ├── .terraform
│ │ │ │ ├── providers
│ │ │ │ │ └── registry.terraform.io
│ │ │ │ │ └── hashicorp
│ │ │ │ │ └── aws
│ │ │ │ │ ├── 5.89.0
│ │ │ │ │ └── 5.90.0
│ │ │ │ └── terraform.tfstate
│ │ │ ├── .terraform.lock.hcl
│ │ │ ├── _backend.tf
│ │ │ ├── _provider.tf
│ │ │ ├── _variable.tf
│ │ │ └── terragrunt.hcl
│ │ ├── terragrunt.hcl
│ │ └── tfmigrate
│ │ └── .gitkeep
│ ├── prod
│ │ ├── aws-ap-northeast-1
---
│ ├── stag
│ │ ├── aws-ap-northeast-1
---
│ └── terragrunt.hcl
├── lefthook.yml
└── modules
├── .gitkeep
└── test_sg
├── main.tf
├── outputs.tf
└── variables.tf
192 directories, 328 files
terragrunt による自動生成
下記のように一番トップの terragrunt.hcl に値を埋め込んで、terragrunt init で _variable.tf などを自動生成します。
_*.tf という形式のファイルと tfmigrate.hcl は terragrunt によって自動生成されたものです。
locals {
---
environment = regex("^(messi|stag|prod|common).*$", path_relative_to_include())[0]
---
vpc_cidr_1 = (
local.environment == "messi" ? (
local.provider_and_region == "aws-ap-northeast-1" ?
"10.11.0.0/16" :
local.provider_and_region == "aws-us-east-1" ?
"10.22.0.0/16" :
"_"
) : local.environment == "stag" ? (
local.provider_and_region == "aws-ap-northeast-1" ?
"10.33.0.0/16" :
local.provider_and_region == "aws-us-east-1" ?
"10.44.0.0/16" :
"_"
) : local.environment == "prod" ? (
local.provider_and_region == "aws-ap-northeast-1" ?
"10.55.0.0/16" :
local.provider_and_region == "aws-us-east-1" ?
"10.66.0.0/16" :
"_"
) : "_"
)
---
}
generate "variables" {
path = "_variable.tf"
if_exists = "overwrite"
disable = local.is_directly_under_provider_and_region # プロバイダ&リージョン ディレクトリ直下は .tfmigrate.hcl のみしか配置しない。
contents = <<EOF
%{ if local.environment == "common" &&
(local.provider_and_region == "aws-global") &&
(local.service == "hub") }
variable "domain" {
description = "_"
type = string
default = "${local.domain}"
}
%{ endif }
---
}
---
ブランチ戦略
ブランチ戦略を採用します。
messi 環境のインフラ状態は messi ブランチの messi ディレクトリ, stag 環境のインフラ状態は stag ブランチの stag ディレクトリ, prod環境のインフラ状態は prod ブランチの prod ディレクトリと一致します。
また、common ブランチと common ディレクトリも作ってあり、ホストゾーンなどの全環境で共通して使用するものや、手元で使う SSO の許可セットなどを定義しています。全環境に影響する可能性があるので、common ブランチへのマージはコードオーナーで制限をつけるなどの工夫が必要です。
CI
-
terraform plan- 実際は
terragrunt plan -
pull_requestイベント - 流れ
- (1)
tfmigrate planを実行して、ログを出すと同時にtfmigrate対象ディレクトリを特定 - (2)
terraform planの対象となるステートディレクトリは下記を計算したものです- ( + ) git 差分のあるステートディレクトリ
- ( + ) モジュール(
modulesディレクトリ)に変更が入っている場合、それを使っているリソースのステートディレクトリ - ( - )
tfmigrate対象ディレクトリ - ( check ) リソースが残っていたのにディレクトリごと削除された場合にエラーを吐く。リソースが残っているのに
terraform管理外になるという最悪の事態を避ける必要があります。ディレクトリごと削除できるのはリソースが残っていない場合のみに制限する必要があります
- (3) 上記で取得したステートディレクトリは、
merge_groupイベントでも使うので、アーティファクトに保存しておく - (4) 上記で取得したステートディレクトリに対して、
terraform planを実施する
- (1)
- 実際は
-
terraform apply- 実際は
terragrunt apply -
merge_groupイベント - 流れ
- (1) アーティファクトから
terraform apply対象ディレクトリを取得する- 改めて
terraform apply対象ディレクトリを探索してもいいですが二度もやるのは面倒なのでアーティファクトを使いましょう。terraform applyをpushイベントでやる場合はデフォルトブランチとの差分を取得するのが無理なので、アーティファクトの利用が必須です
- 改めて
- (2) 各ステートディレクトリに
terraform applyを実施する - (3) 各プロバイダー&リージョンディレクトリに対して
tfmigrate applyを実施する-
tfmigrate applyは、必ずterraform applyの後に実施してください。tfmigrate applyを先にやるとtfmigrate applyが成功して、terraform applyが失敗したとき、PR内のその後のコミットでtfmigrate applyが成功したステートディレクトリに対して変更を加えてしまうと、そのPRは次の環境のブランチに対して適用不可能となってしまいます
-
- (1) アーティファクトから
- 実際は
- 別ブランチに対してPR作成
-
merge_groupイベント - stag ブランチにマージしたのであれば、そのPRをそのまま prod に作成します
- PRは毎回作成します
- ステート操作があるので、stag ブランチにたまった複数のPRをまとめて prod ブランチに適用しようとするとエラーになります
-
-
terragrunt init-
pull_requestイベント -
terragrunt initによる自動生成が行われているかをチェックします。
-
コードオーナー
例えば、prod ブランチへのPRはコードオーナーのレビューを必須にします。ここは適宜設定してください。
merge_groupイベント
terraform CI/CD では merge_group イベントが非常に便利です。
terraform plan が成功していても terraform apply で失敗することは頻繁にあります。push イベントを使うと terraform apply が失敗してもマージされてしまうので、デフォルトブランチが汚染されてしまいます。
ただ、Enterprise プランしか使えないのがキツイです。
matrix ジョブの利用
plan(apply) を実施するステートディレクトリを特定したら、matrix ジョブを使います。
私の場合、プロバイダー&リージョンディレクトリに対しても matrix ジョブを使いたいし、ステートディレクトリに対しても matrix ジョブを使いたかったのですが、matrix ジョブからさらに matrix ジョブを呼ぶというのは技術的にできません。なので、プロバイダー&リージョンディレクトリに対する処理を workflow_call イベントとしてまとめることで、workflow_call を matrix ジョブで呼び出し、その中でさらに matrix ジョブを呼び出して、ステートディレクトリそれぞれに対して plan(apply)を実行しています。
シークレット管理
AWS Secrets Manager を使います。
Secrets Manager の箱は terraform で作って、実際の値は AWS CLI もしくは AWS コンソールから変更します。
例えば、DBのパスワードを更新したければ、AWS コンソール で更新した後に terraform でmaster_password_wo_version の値を引き上げます。
master_password ではなくて、master_password_wo と master_password_wo_version を使うようにしてください。そうしないと、ステートファイルにパスワードが書き込まれてしまいます。
data "aws_secretsmanager_secret_version" "data_aws_secretsmanager_secret_version_aurora" {
count = var.secretsmanager_secret_hub_id == "ThisIsAMockValue" ? 0 : 1
secret_id = var.secretsmanager_secret_hub_id
}
resource "aws_rds_cluster" "aws_rds_cluster_aurora" {
---
master_password_wo = try(
jsondecode(data.aws_secretsmanager_secret_version.data_aws_secretsmanager_secret_version_aurora[0].secret_string)["POSTGRES_PASSWORD"],
"POSTGRES_PASSWORD_NOT_SET"
)
master_password_wo_version = 6
---
}
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を使うと、自動テストが流れません - PR を作成して承認したいとき、別ユーザでないと承認できません。なので、作成を GitHub Apps Token で行い、承認を
secrets.GITHUB_TOKENで行う必要があります
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 のコードの複雑さを考えると、マルチテナントの方が圧倒的に見通しが良くなります。
デメリットとして、耐障害性が低いことが挙げられます。
k8s の CI/CD (myClusProjectレポジトリ)
各ツールの特徴
-
argocd-vault-plugin← 使用しています- AWS Secrets Manager からシークレットを取得してきて、argoCD 上で Manifest ファイルに埋め込んでくれます
- 他に選択肢が多くあったので迷いましたが、スター数とドキュメントの充実度から採用しました。採用理由があまり参考にならなくてすみません
-
helmfile← 使用しています-
helmチャートで DRY に Manifest を書けるので便利です - 他の選択肢として挙げられるのは
kustomizeですかね - 会社で少し経験があったから採用しました。こちらもあまり参考にならなくてすみません
-
-
sops← 使用しています- 例えば、argoCD は argoCD で管理しない方針をとっているので、argoCD にシークレットを注入したい場合、
argocd-vault-pluginが使えません。なので、それらは sops で暗号化してから GitHub に push しています - sops で暗号化したシークレットは、CI 上で復号できることを確認するといいかもしれません
- 例えば、argoCD は argoCD で管理しない方針をとっているので、argoCD にシークレットを注入したい場合、
- AWS Secrets Manager ← 使用しています
- AWS Secrets Manager にセットしておけば、
terraform側でも使えるし、k8s 側でも使えるのがいいですね -
HashiCorp Vaultが他に選択肢としてあると思うが、terraformのコードに注入する場合、これ専用のプロバイダーが必要みたいで、少し面倒だと感じました
- AWS Secrets Manager にセットしておけば、
ディレクトリ構成
最下部のディレクトリが argoCD 上のアプリケーション名となります。そして同時に、k8s上のリソース名となります。
その一つ上のディレクトリが argoCD 上の名前空間となります。そしてほとんどの場合、k8s上の名前空間となります。ただ必ずしも、k8s上の名前空間となりません。例えば、ingressclass/alb を見ると、ingressclass は argoCD 上の名前空間ですが、k8s上の名前空間にはなりません。ingressClass リソースには名前空間という概念がないからです。
~/myClusProject$ tree -a -L 9 -I ".git"
.
├── .github
│ ├── actions
│ │ ├── decrypt_secret_files_by_sops
│ │ │ └── action.yml
│ │ ├── install_tools
│ │ │ └── action.yml
│ │ ├── kubectl_apply
│ │ │ └── action.yml
│ │ ├── kubectl_diff
│ │ │ └── action.yml
│ │ └── update_kubeconfig
│ │ └── action.yml
│ └── workflows
│ ├── exec_actionlint.yml
│ ├── kubectl_apply_files_not_managed_by_argocd.yml
│ ├── pre-commit_pre-push_check.yml
│ ├── restrict_helmfile_name_and_exec_helmfile_template.yml
│ └── show_kubectl_diff_in_git_diff_dirs.yml
├── .lefthook
│ ├── ci
│ │ └── restrict_helmfile_name_and_exec_helmfile_template.sh
│ ├── pre-commit
│ │ └── prevent_commiting_secrets.sh
│ └── pre-push
│ └── test_push.sh
├── .sops.yaml
├── README.md
├── charts
│ ├── appproject
│ │ ├── Chart.yaml
│ │ ├── templates
│ │ │ └── appproject.yaml
│ │ └── values.yaml
│ ├── generalapp
│ │ ├── Chart.yaml
│ │ ├── templates
│ │ │ ├── generalapp-deployment.yaml
│ │ │ ├── generalapp-ingress.yaml
│ │ │ ├── generalapp-secret.yaml
│ │ │ └── generalapp-service.yaml
│ │ └── values.yaml
│ └── ingressclass
│ ├── Chart.yaml
│ ├── templates
│ │ ├── ingressclass.yaml
│ │ └── ingressclassparams.yaml
│ └── values.yaml
├── envs
│ ├── messi
│ │ ├── aws-ap-northeast-1
│ │ │ ├── argocd
│ │ │ │ ├── applicationset
│ │ │ │ │ └── applicationset.yaml
│ │ │ │ ├── appproject
│ │ │ │ │ ├── messi-appproject-auto-generated.yaml
│ │ │ │ │ ├── messi-appproject-helmfile.yaml
│ │ │ │ │ ├── messi-appproject-values.yaml
│ │ │ │ │ ├── prod-appproject-auto-generated.yaml
│ │ │ │ │ ├── prod-appproject-helmfile.yaml
│ │ │ │ │ ├── prod-appproject-values.yaml
│ │ │ │ │ ├── stag-appproject-auto-generated.yaml
│ │ │ │ │ ├── stag-appproject-helmfile.yaml
│ │ │ │ │ └── stag-appproject-values.yaml
│ │ │ │ └── argocd
│ │ │ │ ├── README_archived.md
│ │ │ │ ├── argocd-nucleus-auto-generated.yaml
│ │ │ │ ├── argocd-nucleus-helmfile.yaml
│ │ │ │ ├── argocd-nucleus-values.yaml
│ │ │ │ └── myclus-ssh-private-key-secret.yaml
│ │ │ ├── gobackend
│ │ │ │ └── gobackend
│ │ │ │ ├── gobackend-auto-generated.yaml
│ │ │ │ ├── gobackend-helmfile.yaml
│ │ │ │ └── gobackend-values.yaml
│ │ │ ├── ingressclass
│ │ │ │ └── alb
│ │ │ │ ├── alb-auto-generated.yaml
│ │ │ │ ├── alb-helmfile.yaml
│ │ │ │ └── alb-values.yaml
│ │ │ └── my-nginx
│ │ │ └── my-nginx
│ │ │ ├── my-nginx-auto-generated.yaml
│ │ │ ├── my-nginx-helmfile.yaml
│ │ │ └── my-nginx-values.yaml
│ │ └── aws-us-east-1
│ │ └── .gitkeep
│ ├── prod
│ │ ├── aws-ap-northeast-1
│ │ │ └── .gitkeep
│ │ └── aws-us-east-1
│ │ └── .gitkeep
│ └── stag
│ ├── aws-ap-northeast-1
│ │ └── .gitkeep
│ └── aws-us-east-1
│ └── .gitkeep
└── lefthook.yml
39 directories, 59 files
argoCD の監視
applicationSet リソースの Git Generator で argoCD の監視対象を決めます。
私は、argoCD 自身に関わるリソースは監視対象から外したいので、argoCD 名前空間を示すディレクトリは、argoCD 監視対象から除外しています。
messi 環境の EKS は messi ブランチの messi ディレクトリ、stag 環境の EKS は stag ブランチの stag ディレクトリ、prod 環境の EKS は prod ブランチの prod ディレクトリを監視します。
ブランチ戦略
上述したように、messi 環境の EKS は messi ブランチの messi ディレクトリ、stag 環境の EKS は stag ブランチの stag ディレクトリ、prod 環境の EKS は prod ブランチの prod ディレクトリと状態が一致します。
terraform プロジェクトと違って、PRがマージされるたびに次の環境へのPRを自動作成する必要はないです。
stag へリリースしたいときは、messi ブランチを stag ブランチにマージします。
argocd-vault-plugin
公式Docにある argocd-vault-plugin generate . をそのまま使用すると、 helmfile 等がパースエラーになってしまうので、少し修正して余計なファイルをargocd-vault-plugin generate コマンドの対象外にしましょう。
helmfile
helm チャートを使っていますが、helmfile は argoCD の監視対象にはしません。
helmfile template によって helmfile を通常の Manifest に変換するのはローカルで済ませておきます。argocd-vault-plugin と同じように、helmfile template の実行を argoCD に任せることもできますが、それ用のOSSが広く普及している印象を受けず、運用コストが高くなりそうと考えたので、手元で helmfile template を実行する方法を選択しました。もちろん、helmfile templateがしっかり実行されていることは、CI で確認します。
ingress リソースのグループ化
alb のコストを削減するために、一つのalbを複数サービスで使えるように ingress group を設定しましょう。
EKS Auto Mode では ingressClassParams リソースで設定します。
CI
-
kubectl diff-
pull_requestイベント - 差分が生じた yaml ファイルに対して、
kubectl diffを実行する
-
-
helmfile template-
pull_requestイベント - 実行漏れをチェックする
-
-
kubectl apply-
merge_groupイベント - argoCD 監視対象外の Manifest は CI で
applyします
-
共通アクション用のレポジトリ (myActionProjectレポジトリ)
ディレクトリ構成
自動化には大量のシェル芸が必須です。汎用的なアクションをここに配置しましょう。
例えば、タグ付けしてリリースするアクションは、たくさんのレポジトリで使用すると思うので、ここに切り出しましょう。
~/myActionProject$ tree -a -L 9 -I ".git"
.
├── .github
│ ├── actions
│ │ ├── private_build_push_ecr
│ │ │ └── action.yml
---
│ │ └── public_update_comment_for_log
│ │ └── action.yml
│ └── workflows
│ ├── private_exec_actionlint.yml
│ └── private_exec_tag_release.yml
├── README.md
└── version.txt
53 directories, 53 files
コンポジットアクションとworkflow_call
workflow_callを余計に使うと実行時間が無駄に増えるので(ジョブが増えるから)、コンポジットアクションで済むのなら、workflow_callを気軽に使わずにコンポジットアクションを使うことが重要です。
私は、terraform プロジェクトで、matrix ジョブから matrix ジョブを呼ぶ時のみworkflow_callを使用しています。
アプリプロジェクト (myAppProject)
ディレクトリ構成
~/myAppProject$ tree -a -L 9 -I ".git"
.
├── .env
├── .github
│ ├── actions
│ │ └── build_all_images_using_cache_and_up
│ │ └── action.yml
│ ├── dependabot.yml
│ └── workflows
│ ├── build_push_and_tag_release_and_create_pr.yml
│ ├── cache_docker_image_to_default_branch.yml
│ ├── exec_actionlint.yml
│ ├── go_test.yml
│ ├── pre-commit_pre-push_check.yml
│ └── static_analysis.yml
├── .gitignore
├── .lefthook
│ ├── pre-commit
│ │ ├── required_command_check.sh
│ │ ├── static_analysis.sh
│ │ └── static_analysis_format.sh
│ └── pre-push
│ └── test_push.sh
├── README.md
├── docker
│ ├── migrate
│ │ └── Dockerfile
│ └── web
│ └── Dockerfile
├── docker-compose.yml
├── lefthook.yml
├── migrate
│ ├── ...
---
├── version_migrate.txt
├── version_web.txt
└── web
├── ...
---
Docker
- migrate イメージは Manifest の
initContainersに配置します - web イメージは Manifest の普通のコンテナに配置します
- イメージサイズが大きすぎるとPULLに時間がかかりますし、ECR の金額も上がってしまうので、積極的にマルチステージビルドを使いましょう
- CI で使う場合は
build-push-actionアクションを使用しましょう
CI
- ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成
-
dispatch_workflowイベント - 共通アクションレポジトリの「ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成」アクションを利用します。これによりボタン一つで k8s プロジェクトにPRが作成されます。それをマージすればリリースです
- PRを自動作成するときに、
sedコマンドによって前回イメージを新しいイメージに置き換えますが、タグ名を抜いたイメージURIを使って置き換え対象を見つけましょう。version_web.txt から取得した前回イメージのタグ名を含めたフルURIを使うと、前回マージしてなかった場合、sedコマンドでイメージを捕捉できなくなります
-
その他
金額
上記の構成で数か月間続けましたが、AWS に月額300ドルくらいかかりました。
GitHub Enterprise, ChatGPT, GitHub Copilot の課金もあるので、全体で月額400ドルくらいです。
AWS高すぎます...
postgress or mysql
私はライターをスケールさせたいので、この前 postgress に変更しました。
ライターのスケールを考えると、Google Spanner, TiDB, DSQL です。
Google Spanner と DSQL は Postgress しか使えません。TiDB は MySQL しか使えません。
オンラインゲームのDB(ライターの急激な上昇)で必ず登場するのが Google Spanner なので、 postgress に変更しようかなーと思っていましたが、DSQL の登場により決心がつきました。
terragrunt & tfmigrate
terragrunt v0.73.0+ 以降では、TFMIGRATE_EXEC_PATH="terragrunt" を指定しても、tfmigrate plan がエラーになってしまいます。
TFMIGRATE_EXEC_PATH="terragrunt run --" を指定すれば成功します。


