Table of Contents
Terraform を Github Action 上で実行、自動で適用するまでの仕組みを作った
趣味開発で GCP を使用しており、全てのインフラ構成を Terraform により IaC 化している。現在、次のフローによって Terraform を更新し、その更新をインフラへ反映させている。
- (1)
.tf
ファイルを更新 - (2) 手動で
terraform apply
コマンドを実行し、.tf
ファイルに対する変更を反映。 - (3) Terraform の変更を main ブランチへ直接コミット。
新機能開発で必要となるリソースを追加するだけであれば、上記で述べた手動運用でも問題ないが、毎日の Terraform の運用が面倒くさい。具体的には下記のような問題。
- dependabot により
.terraform.lock.hcl
中に書かれている Provider などのバージョンが更新されたり。割と頻繁に更新される。 - モジュールのリファクタリング。とあるモジュールをリファクタリングしたとき、そのモジュールへ依存しているルートモジュールを全て更新する必要があったり。
ということで今回、自動化しちゃった。
趣味開発でそこまでやるか?という意見もあるが、自分としては、趣味開発だからこそ自動化が必要であると考える。 日々の多くの時間を本業や家事へ費やしているため、趣味開発への時間があまり取れない。そうなると、コードを書く時間は必然的に土日の空いた時間へ偏ることになる。コードを触れるのは週 1 日ぐらい。必然的にインフラを触るのも週 1 日となる。インフラを触れるのが週 1 日だけとなると、terraform による手動更新手順すら忘れてしまう。だから自動化する。
何を作ったか?
Terraform を変更し PR を作成すると、Github Action 上で自動で plan が実行される。terraformga apply
とコメントすると、Github Action 上で自動で apply が実行され、PR がマージされる。
例
https://github.com/suzuito/sandbox2-terraform/pull/69
Root moduleorganizations/tach.dev/products/blog/services/server/environments/stg/
のlocal.server_env.hoge
を変更する PR を作る。すると Github Action 上でterraform plan
が実行される(下図の check_in_PR)。
その他、tflint
やコードレビューで問題がなければ、terraformga apply
とコメントする(下図)。
問題なく apply ができたら、PR が自動でマージされる。
仕様
Atlantisを参考にしている。
Terraform 更新フロー
.tf
(.terraform.lock.hcl
も含む)ファイルの修正- (手動)
main
ブランチをベースとするfeature
ブランチを作成し、feature
ブランチ上で.tf
ファイルを変更する。
- (手動)
- レビュー
- (手動)
main
<-feature
の PR を作る。 - レビュー項目(Github Action の status check)
- (自動 on Github Action)
terraform plan
のエラーがない。 - (自動 on Github Action) tflint のエラーがない。
- (手動) 目視によるレビュー。問題なければ承認。
- (自動 on Github PR) PR をマージする前にベースブランチの最新の変更が取り込まれていること(Update branch 機構の有効化)。
- (自動 on Github Action) 1 つの PR の中で、1 つの 1 つの root module に対する変更のみを
apply
すれば良い状態になっていること(※1)。
- (自動 on Github Action)
- (手動)
- リリース
- (自動 on Github PR) リリース承認判定。
- 上記のレビュー項目を全てパスしていれば、
teraform apply
が実行可能となる。terraform apply
の実行方法。
- 上記のレビュー項目を全てパスしていれば、
- (手動) PR にて
terraformga apply
をコメントすると、Github Action がterraform apply
を実行する。
- (自動 on Github PR) リリース承認判定。
- マージ
terraformga apply
が成功後- (自動 on Github PR) ブランチがマージされる。
terraformga apply
が失敗した場合、ブランチはマージされない。- (手動) 問題を解決し、
terraform apply
まで実行すること。 - (手動) 問題を解決できたらブランチをマージする。
- (手動) 問題を解決し、
(※1)この制約を設けている理由は、1 回の terraform の apply 実行における更新数をなるべく少なくしたいため。terraform によるリソースの変更範囲が大きくなるほど、リソース更新失敗のリスクが大きくなる。今回、試験的に terraform の自動実行を導入しているという経緯から、保守的な姿勢を取る。という感じなので、もしかするとこの制約は不要なのかもしれない。心配しすぎ?
設計
Terraform ディレクトリ構成
自動化の対象となるルートモジュールは 7 つあり、これら Root module の中で使用される Child module がいくつかある。
- Root modules
- products/blog/services/common/environments/stg
- Google cloud project=blog-stg の common サービスのリソース
- products/blog/services/common/environments/prd
- Google cloud project=blog-prd の common サービスのリソース
- products/blog/services/server/environments/stg
- Google cloud project=blog-stg の server サービスのリソース
- products/blog/services/server/environments/prd
- Google cloud project=blog-prd の server サービスのリソース
- products/products-common/services/dns/environments/common
- Google cloud project=products-common の dns サービスのリソース
- products/products-common/services/mysql_sandbox/environments/common
- Google cloud project=products-common の mysql_sandbox サービスのリソース
- products/products-common/services/secret/environments/common
- Google cloud project=products-common の secret サービスのリソース
- products/blog/services/common/environments/stg
- Child modules
Terraform モジュールに対する制約。
- 1 つの Root module の中で管理するリソースは、1 つだけの Google Cloud Project に紐づいている。例えば、ある 1 つの Root module 配下の A リソースが a プロジェクト、B リソースが b プロジェクト、みたいな Root module を禁止している。
- 何故この制約を設けたか。Google Cloud Platform のリソースが、どの Root module から作成、更新されているのか?がわかり難くなることを防ぐため。というのと、後述する Terraform 実行用のサービスアカウントの権限範囲を絞るため。
Github Action
- terraform_plan ジョブ
- Github Action の pull_request トリガー。Terraform に対する何らかの変更があった場合、関係する Root module モジュールに対して
plan
を実行する。
- Github Action の pull_request トリガー。Terraform に対する何らかの変更があった場合、関係する Root module モジュールに対して
- terraform_apply ジョブ
- Github Action の issue_comment トリガー。PR に
terraformga apply
とコメントすると、Github Action 上でterraform apply
が自動実行される。PR のレビュー項目を全てパスしていない場合、terraform apply
を実行できない(あかん・・・実装忘れてた・・・今後の課題とします・・・)。
- Github Action の issue_comment トリガー。PR に
Terraform 実行用のサービスアカウント
1 つの Google Cloud Project に、1 つの Terraform 実行用のサービスアカウントがいる。このアカウントは、アカウントが所属するプロジェクトのリソースに対する更新権限を持つ。他のプロジェクトに対する更新権限は持っていない。全プロジェクトの更新権限を持つ神のようなサービスアカウントを作らないようにしている(※2)。
(※2)厳密に言うと、神のようなサービスアカウントは 1 つだけある。Google cloud folder、Google cloud project を管理している Root module があるのだが、神のようなアカウントはこの Root module を apply する。この Root module だけは、自動化対象外としている。
terraform plan ジョブの中身
Go 言語で書いた。
- (処理 1)Github API を用いて PR に含まれる変更ファイルを取得する
- (処理 2)レポジトリ上の Terraform のモジュール木構造(図 1)を取得する
- モジュール木構造とは、module をノードとし、module 間の依存(module と module ブロック)をリンクとする木(ただし、インターネット上に公開されている moduleは含まない)。
- 処理 1 と 2 で得られた「変更ファイル」と「モジュール木構造」から、plan することが必要となる Terraform の Root module を抽出する。Terraform module を plan して差分が検知された場合、その module を apply する必要があると判定する。
(図 1)モジュール木構造。Root module を最上位のノードとし、依存するモジュールがリンクで接続されている木構造。図中の modules/backend が変更された場合、root_modules/web を plan する必要がある。図中の modules/cd が変更された場合、root_modules/web、root_modules/batch を plan する必要がある。
モジュール木構造の作り方(参考 ParseModuleDir)
レポジトリ上のディレクトリを探索し、HashiCorp 社が提供する HCL パーサを用いて.tf
ファイルをパースし、Terraform module 情報を抽出して木構造を作っている。モジュールが Root であるかどうか?の判定条件は、.terraform.lock.hcl
がディレクトリに存在するかどうか?としている。この判定条件が正しいことの妥当性は下記を根拠としている。
The dependency lock file is a file that belongs to the configuration as a whole, rather than to each separate module in the configuration. For that reason Terraform creates it and expects to find it in your current working directory when you run Terraform, which is also the directory containing the .tf files for the root module of your configuration. 参考
-> .terraform.lock.hcl
がディレクトリ上に存在するならば、そのディレクトリは Root module である。と解釈できる(多分、あんまり自信ない)。
今後の課題
- (実装を忘れた) PR のレビュー項目を全てパスしていない場合、
terraform apply
を実行できないようにしたい。
感想
これまでは、後の運用のことを考えるとインフラ構築が億劫になることがあった(自分が構築したインフラ構成と Terraform をすぐに忘れてしまうので)が、今後は心置きなくインフラをガンガン構築できる!わーい。