私の CI/CD の結論 v2

# 前書き
私の個人開発で採用している 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)
※ 後述しますが、本プロジェクトは github flow を使っていますが、git flow にいつでも移行できるように、デフォルトブランチは main ではなくて develop としています。

## (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 
## 用語
- `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` を一番上位の `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` 差分がある場合でもエラーを吐いてくれません
  - 保守性の観点から依存するツールはできる限り減らしたいと思っているので、tfmigrate を使わない構成で試しています
    - 以前、`terragrunt` と `tfmigrate` の併用がうまくいかないことがあり、[解決する](https://github.com/minamijoyo/tfmigrate/pull/205)のに時間がかかってしまいました。`terragrunt` と `tfmigrate`を併用しているユーザがあまりいないのは `tfmigrate` を使わなくなった理由の一つです
- `tfcmt`(`github-comment`) ← 私は使用していません
  - `terraform plan` などのをログをPRのコメントに出してくれます
  - PRのコメントに長いログが出ると、スクロールが面倒になるので、採用しませんでした
  - 私は `merge_group` イベント内の `terraform apply` 時は、ログへのリンクとステータスを GitHub API を叩いてPRのコメントに表示しています
    - `merge_group`イベントは、実行中はそのPRからリンクを辿れますが、完了してしまうとその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` を使っていましたが、`import`, `mv`, `rm` ブロックを使うようにします
    - `tfmigrate` よりも慎重にステート操作をする必要が出てきますが、保守性を考えて `tfmigrate` を使わない構成にしています

```
~/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
│   │   ├── terraform_apply
│   │   │   └── action.yml
│   │   └── terraform_plan
│   │       └── action.yml
│   └── workflows
│       ├── detect_git_diff_among_envs.yml
│       ├── detect_git_diff_in_common_dir_from_common_branch.yml
│       ├── exec_actionlint.yml
│       ├── pre-commit_pre-push_check.yml
│       ├── reusable_terraform_apply.yml
│       ├── reusable_terraform_plan.yml
│       ├── terraform_apply_caller.yml
│       ├── terraform_init_validate.yml
│       └── 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-global
│   │       └── hub
│   │           ├── .terraform
│   │           ├── .terraform.lock.hcl
│   │           ├── _backend.tf
│   │           ├── _provider.tf
│   │           ├── _variable.tf
│   │           ├── aws_route53_zone.tf
│   │           ├── aws_route53domains_registered_domain.tf
│   │           ├── output.tf
│   │           └── terragrunt.hcl
│   ├── messi
│   │   ├── aws-ap-northeast-1
│   │   │   ├── cdn
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_cloudfront_distribution.tf
│   │   │   │   ├── aws_cloudfront_key_group.tf
│   │   │   │   ├── aws_cloudfront_origin_access_control.tf
│   │   │   │   ├── aws_cloudfront_public_key.tf
│   │   │   │   ├── aws_cloudwatch_event_rule.tf
│   │   │   │   ├── aws_cloudwatch_event_target.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── aws_lambda_function.tf
│   │   │   │   ├── aws_lambda_permission.tf
│   │   │   │   ├── aws_s3_bucket.tf
│   │   │   │   ├── aws_s3_bucket_cors_configuration.tf
│   │   │   │   ├── aws_s3_bucket_notification.tf
│   │   │   │   ├── aws_s3_bucket_policy.tf
│   │   │   │   ├── aws_sns_topic.tf
│   │   │   │   ├── aws_sns_topic_policy.tf
│   │   │   │   ├── aws_sns_topic_subscription.tf
│   │   │   │   ├── data_archive_file.tf
│   │   │   │   ├── data_aws_secretsmanager_secret_version.tf
│   │   │   │   ├── lambda
│   │   │   │   │   ├── mediaconvert
│   │   │   │   │   │   ├── main.py
│   │   │   │   │   │   └── mediaconverter_post_schema.json
│   │   │   │   │   ├── mediaconvert.zip
│   │   │   │   │   ├── mediaconvert_notification
│   │   │   │   │   │   └── main.py
│   │   │   │   │   └── mediaconvert_notification.zip
│   │   │   │   ├── output.tf
│   │   │   │   ├── public_key.pem
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── cognito
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_cognito_identity_pool.tf
│   │   │   │   ├── aws_cognito_identity_pool_roles_attachment.tf
│   │   │   │   ├── aws_cognito_identity_provider.tf
│   │   │   │   ├── aws_cognito_user_pool.tf
│   │   │   │   ├── aws_cognito_user_pool_client.tf
│   │   │   │   ├── aws_cognito_user_pool_domain.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── data_aws_region.tf
│   │   │   │   ├── data_aws_secretsmanager_secret_version.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── db
│   │   │   │   ├── .terraform
│   │   │   │   ├── .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
│   │   │   ├── ecr
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_ecr_repository_asymmetry.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── hub
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_eip.tf
│   │   │   │   ├── aws_internet_gateway.tf
│   │   │   │   ├── aws_internet_gateway_attachment.tf
│   │   │   │   ├── aws_nat_gateway.tf
│   │   │   │   ├── aws_route.tf
│   │   │   │   ├── aws_route_table.tf
│   │   │   │   ├── aws_route_table_association.tf
│   │   │   │   ├── aws_subnet.tf
│   │   │   │   ├── aws_vpc.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── main
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.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
│   │   │   │   ├── data_aws_iam_policy_document.tf
│   │   │   │   ├── data_aws_iam_policy_document_asymmetry_avp.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── route
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   └── secret
│   │   │       ├── .terraform
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── _variable.tf
│   │   │       ├── aws_kms_alias.tf
│   │   │       ├── aws_kms_key.tf
│   │   │       ├── aws_secretsmanager_secret.tf
│   │   │       ├── aws_secretsmanager_secret_version.tf
│   │   │       ├── output.tf
│   │   │       └── terragrunt.hcl
│   │   └── aws-us-east-1
│   │       ├── hub

---

│   ├── prod
│   │   ├── aws-ap-northeast-1

---

│   ├── stag
│   │   ├── aws-ap-northeast-1

---

│   └── terragrunt.hcl
├── lefthook.yml
└── modules
    ├── .gitkeep
    └── test_sg
        ├── main.tf
        ├── outputs.tf
        └── variables.tf
```

## `terragrunt` による自動生成
下記のように一番トップの `terragrunt.hcl` に値を埋め込んで、`terragrunt init` で `_variable.tf` などを自動生成します。
`_*.tf` という形式のファイルは `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"

  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 ディレクトリも作ってあり、ホストゾーンなどの全環境で共通して使用するものを定義しています。全環境に影響する可能性があるので、common ブランチへのマージはコードオーナーで制限をつけます。

## CI

- `terraform plan`
  - 実際は `terragrunt plan`
  - `pull_request` イベント
  - 流れ
    - (1) `terraform plan` の対象となるステートディレクトリは下記を計算したものです
      - ( **+** ) git 差分のあるステートディレクトリ
      - ( **+** ) モジュール(`modules`ディレクトリ)に変更が入っている場合、それを使っているリソースのステートディレクトリ
      - ( **-** ) `tfmigrate` 対象ディレクトリ
      - ( **check** ) リソースが残っていたのにディレクトリごと削除された場合にエラーを吐く。リソースが残っているのに `terraform` 管理外になるという最悪の事態を避ける必要があります。ディレクトリごと削除できるのはリソースが残っていない場合のみに制限する必要があります
    - (2) 上記で取得したステートディレクトリは、`merge_group` イベントでも使うので、アーティファクトに保存しておく
    - (3) 上記で取得したステートディレクトリに対して、`terraform plan` を実施する
- `terraform apply`
  - 実際は `terragrunt apply`
  - `merge_group` イベント
  - 流れ
    - (1) アーティファクトから `terraform apply` 対象ディレクトリを取得する
      - 改めて `terraform apply` 対象ディレクトリを探索してもいいですが二度もやるのは面倒なのでアーティファクトを使いましょう。`terraform apply` を `push` イベントでやる場合はデフォルトブランチとの差分を取得するのが無理なので、アーティファクトの利用が必須です
    - (2) 各ステートディレクトリに `terraform apply` を実施する
    - (3) 次のブランチに対してPR作成
      - stag ブランチにマージしたのであれば、そのPRをそのまま prod に作成します
      - PRは毎回作成します
        - ステート操作があるので、stag ブランチにたまった複数のPRをまとめて一つのPRとして prod ブランチに適用しようとするのは避けましょう
          - 例えば、messi 環境で `import` ブロックを使ってリソースを `import` して、その後 `import`ブロックを削除したとします。その後に、messi 環境から stag 環境にまとめて PR を作成したら、ステート操作ではなくて、新しくリソースが作成されてしまいます
- `terragrunt init` & `terragrunt validate`
  - `pull_request` イベント
  - 流れ
    - (1) `terragrunt init` による自動生成が行われているかをチェックします。 
    - (2) `terragrunt validate` もやります
      - `terragrunt validate` 単体で実行すると、`terragrunt init` が済んでない場合、事前に `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` の値を引き上げます。
これにより、GitHub にシークレットが載ることはなくなりました。

ただ、ステートファイルへのシークレットの書き込みは完全に防げてません。
`aws_rds_cluster`リソースについては、`master_password` ではなくて、`master_password_wo` と `master_password_wo_version` を使うことで、ステートファイルへのパスワードの書き込みを防げます。
しかし、`aws_secretsmanager_secret_version` ブロックは、シークレットをステートファイルに書き込んでしまいます。

調べたところ、以下の二つが選択肢のようです
- local-exec を使うことで、動的にシークレットを取得する
- tfstate ファイル自体をシークレットとして管理する

本プロジェクトは検討中です。
後者の場合、tfstate を管理しているバケットに対する権限を Deny した IAM ロールを発行することになりそうでしょうか?
有識者いたら教えてください。

```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
## 各ツールの特徴
- `argocd-vault-plugin` ← 使用しています
  - AWS Secrets Manager からシークレットを取得してきて、argoCD 上で Manifest ファイルに埋め込んでくれます
  - 他に選択肢が多くあったので迷いましたが、スター数とドキュメントの充実度から採用しました。採用理由があまり参考にならなくてすみません
- `helm` ← 使用しています
  - `kustomize` か `helm` が選択肢だと思います
  - 既に会社で使っていたのも理由としてありますが、より多くのユーザがいると思ったのと、環境間のDRYを徹底できそうという理由で採用しました
  - `application`(`applicationSet`) に `helm` の設定を定義することで、`helm` はそのまま argoCD に解釈してもらえます。しかし私は 生の Manifest に変換しているので `helm` をそのまま解釈してもらうことはしていません
- `helmfile` ← 使用しています
  - `helmfile` を使わずに `helm` だけを使う方法の方がより一般的かもしれませんが、私は `helmfile`を採用しています
  - 私は可読性のために、また、生の Manifest を追加できるようにするために、生の Manifest を GitHub で管理して、さらには、argoCD の監視対象としています。そのために、手元やCIで `helm template` コマンドもしくは `helmfile template` コマンドをする必要があります。`helm template` コマンドを使うとしたら、コマンドの引数に Helm Chart などを指定する必要があり、自動化のスクリプトの複雑性が増します。対して`helmfile template` コマンドは、helmfile ファイルを定義してあげれば、非常に単純になるので、自動化スクリプトも単純になります
  - argoCD はネイティブで helmfile を解釈しません。なので、`helmfile` を argoCD 上で展開するためには、argoCD に少し複雑な設定をしないといけません。このための OSS はありますが、あまり普及していない印象です
- `sops` ← 使用しています
  - argoCD 監視対象外でシークレットを注入したいときに使います。例えば、argoCD は argoCD で管理しない方針をとっているので、argoCD にシークレットを注入したい場合、`argocd-vault-plugin` が使えません。なので、それらは sops で暗号化してから GitHub に push しています
  - sops で暗号化したシークレットは、CI 上で復号してから、`kubectl apply`されます
- 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
│   ├── generalapp
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── generalapp-deployment.yaml
│   │       ├── generalapp-ingress-internal.yaml
│   │       ├── generalapp-ingress.yaml
│   │       ├── generalapp-secret.yaml
│   │       └── generalapp-service.yaml
│   └── ingressclass
│       ├── Chart.yaml
│       └── templates
│           ├── ingressclass.yaml
│           └── ingressclassparams.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-namespace.yaml
│   │   │   │       ├── 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
│   │   │   │   └── alb-internal
│   │   │   │       ├── alb-internal-auto-generated.yaml
│   │   │   │       ├── alb-internal-helmfile.yaml
│   │   │   │       └── alb-internal-values.yaml
│   │   │   └── my-nginx
│   │   │       └── my-nginx
│   │   │           ├── my-nginx-auto-generated.yaml
│   │   │           ├── my-nginx-helmfile.yaml
│   │   │           └── my-nginx-values.yaml
│   │   └── aws-us-east-1
│   │       ├── ---

---

│   ├── prod
│   │   ├── aws-ap-northeast-1

---

│   └── stag
│       ├── aws-ap-northeast-1

---

└── lefthook.yml
```

## argoCD の監視
`applicationSet` リソースの Git Generator で argoCD の監視対象を設定します。
私は、argoCD 自身に関わるリソースは監視対象から外したいので、argoCD 名前空間を示すディレクトリは、argoCD 監視対象から除外しています。

以下のように、EKSとディレクトリを対応させます。

- messi 環境 & aws-ap-northeast-1 の EKS は messi ブランチの envs/messi/aws-ap-northeast-1 ディレクトリに対応する
- messi 環境 & aws-us-east-1 の EKS は messi ブランチの envs/messi/aws-us-east-1 ディレクトリに対応する
- stag 環境 & aws-ap-northeast-1 の EKS は stag ブランチの envs/stag/aws-ap-northeast-1 ディレクトリに対応する
- stag 環境 & aws-us-east-1 の EKS は stag ブランチの envs/stag/aws-us-east-1 ディレクトリに対応する
- ...

## ブランチ戦略
上述したように、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` コマンドの対象外にしましょう。

## `ingress` リソースのグループ化
alb のコストを削減するために、一つのalbを複数サービスで使えるように ingress group を作成しましょう。EKS Auto Mode では `ingressClassParams` リソースを使って、グループを設定します。
ただし、パブリックな alb とインターナルな alb は scheme が違うので、別々の alb にする必要があります。

## CI
- `kubectl diff`
  - `pull_request` イベント
  - 差分が生じた yaml ファイルに対して、`kubectl diff` を実行する
    - 差分が生じた yaml ファイルは以下の手順で求められます
      - (**+**) `*.yaml`
      - (**+**) 変更があった Helm Chart を使っている `*.yaml`
      - (**-**) `*values.yaml`
        - もし、このファイル名の形式を守っていなかったとしても、`kubectl diff`実行時にエラーになってくれるので気づける
      - (**-**) `*helmfile.yaml`
        - もし、このファイル名の形式を守っていなかったとしても、`kubectl diff`実行時にエラーになってくれるので気づける
  - `kubectl diff`は、差分があるときも、致命的なエラーの時も、どっちもエラー判定になってしまうので、エラーログを見て、致命的なエラーの時だけCIをエラーにする必要があります
- `helmfile template`
  - `pull_request` イベント
  - `helmfile template` コマンドの実行漏れをチェックする 
    - もちろん、全ての helmfile ファイルに対して、`helmfile template` を実行する自動化スクリプトをあらかじめ用意しておきます。
  - helmfile ファイルが、`*helmfile.yaml`の形式であることもチェックする
- `kubectl apply` 
  - `merge_group` イベント
  - argoCD 監視対象外の Manifest を CI で `apply`します
    - `apply` 対象のアプリケーションは、手動でセットします

# 共通アクション用のレポジトリ
## ディレクトリ構成
自動化には大量のシェル芸が必須です。汎用的なアクションをここに配置しましょう。
例えば、タグ付けしてリリースするアクションは、たくさんのレポジトリで使用すると思うので、ここに切り出しましょう。
```
~/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

55 directories, 56 files
```

## コンポジットアクションと`workflow_call`
`workflow_call`を余計に使うと実行時間が無駄に増えるので(ジョブが増えるから)、コンポジットアクションで済むのなら、`workflow_call`を気軽に使わずにコンポジットアクションを使うことが重要です。
私は、`terraform` プロジェクトで、`matrix` ジョブから `matrix` ジョブを呼ぶ時のみ`workflow_call`を使用しています。

# アプリプロジェクト 
## ディレクトリ構成
```
~/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
- イメージサイズが大きすぎるとPULLに時間がかかりますし、ECR の金額も上がってしまうので、積極的にマルチステージビルドを使いましょう
- CI で使う場合は `build-push-action` アクションを使用しましょう

## k8s
- migrate イメージは Manifest の `initContainers` に配置します
- web イメージは Manifest の普通のコンテナに配置します

## CI
- ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成
  - `dispatch_workflow` イベント
  - 共通アクションレポジトリの「ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成」アクションを利用します。これによりボタン一つで k8s プロジェクトにPRが作成されます。それをマージすればリリースです
  - PRを自動作成するときに、`sed` コマンドによって前回イメージを新しいイメージに置き換えますが、タグ名を抜いたイメージURIを使って置き換え対象を見つけましょう。version_web.txt から取得した前回イメージのタグ名を含めたフルURIを使うと、前回のリリースをマージしてなかった場合、`sed` コマンドでイメージを捕捉できなくなります

## git flow or github flow
常にデフォルトブランチの最新コミットをリリースしたい場合は github flow を使い、デフォルトブランチの特定のコミットでリリースしたい場合は git flow を使うことになるでしょう。
本プロジェクトはまだ世にリリースしておらず、git flow を使う意味は全くないので、github flow を使っています。デフォルトブランチとそこから切り出した feature ブランチのみを使います。デフォルトブランチは通常は、main ブランチですが、私は git flow にいつでも移行できるように develop ブランチをデフォルトブランチとしています。
個人的に git flow は冗長だと思うので、git flow を使う場合は、少しカスタマイズしてもいいと思っています。具体的には、release ブランチを使わずに、develop (デフォルト)ブランチ,  develop ブランチから切り出した feature ブランチ, main ブランチ, main ブランチから切り出した hotfix ブランチのみを使います。

# その他
## postgress or mysql
私はライターをスケールさせたいので、この前 postgress に変更しました。
ライターのスケールを考えると、Google Spanner, TiDB, DSQL です。
Google Spanner と DSQL は Postgress しか使えません。TiDB は MySQL しか使えません。
オンラインゲームのDB(ライターの急激な上昇)で必ず登場するのが Google Spanner なので、 postgress に変更しようかなーと思っていましたが、DSQL の登場により決心がつきました。

# 前回バージョンとの差分
[前回バージョン「私の CI/CD の結論」](https://qiita.com/ShiroatoHiro/items/0efa89ca8b60bbb1ee83) との大きな差分は、以下の三つになります。

- tfmigrate を使わなくなりました
- 前回バージョンのシークレット管理方法を使えば、シークレットが tfstate ファイルに書き込まれるのを防げる言っていましたが、実際は不十分だったので訂正します。すみません
- 前回バージョンでは、他の方法との比較が不十分なまま helmfile を使っていましたが、今回バージョンは helmfile を使うことに自信を持っています