これは、なにをしたくて書いたもの?
Terraformを使うの時にTerragrantというラッパーを使うと便利、みたいな話があるようなので少し試してみようかなと。
Terragrunt | Terraform wrapper
Terragrunt
TerragruntとはGruntworkが作成しているTerraformのラッパーで、Terraformの設定やモジュール、Remote Stateの管理をDRYに
するためのツールです。
Terragrunt is a thin wrapper that provides extra tools for keeping your configurations DRY, working with multiple Terraform modules, and managing remote state.
トップページに書いている主な機能は、ざっくりこんな感じのようです。
- Keep your Terraform code DRY
- 複数環境を管理するTerraformコードをDRYに保つ
- Keep your backend configuration DRY
- ルートディレクトリにTerraformのStateに関する定義を1度書けば、子モジュールはその定義を引き継いでTerraform Backendに関する定義の重複コードを削除できる
- Keep your Terraform CLI arguments DRY
- Terraformに繰り返して指定する引数を設定できる
- Execute Terraform commands on multiple modules at once
- 各モジュールに対して個別に
terraform
コマンド(plan
、apply
、destroy
など)を実行するのではなく、すべてのモジュールに対してまとめてコマンドを実行できる
- Before & After hooks
さらに細かく機能を持っているようですが、詳しくこちらへ。
Features
TerragruntがサポートするTerraformのバージョンは、こちらに書かれています。
Terraform Version Compatibility Table
CIでテストしているものを記載しているので、実際にはドキュメント内に書かれている表に入らない範囲でも動くことは
あるようです。
また、Terragruntを使うとTerraformのコマンドもTerragrunt越しに使うことになります。
CLI options / All Terraform built-in commands
説明はこんな感じにして、今回はTerraform Backendの構成をDRYにしてみましょう。
Terragruntをインストールする
まずは、Terragruntをインストールします。
Install
Terragruntを使うまでのステップは、以下のようです。
- Terraformをインストールする
- Terragruntをインストールする
terragrunt.hcl
を作成する
今回、Terraformはこちらを使います。
$ terraform version
Terraform v0.14.6
また、インストール先の環境はUbuntu Linux 20.04 LTSです。
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
$ uname -srvmpio
Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
この場合、GitHubのReleasesページからバイナリをダウンロードします。
Releases · gruntwork-io/terragrunt · GitHub
今回は、/usr/local/bin
に置きました。実行権限も付与しておきます。
$ sudo curl -Ls https://github.com/gruntwork-io/terragrunt/releases/download/v0.28.4/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt
$ sudo chmod a+x /usr/local/bin/terragrunt
インストールされたTerragruntのバージョンは、0.28.4です。
$ terragrunt -v
terragrunt version v0.28.4
では、使っていってみましょう。
Terraform BackendをDRYに設定するとは?
Terraform Backendの設定をDRYにするとは、どういうことでしょうか?こちらのページの内容を、少し書いてみます。
Quick start / Keep your backend configuration DRY
Terraformをチームなどで使う時には、TerraformのStateをRemoteのS3などに保存します。
ですが、この設定はTerraformの各ルートモジュールに散らばることになります。
Terraform Backendの設定には、変数や式を使えないからです。
そして、各ルートモジュールに定義されるTerraform Backendの設定は、ほぼ似たような構造で一部だけ異なるようなものが
コピーされることになります。大半は、Stateを保存するパスに相当するものでしょう。
Terragruntは、共通の設定ファイルからBackendに関する構成ファイルを自動生成するアプローチで、この問題を解決しようとします。
なお、より詳細な内容はこちらのページにあるようです。
Keep your remote state configuration DRY
お題
今回はTerraform BackendとしてConsulを、Terraform ProviderとしてMySQL Providerを使うことにします。
Backend Type: consul - Terraform by HashiCorp
Provider: MySQL - Terraform by HashiCorp
利用するConsulとMySQLについてです。
Consulはバージョン1.9.3が172.17.0.2で動作しているものとし、MySQLはバージョン8.0.23が172.17.0.3で動作しているものとします。
そして、以下の内容に関連するリソースを定義するルートモジュールをそれぞれ作成し、StateはConsulに保存するような構成に
していきましょう。
これを、Terraformのみ、Terragruntを使った場合の2種類の設定、実行方法を見ていきます。
ベースの構成を作る
まずは、Terragruntを使わずにTerraformだけで構成していきましょう。
ルートモジュール用のディレクトリを作成。
$ mkdir database users
最初はデータベース側を作りましょう。
$ cd database
main.tf
は、こんな感じで構成。Terraform BackendoはConsulです。
main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
backend "consul" {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/database"
lock = true
}
}
provider "mysql" {
endpoint = "172.17.0.3:3306"
username = "root"
password = "password"
}
resource "mysql_database" "app" {
name = "my_database"
default_character_set = "utf8mb4"
default_collation = "utf8mb4_ja_0900_as_cs_ks"
}
output "database_name" {
value = mysql_database.app.name
}
init
してapply
。
$ 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"
次は、ユーザーおよび権限側。
$ cd ../users
なんとなく、2種類のユーザーと割り当てる権限を作成。
main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
backend "consul" {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/users"
lock = true
}
}
provider "mysql" {
endpoint = "172.17.0.3:3306"
username = "root"
password = "password"
}
locals {
database_name = "my_database"
}
resource "mysql_user" "admin_user" {
user = "admin"
plaintext_password = "password"
host = "%"
}
resource "mysql_grant" "admin_user" {
user = mysql_user.admin_user.user
host = mysql_user.admin_user.host
database = local.database_name
privileges = ["ALL"]
}
resource "mysql_user" "application_user" {
user = "appuser"
plaintext_password = "password"
host = "%"
}
resource "mysql_grant" "application_user" {
user = mysql_user.application_user.user
host = mysql_user.application_user.host
database = local.database_name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}
output "admin_user_name" {
value = mysql_user.admin_user.user
}
output "admin_user_privileges" {
value = mysql_grant.admin_user.privileges
}
output "app_user_name" {
value = mysql_user.application_user.user
}
output "app_user_privileges" {
value = mysql_grant.application_user.privileges
}
データベース名は先ほど作ったデータベースのOutputから引っ張ってきたいところですが、今回はハードコードしました。
このあたりもTerragruntを絡めることができるようなので、また後日確認しましょう。
init
してapply
。
$ terraform init
$ terraform apply
こちらも、リソースができることを確認。
mysql_user.application_user: Creating...
mysql_user.admin_user: Creating...
mysql_user.admin_user: Creation complete after 0s [id=admin@%]
mysql_grant.admin_user: Creating...
mysql_user.application_user: Creation complete after 0s [id=appuser@%]
mysql_grant.application_user: Creating...
mysql_grant.admin_user: Creation complete after 0s [id=admin@%:`my_database`]
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`my_database`]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
admin_user_name = "admin"
admin_user_privileges = toset([
"ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
"DELETE",
"INSERT",
"SELECT",
"UPDATE",
])
これで、ふつうにTerraformを使ってリソースの構築およびConsulへのStateへの保存ができました。
いったん、リソースは破棄しておきます。
$ terraform destroy
$ cd ../database
$ terraform destroy
上位ディレクトリにも戻っておきましょう。
$ cd ..
Terragruntを使って、Terraform Backendの定義をまとめる
続いて、この構成をTerragruntを使って変更していきましょう。
先ほどの、Terraformのルートモジュールの何が問題だったか?backend
の設定が、2つのファイルでほぼ同じであることです。
database/main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
backend "consul" {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/database"
lock = true
}
}
users/main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
backend "consul" {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/users"
lock = true
}
}
2つのファイルのbackend
に関する設定で、違うのはpath
の部分だけです。
backend
には変数や式を使うことができないため、このように一部だけ内容が異なるbackend
の定義をコピー&ペーストして
変更…という形を取ることになります。
これをなんとかしたい、というのがTerragruntが書いているRemote Stateの設定をDRYにする、ということですね。
では、構成を変えていきましょう。最初にQuick startの内容に従い、terragrunt.hcl
というファイルを作成します。
Quick start / Keep your backend configuration DRY
terragrunt.hcl
remote_state {
backend = "consul"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/${path_relative_to_include()}"
lock = true
}
}
path
の部分がポイントですね。
このファイルは、先ほどまで作っていたルートモジュールを置いたディレクトリと同じ階層に作成します。
$ ls -1
database
terragrunt.hcl
users
terragrunt.hcl
は、hclfmt
でフォーマットできます。
$ terragrunt hclfmt
ちなみに、terragrunt.hcl
のリファレンスはこちらで
Configuration Blocks and Attributes
今回使っているremote_state
に関する部分はここですね。
Configuration Blocks and Attributes / remote_state
このファイルを作成すると、Terragrunt経由でTerraformのサブコマンドが使用できるようになります(ファイル作成前に使うと、
エラーになります)。
$ terragrunt version
Terraform v0.14.6
terraform version
で、ローカルにインストールしているTerraformのバージョンが確認できます。
これ以降、Terraformのコマンドはterragrunt
を使って実行します。
terragrunt-info
で、Terragruntの情報を表示。
$ terragrunt terragrunt-info
{
"ConfigPath": "/path/to/terragrunt.hcl",
"DownloadDir": "/path/to/.terragrunt-cache",
"IamRole": "",
"TerraformBinary": "terraform",
"TerraformCommand": "terragrunt-info",
"WorkingDir": "/path/to"
}
ちなみに、terragrunt version
を実行した時に、こんなファイルができていました…。
backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "consul" {
address = "172.17.0.2:8500"
lock = true
path = "terraform/state/mysql/."
scheme = "http"
}
}
では、データベース側に移ります。
$ cd database
このディレクトリ内でも、terragrunt.hcl
を作成します。ただ、内容は親ディレクトリの内容を参照するものになります。
terragrunt.hcl
include {
path = find_in_parent_folders()
}
main.tf
からは、Terraform Backendに関する内容を削除します。
main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
# backend "consul" {
# address = "172.17.0.2:8500"
# scheme = "http"
# path = "terraform/state/mysql/database"
# lock = true
# }
}
provider "mysql" {
endpoint = "172.17.0.3:3306"
username = "root"
password = "password"
}
resource "mysql_database" "app" {
name = "my_database"
default_character_set = "utf8mb4"
default_collation = "utf8mb4_ja_0900_as_cs_ks"
}
output "database_name" {
value = mysql_database.app.name
}
plan
を実行しようとすると、init
済みでないと言われたので…
$ terragrunt plan
Error: Could not load plugin
Plugin reinitialization required. Please run "terraform init".
Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.
Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints, run "terraform providers".
Failed to instantiate provider
"registry.terraform.io/terraform-providers/mysql" to obtain schema: unknown
provider "registry.terraform.io/terraform-providers/mysql"
ERRO[0000] Hit multiple errors:
Hit multiple errors:
exit status 1
terragrunt
越しにinit
しておきます…。
$ terragrunt init
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.
あとで.terraform
とかごっそり削除するとこういう怒られ方はしなくなったので、先にTerraformで実行していたのが良くなかったのかも
しれませんが…。
apply
。
$ terragrunt apply
うまくいきました。
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"
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
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"
Consul側も確認してみると、Stateが保存されていました。
この時、カレントディレクトリを見るとbackend.tf
というファイルが生成されていたりします。
backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "consul" {
address = "172.17.0.2:8500"
lock = true
path = "terraform/state/mysql/database"
scheme = "http"
}
}
Terraform Backendの定義ですね。path
も埋まっています。
terragrunt-info
が認識しているのは、あくまで現在のディレクトリ内にあるterragrunt.hcl
です。
$ terragrunt terragrunt-info
{
"ConfigPath": "/path/to/database/terragrunt.hcl",
"DownloadDir": "/path/to/database/.terragrunt-cache",
"IamRole": "",
"TerraformBinary": "terraform",
"TerraformCommand": "terragrunt-info",
"WorkingDir": "/path/to/database"
}
さて、もう1度terragrunt.hcl
を見てみましょう。
terragrunt.hcl
include {
path = find_in_parent_folders()
}
find_in_parent_folders
というのは、カレントディレクトリよりも上位ディレクトリにあるterragrunt.hcl
ファイルを探す関数です。
find_in_parent_folders
親ディレクトリにあるterragrunt.hcl
も、もう1度見てみます。
../terragrunt.hcl
remote_state {
backend = "consul"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/${path_relative_to_include()}"
lock = true
}
}
よく見るとbackend.tf
を生成するという設定になっていて
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config
のpath
の部分は、path_relative_to_include
という関数を使っています。
config = {
address = "172.17.0.2:8500"
scheme = "http"
path = "terraform/state/mysql/${path_relative_to_include()}"
lock = true
}
path_relative_to_include
関数は、ルート(より上位にある)terragrunt.hcl
ファイルと、現在のディレクトリのterragrunt.hcl
の間の
相対パスを返します。
path_relative_to_include
で、今回の場合database
というパスが入り、生成されたのがこちらのbackend.tf
だ、ということですね。
backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "consul" {
address = "172.17.0.2:8500"
lock = true
path = "terraform/state/mysql/database"
scheme = "http"
}
}
なるほどですね。
次は、ユーザーおよび権限側。
$ cd ../users
terragrunt.hcl
ファイルの内容はデータベース側と同じになるので、コピーしましょうか。
$ cp ../database/terragrunt.hcl ./.
データベースの時と同様に、main.tf
からbackend
の設定を削除します。
main.tf
terraform {
required_version = "0.14.6"
required_providers {
mysql = {
source = "terraform-providers/mysql"
version = "1.9.0"
}
}
# backend "consul" {
# address = "172.17.0.2:8500"
# scheme = "http"
# path = "terraform/state/mysql/users"
# lock = true
# }
}
provider "mysql" {
endpoint = "172.17.0.3:3306"
username = "root"
password = "password"
}
locals {
database_name = "my_database"
}
resource "mysql_user" "admin_user" {
user = "admin"
plaintext_password = "password"
host = "%"
}
resource "mysql_grant" "admin_user" {
user = mysql_user.admin_user.user
host = mysql_user.admin_user.host
database = local.database_name
privileges = ["ALL"]
}
resource "mysql_user" "application_user" {
user = "appuser"
plaintext_password = "password"
host = "%"
}
resource "mysql_grant" "application_user" {
user = mysql_user.application_user.user
host = mysql_user.application_user.host
database = local.database_name
privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}
output "admin_user_name" {
value = mysql_user.admin_user.user
}
output "admin_user_privileges" {
value = mysql_grant.admin_user.privileges
}
output "app_user_name" {
value = mysql_user.application_user.user
}
output "app_user_privileges" {
value = mysql_grant.application_user.privileges
}
init
。
$ terragrunt init
生成されたbackend.tf
は、こちら。このモジュールだと、path
にusers
が入っています。
backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "consul" {
address = "172.17.0.2:8500"
lock = true
path = "terraform/state/mysql/users"
scheme = "http"
}
}
apply
。
$ terragrunt apply
リソースがうまく作成できました。
mysql_user.application_user: Creating...
mysql_user.admin_user: Creating...
mysql_user.admin_user: Creation complete after 0s [id=admin@%]
mysql_grant.admin_user: Creating...
mysql_user.application_user: Creation complete after 0s [id=appuser@%]
mysql_grant.application_user: Creating...
mysql_grant.admin_user: Creation complete after 0s [id=admin@%:`my_database`]
mysql_grant.application_user: Creation complete after 0s [id=appuser@%:`my_database`]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
admin_user_name = "admin"
admin_user_privileges = toset([
"ALL",
])
app_user_name = "appuser"
app_user_privileges = toset([
"DELETE",
"INSERT",
"SELECT",
"UPDATE",
])
TerraformのStateが、Consulに保存されたところも確認しておきましょう。
と、こんな感じで確認できましたと。
まとめ
Terragruntを使って、Terraform Backendに関する設定をまとめてみるということをやってみました。
TerragruntとしてはDRYと言いつつ、実際にはTerraform Backendの設定を自動生成する仕組みで、あくまでTerraformの
やり方や制限事項の上に乗ったものなんだなぁと。
これくらいの仕組みだと、Terraform Backendの設定さえ生成してしまえばTerragruntなしでも使えますし、いざとなれば
Terragruntを切り離すことも簡単にできそうですね。
覚えておきましょう。