1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

私の CI/CD の結論

Last updated at Posted at 2025-03-06

前書き

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

自動化のすばらしさを知るために、試しにリリースしてみる ※ これは terraform は関係ない

(1) アプリケーションコードに変更を加える前

image.png
image.png

(2) アプリのレポジトリで開発する

image.png

(3) 「ビルド & ECRへの push & タグ付け & k8sレポジトリへのPR作成」をボタン一つで完結

GitHub Actions の dispatch_workflow イベントを使って、Dockerイメージのビルド, ECRへの push, タグ付け, k8sレポジトリへのPR作成 を自動化してます。
image.png

(4) 自動作成されたk8sプロジェクトへのPRをマージする

k8s レポジトリへのPRは、イメージ名だけ変更したPRとなっています。sed コマンドを使って前回イメージ名を新しいイメージ名に置き換えてます。
自動作成されたPRを確認してマージします。
Screenshot 2025-03-07 045222.png

(5) アプリケーションコードに変更を加えた後

k8sレポジトリにマージされたら、argoCD が監視しているので勝手にデプロイされます。
image.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.hclterragrunt によって自動生成されたものです。

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 applypush イベントでやる場合はデフォルトブランチとの差分を取得するのが無理なので、アーティファクトの利用が必須です
      • (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_callmatrix ジョブで呼び出し、その中でさらに matrix ジョブを呼び出して、ステートディレクトリそれぞれに対して plan(apply)を実行しています。

シークレット管理

AWS Secrets Manager を使います。
Secrets Manager の箱は terraform で作って、実際の値は AWS CLI もしくは AWS コンソールから変更します。
例えば、DBのパスワードを更新したければ、AWS コンソール で更新した後に terraformmaster_password_wo_version の値を引き上げます。
master_password ではなくて、master_password_womaster_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-controllerPod Identity Agent が予めインストールされており、自らインストールする必要がないので便利です。
EC2のスペックを考える必要もないです。

アクセスエントリー

従来は IAM ロール に EKS へのアクセス権限を付与するために、aws-auth という ConfigMap をいじっていたかと思いますが、アクセスエントリーを使うことで ConfigMap をいじらないで済みます。
aws-auth という ConfigMapterraform で設定する場合、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 --" を指定すれば成功します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?