TerraformをGithub Action上で実行、自動で適用するまでの仕組みを作った
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をすぐに忘れてしまうので)が、今後は心置きなくインフラをガンガン構築できる!わーい。