0
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 (terraform, argoCD, EKS)

Last updated at Posted at 2025-08-21

前書き

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

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

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

image.png
image.png

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

image.png

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

GitHub Actions の dispatch_workflow イベントを使って、Dockerイメージのビルド, ECRへの push, タグ付け, version.txt の更新, k8sレポジトリへのPR作成 を自動化してます。
image.png
※ 後述しますが、本プロジェクトは github flow を使っていますが、git flow にいつでも移行できるように、デフォルトブランチは main ではなくて develop としています。

(4) 自動作成されたk8sレポジトリへのPRをマージする

k8s レポジトリへのPRは、イメージ名だけ変更したPRとなっています。CI で前回イメージ名を新しいイメージ名に置き換えてます。
自動作成されたPRを確認してマージします。マージの手間が面倒であれば、マージまで自動化してもいいと思います。
Screenshot 2025-03-07 045222.png

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

k8sレポジトリにマージされたら、argoCD が監視しているので勝手にデプロイされます。
image.png

terraform の CI/CD

用語

  • messi は integration 環境の名前です
  • messi, prod ディレクトリのようなディレクトリを 環境ディレクトリ と呼びます
  • aws-ap-northeast-1のようなディレクトリを プロバイダー&リージョンディレクトリ と呼びます
  • eks, networkなどの tfstate 単位のディレクトリをステートディレクトリと呼ぶことにします

各ツールの特徴

  • tfaction ← 本プロジェクトでは使用していない
    • terraform の自動化のフレームワークであり、これを使うか使わないかは大きな分かれ道だと思います。私は以下の理由で tfaction を使わずにシェル芸をメインに terraform CI/CD を構築する選択をしました
      • フレームワークだけあって一回使ってしまうと抜け出せなさそう、メンテが終了してしまったときに対処するのが大変そうという印象を受けたからです
      • 自分のやりたいことが明確に見えていたので、フレームワークに縛られたくなかったからです
      • merge_group イベントではなくてpush イベントを利用して、apply に失敗したらPR が自動で作成される仕組みにして、merge_queue の待ち時間をなくしている点は素晴らしいです
  • terragrunt ← 私は使用しています
    • terraform の薄いラッパーです
    • コードの自動生成で環境間などをDRYにできます
      • 私の場合、_provider.tf, _backend.tf を一番上位の root.hcl でまとめて設定しています
      • terragruntprovider.tfbackend.tf を自動生成してくれます。本プロジェクトでは、これだけのために terragrunt を使っています。これによって、tfstate の keyallowed_account_ids を機械的に生成することができるので、ミスがなくなります。コピペして、tfstate の key とかの更新を忘れると、間違った tfstate で管理してしまったりするので、それを防ぐことができます。まあ、ただ、terraform だけでも、shell スクリプトとかでチェックできるので、terragrunt をそこまで無理して使う必要はないと思います。
    • 強みでもあり、弱みでもあるのですが、dependency ブロックによって、run-all で順序制御しながら plan, apply などを実施できる一方で、単一のディレクトリに terragrunt init, validate, plan, apply を打つと、依存先のディレクトリの初期化までも行います。これが大規模プロジェクトというか、ディレクトリごとに plan などを実施するプロジェクト(ほとんどの企業が採用)では致命的です。初期化(init)は時間のかかる処理です。また、依存先のステートディレクトリの値がplan の時点では決定していない時、普通なら plan の時点でエラーになるが、モック値を使うことで plan 時のエラーを回避できる機能もありますが、なので、大規模を想定している本記事では dependency の使用を一切禁止しています。絶対に使ってはいけません。
    • dependency を使わずに、terraform ネイティブの terraform_remote_state を使います。
  • tfmigrate ← 本プロジェクトでは使用しなくなりました
    • import, mv, rm 操作をするときに、適用後に plan 差分がないかどうかをチェックできます
    • import, mv, rm 操作を GitHub で管理できます
    • import, moved, removed ブロックの登場により必須級ではなくなったが、適用後に plan 差分がある場合にエラーを吐いてくれるので、import, moved, removed ブロックには無い性能を持っています。import ブロック等は適用後に plan 差分がある場合でもエラーを吐いてくれません
    • 保守性の観点から依存するツールはできる限り減らしたいと思っているので、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 のような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまいます

ディレクトリ構成

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

  • 環境ごとのインフラ状態はできるかぎり同じ状態にしたいです。つまり、messi でバグが見つかれば prod でもバグが見つかるというような状態にしたいです
    • messi と prod で同期対象として明示的に指定したステートディレクトリは、完全に一致することを CI で強制します
      • 前提として、_provider.tf, _backend.tfterragrunt initで自動生成されたファイルは環境特有の値が入っているので、もちろん環境ごとに違います
      • 環境間を同期するシェルスクリプトで、同期するステートディレクトリを明示的に指定します
        • 同期するディレクトリは明示的に指定します。_provider.tf, _backend.tf は自動生成ファイルのため同期対象から外します。locals.tf も同期対象から外しますが、tflintで余計な変数を、terraform validateで変数不足をチェックしているため過不足がないことが保証されます
        • 例えば、VPNは複数環境に配置せず、prodだけに置くので、環境間・リージョン間の同期対象として指定しません
  • リージョンごとのインフラ状態はできる限り同じ状態にしたいです
    • 環境間の同期と全く同じ方法をとります
  • VPC CIDR や DBのスペック などの環境変数は一元管理したいです
    • 一番上位の root.hcl で一元管理して、terragrunt init で自動生成します
  • 原神, Apex 等のゲームみたいに、ユーザがどのリージョンのサーバを使うのかを選択できるようにしたいです
    • aws-ap-northeast-1, aws-us-east-1 のようにしてディレクトリを切ります
    • リージョンを増やしたければ、そのときにディレクトリを増やします
  • 適切にステート分割を行うことで plan(apply) の実行時間を短縮したい
    • network(ネットワーク系), eks(EKS など), db(データベースなど)...のようにして切ります。新たにステートディレクトリを切りたいときはここに並べます
