CLOVER🍀

That was when it all began.

Terraformリソース間の依存関係を確認する

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

Terraformのリソース間の依存関係について、ちゃんと学んでおきたいなと思いまして。

リソース間の依存関係について

Terraformのドキュメントで、リソースについて書かれているページを見てみます。

Resources - Configuration Language - Terraform by HashiCorp

Terraformは、「resource」の定義に使われている式を分析して、他のリソースオブジェクトへの参照を見つけるとリソースを
作成、更新、破棄する時にこれらの間に依存関係があるものとして順序付けて扱います。

Most resource dependencies are handled automatically. Terraform analyses any expressions within a resource block to find references to other objects, and treats those references as implicit ordering requirements when creating, updating, or destroying resources. Since most resources with behavioral dependencies on other resources also refer to those resources' data, it's usually not necessary to manually specify dependencies between resources.

ただ、Terraformが認識できない暗黙的な依存関係もあります。

However, some dependencies cannot be recognized implicitly in configuration. For example, if Terraform must manage access control policies and take actions that require those policies to be present, there is a hidden dependency between the access policy and a resource whose creation depends on it. In these rare cases, the depends_on meta-argument can explicitly specify a dependency.

このようなものについては、「depends_on」を使用することで依存関係を明示的に指定することができます。こうすると、リソースの
依存関係をTerraformに認識させることができます。

depends_on: Explicit Resource Dependencies

「依存関係を明示的に指定する必要がある(depends_onを使う)のは、引数で他のリソースのデータにアクセスしない場合のみ」
と言っているので、 通常はリソースの引数で関連付ける方がよいのでしょうか?

Explicitly specifying a dependency is only necessary when a resource relies on some other resource's behavior but doesn't access any of that resource's data in its arguments.

ドキュメントでは「last resort」と書いていますし、積極的に使うものではないようにも見えます。

The depends_on argument should be used only as a last resort. When using it, always include a comment explaining why it is being used, to help future maintainers understand the purpose of the additional dependency.

今回は、このあたりの動きを確認してみたいと思います。

お題

MySQL Providerを使って、簡単に確認してみます。

MySQL Providerが提供するリソースのうち、データベース、ユーザー、Grantを使います。これらは、ものとしては依存関係が
あるのですが、リソースの出力結果を使わないと他のリソースが作れない、なんてことはないので依存関係ができるかどうかは
定義次第です。

MySQL: mysql_database - Terraform by HashiCorp

MySQL: mysql_user - Terraform by HashiCorp

MySQL: mysql_grant - Terraform by HashiCorp

順番的には、データベースがあり、ユーザーがあり、ユーザーに権限を与える、ですね。データベースとユーザーは並行に作成
することはできますが、Grantは対象となるデータベースやユーザーの作成が終わっている必要があります。

このあたりを見ていってみましょう。

環境

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

$ terraform version
Terraform v0.12.26
+ provider.mysql v1.9.0

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

シンプルに定義する

まずは、Providerの定義などを用意します。
main.tf

terraform {
  required_version = "0.12.26"
}

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

  version = "1.9.0"
}

# ここに、リソース定義を書く!!
依存関係を作らない場合

なにも考えずにリソース定義をしてみましょう。

resource "mysql_database" "this" {
  name = "database"
}

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

resource "mysql_grant" "grant" {
  user       = "user"
  host       = "%"
  database   = "database"
  privileges = ["ALL"]
}

この状態で動くには動くのですが、タイミングによってはapplyやdestroyの時に失敗したりします。

$ terraform apply -auto-approve
mysql_database.this: Creating...
mysql_grant.grant: Creating...
mysql_user.user: Creating...
mysql_user.user: Creation complete after 0s [id=user@%]
mysql_database.this: Creation complete after 0s [id=database]

