前書き
私の個人開発で採用している 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
を使わずにシェル芸をメインにterraform
CI/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 --"
を指定すれば成功します。