CLOVER🍀

That was when it all began.

Terragruntを使って、複数のTerraformモジュールの操作を1回のコマンド実行で行う

これは、なにをしたくて書いたもの?

Terragruntを学ぶシリーズ。今回でひと区切りの予定です。

最後は、Terragruntを使って複数のTerraformモジュールの操作を1回のコマンド実行で行ってみます。

Terragruntで、Terraformモジュールの操作を1度に行う

Terraformで環境を作っていく際に、適用する(ルート)モジュールが複数あると、その数だけterraform applyを実行してリソースを
作成したり、あるいはterraform destroyして環境を破棄したりすることになります。

Terragruntのドキュメントの記載例ですが、以下の記載例だとterraformコマンドを少なくとも5回は実行することになります。
また、各モジュールの間には依存関係もあるでしょう。

root
├── backend-app
│   └── main.tf
├── frontend-app
│   └── main.tf
├── mysql
│   └── main.tf
├── redis
│   └── main.tf
└── vpc
    └── main.tf

Terragruntは、このような状況下でのTerraformの実行を簡単にしてくれるようです。

Execute Terraform commands on multiple modules at once

今回は、こちらの機能を試してみたいと思います。

環境

今回の環境は、こちらです。

$ terraform version
Terraform v0.14.7


$ terragrunt -v
terragrunt version v0.28.7

Terraform Providerは、MySQL用のものを使用します。

Provider: MySQL - Terraform by HashiCorp

使用するMySQLは8.0.23とし、172.17.0.2で動作しているものとします。

最後にオマケで、Consulも追加します。

お題

MySQL Providerを使った、以下の3つのルートモジュールを定義します。

  • データベース
  • ユーザー
  • 権限

これらの3つのモジュールに、依存関係を定義しつつTerragruntを使って一括で操作してみましょう。

Terragruntなしで構成する

最初は、TerragruntなしでシンプルにTerraformのみで構築してみたいと思います。

各モジュール用のディレクトリを作成。

$ mkdir database users roles

中身は、こんな感じになりました。

$ tree
.
├── database
│   ├── main.tf
│   └── terraform.tfvars
├── grants
│   ├── main.tf
│   └── terraform.tfvars
└── users
    ├── main.tf
    └── terraform.tfvars

3 directories, 6 files

データベース用モジュール。

$ cd database

main.tf

terraform {
  required_version = "0.14.7"

  required_providers {
    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }
}

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

variable "database_name" {
  type = string
}