ディレクトリ構成
~/MyTerraformProject$ tree -a -L 10 -I ".git"
.
├── .claude
│   └── settings.local.json
├── .github
│   ├── CODEOWNERS
│   ├── actions
│   │   ├── check_matrix_job_result
│   │   │   └── action.yml
│   │   ├── configure_credentials_from_path
│   │   │   └── action.yml
│   │   ├── convert_space_separated_array_string_to_json_array_string
│   │   │   └── action.yml
│   │   ├── create_comment_for_log_getting_pr_number_from_git_log
│   │   │   └── action.yml
│   │   ├── create_prs_to_another_environments
│   │   │   └── action.yml
│   │   ├── get_dirs_for_terraform_plan_apply
│   │   │   └── action.yml
│   │   ├── get_dirs_under_target_dir_by_depth
│   │   │   └── action.yml
│   │   ├── git_switch_commit_fetch_merge_push_with_retries
│   │   │   └── action.yml
│   │   ├── install_tools
│   │   │   └── action.yml
│   │   └── merge_temp_branch_to_pr
│   │       └── action.yml
│   └── workflows
│       ├── code_quality_checks.yml
│       ├── detect_git_diff_among_regions_envs.yml
│       ├── reusable_terraform_apply.yml
│       ├── reusable_terraform_drift_check.yml
│       ├── reusable_terraform_init_validate.yml
│       ├── reusable_terraform_plan.yml
│       ├── terraform_apply_caller.yml
│       ├── terraform_drift_check_caller.yml
│       ├── terraform_init_validate_caller.yml
│       └── terraform_plan_caller.yml
├── .gitignore
├── .lefthook
│   ├── ci
│   │   └── sync_regions_envs_check.sh
│   ├── format
│   │   ├── static_analysis_format.sh
│   │   └── sync_regions_envs.sh
│   └── pre-commit
│       ├── static_analysis.sh
│       └── static_analysis_format_check.sh
├── .tflint.ci.hcl
├── .tflint.hcl
├── .vscode
│   └── settings.json
├── CLAUDE.md
├── create_service_account.log
├── envs
│   ├── management
│   │   ├── aws-ap-northeast-1
│   │   │   ├── sso
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.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_iam_policy_document.tf
│   │   │   │   ├── data_aws_organizations_organization.tf
│   │   │   │   ├── data_aws_ssoadmin_instances.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   └── tfstate-s3-policy
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       └── terragrunt.hcl
│   │   ├── aws-us-east-1
│   │   │   ├── aws-organizations
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_controltower_landing_zone.tf
│   │   │   │   ├── aws_organizations_account.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   └── local-apply-cicd-role
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── aws_iam_openid_connect_provider.tf
│   │   │       ├── aws_iam_policy.tf
│   │   │       ├── aws_iam_role.tf
│   │   │       ├── aws_iam_role_policy_attachment.tf
│   │   │       ├── data_aws_iam_policy_document.tf
│   │   │       ├── data_aws_organizations_organization.tf
│   │   │       ├── locals.tf
│   │   │       ├── output.tf
│   │   │       └── terragrunt.hcl
│   │   └── google-us-east4
│   │       ├── local-apply-cicd-serviceaccount
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── google_iam_workload_identity_pool.tf
│   │       │   ├── google_iam_workload_identity_pool_provider.tf
│   │       │   ├── google_project_iam_member.tf
│   │       │   ├── google_service_account.tf
│   │       │   ├── google_service_account_iam_binding.tf
│   │       │   ├── locals.tf
│   │       │   ├── output.tf
│   │       │   └── terragrunt.hcl
│   │       ├── local-apply-cicd-serviceaccount-attach-org-admin
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── data_terraform_remote_state.tf
│   │       │   ├── google_organization_iam_member.tf
│   │       │   ├── google_project_iam_member.tf
│   │       │   ├── locals.tf
│   │       │   └── terragrunt.hcl
│   │       ├── local-apply-cloud-identity
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── google_cloud_identity_group.tf
│   │       │   ├── google_cloud_identity_group_membership.tf
│   │       │   ├── google_project_iam_member.tf
│   │       │   ├── locals.tf
│   │       │   └── terragrunt.hcl
│   │       ├── projects
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── google_project.tf
│   │       │   ├── locals.tf
│   │       │   └── terragrunt.hcl
│   │       └── service
│   │           ├── .terraform.lock.hcl
│   │           ├── _backend.tf
│   │           ├── _provider.tf
│   │           ├── google_project_service.tf
│   │           ├── locals.tf
│   │           └── terragrunt.hcl
│   ├── messi
│   │   ├── aws-ap-northeast-1
│   │   │   ├── argocd-alias-record
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_aws_lb.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── argocd-avp-pod-identity
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_eks_pod_identity_association.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_policy_document.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── argocd-hostzone-env-acm
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── eks
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_eks_access_entry.tf
│   │   │   │   ├── aws_eks_access_policy_association.tf
│   │   │   │   ├── aws_eks_cluster.tf
│   │   │   │   ├── aws_eks_pod_identity_association.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── data_aws_caller_identity_current.tf
│   │   │   │   ├── data_aws_iam_policy_document.tf
│   │   │   │   ├── data_aws_iam_role.tf
│   │   │   │   ├── data_aws_iam_roles.tf
│   │   │   │   ├── data_aws_region.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── hostzone-private
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_zone.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── mynginx-alias-record
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_aws_lb.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── mynginx-hostzone-env-acm
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── network
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.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
│   │   │   │   ├── data_aws_availability_zones.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-alias-record
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_aws_lb.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-alias-record-internal
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_aws_lb.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-cdn
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_cloudfront_distribution.tf
│   │   │   │   ├── aws_cloudfront_origin_access_control.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_s3_bucket.tf
│   │   │   │   ├── aws_s3_bucket_cors_configuration.tf
│   │   │   │   ├── aws_s3_bucket_notification.tf
│   │   │   │   ├── aws_s3_bucket_policy.tf
│   │   │   │   ├── aws_sqs_queue.tf
│   │   │   │   ├── aws_sqs_queue_policy.tf
│   │   │   │   ├── data_aws_caller_identity.tf
│   │   │   │   ├── data_aws_cloudfront_cache_policy.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-cognito
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.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_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-cognito-mail
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-db
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_db_instance.tf
│   │   │   │   ├── aws_db_parameter_group.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
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-ecr
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_ecr_lifecycle_policy.tf
│   │   │   │   ├── aws_ecr_replication_configuration.tf
│   │   │   │   ├── aws_ecr_repository.tf
│   │   │   │   ├── data_aws_caller_identity.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-hostzone-env-acm
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-kvs
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.12.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_elasticache_parameter_group.tf
│   │   │   │   ├── aws_elasticache_replication_group.tf
│   │   │   │   ├── aws_elasticache_subnet_group.tf
│   │   │   │   ├── aws_security_group.tf
│   │   │   │   ├── data_aws_availability_zones.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-mail
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── aws_ses_configuration_set.tf
│   │   │   │   ├── aws_ses_domain_dkim.tf
│   │   │   │   ├── aws_ses_domain_identity.tf
│   │   │   │   ├── aws_ses_domain_identity_verification.tf
│   │   │   │   ├── aws_ses_domain_mail_from.tf
│   │   │   │   ├── aws_ses_email_identity.tf
│   │   │   │   ├── aws_ses_event_destination.tf
│   │   │   │   ├── aws_ses_identity_notification_topic.tf
│   │   │   │   ├── aws_ses_template.tf
│   │   │   │   ├── aws_sns_topic.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   ├── removed.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-s3-for-ci-token
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_s3_bucket.tf
│   │   │   │   ├── aws_s3_bucket_policy.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-secret
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_kms_alias.tf
│   │   │   │   ├── aws_kms_key.tf
│   │   │   │   ├── aws_secretsmanager_secret.tf
│   │   │   │   ├── aws_secretsmanager_secret_version.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-static-site
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_cloudfront_distribution.tf
│   │   │   │   ├── aws_cloudfront_origin_access_control.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── aws_s3_bucket.tf
│   │   │   │   ├── aws_s3_bucket_policy.tf
│   │   │   │   ├── aws_s3_bucket_public_access_block.tf
│   │   │   │   ├── aws_s3_bucket_website_configuration.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── tfstate-s3-policy
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── waf
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_wafv2_ip_set.tf
│   │   │   │   ├── aws_wafv2_web_acl.tf
│   │   │   │   ├── aws_wafv2_web_acl_association.tf
│   │   │   │   ├── data_aws_lb.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   └── waf-cloudfront
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── aws_wafv2_ip_set.tf
│   │   │       ├── aws_wafv2_web_acl.tf
│   │   │       ├── data_terraform_remote_state.tf
│   │   │       ├── locals.tf
│   │   │       ├── output.tf
│   │   │       └── terragrunt.hcl
│   │   ├── aws-us-east-1
│   │   │   ├── argocd-hostzone-env
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_zone.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── argocd-hostzone-env-acm
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── hostzone
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── local-apply-cicd-role
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_iam_openid_connect_provider.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── data_aws_iam_policy_document.tf
│   │   │   │   ├── data_aws_organizations_organization.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── mynginx-hostzone-env
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.11.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_zone.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── mynginx-hostzone-env-acm
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-ecr-replicated
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_ecr_lifecycle_policy.tf
│   │   │   │   ├── aws_ecr_registry_policy.tf
│   │   │   │   ├── aws_region.tf
│   │   │   │   ├── data_aws_ecr_repository.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-hostzone-env
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_zone.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-hostzone-env-acm
│   │   │   │   ├── .terraform
│   │   │   │   │   ├── providers
│   │   │   │   │   │   └── registry.terraform.io
│   │   │   │   │   │       └── hashicorp
│   │   │   │   │   │           └── aws
│   │   │   │   │   │               └── 6.12.0
│   │   │   │   │   └── terraform.tfstate
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── myservice-hostzone-env-dmarc-record
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── data_terraform_remote_state.tf
│   │   │   │   ├── import.tf
│   │   │   │   ├── locals.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   └── myservice-secret
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── aws_kms_alias.tf
│   │   │       ├── aws_kms_key.tf
│   │   │       ├── aws_secretsmanager_secret.tf
│   │   │       ├── aws_secretsmanager_secret_version.tf
│   │   │       ├── locals.tf
│   │   │       ├── output.tf
│   │   │       └── terragrunt.hcl
│   │   ├── google-asia-northeast1
│   │   │   └── network
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── google_compute_network.tf
│   │   │       ├── google_compute_subnetwork.tf
│   │   │       ├── locals.tf
│   │   │       └── terragrunt.hcl
│   │   └── google-us-east4
│   │       ├── local-apply-cicd-serviceaccount
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── google_iam_workload_identity_pool.tf
│   │       │   ├── google_iam_workload_identity_pool_provider.tf
│   │       │   ├── google_project_iam_member.tf
│   │       │   ├── google_service_account.tf
│   │       │   ├── google_service_account_iam_binding.tf
│   │       │   ├── locals.tf
│   │       │   ├── output.tf
│   │       │   └── terragrunt.hcl
│   │       ├── network
│   │       │   ├── .terraform.lock.hcl
│   │       │   ├── _backend.tf
│   │       │   ├── _provider.tf
│   │       │   ├── google_compute_network.tf
│   │       │   ├── google_compute_subnetwork.tf
│   │       │   ├── locals.tf
│   │       │   └── terragrunt.hcl
│   │       └── service
│   │           ├── .terraform.lock.hcl
│   │           ├── _backend.tf
│   │           ├── _provider.tf
│   │           ├── google_project_service.tf
│   │           ├── locals.tf
│   │           └── terragrunt.hcl
│   └── prod
│       ├── aws-ap-northeast-1
│       │   ├── argocd-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── mynginx-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-cdn
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_cloudfront_distribution.tf
│       │   │   ├── aws_cloudfront_origin_access_control.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_s3_bucket.tf
│       │   │   ├── aws_s3_bucket_cors_configuration.tf
│       │   │   ├── aws_s3_bucket_notification.tf
│       │   │   ├── aws_s3_bucket_policy.tf
│       │   │   ├── aws_sqs_queue.tf
│       │   │   ├── aws_sqs_queue_policy.tf
│       │   │   ├── data_aws_caller_identity.tf
│       │   │   ├── data_aws_cloudfront_cache_policy.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-cognito
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.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_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-cognito-mail
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-ecr-replicated
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_ecr_lifecycle_policy.tf
│       │   │   ├── aws_ecr_registry_policy.tf
│       │   │   ├── aws_region.tf
│       │   │   ├── data_aws_ecr_repository.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-mail
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_iam_role.tf
│       │   │   ├── aws_iam_role_policy.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── aws_ses_configuration_set.tf
│       │   │   ├── aws_ses_domain_dkim.tf
│       │   │   ├── aws_ses_domain_identity.tf
│       │   │   ├── aws_ses_domain_identity_verification.tf
│       │   │   ├── aws_ses_domain_mail_from.tf
│       │   │   ├── aws_ses_email_identity.tf
│       │   │   ├── aws_ses_event_destination.tf
│       │   │   ├── aws_ses_identity_notification_topic.tf
│       │   │   ├── aws_ses_template.tf
│       │   │   ├── aws_sns_topic.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   ├── removed.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-secret
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_kms_alias.tf
│       │   │   ├── aws_kms_key.tf
│       │   │   ├── aws_secretsmanager_secret.tf
│       │   │   ├── aws_secretsmanager_secret_version.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-static-site
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_cloudfront_distribution.tf
│       │   │   ├── aws_cloudfront_origin_access_control.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── aws_s3_bucket.tf
│       │   │   ├── aws_s3_bucket_policy.tf
│       │   │   ├── aws_s3_bucket_public_access_block.tf
│       │   │   ├── aws_s3_bucket_website_configuration.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── tfstate-s3-policy
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_iam_policy_document.tf
│       │   │   ├── aws_s3_bucket_ownership_controls.tf
│       │   │   ├── aws_s3_bucket_policy.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── vpn
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_availability_zones.tf
│       │   │   ├── aws_eip.tf
│       │   │   ├── aws_eip_association.tf
│       │   │   ├── aws_iam_instance_profile.tf
│       │   │   ├── aws_iam_policy.tf
│       │   │   ├── aws_iam_role.tf
│       │   │   ├── aws_iam_role_policy_attachment.tf
│       │   │   ├── aws_instance.tf
│       │   │   ├── aws_internet_gateway.tf
│       │   │   ├── aws_route_table.tf
│       │   │   ├── aws_route_table_association.tf
│       │   │   ├── aws_security_group.tf
│       │   │   ├── aws_ssm_parameter.tf
│       │   │   ├── aws_subnet.tf
│       │   │   ├── aws_vpc.tf
│       │   │   ├── data_aws_ami.tf
│       │   │   ├── data_aws_caller_identity.tf
│       │   │   ├── data_aws_region.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   ├── sync_wireguard_config.sh
│       │   │   ├── terraform_data.tf
│       │   │   ├── terragrunt.hcl
│       │   │   └── wireguard_user_data.sh
│       │   └── waf-cloudfront
│       │       ├── .terraform.lock.hcl
│       │       ├── _backend.tf
│       │       ├── _provider.tf
│       │       ├── aws_wafv2_ip_set.tf
│       │       ├── aws_wafv2_web_acl.tf
│       │       ├── data_terraform_remote_state.tf
│       │       ├── locals.tf
│       │       ├── output.tf
│       │       └── terragrunt.hcl
│       ├── aws-us-east-1
│       │   ├── argocd-hostzone
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── argocd-hostzone-env
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── argocd-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── argocd-hostzone-ns-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── hostzone
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   └── terragrunt.hcl
│       │   ├── hostzone-root
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── aws_route53domains_registered_domain.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   ├── removed.tf
│       │   │   └── terragrunt.hcl
│       │   ├── hostzone-root-dmarc-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── hostzone-root-gmail-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── hostzone-root-ns-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── local-apply-cicd-role
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_iam_openid_connect_provider.tf
│       │   │   ├── aws_iam_policy.tf
│       │   │   ├── aws_iam_role.tf
│       │   │   ├── aws_iam_role_policy_attachment.tf
│       │   │   ├── data_aws_iam_policy_document.tf
│       │   │   ├── data_aws_organizations_organization.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── mynginx-hostzone
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── mynginx-hostzone-env
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── mynginx-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── mynginx-hostzone-ns-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-ecr-replicated
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_ecr_lifecycle_policy.tf
│       │   │   ├── aws_ecr_registry_policy.tf
│       │   │   ├── aws_region.tf
│       │   │   ├── data_aws_ecr_repository.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-env
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_zone.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-env-acm
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_acm_certificate.tf
│       │   │   ├── aws_acm_certificate_validation.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-env-dmarc-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── import.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-env-gmail-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   ├── myservice-hostzone-ns-record
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── aws_route53_record.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── locals.tf
│       │   │   └── terragrunt.hcl
│       │   └── myservice-secret
│       │       ├── .terraform.lock.hcl
│       │       ├── _backend.tf
│       │       ├── _provider.tf
│       │       ├── aws_kms_alias.tf
│       │       ├── aws_kms_key.tf
│       │       ├── aws_secretsmanager_secret.tf
│       │       ├── aws_secretsmanager_secret_version.tf
│       │       ├── locals.tf
│       │       ├── output.tf
│       │       └── terragrunt.hcl
│       ├── github-null
│       │   ├── organization-settings
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── github_actions_organization_permissions.tf
│       │   │   ├── github_actions_organization_variable.tf
│       │   │   ├── github_organization_settings.tf
│       │   │   ├── github_team.tf
│       │   │   ├── github_team_membership.tf
│       │   │   ├── output.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-api-service
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-docs-generator
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-main-app
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-shared-actions
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-backend-service
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-terraform-infra
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-math-tools
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-desktop-app
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-cluster-config
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-sandbox
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-sandboxwindows
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   ├── repository-setup-scripts
│       │   │   ├── .terraform.lock.hcl
│       │   │   ├── _backend.tf
│       │   │   ├── _provider.tf
│       │   │   ├── data_terraform_remote_state.tf
│       │   │   ├── github_repository.tf
│       │   │   ├── github_team_repository.tf
│       │   │   └── terragrunt.hcl
│       │   └── repository-static-site
│       │       ├── .terraform.lock.hcl
│       │       ├── _backend.tf
│       │       ├── _provider.tf
│       │       ├── data_terraform_remote_state.tf
│       │       ├── github_repository.tf
│       │       ├── github_team_repository.tf
│       │       └── terragrunt.hcl
│       ├── google-asia-northeast1
│       │   └── network
│       │       ├── .terraform.lock.hcl
│       │       ├── _backend.tf
│       │       ├── _provider.tf
│       │       ├── google_compute_network.tf
│       │       ├── google_compute_subnetwork.tf
│       │       ├── locals.tf
│       │       └── terragrunt.hcl
│       └── google-us-east4
│           ├── local-apply-cicd-serviceaccount
│           │   ├── .terraform.lock.hcl
│           │   ├── _backend.tf
│           │   ├── _provider.tf
│           │   ├── google_iam_workload_identity_pool.tf
│           │   ├── google_iam_workload_identity_pool_provider.tf
│           │   ├── google_project_iam_member.tf
│           │   ├── google_service_account.tf
│           │   ├── google_service_account_iam_binding.tf
│           │   ├── locals.tf
│           │   ├── output.tf
│           │   └── terragrunt.hcl
│           ├── network
│           │   ├── .terraform.lock.hcl
│           │   ├── _backend.tf
│           │   ├── _provider.tf
│           │   ├── google_compute_network.tf
│           │   ├── google_compute_subnetwork.tf
│           │   ├── locals.tf
│           │   └── terragrunt.hcl
│           └── service
│               ├── .terraform.lock.hcl
│               ├── _backend.tf
│               ├── _provider.tf
│               ├── google_project_service.tf
│               ├── locals.tf
│               └── terragrunt.hcl
├── lefthook.yml
├── modules
│   └── test_sg
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── myarticle.md
└── root.hcl

