CLOVER🍀

That was when it all began.

TerraformでRemote Stateを参照して、Data Sourceとして扱う

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

TerraformのRemote Stateを参照することで、Data Sourceとして扱えるらしいので、試してみることにしました。

Remote StateをData Sourceとして使う

Remote Stateは、デフォルトではローカルに保存されるStateを、別のリモートデータストアに格納する方法です。

State: Remote Storage - Terraform by HashiCorp

Remote Stateを使うことで、チーム内でStateを共有するといったことや、ロックを使用した排他などが可能になります。

また、Remote StateをData Sourceとして扱うことで、StateのOutputを利用することができるようになります。

Terraform: terraform_remote_state - Terraform by HashiCorp

Root Outputs Only

他のモジュールの実行結果(=Output)を利用することで、別のモジュールで作成されたリソースの情報を参照して
リソース定義に利用することができるようになります。Stateを管理したい単位で分割できる一方で、Remote Stateという形で
モジュール間に依存関係を持ち込むことになるという点には注意する必要があります。

今回は、これを利用してみたいと思います。

お題

Remote StateのBackendとして、Consulを使用します。

Backend Type: consul - Terraform by HashiCorp

このBackendを使う、2つのTerraformルートモジュールを作成します。扱うProviderは、MySQLとしましょう。

  • データベースを作成するモジュール
  • ユーザーおよび権限の紐付けを行うモジュール

2つ目のモジュールではデータベース名が必要になるのですが、これを1つ目のモジュールでOutputとしてRemote Stateに
保存して、2つ目のモジュールから利用してみたいと思います。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.12.24

Consulのバージョンは1.7.2で172.17.0.2で動作しているものとし、MySQLのバージョンは8.0.20で172.17.0.3で動作している
ものとします。

というわけで、各ルートモジュール用のディレクトリを作成。

$ mkdir database users

ここから始めていきます。

データベースを作成する

では、最初はMySQLにデータベースを作成するTerraformモジュールを作成していきます。

$ cd database

リソース定義、Output Variablesなどありますが、今回はひとつのファイルにまとめてしまいました。
main.tf

terraform {
  required_version = ">= 0.12.24"

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/database"
  }
}

provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"

  version = "1.9.0"
}

resource "mysql_database" "app" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

本当に、データベースを作るだけです。

ポイントは、Remote StateのBackendをConsulにしていることと

terraform {
  required_version = ">= 0.12.24"

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/database"
  }
}

Output Variableを定義していること。

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

では、init等々行って

$ terraform init
$ terraform version
Terraform v0.12.24
+ provider.mysql v1.9.0
$ terraform fmt -check -recursive
$ terraform validate

apply。

$ terraform apply -auto-approve

データベースが作成されました。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| my_database        |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.02 sec)

この時、Stateが保存されたConsulの方を見てみます。

f:id:Kazuhira:20200503172117p:plain

{
  "version": 4,
  "terraform_version": "0.12.24",
  "serial": 1,
  "lineage": "3165f563-a48e-8a4c-5c96-c7cefccd8879",
  "outputs": {
    "app_mysql_database_name": {
      "value": "my_database",
      "type": "string"
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "mysql_database",
      "name": "app",
      "provider": "provider.mysql",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "default_character_set": "utf8mb4",
            "default_collation": "utf8mb4_ja_0900_as_cs_ks",
            "id": "my_database",
            "name": "my_database"
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

こんな感じで、Stateが保存されています。

データベースにユーザーを作成して、権限付与する

次に、MySQLデータベースにユーザーを作成して、権限を付けていきましょう。

もうひとつのモジュール用のディレクトリに移ります。

$ cd users

作成した定義ファイルは、こちら。
main.tf

terraform {
  required_version = ">= 0.12.24"

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/users"
  }
}

data "terraform_remote_state" "database" {
  backend = "consul"

  config = {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/database"
  }
}

provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"

  version = "1.9.0"
}

resource "mysql_user" "admin_user" {
  user               = "adminuser"
  plaintext_password = "password"
  host               = "%"
}

resource "mysql_grant" "admin_user" {
  user       = "adminuser"
  host       = "%"
  database   = data.terraform_remote_state.database.outputs.app_mysql_database_name
  privileges = ["ALL"]

  depends_on = [mysql_user.admin_user]
}

resource "mysql_user" "application_user" {
  user               = "appuser"
  plaintext_password = "password"
  host               = "%"
}

resource "mysql_grant" "application_user" {
  user       = "appuser"
  host       = "%"
  database   = data.terraform_remote_state.database.outputs.app_mysql_database_name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]

  depends_on = [mysql_user.application_user]
}

こちらも、Stateの保存先はConsulにしました。

terraform {
  required_version = ">= 0.12.24"

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/users"
  }
}

ポイントとなるのはこちらで、先ほどのデータベース作成を行うモジュールのStateを参照するようにData Sourceを定義します。

data "terraform_remote_state" "database" {
  backend = "consul"

  config = {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "terraform/state/mysql/database"
  }
}

backend、configは、取得対象のRemote Stateに合わせた定義を行います。取得先のStateもConsulで管理されているので、
そちらに合わせます。config内には、Backendで設定した時の引数とほぼ同じものが使えるようです。

