Pantsを使ってTerraform monorepoの変更があった部分だけplan/applyを実行する

2024/10/21に公開

はじめに

この記事では、monorepo 上の Terraform コードをビルドツールの Pants を使って管理する方法について紹介します。Pants を使うことで、コード変更による影響範囲だけを対象にterraform planterraform 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 fmtterraform validatetflintが実行されます。

BUILD
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.tfapp/foo/main.tf はそれぞれmoduleディレクトリを参照しています。ここでは簡単のために backend は local で管理していますが、実際の運用では s3 や gcs などのリモートバックエンドを使うことを想定しています。

app/bar/main.tf
terraform {
  backend "local" {
    path = "/tmp/tfstate/bar.tfstate"
  }
}

module "name" {
  source     = "../../module"
  input_text = "world"
}
app/foo/main.tf
terraform {
  backend "local" {
    path = "/tmp/tfstate/foo.tfstate"
  }
}

module "name" {
  source     = "../../module"
  input_text = "hello"
}
module/random.tf
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を作成します。

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 つとも次のようになります。

**/BUILD
terraform_module()

ここまでの設定をすると、ディレクトリ構造は次のようになります。

  .
  ├── infra
  │  └── terraform
  │     ├── app
  │     │  ├── bar
+ │     │  │  ├── BUILD
  │     │  │  └── main.tf
  │     │  └── foo
+ │     │     ├── BUILD
  │     │     └── main.tf
  │     └── module
+ │        ├── BUILD
  │        └── random.tf
+ └── pants.toml

terraform apply のためのターゲットを追加

terraform applyを実行するためのターゲットterraform_deploymentapp/*/BUILDに追加します。terraform_deployment から terraform_module を参照しやすいように名前をつけてroot_moduleに指定します。

app/*/BUILD
- terraform_module()
+ terraform_module(name="tf_code")
+ terraform_deployment(name="deploy", root_module=":tf_code")

これでterraform applyを実行する準備が整いました。pants experimental-deploy infra/terraform/app/::を実行すると、terraform applybar:deployfoo:deployを対象に実行されます。

差分実行

複数のターゲットを対象にterraform applyを実行

pants experimental-deployを実行する際に、変更したコードの影響範囲だけを検出して実行できます。ここまでのコードを commit し、この commit sha から差分を取得してterraform applyを実行してみます。module のbyte_length7に変更して app 側のターゲットに影響を与えるようにします。

module/random.tf
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:deployfoo:deployを対象にterraform applyが実行されました。

特定のターゲットを対象にterraform applyを実行

先ほどと同様にここまでのコードを commit し、app/foo/main.tf だけに変更を加えて差分実行を実行します。

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