terragrunt との付き合い方

本プロジェクトでは terragrunt を最小限の機能のみで使用しています。

自動生成機能の活用

下記のように一番トップの root.hcl に値を埋め込んで、terragrunt init_backend.tf_provider.tf を自動生成します。
_*.tf という形式のファイルは terragrunt によって自動生成されたものです。

dependency を使わない設計

dependency ブロックを使用すると、単一のディレクトリで terragrunt init, validate, plan, apply を実行する際に、依存先のディレクトリの初期化も自動で行われてしまいます。大規模プロジェクトでは、この初期化処理により実行時間が大幅に増加するため、本プロジェクトでは dependency の使用を一切禁止しています。

代わりに、terraform ネイティブの terraform_remote_state を使用することで、依存関係を管理しながらも、各ディレクトリを独立して操作できるようにしています。これにより、terraform コマンドでも全ての操作を完結でき、terragrunt に依存しない設計を実現しています。

root.hcl
locals {
  regex_list = regex("^envs/(management|messi|prod)/(aws|google|github)-(ap-northeast-1|us-east-1|asia-northeast1|us-east4|null)/(.*)$", path_relative_to_include())
  environment = local.regex_list[0]
  provider = local.regex_list[1]
  region = local.regex_list[2]
  state_dir = local.regex_list[3]
  aws_account_id = (
    local.environment == "management" ? 
      "xxxxxxxxxx" :
    local.environment == "messi" ? 
      "xxxxxxxxxx" :
    local.environment == "prod" ? 
      "xxxxxxxxxx" : "_"
  )
  google_project_id = (
    local.environment == "management" ? 
      "xxx-xxx-xxx-xx" :
    local.environment == "messi" ? 
      "xxx-messi" :
    local.environment == "prod" ? 
      "xxx-prod" : "_"
  )
  # 全ての環境のtfstateをprodバケットに集約
  tfstate_bucket_name = "xxx-yyy-tfstate-bucket"
}
generate "backend" {
  path      = "_backend.tf"
  if_exists = "overwrite"
  contents = <<EOF
terraform {
  backend "s3" {
    bucket       = "${local.tfstate_bucket_name}"
    encrypt      = true
    key          = "terraform-infra/${path_relative_to_include()}/terraform.tfstate"
    region       = "ap-northeast-1"
    use_lockfile = true
  }
}
EOF
}
generate "provider" {
  path      = "_provider.tf"
  if_exists = "overwrite"
  contents = <<EOF
terraform {
  required_version = ">= 1.2.0, < 2.0.0"
  required_providers {
%{ if local.provider == "aws" ~}
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
%{ endif ~}
%{ if local.provider == "github" ~}
    github = {
      source  = "integrations/github"
      version = "~> 6.0"
    }
%{ endif ~}
%{ if local.provider == "google" ~}
    google = {
      source  = "hashicorp/google"
      version = "~> 6.0"
    }
%{ endif ~}
  }
}
%{ if local.provider == "aws" ~}
provider "aws" {
  region              = "${local.region}"
  allowed_account_ids = ["${local.aws_account_id}"]
}
provider "aws" {
  alias               = "us_east_1"
  region              = "us-east-1"
  allowed_account_ids = ["${local.aws_account_id}"]
}
%{ endif ~}
%{ if local.provider == "github" ~}
provider "github" {
  owner = "xxx-org"
}
%{ endif ~}
%{ if local.provider == "google" ~}
provider "google" {
  project = "${local.google_project_id}"
  region  = "${local.region}"
%{ if local.environment == "management" && local.state_dir == "local-apply-cloud-identity" ~}
  user_project_override = true
  billing_project       = "${local.google_project_id}"
%{ endif ~}
}
%{ endif ~}
EOF
}

