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 の結論 v2

Posted at

前回バージョンとの差分

前回バージョン「私の CI/CD の結論」 との大きな差分は、以下の三つになります。

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

前書き

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

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

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

image.png
image.png

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

image.png

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

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

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

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

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

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

terraform の CI/CD

用語

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

各ツールの特徴

  • tfaction ← 私は使用していません

    • terraform の自動化のフレームワークであり、これを使うか使わないかは大きな分かれ道だと思います。私は以下の理由で tfaction を使わずにシェル芸をメインに terraform CI/CD を構築する選択をしました
      • フレームワークだけあって一回使ってしまうと抜け出せなさそう、メンテが終了してしまったときに対処するのが大変そうという印象を受けたからです
      • 自分のやりたいことが明確に見えていたので、フレームワークに縛られたくなかったからです
  • terragrunt ← 私は使用しています

    • terraform の薄いラッパーです
    • コードの自動生成で環境間などをDRYにできます
      • 私の場合、_variable.tf, _provider.tf, _backend.tf を一番上位の terragrunt.hcl でまとめて設定しています
    • dependency ブロックを使って他のステートへの依存を簡単に書ける
    • run-all(run --all) を使うことで、ステートディレクトリ間の依存関係を考慮して、順番に plan(apply) を実行してくれる
      • CI では git差分のあるディレクトリに対してのみ plan(apply) を実行する仕組みにしているので基本的に run-all(run --all) で実行することはないと思います。少なくとも私のプロジェクトでは使ってないです。上位のディレクトリで run-all plan をやったら、なんのために ステート分割をしているのか...という話になるので
    • 他のステートディレクトリの値に依存していて、その値は apply で決定するが plan の時点では決定していない時、普通なら plan の時点でエラーになるが、モック値を使うことで plan 時のエラーを回避できます
      • ステート分割して各ディレクトリにplanを実行する場合、つまり run-all(run --all)を使わない場合、ステートディレクトリの実行順序は決まってないので、plan はモック値を使うことでパスしたが、applyの時点でも依存先のステートディレクトリの値が決まっておらずにモック値が使用されて失敗するということが起きえます。ただ、複数ステートディレクトリに渡りコードを書いたとき、依存先のステートディレクトリを applyしてから、依存元のステートディレクトリをコメントインするというような作業をせずに、複数回 merge_group イベントに突っ込んで apply すれば済むのはメリットだと思います
  • tfmigrate ← 私は使用しなくなりました

    • import, mv, rm 操作をするときに、適用後に plan 差分がないかどうかをチェックできます
    • import, mv, rm 操作を GitHub で管理できます
    • import, mv, rm ブロックの登場により必須級ではなくなったが、適用後に plan 差分がある場合にエラーを吐いてくれるので、import, mv, rm ブロックには無い性能を持っています。import ブロック等は適用後に plan 差分がある場合でもエラーを吐いてくれません
    • 保守性の観点から依存するツールはできる限り減らしたいと思っているので、tfmigrate を使わない構成で試しています
      • 以前、terragrunttfmigrate の併用がうまくいかないことがあり、解決するのに時間がかかってしまいました。terragrunttfmigrateを併用しているユーザがあまりいないのは tfmigrate を使わなくなった理由の一つです
  • tfcmt(github-comment) ← 私は使用していません

    • terraform plan などのをログをPRのコメントに出してくれます
    • PRのコメントに長いログが出ると、スクロールが面倒になるので、採用しませんでした
    • 私は merge_group イベント内の terraform apply 時は、ログへのリンクとステータスを GitHub API を叩いてPRのコメントに表示しています
      • merge_groupイベントは、実行中はそのPRからリンクを辿れますが、完了してしまうとそのPRからリンクが辿れないからです
  • aqua ← 私は使用していません

    • asdf みたいにツールのバージョンを管理をできます
    • 私は tfenv, asdf のような管理は逆に複雑だと思ってしまうので、バイナリを直接インストールして /usr/local/bin/ に配置しています
      • 私たちは、PATHが通っている /usr/local/bin//bin/ にバイナリを配置して実行することが多いと思いますが、asdf などの抽象化が入るとその前提が崩れてしまい、トラブルシューティングを難しくするときがあります。私は、asdf のような過度な抽象化は、トラブルシューティングの困難さを上げる割に、得られる利便性が少ないと感じてしまいます