Terraform: terraform_remote_state - Terraform by HashiCorp

Argument Reference

今回は、backendに"consul"を指定することになります。

Backend Type: consul - Terraform by HashiCorp

ユーザーや権限の定義。

resource "mysql_user" "admin_user" {
  user               = "adminuser"
  plaintext_password = "password"
  host               = "%"
}

resource "mysql_grant" "admin_user" {
  user       = "adminuser"
  host       = "%"
  database   = data.terraform_remote_state.database.outputs.app_mysql_database_name
  privileges = ["ALL"]

  depends_on = [mysql_user.admin_user]
}

resource "mysql_user" "application_user" {
  user               = "appuser"
  plaintext_password = "password"
  host               = "%"
}

resource "mysql_grant" "application_user" {
  user       = "appuser"
  host       = "%"
  database   = data.terraform_remote_state.database.outputs.app_mysql_database_name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]

  depends_on = [mysql_user.application_user]
}

ここで、権限を扱う時にデータベース名を指定します。

データベース名は、Remote Stateから取得します。「data.terraform_remote_state.「name」.「Output Variable」」という指定に
なりますね。

resource "mysql_grant" "admin_user" {
  user       = "adminuser"
  host       = "%"
  database   = data.terraform_remote_state.database.outputs.app_mysql_database_name
  privileges = ["ALL"]

  depends_on = [mysql_user.admin_user]
}

先ほど確認した、データベース作成を行うモジュールのOutputsを確認してみます。

  "outputs": {
    "app_mysql_database_name": {
      "value": "my_database",
      "type": "string"
    }
  },

ここから、値を取得するわけです。

init等々を行って

$ terraform init
$ terraform version
Terraform v0.12.24
+ provider.mysql v1.9.0
$ terraform fmt -recursive
$ terraform validate

planを見てみます。

$ terraform plan

この時点で、Remote Stateから取得した値を確認することができます。

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

  # 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"
    }

apply。

$ terraform apply -auto-approve

ユーザーが作成されました。

mysql> select user,host from mysql.user;
+------------------+-----------+
| user             | host      |
+------------------+-----------+
| adminuser        | %         |
| appuser          | %         |
| root             | %         |
| mysql.infoschema | localhost |
| mysql.session    | localhost |
| mysql.sys        | localhost |
| root             | localhost |
+------------------+-----------+
7 rows in set (0.00 sec)

Consulの方を見てみます。

f:id:Kazuhira:20200503172826p:plain

こちらのモジュールに対する、State(users)が保存されていますね。

{
  "version": 4,
  "terraform_version": "0.12.24",
  "serial": 0,
  "lineage": "873a958c-fb7d-2477-d70f-b7a3889e4510",
  "outputs": {},
  "resources": [
    {
      "mode": "data",
      "type": "terraform_remote_state",
      "name": "database",
      "provider": "provider.terraform",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "backend": "consul",
            "config": {
              "value": {
                "address": "172.17.0.2:8500",
                "path": "terraform/state/mysql/database",
                "scheme": "http"
              },
              "type": [
                "object",
                {
                  "address": "string",
                  "path": "string",
                  "scheme": "string"
                }
              ]
            },
            "defaults": null,
            "outputs": {
              "value": {
                "app_mysql_database_name": "my_database"
              },
              "type": [
                "object",
                {
                  "app_mysql_database_name": "string"
                }
              ]
            },
            "workspace": "default"
          }
        }
      ]
    },
    {
      "mode": "managed",
      "type": "mysql_grant",
      "name": "admin_user",
      "provider": "provider.mysql",
      "instances": [
        {

〜省略〜

}

こちらは、Outputsがありません。定義していないから、当然ですね。

  "outputs": {},

また、Data Sourceの定義を見ると、ここに他のモジュールのStateの値が取り込まれていることが確認できます。

    {
      "mode": "data",
      "type": "terraform_remote_state",
      "name": "database",
      "provider": "provider.terraform",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "backend": "consul",
            "config": {
              "value": {
                "address": "172.17.0.2:8500",
                "path": "terraform/state/mysql/database",
                "scheme": "http"
              },
              "type": [
                "object",
                {
                  "address": "string",
                  "path": "string",
                  "scheme": "string"
                }
              ]
            },
            "defaults": null,
            "outputs": {
              "value": {
                "app_mysql_database_name": "my_database"
              },
              "type": [
                "object",
                {
                  "app_mysql_database_name": "string"
                }
              ]
            },
            "workspace": "default"
          }
        }
      ]
    },

ここですね。

            "outputs": {
              "value": {
                "app_mysql_database_name": "my_database"
              },
              "type": [
                "object",
                {
                  "app_mysql_database_name": "string"
                }
              ]
            },

他から取得したData Sourceとはいえ、applyした時の値が保存されます、と。

ここで、Data Sourceの値がいつ更新されるかという話は、こちら。

Data Resource Behavior

planなどの時には、現在の最新の値が表示されます。

ということは、Remote StateをData Sourceとするモジュールがいる場合に、不用意にOutput Variablesを変更すると
Remote Stateに依存したモジュールに影響を与えることになるので要注意、ということになりそうな気がします。

Output Variablesでつなぎ合わせるので、なかなか見えにくい依存関係ですね…。