resource "mysql_database" "app" {
  name                  = var.database_name
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

output "database_name" {
  value = mysql_database.app.name
}

変数は、あらかじめ用意しておきます。

terraform.tfvars

database_name = "my_database"

ユーザー用モジュール。

$ cd ../users

main.tf

terraform {
  required_version = "0.14.7"

  required_providers {

    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }
}

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

variable "administrator_username" {
  type = string
}

variable "administrator_password" {
  type = string
}

variable "administrator_allow_host" {
  type = string
}

variable "application_user_username" {
  type = string
}

variable "application_user_password" {
  type = string
}

variable "application_user_allow_host" {
  type = string
}

resource "mysql_user" "administrator_user" {
  user               = var.administrator_username
  plaintext_password = var.administrator_password
  host               = var.administrator_allow_host
}

resource "mysql_user" "application_user" {
  user               = var.application_user_username
  plaintext_password = var.application_user_password
  host               = var.application_user_allow_host
}

output "administrator_username" {
  value = mysql_user.administrator_user.user
}

output "administrator_password" {
  value     = mysql_user.administrator_user.plaintext_password
  sensitive = true
}

output "administrator_allow_host" {
  value = mysql_user.administrator_user.host
}

output "application_user_username" {
  value = mysql_user.application_user.user
}

output "application_user_password" {
  value     = mysql_user.application_user.plaintext_password
  sensitive = true
}

output "application_user_allow_host" {
  value = mysql_user.application_user.host
}

terraform.tfvars

administrator_username   = "admin"
administrator_password   = "admin_password"
administrator_allow_host = "%"

application_user_username   = "appuser"
application_user_password   = "appuser_password"
application_user_allow_host = "%"

権限用モジュール。

$ cd ../grants

main.tf

terraform {
  required_version = "0.14.7"

  required_providers {

    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }
}

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

variable "database_name" {
  type = string
}

variable "administrator_username" {
  type = string
}

variable "administrator_allow_host" {
  type = string
}

variable "application_user_username" {
  type = string
}

variable "application_user_allow_host" {
  type = string
}

resource "mysql_grant" "administrator_user" {
  user       = var.administrator_username
  host       = var.administrator_allow_host
  database   = var.database_name
  privileges = ["ALL"]
}

resource "mysql_grant" "application_user" {
  user       = var.application_user_username
  host       = var.application_user_allow_host
  database   = var.database_name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

output "administrator_username" {
  value = mysql_grant.administrator_user.user
}

output "administrator_allow_host" {
  value = mysql_grant.administrator_user.host
}

output "administrator_privileges" {
  value = mysql_grant.administrator_user.privileges
}

output "application_user_username" {
  value = mysql_grant.application_user.user
}

output "application_user_allow_host" {
  value = mysql_grant.application_user.host
}

output "application_user_privileges" {
  value = mysql_grant.application_user.privileges
}

terraform.tfvars

database_name = "my_database"

administrator_username   = "admin"
administrator_allow_host = "%"

application_user_username   = "appuser"
application_user_allow_host = "%"

いずれも、リソース定義と必要なVariableは用意してあるので、単純にinitしてapplyすればリソースが構築されます。

$ cd ../database
$ terraform init
$ terraform apply


$ cd ../users
$ terraform init
$ terraform apply


$ cd ../grants
$ terraform init
$ terraform apply

リソースができました。

mysql_database.app: Creating...
mysql_database.app: Creation complete after 0s [id=my_database]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

database_name = "my_database"


mysql_user.administrator_user: Creating...
mysql_user.application_user: Creating...
mysql_user.administrator_user: Creation complete after 0s [id=admin@%]
mysql_user.application_user: Creation complete after 0s [id=appuser@%]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"


mysql_grant.administrator_user: Creating...
mysql_grant.application_user: Creating...
mysql_grant.administrator_user: Creation complete after 0s [id=admin@%:`my_database`]
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`my_database`]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_privileges = toset([
  "ALL",
])
administrator_username = "admin"
application_user_allow_host = "%"
application_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])
application_user_username = "appuser"

動作確認したので、いったんリソースを破棄しましょう。

$ cd ../grants
$ terraform destroy


$ cd ../users
$ terraform destroy


$ cd ../database
$ terraform destroy

この時、applyの逆順で実行しているわけですが、データベースとユーザーは独立しているのですが、権限の方は先に他のモジュールが
適用されていることが実は前提になっていたりします。

それに、こう何回もapplyしたりdestroyしたりするのは面倒に思うかもしれません。このあたりをなんとかしようとするのが
Terragruntの機能のひとつです。

Terragruntを使って一括実行する

では、Terragruntを使っていきましょう。

先ほどのTerraformのみで実行した構成をまるっとコピーして、Terragrunt用のファイルを加えた以下の構成を作りました。

$ tree
.
├── database
│   ├── main.tf
│   └── terragrunt.hcl
├── grants
│   ├── main.tf
│   └── terragrunt.hcl
├── terragrunt.hcl
└── users
    ├── main.tf
    └── terragrunt.hcl

3 directories, 7 files

トップレベルのディレクトリにあるterragrunt.hclでは、Provider定義をまとめておきました。

terragrunt.hcl

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}
EOF
}

これで、各モジュールの定義からはProviderの定義を削除できます。代表で、データベース用のモジュールだけ載せておきましょう。

database/main.tf

terraform {
  required_version = "0.14.7"

  required_providers {
    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }
}

# provider "mysql" {
#   endpoint = "172.17.0.2:3306"
#   username = "root"
#   password = "password"
# }

variable "database_name" {
  type = string
}