環境間の DRY にモジュールを使用しない

環境間のリソース定義の同期はシェルスクリプトを用意することで対応しています。余計な抽象化は可読性を下げるだけです。

それぞれの環境で頻繁に使う設定があれば、モジュールを使うことは許容されます。

ブランチ戦略

環境ごとのブランチ戦略を採用します。
messi 環境のインフラ状態は messi ブランチの messi ディレクトリ, prod環境のインフラ状態は prod ブランチの prod ディレクトリ, management環境のインフラ状態は management ブランチの management ディレクトリと一致します。

CI

  • terraform plan
    • dependency を使用しない設計により、terraform コマンドのみで実行可能(terragrunt plan は使わない)
    • terraform ネイティブなコマンドを使用することで、dependency が使われていないことのチェックも行える
    • pull_request イベント
    • 流れ
      • (1) terraform plan の対象となるステートディレクトリは下記を計算したものです
        • ( + ) git 差分のあるステートディレクトリ
        • ( + ) モジュール(modulesディレクトリ)に変更が入っている場合、それを使っているリソースのステートディレクトリ
        • ( check ) リソースが残っていたのにディレクトリごと削除された場合にエラーを吐く。リソースが残っているのに terraform 管理外になるという最悪の事態を避ける必要があります。ディレクトリごと削除できるのはリソースが残っていない場合のみに制限する必要があります
      • (2) pushイベントでapplyをする場合、applyするべきディレクトリを特定するのが難しいので、この時点でアーティファクトに保存しておいてapply前に取り出すのがいいと思います
        • GitHub Enterprise プランではない場合、merge_group イベントを使えないので、pushイベントでapplyする必要があります
      • (3) 上記で取得したステートディレクトリに対して、terraform plan を実施する
  • terraform apply
    • dependency を使用しない設計により、terraform コマンドのみで実行可能(terragrunt apply は使わない)
    • terraform ネイティブなコマンドを使用することで、dependency が使われていないことのチェックも行える
    • merge_group イベント
    • 流れ
      • (1) planの時と同じように、apply対象ディレクトリを特定する
        • pushイベントの場合は、アーティファクトから terraform apply 対象ディレクトリを取得する
      • (2) 各ステートディレクトリに terraform apply を実施する
      • (3) 次のブランチに対してPR作成
        • messi ブランチにマージしたのであれば、そのPRをそのまま prod と management に作成します
        • PRは毎回作成します
          • ステート操作があるので、messi ブランチにたまった複数のPRをまとめて一つのPRとして prod ブランチに適用しようとするのは避けましょう
            • 例えば、messi 環境で import ブロックを使ってリソースを import して、その後 importブロックを削除したとします。その後に、messi 環境から prod 環境にまとめて PR を作成したら、ステート操作ではなくて、新しくリソースが作成されてしまいます
  • terragrunt init & terraform validate
    • pull_request イベント
    • 流れ
      • (1) git差分があるディレクトリを特定します
      • (2) 特定されたディレクトリに対して terragrunt init で自動生成を実行し、差分があれば自動コミットします
      • (3) terraform validate もやります
        • terraform validate 単体で実行すると、terraform init が済んでない場合、事前に terraform init が実行されるので、別ジョブにする意味はありません
  • terraform fmt
    • フォーマット
  • tflint
    • 静的解析
  • 環境間, リージョン間のコードが適切に同期されているかどうかをチェックする
  • terraform drift check
    • ベースブランチの実インフラとtfstateの差分チェック
    • pull_request イベント
    • 流れ
      • (1) git差分があるステートディレクトリを特定(planと同様)
      • (2) ベースブランチに切り替えて、対象ディレクトリで terraform plan -detailed-exitcode を実行
      • (3) ベースブランチで既にドリフトが存在する場合、PRコメントで警告