ディレクトリ構成

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

  • 環境ごとのインフラ状態はできるかぎり同じ状態にしたいです。つまり、stag でバグが見つかれば prod でもバグが見つかるというような状態にしたいです
    • messi と stag, stag と prod のディレクトリの状態が一部のファイルを除き、完全に一致することを pre-commit と CI で強制します
      • 一致しないファイルとして許容されるのは、_variable.tf などの terragrunt initで自動生成された環境特有の値が入っているファイルと、_asymmetry という文字列が入ったファイルです
        • 例えば、argocd-vault-plugin が AWS Secrets Manager にアクセスするには、argocd-repo-server のサービスアカウントに IAM Role を付与するための Pod Identity を定義する必要があります。このリソースは一つの環境で定義すれば十分なので(argoCD は一つで全部のEKSを管理するから)、環境間で一致させる必要はありません。なので、ファイル名に _asymmetry 含めて特有の環境だけに存在することを許可します
  • VPC CIDR や DBのスペック などの環境変数は一元管理したいです
    • 一番上位の terragrunt.hcl で一元管理して、terragrunt init で自動生成します
  • 原神, Apex 等のゲームみたいに、ユーザがどのリージョンのサーバを使うのかを選択できるようにしたいです
    • aws-ap-northeast-1, aws-us-east-1 のようにしてディレクトリを切ります
    • リージョンを増やしたければ、そのときにディレクトリを増やします
    • aws-ap-northeast-1, aws-us-east-1, ...(プロバイダー&リージョンディレクトリ) 間の一致は強制しません。例えば、アークナイツでは日本版と大陸版のリリースタイミングは全然違います
  • 適切にステート分割を行うことで plan(apply) の実行時間を短縮したい
    • hub(ネットワーク系), main(EKS など), db(データベースなど)...のようにして切ります。新たにステートディレクトリを切りたいときはここに並べます
  • リソースを増やすときにステート操作で整理したいときがあると思うが、気軽にステート操作をできるようにしておきたいです
    • 今までは tfmigrate を使っていましたが、import, mv, rm ブロックを使うようにします
      • tfmigrate よりも慎重にステート操作をする必要が出てきますが、保守性を考えて tfmigrate を使わない構成にしています