resource "mysql_database" "app" {
  name                  = var.database_name
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

output "database_name" {
  value = mysql_database.app.name
}

ディレクトリ内のterragrunt.hclには、上位ディレクトリにあるterragrunt.hclファイルへの参照と、Variablesをまとめました。
なので、いずれのディレクトリからもterraform.tfvarsファイルは削除しています。

database/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

inputs = {
  database_name = "my_database"
}

ユーザー用モジュール。

users/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

inputs = {
  administrator_username   = "admin"
  administrator_password   = "admin_password"
  administrator_allow_host = "%"

  application_user_username   = "appuser"
  application_user_password   = "appuser_password"
  application_user_allow_host = "%"
}

権限用モジュール。

grants/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

inputs = {
  database_name = "my_database"

  administrator_username   = "admin"
  administrator_allow_host = "%"

  application_user_username   = "appuser"
  application_user_allow_host = "%"
}

ここで、トップディレクトリにいる状態で

$ ll
合計 24
drwxrwxr-x 5 xxxxx xxxxx 4096  2月 27 15:12 ./
drwxrwxr-x 5 xxxxx xxxxx 4096  2月 27 15:05 ../
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 27 15:16 database/
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 27 15:16 grants/
-rw-rw-r-- 1 xxxxx xxxxx  209  2月 27 15:15 terragrunt.hcl
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 27 15:16 users/

run-allを使うことで、コマンドを一気に実行できます。たとえばplan

$ terragrunt run-all plan

こんな感じで、複数のモジュールに対するplanが一気に実行されます。

INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [])
  => Module /path/to/users (excluded: false, dependencies: []) 

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_grant.administrator_user will be created
  + resource "mysql_grant" "administrator_user" {
      + database   = "my_database"
      + grant      = false
      + host       = "%"
      + id         = (known after apply)
      + privileges = [
          + "ALL",
        ]
      + table      = "*"
      + tls_option = "NONE"
      + user       = "admin"
    }

  # mysql_grant.application_user will be created
  + resource "mysql_grant" "application_user" {
      + database   = "my_database"
      + grant      = false
      + host       = "%"
      + id         = (known after apply)
      + privileges = [
          + "DELETE",
          + "INSERT",
          + "SELECT",
          + "UPDATE",
        ]
      + table      = "*"
      + tls_option = "NONE"
      + user       = "appuser"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + administrator_allow_host    = "%"
  + administrator_privileges    = [
      + "ALL",
    ]
  + administrator_username      = "admin"
  + application_user_allow_host = "%"
  + application_user_privileges = [
      + "DELETE",
      + "INSERT",
      + "SELECT",
      + "UPDATE",
    ]
  + application_user_username   = "appuser"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.


An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  + resource "mysql_database" "app" {
      + default_character_set = "utf8mb4"
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "my_database"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + database_name = "my_database"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.


An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_user.administrator_user will be created
  + resource "mysql_user" "administrator_user" {
      + host               = "%"
      + id                 = (known after apply)
      + plaintext_password = (sensitive value)
      + tls_option         = "NONE"
      + user               = "admin"
    }

  # mysql_user.application_user will be created
  + resource "mysql_user" "application_user" {
      + host               = "%"
      + id                 = (known after apply)
      + plaintext_password = (sensitive value)
      + tls_option         = "NONE"
      + user               = "appuser"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + administrator_allow_host    = "%"
  + administrator_password      = (sensitive value)
  + administrator_username      = "admin"
  + application_user_allow_host = "%"
  + application_user_password   = (sensitive value)
  + application_user_username   = "appuser"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

INFO[0003] 
Initializing the backend...

Initializing provider plugins...
- Finding terraform-providers/mysql versions matching "1.9.0"...
- Installing terraform-providers/mysql v1.9.0...
- Installed terraform-providers/mysql v1.9.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary. 
INFO[0003] 
Initializing the backend...

Initializing provider plugins...
- Finding terraform-providers/mysql versions matching "1.9.0"...
- Installing terraform-providers/mysql v1.9.0...
- Installed terraform-providers/mysql v1.9.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary. 
INFO[0003] 
Initializing the backend...

Initializing provider plugins...
- Finding terraform-providers/mysql versions matching "1.9.0"...
- Installing terraform-providers/mysql v1.9.0...
- Installed terraform-providers/mysql v1.9.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary. 

一緒にinitも実行されています。

結果、provider.tfも自動生成されました。

$ tree
.
├── database
│   ├── main.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── grants
│   ├── main.tf
│   ├── provider.tf
│   └── terragrunt.hcl
├── terragrunt.hcl
└── users
    ├── main.tf
    ├── provider.tf
    └── terragrunt.hcl

3 directories, 10 files

database/provider.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}

run-all applyしてみましょう。

$ terragrunt run-all apply

うまくリソースが構築できたようです。

INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [])
  => Module /path/to/users (excluded: false, dependencies: []) 
Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y
mysql_user.administrator_user: Creating...
mysql_user.application_user: Creating...
mysql_grant.administrator_user: Creating...
mysql_grant.application_user: Creating...
mysql_user.application_user: Creation complete after 0s [id=appuser@%]
mysql_database.app: Creating...
mysql_user.administrator_user: Creation complete after 0s [id=admin@%]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"
mysql_database.app: Creation complete after 0s [id=my_database]
mysql_grant.administrator_user: Creation complete after 0s [id=admin@%:`my_database`]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

database_name = "my_database"
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`my_database`]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_privileges = toset([
  "ALL",
])
administrator_username = "admin"
application_user_allow_host = "%"
application_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])
application_user_username = "appuser"