コードオーナー

例えば、prod ディレクトリ配下や management ディレクトリ配下の変更を含むPRはコードオーナーのレビューを必須にします。

merge_groupイベント

terraform CI/CD では merge_group イベントが非常に便利です。
terraform plan が成功していても terraform apply で失敗することは頻繁にあります。push イベントを使うと terraform apply が失敗してもマージされてしまうので、デフォルトブランチが汚染されてしまいます。
ただし、Enterprise プランしか使えません。
私は、Enterprise プランに加入しています。merge_groupイベントを使いたいのはもちろんのこと、ジョブの実行時間が Team プランでは全然足りないからです。
なお、tfactionは、pushイベントでapplyして、失敗したら新しいPRを作る方針でやっているので、merge_groupを使う必要性はありません。ブランチ汚染はありますが、merge queue が詰まって、なかなか apply ができないという状況を防げるので便利です。

matrix ジョブの利用

initplanapply を実施するステートディレクトリを特定したら、matrix ジョブを使います。
私の場合、プロバイダー&リージョンディレクトリに対しても matrix ジョブを使いたいし、ステートディレクトリに対しても matrix ジョブを使いたかったのですが、matrix ジョブからさらに matrix ジョブを呼ぶというのは技術的にできません。なので、プロバイダー&リージョンディレクトリに対する処理を workflow_call イベントとしてまとめることで、workflow_callmatrix ジョブで呼び出し、その中でさらに matrix ジョブを呼び出して、ステートディレクトリそれぞれに対して initplanapplyを実行しています。