~/myTfProject$ tree -a -L 10 -I ".git"
.
├── .github
│   ├── CODEOWNERS
│   ├── actions
│   │   ├── check_if_not_deleting_tfstate_with_resources
│   │   │   └── action.yml
│   │   ├── create_pr_to_another_environment
│   │   │   └── action.yml
│   │   ├── install_tools
│   │   │   └── action.yml
│   │   ├── terraform_apply
│   │   │   └── action.yml
│   │   └── terraform_plan
│   │       └── action.yml
│   └── workflows
│       ├── detect_git_diff_among_envs.yml
│       ├── detect_git_diff_in_common_dir_from_common_branch.yml
│       ├── exec_actionlint.yml
│       ├── pre-commit_pre-push_check.yml
│       ├── reusable_terraform_apply.yml
│       ├── reusable_terraform_plan.yml
│       ├── terraform_apply_caller.yml
│       ├── terraform_init_validate.yml
│       └── terraform_plan_caller.yml
├── .gitignore
├── .lefthook
│   ├── ci
│   │   ├── terraform_init.sh
│   │   └── terraform_validate.sh
│   ├── pre-commit
│   │   ├── static_analysis.sh
│   │   ├── static_analysis_format.sh
│   │   └── static_analysis_sync_envs.sh
│   └── pre-push
│       └── test_pre-push.sh
├── .tflint.hcl
├── README.md
├── envs
│   ├── common
│   │   └── aws-global
│   │       └── hub
│   │           ├── .terraform
│   │           ├── .terraform.lock.hcl
│   │           ├── _backend.tf
│   │           ├── _provider.tf
│   │           ├── _variable.tf
│   │           ├── aws_route53_zone.tf
│   │           ├── aws_route53domains_registered_domain.tf
│   │           ├── output.tf
│   │           └── terragrunt.hcl
│   ├── messi
│   │   ├── aws-ap-northeast-1
│   │   │   ├── cdn
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_cloudfront_distribution.tf
│   │   │   │   ├── aws_cloudfront_key_group.tf
│   │   │   │   ├── aws_cloudfront_origin_access_control.tf
│   │   │   │   ├── aws_cloudfront_public_key.tf
│   │   │   │   ├── aws_cloudwatch_event_rule.tf
│   │   │   │   ├── aws_cloudwatch_event_target.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── aws_lambda_function.tf
│   │   │   │   ├── aws_lambda_permission.tf
│   │   │   │   ├── aws_s3_bucket.tf
│   │   │   │   ├── aws_s3_bucket_cors_configuration.tf
│   │   │   │   ├── aws_s3_bucket_notification.tf
│   │   │   │   ├── aws_s3_bucket_policy.tf
│   │   │   │   ├── aws_sns_topic.tf
│   │   │   │   ├── aws_sns_topic_policy.tf
│   │   │   │   ├── aws_sns_topic_subscription.tf
│   │   │   │   ├── data_archive_file.tf
│   │   │   │   ├── data_aws_secretsmanager_secret_version.tf
│   │   │   │   ├── lambda
│   │   │   │   │   ├── mediaconvert
│   │   │   │   │   │   ├── main.py
│   │   │   │   │   │   └── mediaconverter_post_schema.json
│   │   │   │   │   ├── mediaconvert.zip
│   │   │   │   │   ├── mediaconvert_notification
│   │   │   │   │   │   └── main.py
│   │   │   │   │   └── mediaconvert_notification.zip
│   │   │   │   ├── output.tf
│   │   │   │   ├── public_key.pem
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── cognito
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_cognito_identity_pool.tf
│   │   │   │   ├── aws_cognito_identity_pool_roles_attachment.tf
│   │   │   │   ├── aws_cognito_identity_provider.tf
│   │   │   │   ├── aws_cognito_user_pool.tf
│   │   │   │   ├── aws_cognito_user_pool_client.tf
│   │   │   │   ├── aws_cognito_user_pool_domain.tf
│   │   │   │   ├── aws_iam_policy.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── data_aws_region.tf
│   │   │   │   ├── data_aws_secretsmanager_secret_version.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── db
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_db_subnet_group.tf
│   │   │   │   ├── aws_rds_cluster.tf
│   │   │   │   ├── aws_rds_cluster_instance.tf
│   │   │   │   ├── aws_security_group.tf
│   │   │   │   ├── aws_security_group_rule.tf
│   │   │   │   ├── data_aws_secretsmanager_secret_version.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── ecr
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_ecr_repository_asymmetry.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── hub
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_eip.tf
│   │   │   │   ├── aws_internet_gateway.tf
│   │   │   │   ├── aws_internet_gateway_attachment.tf
│   │   │   │   ├── aws_nat_gateway.tf
│   │   │   │   ├── aws_route.tf
│   │   │   │   ├── aws_route_table.tf
│   │   │   │   ├── aws_route_table_association.tf
│   │   │   │   ├── aws_subnet.tf
│   │   │   │   ├── aws_vpc.tf
│   │   │   │   ├── output.tf
│   │   │   │   └── terragrunt.hcl
│   │   │   ├── main
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_eks_access_entry.tf
│   │   │   │   ├── aws_eks_access_policy_association.tf
│   │   │   │   ├── aws_eks_cluster.tf
│   │   │   │   ├── aws_eks_pod_identity_association_asymmetry_avp.tf
│   │   │   │   ├── aws_iam_policy_asymmetry_avp.tf
│   │   │   │   ├── aws_iam_role.tf
│   │   │   │   ├── aws_iam_role_asymmetry_avp.tf
│   │   │   │   ├── aws_iam_role_policy_attachment.tf
│   │   │   │   ├── aws_iam_role_policy_attachment_asymmetry_avp.tf
│   │   │   │   ├── data_aws_iam_policy_document.tf
│   │   │   │   ├── data_aws_iam_policy_document_asymmetry_avp.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   ├── route
│   │   │   │   ├── .terraform
│   │   │   │   ├── .terraform.lock.hcl
│   │   │   │   ├── _backend.tf
│   │   │   │   ├── _provider.tf
│   │   │   │   ├── _variable.tf
│   │   │   │   ├── aws_acm_certificate.tf
│   │   │   │   ├── aws_acm_certificate_validation.tf
│   │   │   │   ├── aws_route53_record.tf
│   │   │   │   ├── terragrunt.hcl
│   │   │   │   └── variable.tf
│   │   │   └── secret
│   │   │       ├── .terraform
│   │   │       ├── .terraform.lock.hcl
│   │   │       ├── _backend.tf
│   │   │       ├── _provider.tf
│   │   │       ├── _variable.tf
│   │   │       ├── aws_kms_alias.tf
│   │   │       ├── aws_kms_key.tf
│   │   │       ├── aws_secretsmanager_secret.tf
│   │   │       ├── aws_secretsmanager_secret_version.tf
│   │   │       ├── output.tf
│   │   │       └── terragrunt.hcl
│   │   └── aws-us-east-1
│   │       ├── hub

