前書き
私の個人開発で採用している CI/CD を参考に、CI/CD の全体像を理解することが目的です。
私は個人開発でやってますが、どの場面においても複数人, 中・大規模での開発を想定しているので、その前提で読んでください。例えば、小規模開発であればステート分割などをせずに全体に対して terraform plan
(apply
) を実行すればいいですが、複数人, 中・大規模での開発を想定するのであれば、適切にステート分割を行い差分のあるディレクトリのみを terraform plan
(apply
) するということが必要になってきます。
自動化のすばらしさを知るために、試しにリリースしてみる ※ これは terraform
は関係ない
(1) アプリケーションコードに変更を加える前
(2) アプリのレポジトリで開発する
(3) 「ビルド & ECRへの push
& タグ付け & version.txt の更新 & k8sレポジトリへのPR作成」をボタン一つで完結
GitHub Actions の dispatch_workflow
イベントを使って、Dockerイメージのビルド, ECRへの push
, タグ付け, version.txt の更新, k8sレポジトリへのPR作成 を自動化してます。
※ 後述しますが、本プロジェクトは github flow を使っていますが、git flow にいつでも移行できるように、デフォルトブランチは main ではなくて develop としています。
(4) 自動作成されたk8sレポジトリへのPRをマージする
k8s レポジトリへのPRは、イメージ名だけ変更したPRとなっています。CI で前回イメージ名を新しいイメージ名に置き換えてます。
自動作成されたPRを確認してマージします。マージの手間が面倒であれば、マージまで自動化してもいいと思います。
(5) アプリケーションコードに変更を加えた後
k8sレポジトリにマージされたら、argoCD が監視しているので勝手にデプロイされます。
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
でまとめて設定しています -
terragrunt
はprovider.tf
やbackend.tf
を自動生成してくれます。本プロジェクトでは、これだけのためにterragrunt
を使っています。これによって、tfstate のkey
やallowed_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
のような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまいます
- 私たちは、PATHが通っている
-
ディレクトリ構成
私が目標とするインフラ状態に対してどのようなディレクトリ構成にするのかを、箇条書きにしました。
- 環境ごとのインフラ状態はできるかぎり同じ状態にしたいです。つまり、messi でバグが見つかれば prod でもバグが見つかるというような状態にしたいです
- messi と prod で同期対象として明示的に指定したステートディレクトリは、完全に一致することを CI で強制します
- 前提として、
_provider.tf
,_backend.tf
のterragrunt init
で自動生成されたファイルは環境特有の値が入っているので、もちろん環境ごとに違います - 環境間を同期するシェルスクリプトで、同期するステートディレクトリを明示的に指定します
- 同期するディレクトリは明示的に指定します。
_provider.tf
,_backend.tf
は自動生成ファイルのため同期対象から外します。locals.tf
も同期対象から外しますが、tflintで余計な変数を、terraform validateで変数不足をチェックしているため過不足がないことが保証されます - 例えば、VPNは複数環境に配置せず、prodだけに置くので、環境間・リージョン間の同期対象として指定しません
- 同期するディレクトリは明示的に指定します。
- 前提として、
- messi と prod で同期対象として明示的に指定したステートディレクトリは、完全に一致することを CI で強制します
- リージョンごとのインフラ状態はできる限り同じ状態にしたいです
- 環境間の同期と全く同じ方法をとります
- 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
する必要があります
- GitHub Enterprise プランではない場合、
- (3) 上記で取得したステートディレクトリに対して、
terraform plan
を実施する
- (1)
- dependency を使用しない設計により、
-
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 を作成したら、ステート操作ではなくて、新しくリソースが作成されてしまいます
- 例えば、messi 環境で
- ステート操作があるので、messi ブランチにたまった複数のPRをまとめて一つのPRとして prod ブランチに適用しようとするのは避けましょう
- (1)
- dependency を使用しない設計により、
-
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コメントで警告
- (1) git差分があるステートディレクトリを特定(
コードオーナー
例えば、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
ジョブの利用
init
、plan
、apply
を実施するステートディレクトリを特定したら、matrix
ジョブを使います。
私の場合、プロバイダー&リージョンディレクトリに対しても matrix
ジョブを使いたいし、ステートディレクトリに対しても matrix
ジョブを使いたかったのですが、matrix
ジョブからさらに matrix
ジョブを呼ぶというのは技術的にできません。なので、プロバイダー&リージョンディレクトリに対する処理を workflow_call
イベントとしてまとめることで、workflow_call
を matrix
ジョブで呼び出し、その中でさらに matrix
ジョブを呼び出して、ステートディレクトリそれぞれに対して init
、plan
、apply
を実行しています。
シークレット管理
以下に記載してあります。
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"
を実行します
- 自動テストを待たなくてもいい場合は、GitHub Apps Token で PR作成を行い、PR承認をスキップして、再び GitHub Apps Token で
マルチアカウントによる権限管理
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
を実行できます
- これによりプロダクトチームでもprod環境に対して
- 制限事項: 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を持たせるには以下の方法があります:
- Self-hosted Runner: 自前でRunnerをホストし固定IPを設定
- GitHub Hosted Runner(有料): Enterprise Planに加えてさらに課金し、Larger Runnerを使用して固定IPを取得
私は、GitHub Hosted Runnerに追加課金して固定IPを取得しています。
EKS
EKS Auto Mode
aws-load-balancer-controller
や Pod Identity Agent
が予めインストールされており、自らインストールする必要がないので便利です。
EC2のスペックを考える必要もないです。
アクセスエントリー
従来は IAM ロール に EKS へのアクセス権限を付与するために、aws-auth
という ConfigMap
をいじっていたかと思いますが、アクセスエントリーを使うことで ConfigMap
をいじらないで済みます。
aws-auth
という ConfigMap
を terraform
で設定する場合、k8s プロバイダーを定義する必要があったので面倒でしたが、EKS API(アクセスエントリー) で設定できるようになり k8s プロバイダーを定義する必要がなくなりました。
Pod Identity
サービスアカウントに IAM ロール を付与するために、従来なら terraform
側で完結せずに、Manifest ファイルでの設定も必要でしたが、Pod Identity の登場により terraform 側で完結するようになりました。
マルチテナント vs シングルテナント
私は費用と運用コストを考えて、マルチテナントを採用しています。
CI/CD の構築や terraform のコードの複雑さを考えると、マルチテナントの方が圧倒的に見通しが良くなります。
デメリットとして、耐障害性が低いことや、それぞれのマイクロサービスのコストを把握しづらいことが挙げられます。
ECRレプリケーション
DockerイメージはCIで aws-ap-northeast-1
のECRにpushし、ECR Replicationで他のリージョンや環境に自動複製します。これによりイメージpull時のコストと時間を削減できます。
k8s の CI/CD
各ツールの特徴
-
argocd-vault-plugin
← 使用しています- AWS Secrets Manager からシークレットを取得してきて、argoCD 上で Manifest ファイルに埋め込んでくれます
- 他に選択肢が多くあったので迷いましたが、スター数とドキュメントの充実度から採用しました。採用理由があまり参考にならなくてすみません
-
helm
← 使用しています-
kustomize
かhelm
が選択肢だと思います - 既に会社で使っていたのも理由としてありますが、より多くのユーザがいると思ったのと、環境間のDRYを徹底できそうという理由で採用しました
-
application
(applicationSet
) にhelm
の設定を定義することで、helm
はそのまま argoCD にネイティブで解釈してもらえます。しかし私は 生の Manifest に変換しているのでhelm
をそのまま解釈してもらうことはしていません
-
-
helmfile
← 使用しています-
helmfile
を使わずにhelm
だけを使う方法の方がより一般的かもしれませんが、私はhelmfile
を採用しています - 私は可読性のために、また、生の Manifest を追加できるようにするために、生の Manifest を GitHub で管理して、さらには、argoCD の監視対象としています。そのために、手元やCIで
helm template
コマンドもしくはhelmfile template
コマンドをする必要があります。helm template
コマンドを使うとしたら、コマンドの引数に Helm Chart などを指定する必要があり、自動化のスクリプトの複雑性が増します。対してhelmfile template
コマンドは、helmfile ファイルを定義してあげれば、非常に単純になるので、自動化スクリプトも単純になります - argoCD はネイティブで helmfile を解釈しません。なので、
helmfile
を argoCD 上で展開するためには、argoCD に少し複雑な設定をしないといけません。このための OSS はありますが、あまり普及していない印象です
-
-
sops
← 使用しています- argoCD 監視対象外でシークレットを注入したいときに使います。例えば、argoCD 自体は argoCD で管理しない方針をとっているので、argoCD にシークレットを注入したい場合、
argocd-vault-plugin
が使えません。なので、それらは sops で暗号化してから GitHub に push しています - sops で暗号化したシークレットは、CI 上で復号してから、
kubectl apply
されます
- argoCD 監視対象外でシークレットを注入したいときに使います。例えば、argoCD 自体は argoCD で管理しない方針をとっているので、argoCD にシークレットを注入したい場合、
- AWS Secrets Manager ← 使用します
- AWS Secrets Manager にセットしておけば、
terraform
側でも使えるし、k8s 側でも使えるのが便利です - 他に選択肢として
HashiCorp Vault
があると思うが、terraform
のコードに注入する場合、これ専用のプロバイダーが必要みたいで、少し面倒だと感じました
- AWS Secrets Manager にセットしておけば、
ディレクトリ構成
ディレクトリ構成
~/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 ファイルが、
*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 ブランチのみを使います。