シークレット管理

以下に記載してあります。

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を使うと、自動テストが流れません
  • GitHub の IaC を GitHub Actions で apply するためにも必要です
  • PR を作成して承認したいとき、別ユーザでないと承認できません。なので、作成を GitHub Apps Token で行い、承認を secrets.GITHUB_TOKENで行います。その後の、Enable auto-merge は、どちらのトークンが行っても構いません
    • 自動テストを待たなくてもいい場合は、GitHub Apps Token で PR作成を行い、PR承認をスキップして、再び GitHub Apps Token で gh pr merge --admin --merge "$PR_URL" を実行します

マルチアカウントによる権限管理

AWS OrganizationsとAWS SSOを使用したマルチアカウント環境での権限管理について説明します。

本プロジェクトでは、prod環境のS3バケットで全環境のtfstateを集約管理しています。

許可セット

2つの許可セットを作成します:

  • admin: 管理者権限(tfstateを管理しているS3バケットの自分の環境のキー配下への書き込み権限と、全てのキーに対する読み取り権限を含む)
  • readonly: 読み取り専用権限(tfstateを管理しているS3バケットの全てのキーに対する読み取り権限を含む)

グループと権限割り当て

2つのグループを定義し、それぞれに異なる権限を割り当てます:

インフラチーム(強い権限)

  • management × admin
  • management × readonly
  • prod × admin
  • prod × readonly
  • messi × admin
  • messi × readonly

プロダクトチーム(制限された権限)

  • management × readonly
  • prod × readonly
  • messi × admin
  • messi × readonly

terraform plan の実行

  • 全てのロールは全アカウントのtfstate用バケットを読み取れるため、terraform plan時のremote参照が可能です
  • 書き込み権限がない環境に対するplanでは-lock=falseオプションを使用します
    • これによりプロダクトチームでもprod環境に対してplanを実行できます
  • 制限事項: KMSによる復号化などはreadonly権限に含まれないため、これを必須とするplanはプロダクトチームでは実行できません

セキュリティ・ネットワーク制限

WAFによるIP制限

prod以外の環境では、WAFを使用してIPアクセス制限を実装します。

VPN

固定IPを持つためにVPNを構築します。

許可IP

WAFが許可するIPアドレスは以下のみです:

  • VPNのIP
  • GitHub ActionsのRunnerの固定IP(場合により)

GitHub Actionsからのアクセス

GitHub ActionsからargoCDを操作したり、e2eテストを実行するには、対象サービスのALBに設定されたWAFを通過する必要があります。GitHub Actionsに固定IPを持たせるには以下の方法があります:

  1. Self-hosted Runner: 自前でRunnerをホストし固定IPを設定
  2. GitHub Hosted Runner(有料): Enterprise Planに加えてさらに課金し、Larger Runnerを使用して固定IPを取得

私は、GitHub Hosted Runnerに追加課金して固定IPを取得しています。

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 のコードの複雑さを考えると、マルチテナントの方が圧倒的に見通しが良くなります。
デメリットとして、耐障害性が低いことや、それぞれのマイクロサービスのコストを把握しづらいことが挙げられます。

ECRレプリケーション

DockerイメージはCIで aws-ap-northeast-1 のECRにpushし、ECR Replicationで他のリージョンや環境に自動複製します。これによりイメージpull時のコストと時間を削減できます。

k8s の CI/CD

各ツールの特徴

  • argocd-vault-plugin ← 使用しています
    • AWS Secrets Manager からシークレットを取得してきて、argoCD 上で Manifest ファイルに埋め込んでくれます
    • 他に選択肢が多くあったので迷いましたが、スター数とドキュメントの充実度から採用しました。採用理由があまり参考にならなくてすみません
  • helm ← 使用しています
    • kustomizehelm が選択肢だと思います
    • 既に会社で使っていたのも理由としてありますが、より多くのユーザがいると思ったのと、環境間の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 のコードに注入する場合、これ専用のプロバイダーが必要みたいで、少し面倒だと感じました

ディレクトリ構成