Error: Erro[f:id:Kazuhira:20200607170554p:plain]r running SQL (GRANT ALL ON `database`.* TO 'user'@'%'): Error 1410: You are not allowed to create a user with GRANT

  on main.tf line 23, in resource "mysql_grant" "grant":
  23: resource "mysql_grant" "grant" {



$ terraform destroy -force
mysql_database.this: Refreshing state... [id=database]
mysql_user.user: Refreshing state... [id=user@%]
mysql_grant.grant: Refreshing state... [id=user@%:`database`]
mysql_database.this: Destroying... [id=database]
mysql_grant.grant: Destroying... [id=user@%:`database`]
mysql_user.user: Destroying... [id=user@%]
mysql_user.user: Destruction complete after 0s
mysql_database.this: Destruction complete after 0s

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

依存関係のグラフを作成してみます。

$ terraform graph | dot -Tpng > graph.png

見事にバラバラですね。

f:id:Kazuhira:20200607170554p:plain

リソースの引数を使って、暗黙的な依存関係を作る

次に、Grantの方にデータベースおよびユーザーの方に依存関係を作るようにしてみましょう。

resource "mysql_database" "this" {
  name = "database"
}

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

resource "mysql_grant" "grant" {
  # user       = "user"
  user = mysql_user.user.user
  host       = "%"
  # database   = "database"
  database = mysql_database.this.name
  privileges = ["ALL"]
}

「mysql_grant」の引数に、mysql_databaseおよびmysql_userの定義を参照するようにしました。

この状態で、依存関係のグラフを作って見てみると、mysql_grantから他のリソースに依存ができるようになっています。

f:id:Kazuhira:20200607171054p:plain

こうすると、applyやdestroyの時にエラーにならなくなります。

depends_onを使う

最後、depends_onで依存関係を明示してみましょう。

resource "mysql_database" "this" {
  name = "database"
}

resource "mysql_user" "user" {
  user               = "user"
  host               = "%"
  plaintext_password = "password"

  depends_on = [mysql_database.this]
}

resource "mysql_grant" "grant" {
  user       = "user"
  host       = "%"
  database   = "database"
  privileges = ["ALL"]

  depends_on = [mysql_database.this, mysql_user.user]
}

mysql_grantで他のリソースの値を指定するのをやめ、depends_onでmysql_database、mysql_userを指すようにしました。
あと、あまり関係ないですが、mysql_userからmysql_databaseにも依存関係を定義してみます。

この状態でグラフを作ると、こんな感じになります。

f:id:Kazuhira:20200607170943p:plain

mysql_userがmysql_databaseに依存しているので、依存関係としてはこうなりますね。

こうやって、リソース間の依存関係を定義できることが確認できました、と。

もっと複雑なパターンで

次に、今の3つのリソースをループで作る例を考えてみます。

まず、Provider等の定義を。
main.tf

main.tf 
terraform {
  required_version = "0.12.26"
}

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

  version = "1.9.0"
}

# ローカル変数

# リソース定義

リソースをループで作るための情報は、今回はローカル変数で定義します。

locals {
  database_definitions = [
    {
      name = "foo_database"
      users = [
        {
          name               = "foo_user"
          host               = "%"
          plaintext_password = "password"
        },
        {
          name               = "foo_admin"
          host               = "%"
          plaintext_password = "password"
        }
      ]
      grant = [
        {
          user       = "foo_user"
          host       = "%"
          database   = "foo_database"
          privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
        },
        {
          user       = "foo_admin"
          host       = "%"
          database   = "foo_database"
          privileges = ["ALL"]
        }
      ]
    },
    {
      name = "bar_database"
      users = [
        {
          name               = "bar_user"
          host               = "%"
          plaintext_password = "password"
        },
        {
          name               = "bar_admin"
          host               = "%"
          plaintext_password = "password"
        }
      ]
      grant = [
        {
          user       = "bar_user"
          host       = "%"
          database   = "bar_database"
          privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
        },
        {
          user       = "bar_admin"
          host       = "%"
          database   = "bar_database"
          privileges = ["ALL"]
        }
      ]
    }
  ]
}
依存関係を作らない場合

まずは、先ほどと同様にそれぞれのリソースを独立して定義します。

resource "mysql_database" "this" {
  count = length(local.database_definitions)

  name = local.database_definitions[count.index].name
}

resource "mysql_user" "user" {
  count = length(flatten(local.database_definitions.*.users))

  user               = flatten(local.database_definitions.*.users)[count.index].name
  host               = flatten(local.database_definitions.*.users)[count.index].host
  plaintext_password = flatten(local.database_definitions.*.users)[count.index].plaintext_password
}

resource "mysql_grant" "grant" {
  count = length(flatten(local.database_definitions.*.grant))

  user       = flatten(local.database_definitions.*.grant)[count.index].user
  host       = flatten(local.database_definitions.*.grant)[count.index].host
  database   = flatten(local.database_definitions.*.grant)[count.index].database
  privileges = flatten(local.database_definitions.*.grant)[count.index].privileges
}

この定義だと、applyやdestroyの時に、高確率で失敗します。

