これは、なにをしたくて書いたもの?
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
見事にバラバラですね。
リソースの引数を使って、暗黙的な依存関係を作る
次に、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から他のリソースに依存ができるようになっています。
こうすると、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にも依存関係を定義してみます。
この状態でグラフを作ると、こんな感じになります。
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" {
依存関係のグラフを作成してみます。
当然のことながら、どのリソースにも依存関係はありません。
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に依存関係を作っていますが、本来はなくてもかまいません
ちょっとわかりづらそうな気はしますが、こういう解法もあるにはあると覚えておきましょうかね…。
今回の話は、こちらをヒントにしています。