ディレクトリ構成
~/k8s-manifests$ tree -a -L 9 -I ".git"
.
├── .claude
│   └── settings.local.json
├── .github
│   ├── actionlint.yml
│   ├── actions
│   │   ├── check_matrix_job_result
│   │   │   └── action.yml
│   │   ├── convert_space_separated_array_string_to_json_array_string
│   │   │   └── action.yml
│   │   ├── create_comment_for_log_getting_pr_number_from_git_log
│   │   │   └── action.yml
│   │   ├── determine_argocd_token_host_from_dir_path
│   │   │   └── action.yml
│   │   ├── filter_dirs_by_argocd_app_list
│   │   │   └── action.yml
│   │   ├── get_dirs_for_argocd_app_diff
│   │   │   └── action.yml
│   │   ├── get_dirs_under_target_dir_by_depth
│   │   │   └── action.yml
│   │   ├── get_files_for_kubectl_diff_apply
│   │   │   └── action.yml
│   │   ├── install_tools
│   │   │   └── action.yml
│   │   ├── kubectl_apply
│   │   │   └── action.yml
│   │   └── kubectl_diff
│   │       └── action.yml
│   └── workflows
│       ├── argocd_app_diff_caller.yml
│       ├── argocd_cluster_add.yml
│       ├── code_quality_checks.yml
│       ├── kubectl_apply_argocd_caller.yml
│       ├── kubectl_diff_argocd_caller.yml
│       ├── reusable_argocd_app_diff_provider_region.yml
│       ├── reusable_kubectl_apply_argocd_dirs.yml
│       └── reusable_kubectl_diff_argocd_dirs.yml
├── .lefthook
│   ├── ci
│   │   └── helmfile_template_check.sh
│   ├── format
│   │   └── helmfile_template.sh
│   └── pre-commit
│       └── prevent_commiting_secrets.sh
├── .sops.yaml
├── CLAUDE.md
├── charts
│   ├── appproject
│   │   ├── Chart.yaml
│   │   └── templates
│   │       └── appproject.yaml
│   ├── generalapp
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── generalapp-deployment.yaml
│   │       ├── generalapp-hpa.yaml
│   │       ├── generalapp-ingress-internal.yaml
│   │       ├── generalapp-ingress.yaml
│   │       ├── generalapp-secret.yaml
│   │       ├── generalapp-service.yaml
│   │       └── generalapp-serviceaccount.yaml
│   ├── ingressclass
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── ingressclass.yaml
│   │       └── ingressclassparams.yaml
│   └── migrate
│       ├── Chart.yaml
│       └── templates
│           ├── migrate-job.yaml
│           └── migrate-secret.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
│   │   │   │   ├── argocd
│   │   │   │   │   ├── argocd-auto-generated.yaml
│   │   │   │   │   ├── argocd-helmfile.yaml
│   │   │   │   │   ├── argocd-namespace.yaml
│   │   │   │   │   ├── argocd-values.yaml
│   │   │   │   │   ├── github-oauth-secret.yaml
│   │   │   │   │   └── myclus-ssh-private-key-secret.yaml
│   │   │   │   └── ingressclass
│   │   │   │       ├── alb-auto-generated.yaml
│   │   │   │       ├── alb-helmfile.yaml
│   │   │   │       └── alb-values.yaml
│   │   │   ├── customnodepool
│   │   │   │   └── customnodepool
│   │   │   │       └── small-instance-nodepool.yaml
│   │   │   ├── gobackend
│   │   │   │   └── gobackend
│   │   │   │       ├── gobackend-migrateprimary-auto-generated.yaml
│   │   │   │       ├── gobackend-migrateprimary-helmfile.yaml
│   │   │   │       ├── gobackend-migrateprimary-values.yaml
│   │   │   │       ├── gobackend-webcommentlike-auto-generated.yaml
│   │   │   │       ├── gobackend-webcommentlike-helmfile.yaml
│   │   │   │       ├── gobackend-webcommentlike-values.yaml
│   │   │   │       ├── gobackend-webcommentview-auto-generated.yaml
│   │   │   │       ├── gobackend-webcommentview-helmfile.yaml
│   │   │   │       ├── gobackend-webcommentview-values.yaml
│   │   │   │       ├── gobackend-webmediajobresult-auto-generated.yaml
│   │   │   │       ├── gobackend-webmediajobresult-helmfile.yaml
│   │   │   │       ├── gobackend-webmediajobresult-values.yaml
│   │   │   │       ├── gobackend-webmediajobtrigger-auto-generated.yaml
│   │   │   │       ├── gobackend-webmediajobtrigger-helmfile.yaml
│   │   │   │       ├── gobackend-webmediajobtrigger-values.yaml
│   │   │   │       ├── gobackend-webprimary-auto-generated.yaml
│   │   │   │       ├── gobackend-webprimary-helmfile.yaml
│   │   │   │       └── gobackend-webprimary-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-kai
│   │   │           ├── my-nginx-auto-generated.yaml
│   │   │           ├── my-nginx-helmfile.yaml
│   │   │           └── my-nginx-values.yaml
│   │   └── aws-us-east-1
│   │       ├── gobackend
│   │       │   └── gobackend
│   │       │       └── .gitkeep
│   │       └── ingressclass
│   │           ├── alb
│   │           │   └── .gitkeep
│   │           └── alb-internal
│   │               └── .gitkeep
│   └── prod
│       ├── aws-ap-northeast-1
│       │   └── .gitkeep
│       └── aws-us-east-1
│           └── .gitkeep
└── lefthook.yml

argoCD や k8s 上での名前の規則は以下になります

  • 最下部のディレクトリまでハイフンでつなげた名前が argoCD 上の application の名前となります。
  • その一つ上のディレクトリまでハイフンでつなげた名前が k8s 上の namespace となります。applicationSetの設定で、k8s 上の namespaceを自動で作るようにできるので、namespaceリソースを明示的に定義する必要はありません。
  • argoCD に namespace の概念はありません。
  • k8s 上のリソースの名前は自由です。

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 ディレクトリに対応する
  • ...

ブランチ戦略

上述したように、messi 環境の EKS は messi ブランチの messi ディレクトリ、prod 環境の EKS は prod ブランチの prod ディレクトリと状態が一致します。
terraform プロジェクトと違って、PRがマージされるたびに次の環境へのPRを自動作成する必要はありません。
prod へリリースしたいときは、messi ブランチを prod ブランチにマージします。

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

  • argocd app diff
    • pull_request イベント
    • 差分が生じた yaml ファイルを持つ application に対して、argocd app diff を実行します
    • argocd app diffは、差分があるときはエラーステータス1なので、このときは成功判定にします
    • このジョブでは固定IPを持ったrunnerを使用します(GitHub Enterprise Planに加えてさらに課金が必要)
      • argocdにアクセスできるIPをWAFで絞っているためです
  • helmfile template
    • pull_request イベント
    • helmfile template コマンドの実行漏れをチェックします
      • 全ての helmfile ファイルに対して、helmfile template を実行する自動化スクリプトをあらかじめ用意しておきます。
    • helmfile ファイルが、*helmfile.yamlの形式であることもチェックします
  • kubectl diff
    • pull_request イベント
    • argoCD 監視対象外の Manifest を CI で diff します
    • 対象のディレクトリ(アプリケーション)を指定しておきます。CI で、その配下のファイルから helmfile などを除外して diff するべきファイルを特定します
  • kubectl apply
    • merge_group イベント
    • 対象ファイルの特定方法は、diffの時と同じです