---

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

---

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

---

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

terragrunt による自動生成

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

envs/terragrunt.hcl
locals {

---

  environment = regex("^(messi|stag|prod|common).*$", path_relative_to_include())[0]
  
---

  vpc_cidr_1 = (
    local.environment == "messi" ? (
      local.provider_and_region == "aws-ap-northeast-1" ? 
        "10.11.0.0/16" :
      local.provider_and_region == "aws-us-east-1" ? 
        "10.22.0.0/16" :
      "_"
    ) : local.environment == "stag" ? (
      local.provider_and_region == "aws-ap-northeast-1" ? 
        "10.33.0.0/16" :
      local.provider_and_region == "aws-us-east-1" ?
        "10.44.0.0/16" :
      "_"
    ) : local.environment == "prod" ? (
      local.provider_and_region == "aws-ap-northeast-1" ? 
        "10.55.0.0/16" :
      local.provider_and_region == "aws-us-east-1" ?
        "10.66.0.0/16" :
      "_"
    ) : "_"
  )
  
---

}

generate "variables" {
  path      = "_variable.tf"
  if_exists = "overwrite"

  contents = <<EOF
%{ if local.environment == "common" &&
    (local.provider_and_region == "aws-global") &&
    (local.service == "hub") }
variable "domain" {
  description = "_"
  type        = string
  default     = "${local.domain}"
}
%{ endif }

---

}

---

ブランチ戦略

ブランチ戦略を採用します。
messi 環境のインフラ状態は messi ブランチの messi ディレクトリ, stag 環境のインフラ状態は stag ブランチの stag ディレクトリ, prod環境のインフラ状態は prod ブランチの prod ディレクトリと一致します。
また、common ブランチと common ディレクトリも作ってあり、ホストゾーンなどの全環境で共通して使用するものを定義しています。全環境に影響する可能性があるので、common ブランチへのマージはコードオーナーで制限をつけます。

CI

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

コードオーナー

例えば、prod ブランチへのPRはコードオーナーのレビューを必須にします。

merge_groupイベント

terraform CI/CD では merge_group イベントが非常に便利です。
terraform plan が成功していても terraform apply で失敗することは頻繁にあります。push イベントを使うと terraform apply が失敗してもマージされてしまうので、デフォルトブランチが汚染されてしまいます。
ただ、Enterprise プランしか使えません。

matrix ジョブの利用

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

シークレット管理

AWS Secrets Manager を使います。Secrets Manager の箱は terraform で作って、実際の値は AWS CLI もしくは AWS コンソールから変更します。
例えば、DBのパスワードを更新したければ、AWS コンソール で更新した後に terraformmaster_password_wo_version の値を引き上げます。
これにより、GitHub にシークレットが載ることはなくなりました。

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

調べたところ、以下の二つが選択肢のようです

  • local-exec を使うことで、動的にシークレットを取得する
  • tfstate ファイル自体をシークレットとして管理する

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

xxx.tf
data "aws_secretsmanager_secret_version" "data_aws_secretsmanager_secret_version_aurora" {
  count     = var.secretsmanager_secret_hub_id == "ThisIsAMockValue" ? 0 : 1
  secret_id = var.secretsmanager_secret_hub_id
}

resource "aws_rds_cluster" "aws_rds_cluster_aurora" {

---

  master_password_wo = try(
    jsondecode(data.aws_secretsmanager_secret_version.data_aws_secretsmanager_secret_version_aurora[0].secret_string)["POSTGRES_PASSWORD"],
    "POSTGRES_PASSWORD_NOT_SET"
  )
  master_password_wo_version = 6

---
  
}

GitHub Apps Token

PAT もしくは Fine-grained personal access token を半年おきとかに交換する手間が嫌だったり、セキュリティを高くしたりしたいのであれば、 GitHub Apps Token を使いましょう。GitHub Apps Token は毎回一時トークンが発行されるので、セキュリティが高いです。

