Pantsを使ってTerraform monorepoの変更があった部分だけplan/applyを実行する
はじめに
この記事では、monorepo 上の Terraform コードをビルドツールの Pants を使って管理する方法について紹介します。Pants を使うことで、コード変更による影響範囲だけを対象にterraform plan
やterraform apply
を実行できます。
前提となる知識
- Terraform
- Pants
本題に入る前に、Pants について簡単に説明します。
Pants とは
Pants は Python や Java、Go などのプログラミング言語をサポートするビルドツールです。Pants を使うことで、あらゆる言語の test や lint、format、ビルド、デプロイなどの各実行を共通のインターフェイスで実行できます。monorepo ではレポジトリが肥大すると、レポジトリ全体を対象にした各実行は時間がかかるようになります。毎回、レポジトリ全体を対象に各実行を行うのは開発工率が悪いため、変更したコードの影響分だけを対象に各実行を行うことで開発工率を向上させることができます。Pants はそのようなユースケースに対応しています。
ターゲット
Pants にはターゲットという概念があります。ターゲットを宣言することで、各ファイルを Pants から管理できるようになります。このターゲットを使って、ビルドやテスト、lint、format、デプロイなど各実行を行います。ターゲットは BUILD ファイルに記述されます。
次のようなディレクトリ構造を例にターゲットの宣言方法を紹介します。
foo/
└── bar/
└── main.tf
└── BUILD
foo/bar/BUILD
ファイルに次のように記述します。terraform_module
はカレントディレクトリの*.tf
を Pants で管理するためのターゲットです。このターゲットを宣言するとpants fmt
, pants lint
を実行した裏側でterraform fmt
やterraform validate
、tflint
が実行されます。
terraform_module(name="tf_code")
宣言したターゲットに対して format を実行する例は次の通りです。terraform のコードにはterraform fmt
が実行されます。
# <dir>/:<target_name>では単一のターゲットを対象として実行
$ pants fmt foo/bar:tf_code
# <dir>/:ではdir内の全てのターゲットを対象として実行
$ pants fmt foo/bar/:
# <dir>/::では、この階層より下のdirも含めて全てのターゲットを対象として実行
$ pants fmt foo/::
# この階層より下のdirも含めて全てのターゲットを対象として実行
$ pants fmt ::
上記の例では、ターゲットを指定してpants fmt
を実行していますが、pants fmt foo/bar/main.tf
のようにファイルを指定して実行することもできます。
Terraform monorepo を Pants で管理する
ここからが本題です。Terraform monorepo を Pants で管理する方法について紹介します。この記事を執筆しているv2.22.0
時点では、Terraform のサポートが実験的な機能として提供されています。そのため、今後のアップデートで変更される可能性があります。
以降に出てくるサンプルコードはこちらのレポジトリにあります。
Pants 管理のメリット
Pants で Terraform を管理することで、コード変更による影響範囲を検知し、その影響範囲に対してのみterraform plan
,apply
を実行できます。例えば、レポジトリ内のコードが module として複数から参照されており module 側で変更があった際は、その影響範囲だけをまとめてterraform plan
, apply
できます。
pants.toml と BUILD ファイルの作成
次のようなディレクトリ構造を例に、Terraform monorepo を Pants で管理する方法を紹介します。
└── infra
└── terraform
├── app
│ ├── bar
│ │ └── main.tf
│ └── foo
│ └── main.tf
└── module
└── random.tf
app/bar/main.tf
と app/foo/main.tf
はそれぞれmodule
ディレクトリを参照しています。ここでは簡単のために backend は local で管理していますが、実際の運用では s3 や gcs などのリモートバックエンドを使うことを想定しています。
terraform {
backend "local" {
path = "/tmp/tfstate/bar.tfstate"
}
}
module "name" {
source = "../../module"
input_text = "world"
}
terraform {
backend "local" {
path = "/tmp/tfstate/foo.tfstate"
}
}
module "name" {
source = "../../module"
input_text = "hello"
}
provider "random" {
version = "3.6.2"
}
resource "random_id" "id" {
keepers = {
input_text = var.input_text
}
byte_length = 8
}
variable "input_text" {
type = string
default = "default"
}
この状態から、Pants を使うためにpants.toml
を作成します。
[GLOBAL]
pants_version = "2.22.0"
backend_packages = [
"pants.backend.experimental.terraform",
"pants.backend.python",
]
[python]
interpreter_constraints = ["==3.11.*"]
pants にはtailor
というゴール(サブコマンド)があり、ファイルの拡張子からターゲットを自動で生成してくれます。pants tailor ::
を実行すると.tf
があるディレクトリにBUILD
ファイルが生成されます。
生成されたBUILD
ファイルは 3 つとも次のようになります。
terraform_module()
ここまでの設定をすると、ディレクトリ構造は次のようになります。
.
├── infra
│ └── terraform
│ ├── app
│ │ ├── bar
+ │ │ │ ├── BUILD
│ │ │ └── main.tf
│ │ └── foo
+ │ │ ├── BUILD
│ │ └── main.tf
│ └── module
+ │ ├── BUILD
│ └── random.tf
+ └── pants.toml
terraform apply のためのターゲットを追加
terraform apply
を実行するためのターゲットterraform_deployment
をapp/*/BUILD
に追加します。terraform_deployment から terraform_module を参照しやすいように名前をつけてroot_module
に指定します。
- terraform_module()
+ terraform_module(name="tf_code")
+ terraform_deployment(name="deploy", root_module=":tf_code")
これでterraform apply
を実行する準備が整いました。pants experimental-deploy infra/terraform/app/::
を実行すると、terraform apply
がbar:deploy
とfoo:deploy
を対象に実行されます。
差分実行
terraform apply
を実行
複数のターゲットを対象にpants experimental-deploy
を実行する際に、変更したコードの影響範囲だけを検出して実行できます。ここまでのコードを commit し、この commit sha から差分を取得してterraform apply
を実行してみます。module のbyte_length
を7
に変更して app 側のターゲットに影響を与えるようにします。
provider "random" {
version = "3.6.2"
}
resource "random_id" "id" {
keepers = {
input_text = var.input_text
}
- byte_length = 8
+ byte_length = 7
}
variable "input_text" {
type = string
default = "default"
}
この状態から commit sha 54795b7d3688c98f
との差分を取得してterraform apply
を実行します。
pants --changed-since=54795b7d3688c98f --changed-dependents=transitive experimental-deploy
実行結果(長いので省略。クリックで展開)
22:59:38.45 [INFO] Deploying targets...
module.name.random_id.id: Refreshing state... [id=5JAAEz9cu_Q]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# module.name.random_id.id must be replaced
-/+ resource "random_id" "id" {
~ b64_std = "5JAAEz9cu/Q=" -> (known after apply)
~ b64_url = "5JAAEz9cu_Q" -> (known after apply)
~ byte_length = 8 -> 7 # forces replacement
~ dec = "16469663919961324532" -> (known after apply)
~ hex = "e49000133f5cbbf4" -> (known after apply)
~ id = "5JAAEz9cu_Q" -> (known after apply)
# (1 unchanged attribute hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Warning: Version constraints inside provider configuration blocks are deprecated
│
│ on ../../module/random.tf line 2, in provider "random":
│ 2: version = "3.6.2"
│
│ Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of
│ Terraform. To silence this warning, move the provider version constraint into the required_providers block.
╵
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
module.name.random_id.id: Destroying... [id=5JAAEz9cu_Q]
module.name.random_id.id: Destruction complete after 0s
module.name.random_id.id: Creating...
module.name.random_id.id: Creation complete after 0s [id=tEgpIWKqMw]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
module.name.random_id.id: Refreshing state... [id=q76RkCfiV3A]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# module.name.random_id.id must be replaced
-/+ resource "random_id" "id" {
~ b64_std = "q76RkCfiV3A=" -> (known after apply)
~ b64_url = "q76RkCfiV3A" -> (known after apply)
~ byte_length = 8 -> 7 # forces replacement
~ dec = "12375488874391164784" -> (known after apply)
~ hex = "abbe919027e25770" -> (known after apply)
~ id = "q76RkCfiV3A" -> (known after apply)
# (1 unchanged attribute hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Warning: Version constraints inside provider configuration blocks are deprecated
│
│ on ../../module/random.tf line 2, in provider "random":
│ 2: version = "3.6.2"
│
│ Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of
│ Terraform. To silence this warning, move the provider version constraint into the required_providers block.
╵
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
module.name.random_id.id: Destroying... [id=q76RkCfiV3A]
module.name.random_id.id: Destruction complete after 0s
module.name.random_id.id: Creating...
module.name.random_id.id: Creation complete after 0s [id=DFRLKl4sLg]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
✓ infra/terraform/app/bar:deploy deployed
✓ infra/terraform/app/foo:deploy deployed
experimental-deploy
を実行する際に明示的にターゲットを指定していませんが、差分検知によりbar:deploy
とfoo:deploy
を対象にterraform apply
が実行されました。
terraform apply
を実行
特定のターゲットを対象に先ほどと同様にここまでのコードを commit し、app/foo/main.tf
だけに変更を加えて差分実行を実行します。
terraform {
backend "local" {
path = "/tmp/tfstate/foo.tfstate"
}
}
module "name" {
source = "../../module"
- input_text = "hello"
+ input_text = "helloooo"
}
実行結果
pants --changed-since=d4c480294ef348a4f8ef48df103d8fac34aeac45 --changed-dependents=transitive experimental-deploy
23:08:51.67 [INFO] Deploying targets...
module.name.random_id.id: Refreshing state... [id=DFRLKl4sLg]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# module.name.random_id.id must be replaced
-/+ resource "random_id" "id" {
~ b64_std = "DFRLKl4sLg==" -> (known after apply)
~ b64_url = "DFRLKl4sLg" -> (known after apply)
~ dec = "3470381530623022" -> (known after apply)
~ hex = "0c544b2a5e2c2e" -> (known after apply)
~ id = "DFRLKl4sLg" -> (known after apply)
~ keepers = { # forces replacement
~ "inpput_text" = "hello" -> "helloooo"
}
# (1 unchanged attribute hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Warning: Version constraints inside provider configuration blocks are deprecated
│
│ on ../../module/random.tf line 2, in provider "random":
│ 2: version = "3.6.2"
│
│ Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of
│ Terraform. To silence this warning, move the provider version constraint into the required_providers block.
╵
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
module.name.random_id.id: Destroying... [id=DFRLKl4sLg]
module.name.random_id.id: Destruction complete after 0s
module.name.random_id.id: Creating...
module.name.random_id.id: Creation complete after 0s [id=gVNOVOh7rg]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
✓ infra/terraform/app/foo:deploy deployed
module を変更した際の差分実行は依存するターゲット 2 つに対して実行されましたが、今回の差分実行ではfoo:deploy
の一つだけを対象にterraform apply
が実行されました。
おわりに
この記事では、Terraform monorepo を Pants で管理する方法について紹介しました。Pants を使うことで、コード変更による影響範囲だけを対象にterraform apply
を実行できます。terraform module を複数のアプリケーションから参照しているような場面で、個別に更新するのは大変ですが、pants を使うことで一回のコマンド実行で効率的に対応できます。今回は差分実行をメインに紹介しましたが、tag や path を使ったフィルタリングも可能なので、特定の tag や path を含むターゲットだけを対象にまとめて実行できます。
Discussion