今度は、run-all destroyで一気に破棄。

$ terragrunt run-all destroy

すると、エラーになりました。権限モジュールがうまくdestroyできないようです。

INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [])
  => Module /path/to/users (excluded: false, dependencies: []) 
WARNING: Are you sure you want to run `terragrunt destroy` in each folder of the stack described above? There is no undo! (y/n) y
mysql_database.app: Destroying... [id=my_database]
mysql_user.application_user: Destroying... [id=appuser@%]
mysql_user.administrator_user: Destroying... [id=admin@%]
mysql_grant.administrator_user: Destroying... [id=admin@%:`my_database`]
mysql_grant.application_user: Destroying... [id=appuser@%:`my_database`]
mysql_database.app: Destruction complete after 0s
mysql_user.application_user: Destruction complete after 0s
mysql_user.administrator_user: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

Error: error revoking GRANT (REVOKE GRANT OPTION ON `my_database`.* FROM 'appuser'@'%'): Error 1141: There is no such grant defined for user 'appuser' on host '%'



Error: error revoking GRANT (REVOKE GRANT OPTION ON `my_database`.* FROM 'admin'@'%'): Error 1141: There is no such grant defined for user 'admin' on host '%'


ERRO[0002] Module /path/to/grants has finished with an error: Hit multiple errors:
Hit multiple errors:
exit status 1  prefix=[/path/to/grants] 

Destroy complete! Resources: 2 destroyed.
ERRO[0002] Encountered the following errors:
Hit multiple errors:
Hit multiple errors:
exit status 1 

これは、破棄するリソースの順番に依存関係があるからですね。現時点だと、Terragruntはその依存関係を認識していないのです。

仕方ないので、1度applyしなおして

$ terragrunt run-all apply

各モジュールを順を追って個別にdestroyします。

$ cd grants
$ terragrunt destroy


$ cd ../users
$ terragrunt destroy


$ cd ../database
$ terragrunt destroy

トップディレクトリへ戻ります。

$ cd ..

モジュール間に依存関係を作る+Outputを引き継ぐ

先ほどdestroyが失敗したのは、Terragruntがモジュール間の依存関係を認識しておらず、他のモジュールが必要とするリソースを
先に破棄してしまったことが原因でした。

よって、Terragruntに依存関係を教える必要があります。

Execute Terraform commands on multiple modules at once / Dependencies between modules

現在のモジュール間の依存関係はないので、graph-dependenciesで見るとそれぞれが独立していることがわかります。

$ terragrunt graph-dependencies
digraph {
    "database" ;
    "grants" ;
    "users" ;
}

図にした場合。

$ terragrunt graph-dependencies | dot -Tsvg > graph.svg

f:id:Kazuhira:20210227153632p:plain

ドキュメントを参考に、権限用モジュールに対して、データベースおよびユーザー用のモジュールへの依存関係を単純に
定義してみましょう。dependenciesを使い、pathsで依存するモジュールを指定します。

grants/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../database", "../users"]
}

inputs = {
  database_name = "my_database"

  administrator_username   = "admin"
  administrator_allow_host = "%"

  application_user_username   = "appuser"
  application_user_allow_host = "%"
}

dependenciesでは、依存するモジュールを複数定義できます。

Configuration Blocks and Attributes / dependencies

依存グラフは、このように変化しました。

$ terragrunt graph-dependencies
digraph {
    "database" ;
    "grants" ;
    "grants" -> "database";
    "grants" -> "users";
    "users" ;
}

ところで、権限用モジュールのVariableはデータベースおよびユーザー用のモジュールのOutputの値をそのまま使うことができます。

これをうまく利用する方法も、Terragrantは備えています。