共通アクション用のレポジトリ

ディレクトリ構成

自動化には大量のコンポジットアクションが必須です。汎用的なアクションをここに配置しましょう。
例えば、タグ付けしてリリースするアクションは、たくさんのレポジトリで使用すると思うので、ここに切り出しましょう。

ディレクトリ構成
~/MyCompositeActions$ tree -a -L 9 -I ".git"
.
├── .github
│   ├── actions
│   │   ├── actionlint
│   │   │   └── action.yml
│   │   ├── generate_github_apps_token
│   │   │   └── action.yml
│   │   ├── go_setup
│   │   │   └── action.yml
│   │   ├── revoke_github_apps_token
│   │   │   └── action.yml
│   │   ├── tag_release
│   │   │   └── action.yml
│   │   └── tag_release_and_build_push_and_create_pr_to_myclus
│   │       └── action.yml
│   └── workflows
│       ├── private_actionlint.yml
│       └── private_tag_release.yml
├── CLAUDE.md
└── version.txt

コンポジットアクションとworkflow_call

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

アプリのレポジトリ

ディレクトリ構成

ディレクトリ構成
MyAppProject$ tree -a -L 9 -I ".git"
.
├── .claude
│   └── settings.local.json
├── .env
├── .github
│   ├── actionlint.yml
│   ├── actions
│   │   ├── build_all_images_using_cache
│   │   │   └── action.yml
│   │   └── install_tools
│   │       └── action.yml
│   ├── dependabot.yml
│   └── workflows
│       ├── build_push_and_tag_release_and_create_pr.yml
│       ├── cache_docker_image_to_default_branch.yml
│       ├── code_quality_checks.yml
│       ├── go_test.yml
│       ├── local_api_test.yml
│       └── messi_api_test.yml
├── .gitignore
├── .lefthook
│   └── pre-commit
│       ├── check_file_name.sh
│       ├── static_analysis.sh
│       └── static_analysis_format.sh
├── .vscode
│   └── settings.json
├── CLAUDE.md
├── README.md
├── api_test
├── docker-compose.ci.yml
├── docker-compose.yml
├── lefthook.yml
├── migrate
│   ├── Dockerfile
│   ├── entry_point_script
│   │   └── migrate.sh
│   ├── migrations
│   │   ├── 202412081520_create_users_table.down.sql
---
│   └── schema.sql
├── version_migrate.txt
├── version_web.txt
└── web
    ├── .golangci.yml
    ├── Dockerfile
    ├── adapter
    │   ├── in
    │   │   ├── apiinternal
    │   │   │   ├── hello_apiinternal.go
---
    │   │   ├── apiprivate
    │   │   │   ├── admin_comment_patch_apiprivate.go
---
    │   │   ├── middleware
    │   │   │   ├── id_token_middleware.go
---
    │   │   └── worker
    │   │       ├── comment_sync_like_count_worker.go
---
    │   └── out
    │       ├── cognitoadapter
    │       │   ├── user_list_users_cognitoadapter.go
---
    │       ├── dbadapter
    │       │   ├── article_create_dbadapter.go
---
    │       ├── mediaconvertadapter
    │       │   ├── mediaplatform_create_job_mediaconvertadapter.go
---
    │       ├── redisadapter
    │       │   ├── generic_bulk_increment_with_max_count_redisadapter.go
---
    │       ├── s3adapter
    │       │   ├── mediaplatform_get_content_type_s3adapter.go
---
    │       ├── sesadapter
    │       │   ├── notification_bulk_send_email_sesadapter.go
---
    │       └── sqsadapter
    │           ├── generic_batch_delete_message_sqsadapter.go
---
    ├── application
    │   └── domain
    │       ├── appcommandservice
    │       │   └── .gitkeep
    │       ├── appqueryservice
    │       │   └── .gitkeep
    │       ├── commandservice
    │       │   ├── admin_comment_patch_commandservice.go
---
    │       ├── model
    │       │   ├── medium.go
---
    │       └── queryservice
    │           ├── article_get_queryservice.go
---
    ├── cmd
    │   ├── webcommentlike
    │   │   ├── dicontainerwebcommentlike
    │   │   │   └── di_container_webcommentlike.go
    │   │   ├── envconfigwebcommentlike
    │   │   │   └── envconfig_webcommentlike.go
    │   │   └── main.go
    │   ├── webcommentview
    │   │   ├── dicontainerwebcommentview
    │   │   │   └── di_container_webcommentview.go
    │   │   ├── envconfigwebcommentview
    │   │   │   └── envconfig_webcommentview.go
    │   │   └── main.go
    │   ├── webmediajobresult
    │   │   ├── dicontainerwebmediajobresult
    │   │   │   └── di_container_webmediajobresult.go
    │   │   ├── envconfigwebmediajobresult
    │   │   │   └── envconfig_webmediajobresult.go
    │   │   └── main.go
    │   ├── webmediajobtrigger
    │   │   ├── dicontainerwebmediajobtrigger
    │   │   │   └── di_container_webmediajobtrigger.go
    │   │   ├── envconfigwebmediajobtrigger
    │   │   │   └── envconfig_webmediajobtrigger.go
    │   │   └── main.go
    │   └── webprimary
    │       ├── dicontainerwebprimary
    │       │   └── di_container_webprimary.go
    │       ├── envconfigwebprimary
    │       │   └── envconfig_webprimary.go
    │       └── main.go
    ├── contextconfig
    │   └── contextconfig.go
    ├── dbsetting
    │   └── db_setting.go
    ├── enum
    │   ├── articlecategory.go
---
    ├── go.mod
    ├── go.sum
    ├── router
    │   └── router.go
    ├── sqlc.yml
    ├── sqlcgenerated
    │   ├── command_article.sql.go
---
    ├── sqlscript
    │   ├── command_article.sql
---
    ├── testenvconfig
    │   └── testenvconfig.go
    ├── testutil
    │   └── to_ptr.go
    

Docker

  • イメージサイズが大きすぎるとPULLに時間がかかりますし、ECR の金額も上がってしまうので、積極的にマルチステージビルドを使いましょう
  • CI で使う場合は build-push-action アクションを使用しましょう

k8s

  • migrate イメージは job で使います
  • web イメージは Manifest の普通のコンテナに配置します

CI

  • ビルド & ECRへの push & タグ付け & version.txt の更新 & k8sレポジトリへのPR作成
    • dispatch_workflow イベント
    • 共通アクションレポジトリの「ビルド & ECRへの push & タグ付け & version.txt の更新 & 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 ブランチのみを使います。

0
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
0
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?