以下は、PAT もしくは Fine-grained personal access token を使わないという前提で話を進めます。

CI において GitHub Apps Token は以下の理由で必須です。

  • Go の Private モジュール(レポジトリ) のダウンロードには secrets.GITHUB_TOKEN では無理です
  • 別レポジトリへの push や PR 作成は、secrets.GITHUB_TOKENでは無理です
  • PR を作成したときに secrets.GITHUB_TOKENを使うと、自動テストが流れません
  • PR を作成して承認したいとき、別ユーザでないと承認できません。なので、作成を GitHub Apps Token で行い、承認を secrets.GITHUB_TOKENで行う必要があります

EKS

EKS Auto Mode

aws-load-balancer-controllerPod Identity Agent が予めインストールされており、自らインストールする必要がないので便利です。
EC2のスペックを考える必要もないです。

アクセスエントリー

従来は IAM ロール に EKS へのアクセス権限を付与するために、aws-auth という ConfigMap をいじっていたかと思いますが、アクセスエントリーを使うことで ConfigMap をいじらないで済みます。
aws-auth という ConfigMapterraform で設定する場合、k8s プロバイダーを定義する必要があったので面倒でしたが、EKS API(アクセスエントリー) で設定できるようになり k8s プロバイダーを定義する必要がなくなりました。

Pod Identity

サービスアカウントに IAM ロール を付与するために、従来なら terraform 側で完結せずに、Manifest ファイルでの設定も必要でしたが、Pod Identity の登場により terraform 側で完結するようになりました。

マルチテナント vs シングルテナント

私は費用と運用コストを考えて、マルチテナントを採用しています。
CI/CD の構築や terraform のコードの複雑さを考えると、マルチテナントの方が圧倒的に見通しが良くなります。
デメリットとして、耐障害性が低いことや、それぞれのマイクロサービスのコストを把握しづらいことが挙げられます。

k8s の CI/CD

各ツールの特徴

  • 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 のコードに注入する場合、これ専用のプロバイダーが必要みたいで、少し面倒だと感じました

ディレクトリ構成

最下部のディレクトリが argoCD 上のアプリケーション名となります。そして同時に、k8s上のリソース名となります。
その一つ上のディレクトリが argoCD 上の名前空間となります。そしてほとんどの場合、k8s上の名前空間となります。ただ必ずしも、k8s上の名前空間となりません。例えば、ingressclass/alb を見ると、ingressclass は argoCD 上の名前空間ですが、k8s上の名前空間にはなりません。ingressClass リソースには名前空間という概念がないからです。