Execute Terraform commands on multiple modules at once / Passing outputs between modules

dependenciesdependencyに変更し、依存するモジュールに対して名前を与えます。モジュールへのパスはconfig_path
指定するように変更します。

Configuration Blocks and Attributes / dependency

grants/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

dependency "database" {
  config_path = "../database"
}

dependency "users" {
  config_path = "../users"
}

inputs = {
  database_name = dependency.database.outputs.database_name

  administrator_username   = dependency.users.outputs.administrator_username
  administrator_allow_host = dependency.users.outputs.administrator_allow_host

  application_user_username   = dependency.users.outputs.application_user_username
  application_user_allow_host = dependency.users.outputs.application_user_allow_host
}

dependenciesと異なり、dependencyでは他のモジュールのOutputを利用することができます。

他のモジュールのOutputを利用するには、dependency.[dependencyの名前].outputs.[Output名]で指定できます。

  database_name = dependency.database.outputs.database_name

依存グラフを、もう1度見てみましょう。

$ terragrunt graph-dependencies
digraph {
    "database" ;
    "grants" ;
    "grants" -> "database";
    "grants" -> "users";
    "users" ;
}

図にもしてみます。こちらにも、依存関係が表現されていますね。Terragruntは、この依存関係の下から適用していくことになります。

$ terragrunt graph-dependencies | dot -Tsvg > graph.svg

f:id:Kazuhira:20210227154612p:plain

再度、run-all planを実行。

$ terragrunt run-all plan

こちらにも、依存関係が反映されています。

INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [/path/to/database, /path/to/users])
  => Module /path/to/users (excluded: false, dependencies: []) 

その他の内容。

An execution plan has been generated and is shown below.
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # mysql_database.app will be created
  # mysql_user.administrator_user will be created
  + resource "mysql_database" "app" {
  + resource "mysql_user" "administrator_user" {
      + host               = "%"
      + default_character_set = "utf8mb4"
      + id                 = (known after apply)
      + default_collation     = "utf8mb4_ja_0900_as_cs_ks"
      + id                    = (known after apply)
      + name                  = "my_database"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + database_name = "my_database"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

      + plaintext_password = (sensitive value)
      + tls_option         = "NONE"
      + user               = "admin"
    }

  # mysql_user.application_user will be created
  + resource "mysql_user" "application_user" {
      + host               = "%"
      + id                 = (known after apply)
      + plaintext_password = (sensitive value)
      + tls_option         = "NONE"
      + user               = "appuser"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + administrator_allow_host    = "%"
  + administrator_password      = (sensitive value)
  + administrator_username      = "admin"
  + application_user_allow_host = "%"
  + application_user_password   = (sensitive value)
  + application_user_username   = "appuser"

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

WARN[0000] Could not parse remote_state block from target config /path/to/database/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
WARN[0000] Could not parse remote_state block from target config /path/to/users/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
ERRO[0000] Module /path/to/grants has finished with an error: /path/to/users/terragrunt.hcl is a dependency of /path/to/grants/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs. If this is expected, set the skip_outputs flag to true on the dependency block.  prefix=[/path/to/grants] 
INFO[0000]                                              
INFO[0000]                                              
INFO[0000]                                              
ERRO[0000] Encountered the following errors:
/path/to/users/terragrunt.hcl is a dependency of /path/to/grants/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs. If this is expected, set the skip_outputs flag to true on the dependency block. 
ERRO[0000] Unable to determine underlying exit code, so Terragrunt will exit with error code 1 

ちょっと警告が出ていますね…。

WARN[0000] Could not parse remote_state block from target config /path/to/database/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
WARN[0000] Could not parse remote_state block from target config /path/to/users/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 

あと、エラーもあります。こちらは、planの時点では他のモジュールのOutputが決まっていない場合があるからです。
まだapplyもしていませんからね。

ERRO[0000] Module /path/to/grants has finished with an error: /path/to/users/terragrunt.hcl is a dependency of /path/to/grants/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs. If this is expected, set the skip_outputs flag to true on the dependency block.  prefix=[/path/to/grants] 

Terragruntは、この事象を回避するために、他のモジュールのOutputが利用できない時にモックを使う機能もあります。

Execute Terraform commands on multiple modules at once / Passing outputs between modules / Unapplied dependency and mock outputs

今回はこちらは気にせず、そのままrun-all applyしてみましょう。

$ terragrunt run-all apply

こちらは成功します。outputもまとめて見てみましょう。

$ terragrunt run-all output
INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [/path/to/database, /path/to/users])
  => Module /path/to/users (excluded: false, dependencies: []) 
administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"
database_name = "my_database"
WARN[0000] Could not parse remote_state block from target config /path/to/users/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
WARN[0000] Could not parse remote_state block from target config /path/to/database/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
administrator_allow_host = "%"
administrator_privileges = toset([
  "ALL",
])
administrator_username = "admin"
application_user_allow_host = "%"
application_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])
application_user_username = "appuser"

