私の CI/CD の結論

# 最新バージョンを参照するほうがいいかもしれません
[私の CI/CD の結論 v2](https://qiita.com/ShiroatoHiro/items/21c68e25284a346a94a5) が出ました。

# 前書き
私の個人開発で採用している CI/CD を参考に、CI/CD の全体像を理解することが目的です。
私は個人開発でやってますが、どの場面においても複数人, 中・大規模での開発を想定しているので、その前提で読んでください。例えば、小規模開発であればステート分割などをせずに全体に対して `terraform plan`(`apply`) を実行すればいいですが、複数人, 中・大規模での開発を想定するのであれば、適切にステート分割を行い差分のあるディレクトリのみを `terraform plan`(`apply`) するということが必要になってきます。

# 自動化のすばらしさを知るために、試しにリリースしてみる ※ これは `terraform` は関係ない
## (1) アプリケーションコードに変更を加える前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/60a3b1b2-a3f8-448d-86d2-e38caaa59eb7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/2c77e75b-141b-4435-8437-91ad5020a2fd.png)

## (2) アプリのレポジトリで開発する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/4702112a-d37e-473d-82d6-b6163f12b858.png)

## (3) 「ビルド & ECRへの `push` & タグ付け & k8sレポジトリへのPR作成」をボタン一つで完結
GitHub Actions の `dispatch_workflow` イベントを使って、Dockerイメージのビルド, ECRへの `push`, タグ付け, k8sレポジトリへのPR作成 を自動化してます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/1bf7152a-c0f8-46ef-b164-2b4aa93fd884.png)

## (4) 自動作成されたk8sプロジェクトへのPRをマージする
k8s レポジトリへのPRは、イメージ名だけ変更したPRとなっています。`sed` コマンドを使って前回イメージ名を新しいイメージ名に置き換えてます。
自動作成されたPRを確認してマージします。
![Screenshot 2025-03-07 045222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/c9afc424-8751-4b66-ad20-7b93b1075861.png)

## (5) アプリケーションコードに変更を加えた後
k8sレポジトリにマージされたら、argoCD が監視しているので勝手にデプロイされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/781d9f29-ce68-495e-8d2d-189ed8a141f9.png)

# `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` をやったら、なんのために ステート分割をしているのかという話になるので...
  - 他のステートディレクトリの値に依存していて、その値は `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` のような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまうのです

## ディレクトリ構成
私が目標とするインフラ状態に対してどのようなディレクトリ構成にするのかを、箇条書きにしました。

- 環境ごとのインフラ状態はできるかぎり同じ状態にしたいです。つまり、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` 含めて特有の環境だけに存在することを許可します
- 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` によって自動生成されたものです。
``` envs/terragrunt.hcl
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` を実施する
- `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は次の環境のブランチに対して適用不可能となってしまいます
- 別ブランチに対して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` を使うようにしてください。そうしないと、ステートファイルにパスワードが書き込まれてしまいます。**

```xxx.tf
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 上で復号できることを確認するといいかもしれません
- AWS Secrets Manager ← 使用しています
  - AWS Secrets Manager にセットしておけば、`terraform` 側でも使えるし、k8s 側でも使えるのがいいですね
  - `HashiCorp Vault` が他に選択肢としてあると思うが、`terraform` のコードに注入する場合、これ専用のプロバイダーが必要みたいで、少し面倒だと感じました

## ディレクトリ構成
最下部のディレクトリが 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 --"` を指定すれば成功します。

https://github.com/minamijoyo/tfmigrate/pull/203

https://github.com/minamijoyo/tfmigrate/issues/204