$ terraform apply -auto-approve
mysql_user.user[0]: Creating...
mysql_user.user[3]: Creating...
mysql_grant.grant[3]: Creating...
mysql_user.user[1]: Creating...
mysql_user.user[2]: Creating...
mysql_grant.grant[1]: Creating...
mysql_grant.grant[0]: Creating...
mysql_database.this[0]: Creating...
mysql_grant.grant[2]: Creating...
mysql_database.this[1]: Creating...
mysql_user.user[0]: Creation complete after 0s [id=foo_user@%]
mysql_user.user[3]: Creation complete after 0s [id=bar_admin@%]
mysql_user.user[1]: Creation complete after 0s [id=foo_admin@%]
mysql_database.this[1]: Creation complete after 0s [id=bar_database]
mysql_grant.grant[3]: Creation complete after 0s [id=bar_admin@%:`bar_database`]
mysql_database.this[0]: Creation complete after 0s [id=foo_database]
mysql_user.user[2]: Creation complete after 0s [id=bar_user@%]
mysql_grant.grant[1]: Creation complete after 1s [id=foo_admin@%:`foo_database`]
mysql_grant.grant[0]: Creation complete after 1s [id=foo_user@%:`foo_database`]

Error: Error running SQL (GRANT UPDATE, SELECT, DELETE, INSERT ON `bar_database`.* TO 'bar_user'@'%'): Error 1410: You are not allowed to create a user with GRANT

  on main.tf line 90, in resource "mysql_grant" "grant":
  90: resource "mysql_grant" "grant" {

依存関係のグラフを作成してみます。

f:id:Kazuhira:20200607174219p:plain

当然のことながら、どのリソースにも依存関係はありません。

depends_onを使ってみる

こうなると、「先にデータベースやユーザーを作ってから、次のリソースを作成するには」と考えるところですが、これを
depends_onで表現するのはなかなか難しい気がします。

たとえば、Grantでこんな感じに全部のデータベースおよびユーザーを作成した後にGrantの定義を行いたいところですが

resource "mysql_grant" "grant" {
  count = length(flatten(local.database_definitions.*.grant))

  user       = flatten(local.database_definitions.*.grant)[count.index].user
  host       = flatten(local.database_definitions.*.grant)[count.index].host
  database   = flatten(local.database_definitions.*.grant)[count.index].database
  privileges = flatten(local.database_definitions.*.grant)[count.index].privileges

  depends_on = [mysql_database.this.*, mysql_user.user.*]
}

これはエラーになります。

  depends_on = [mysql_database.this.*, mysql_user.user.*]

A single static variable reference is required: only attribute access and
indexing with constant keys. No calculations, function calls, template
expressions, etc are allowed here.

depends_onの説明を見てもわかるのですが、depends_onには式などを使うことができないのです。

depends_on: Explicit Resource Dependencies

Arbitrary expressions are not allowed in the depends_on argument value, because its value must be known before Terraform knows resource relationships and thus before it can safely evaluate expressions.

この状態では、依存関係のグラフを作成することもできません。

リソース間で依存関係を作る

さて、どうしましょうか。リソースに別のリソースへの参照を追加したいのですが、なかなか扱いが難しいように思います。

インデックスやキーを指定して、別のリソースへの参照を作るのが難しそうですからねぇ。

ちょっと微妙ですが、今回はcount内に強引に引き込むことにしました。

resource "mysql_database" "this" {
  count = length(local.database_definitions)

  name = local.database_definitions[count.index].name
}

resource "mysql_user" "user" {
  count = length(mysql_database.this) > 0 ? length(flatten(local.database_definitions.*.users)) : 0

  user               = flatten(local.database_definitions.*.users)[count.index].name
  host               = flatten(local.database_definitions.*.users)[count.index].host
  plaintext_password = flatten(local.database_definitions.*.users)[count.index].plaintext_password
}

resource "mysql_grant" "grant" {
  count = length(mysql_user.user) > 0 ? length(flatten(local.database_definitions.*.grant)) : 0

  user       = flatten(local.database_definitions.*.grant)[count.index].user
  host       = flatten(local.database_definitions.*.grant)[count.index].host
  database   = flatten(local.database_definitions.*.grant)[count.index].database
  privileges = flatten(local.database_definitions.*.grant)[count.index].privileges
}

こうやって、リソース間に依存関係を引き込んでみました。
※今回はmysql_userからmysql_databaseに依存関係を作っていますが、本来はなくてもかまいません

f:id:Kazuhira:20200607175323p:plain

ちょっとわかりづらそうな気はしますが、こういう解法もあるにはあると覚えておきましょうかね…。

今回の話は、こちらをヒントにしています。

For_each depends_on - Terraform - HashiCorp Discuss