こちらもplanの時などと同じく警告が出ています。
よくよく見てみると、「Remote Stateが使えないからOutputにフォールバックしたよ」と言っていますね。

WARN[0000] Could not parse remote_state block from target config /path/to/users/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 
WARN[0000] Could not parse remote_state block from target config /path/to/database/terragrunt.hcl  prefix=[/path/to/grants] 
WARN[0000] Falling back to terragrunt output.            prefix=[/path/to/grants] 

これは、Terragruntの依存関係の定義を使ってOutputを利用しようとすると、利用できるならRemote Stateを使い、そうでない場合は
Outputを利用する、ということを言っているようです。

とりあえずrun-all applyはできたので、run-all destroyします。依存関係を定義しなかった時とは違って、今度はうまくいきます。

$ terragrunt run-all destroy

これで、やりたいことは達成できました。

Remote Stateを導入する

最後に、警告されたままなのもなんなので、Remote Stateを導入してみましょう。

トップレベルのterragrunt.hclファイルに、ConsulをBackendとする定義を追加します。

terragrunt.hcl

remote_state {
  backend = "consul"

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config = {
    address = "172.17.0.3:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/${path_relative_to_include()}"
    lock    = true
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"
}
EOF
}

今回はRemote Stateの保存先として、Consul 1.9.3を選びました。Consulが動作するサーバーは、172.17.0.3です。

Backend Type: consul - Terraform by HashiCorp

Backendが変わったので、init -reconfigureが必要になります。こちらも、run-allで一気に実行できます。

$ terragrunt run-all init -reconfigure

run-all apply

$ terragrunt run-all apply

(先ほどの例ではapplyの方は載せていませんが…)今度は、警告が出なくなります。

INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [/path/to/database, /path/to/users])
  => Module /path/to/users (excluded: false, dependencies: []) 
Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y
mysql_database.app: Refreshing state... [id=my_database]
mysql_user.application_user: Refreshing state... [id=appuser@%]
mysql_user.administrator_user: Refreshing state... [id=admin@%]

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

database_name = "my_database"

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"
mysql_grant.administrator_user: Creating...
mysql_grant.application_user: Creating...
mysql_grant.administrator_user: Creation complete after 0s [id=admin@%:`my_database`]
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`my_database`]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

administrator_allow_host = "%"
administrator_privileges = toset([
  "ALL",
])
administrator_username = "admin"
application_user_allow_host = "%"
application_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])
application_user_username = "appuser"

outputも見てみましょう。

$ terragrunt run-all output
INFO[0000] Stack at /path/to:
  => Module /path/to/database (excluded: false, dependencies: [])
  => Module /path/to/grants (excluded: false, dependencies: [/path/to/database, /path/to/users])
  => Module /path/to/users (excluded: false, dependencies: []) 
administrator_allow_host = "%"
administrator_password = <sensitive>
administrator_username = "admin"
application_user_allow_host = "%"
application_user_password = <sensitive>
application_user_username = "appuser"
database_name = "my_database"
administrator_allow_host = "%"
administrator_privileges = toset([
  "ALL",
])
administrator_username = "admin"
application_user_allow_host = "%"
application_user_privileges = toset([
  "DELETE",
  "INSERT",
  "SELECT",
  "UPDATE",
])
application_user_username = "appuser"

警告が出なくなったので、スッキリしましたね。

後片付け。

$ terragrunt run-all destroy

まとめ

Terragruntを使って、複数のモジュールに対して一括でapply等の操作が行えることを試してみました。また、依存するモジュールの
Outputを使って、別のモジュールのVariableに利用できるのも良いですね。

ここまでで、Terragruntの基本的な使い方はわかったのかな、という気がします。