CLOVER🍀

That was when it all began.

現在保持しているVagrantのBoxを一括でアップデートしたい

vagrant box updateに--boxオプションを指定することで、カレントディレクトリにVagrantfileがない状態でもBoxを
アップデートできることを知りませんでした。

これとvagrant listをうまく使えば、現在保持しているBoxを一括でアップデートできそうですね。

こんな感じでしょうか。

$ vagrant box list | perl -wanl -e 'print $F[0]' | xargs -I {} vagrant box update --box {}

アップデート後、不要になったBoxのpruneまで合わせて行う場合。

$ vagrant box list | perl -wanl -e 'print $F[0]' | xargs -I {} vagrant box update --box {} && vagrant box prune

今回使用したVagrantのバージョンは、こちらです。

$ vagrant version
Installed Version: 2.2.14
Latest Version: 2.2.14
 
You're running an up-to-date version of Vagrant!

TerragruntをUbuntu Linux 20.04 LTSにインストールして、Terraform Backendの定義をまとめてみる

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

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
    • Terraformの実行前後にフックを実行できる

さらに細かく機能を持っているようですが、詳しくこちらへ。

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が保存されていました。

f:id:Kazuhira:20210214214147p:plain

この時、カレントディレクトリを見ると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に保存されたところも確認しておきましょう。

f:id:Kazuhira:20210214215820p:plain

と、こんな感じで確認できましたと。

まとめ

Terragruntを使って、Terraform Backendに関する設定をまとめてみるということをやってみました。

TerragruntとしてはDRYと言いつつ、実際にはTerraform Backendの設定を自動生成する仕組みで、あくまでTerraformの
やり方や制限事項の上に乗ったものなんだなぁと。

これくらいの仕組みだと、Terraform Backendの設定さえ生成してしまえばTerragruntなしでも使えますし、いざとなれば
Terragruntを切り離すことも簡単にできそうですね。

覚えておきましょう。