~/myClusProject$ tree -a -L 9 -I ".git"
.
├── .github
│   ├── actions
│   │   ├── decrypt_secret_files_by_sops
│   │   │   └── action.yml
│   │   ├── install_tools
│   │   │   └── action.yml
│   │   ├── kubectl_apply
│   │   │   └── action.yml
│   │   ├── kubectl_diff
│   │   │   └── action.yml
│   │   └── update_kubeconfig
│   │       └── action.yml
│   └── workflows
│       ├── exec_actionlint.yml
│       ├── kubectl_apply_files_not_managed_by_argocd.yml
│       ├── pre-commit_pre-push_check.yml
│       ├── restrict_helmfile_name_and_exec_helmfile_template.yml
│       └── show_kubectl_diff_in_git_diff_dirs.yml
├── .lefthook
│   ├── ci
│   │   └── restrict_helmfile_name_and_exec_helmfile_template.sh
│   ├── pre-commit
│   │   └── prevent_commiting_secrets.sh
│   └── pre-push
│       └── test_push.sh
├── .sops.yaml
├── README.md
├── charts
│   ├── appproject
│   │   ├── Chart.yaml
│   │   └── templates
│   │       └── appproject.yaml
│   ├── generalapp
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── generalapp-deployment.yaml
│   │       ├── generalapp-ingress-internal.yaml
│   │       ├── generalapp-ingress.yaml
│   │       ├── generalapp-secret.yaml
│   │       └── generalapp-service.yaml
│   └── ingressclass
│       ├── Chart.yaml
│       └── templates
│           ├── ingressclass.yaml
│           └── ingressclassparams.yaml
├── envs
│   ├── messi
│   │   ├── aws-ap-northeast-1
│   │   │   ├── argocd
│   │   │   │   ├── applicationset
│   │   │   │   │   └── applicationset.yaml
│   │   │   │   ├── appproject
│   │   │   │   │   ├── messi-appproject-auto-generated.yaml
│   │   │   │   │   ├── messi-appproject-helmfile.yaml
│   │   │   │   │   ├── messi-appproject-values.yaml
│   │   │   │   │   ├── prod-appproject-auto-generated.yaml
│   │   │   │   │   ├── prod-appproject-helmfile.yaml
│   │   │   │   │   ├── prod-appproject-values.yaml
│   │   │   │   │   ├── stag-appproject-auto-generated.yaml
│   │   │   │   │   ├── stag-appproject-helmfile.yaml
│   │   │   │   │   └── stag-appproject-values.yaml
│   │   │   │   └── argocd
│   │   │   │       ├── README_archived.md
│   │   │   │       ├── argocd-namespace.yaml
│   │   │   │       ├── argocd-nucleus-auto-generated.yaml
│   │   │   │       ├── argocd-nucleus-helmfile.yaml
│   │   │   │       ├── argocd-nucleus-values.yaml
│   │   │   │       └── myclus-ssh-private-key-secret.yaml
│   │   │   ├── gobackend
│   │   │   │   └── gobackend
│   │   │   │       ├── gobackend-auto-generated.yaml
│   │   │   │       ├── gobackend-helmfile.yaml
│   │   │   │       └── gobackend-values.yaml
│   │   │   ├── ingressclass
│   │   │   │   ├── alb
│   │   │   │   │   ├── alb-auto-generated.yaml
│   │   │   │   │   ├── alb-helmfile.yaml
│   │   │   │   │   └── alb-values.yaml
│   │   │   │   └── alb-internal
│   │   │   │       ├── alb-internal-auto-generated.yaml
│   │   │   │       ├── alb-internal-helmfile.yaml
│   │   │   │       └── alb-internal-values.yaml
│   │   │   └── my-nginx
│   │   │       └── my-nginx
│   │   │           ├── my-nginx-auto-generated.yaml
│   │   │           ├── my-nginx-helmfile.yaml
│   │   │           └── my-nginx-values.yaml
│   │   └── aws-us-east-1
│   │       ├── ---

---

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

---

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

---

└── lefthook.yml

argoCD の監視

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

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

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

ブランチ戦略

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

argocd-vault-plugin

公式Docにある argocd-vault-plugin generate . をそのまま使用すると、 helmfile 等がパースエラーになってしまうので、少し修正して余計なファイルをargocd-vault-plugin generate コマンドの対象外にしましょう。

ingress リソースのグループ化

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

CI

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

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

ディレクトリ構成

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

~/myActionProject$ tree -a -L 9 -I ".git"
.
├── .github
│   ├── actions
│   │   ├── private_build_push_ecr
│   │   │   └── action.yml

---

│   │   └── public_update_comment_for_log
│   │       └── action.yml
│   └── workflows
│       ├── private_exec_actionlint.yml
│       └── private_exec_tag_release.yml
├── README.md
└── version.txt

55 directories, 56 files

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

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

アプリプロジェクト

ディレクトリ構成

~/myAppProject$ tree -a -L 9 -I ".git"
.
├── .env
├── .github
│   ├── actions
│   │   └── build_all_images_using_cache_and_up
│   │       └── action.yml
│   ├── dependabot.yml
│   └── workflows
│       ├── build_push_and_tag_release_and_create_pr.yml
│       ├── cache_docker_image_to_default_branch.yml
│       ├── exec_actionlint.yml
│       ├── go_test.yml
│       ├── pre-commit_pre-push_check.yml
│       └── static_analysis.yml
├── .gitignore
├── .lefthook
│   ├── pre-commit
│   │   ├── required_command_check.sh
│   │   ├── static_analysis.sh
│   │   └── static_analysis_format.sh
│   └── pre-push
│       └── test_push.sh
├── README.md
├── docker
│   ├── migrate
│   │   └── Dockerfile
│   └── web
│       └── Dockerfile
├── docker-compose.yml
├── lefthook.yml
├── migrate
│   ├── ...

---

├── version_migrate.txt
├── version_web.txt
└── web
    ├── ...

---

Docker

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

k8s

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

CI

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

git flow or github flow

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

その他

postgress or mysql

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

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?