CLOVER🍀

That was when it all began.

GitLabをTerraformのStateの保存先として使う

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

GitLab 13.0で、TerraformのStateのバックエンドとして利用できるようになったというのを見かけまして。

GitLab 13.0 released with Gitaly Clusters, Epic Hierarchy on Roadmaps, and Auto Deploy to ECS / GitLab HTTP Terraform state backend

ポイントは、このあたりですね。

  • Multiple named state files per project
  • Locking
  • Object storage
  • Encryption at rest

こちらを試してみようかな、と。

Terraform State BackendとしてのTerraform

TerraformのRemote Stateに関しては、以前にこちらで書きました。

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

通常はローカルファイルシステムに保存されるTerraformのStateを、別のリモートのストアに保存できるようにすることですね。

State: Remote Storage - Terraform by HashiCorp

Stateをリモート管理することで、Stateをチーム内で共有したり、ロックを使用した排他制御を行うことでTerraformを同時に
実行して事故になることを避ける、といったことができるようになります。

どこまでできるかは、実際に利用するバックエンド次第ですが。

GitLabのドキュメントにおける、Terraformに関するページはこちらです。

Infrastructure as code with Terraform and GitLab | GitLab

S3などを用意しなくても、Stateをリモート管理できると謳われており、転送中や保存中のStateの暗号化、ロックをサポート
することがポイントとして書かれています。

  • Supporting encryption of the state file both in transit and at rest.
  • Locking and unlocking state.
  • Remote Terraform plan and apply execution.

あとは、CIで使う時の方法が書かれていたり。

CIに組み込む際には、合わせてGitLabで定義されている環境変数を見ることになりそうですね。

Predefined environment variables reference | GitLab

TerraformのStateを管理する機能は、デフォルトで有効になっていて、その設定などはこちらに書かれています。

Terraform state administration (alpha) | GitLab

「/etc/gitlab/gitlab.rb」で設定するようです。

また、GitLabをTerraform State Backendとして使う場合は、HTTPとなり、RESTでのアクセスとなります。

Backend Type: http - Terraform by HashiCorp

と、説明はこのくらいにして、実際に使っていってみましょう。

TerraformのStateをGitLabで管理し、ProviderはMySQLを使って遊んでみることにします。

環境

今回の環境まわりです。

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

GitLabは192.168.0.3のサーバーで動作しているものとします。バージョンは、こちら。

$ sudo gitlab-rake gitlab:env:info

System information
System:     Ubuntu 18.04
Current User:   git
Using RVM:  no
Ruby Version:   2.6.6p146
Gem Version:    2.7.10
Bundler Version:1.17.3
Rake Version:   12.3.3
Redis Version:  5.0.9
Git Version:    2.26.2
Sidekiq Version:5.2.7
Go Version: unknown

GitLab information
Version:    13.0.6
Revision:   5aa982e01ea
Directory:  /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 11.7
URL:        http://192.168.0.3
HTTP Clone URL: http://192.168.0.3/some-group/some-project.git
SSH Clone URL:  git@192.168.0.3:some-group/some-project.git
Using LDAP: no
Using Omniauth: yes
Omniauth Providers: 

GitLab Shell
Version:    13.2.0
Repository storage paths:
- default:  /var/opt/gitlab/git-data/repositories
GitLab Shell path:      /opt/gitlab/embedded/service/gitlab-shell
Git:        /opt/gitlab/embedded/bin/git

また、MySQLは8.0で、172.17.0.2で動作しているものとします。

シンプルに使ってみる

まずは、こちらを見ながらシンプルに使っていってみましょう。

Infrastructure as code with Terraform and GitLab / Get started using local development

ドキュメントを見ると、Terraformの構成ファイルへ書くのはこれくらいで

terraform {
  backend "http" {
  }
}

残りは「terraform init」時に指定、という雰囲気で書かれていますが、ちょっと面倒だなぁと思ったので…。

$ terraform init \
    -backend-config="address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>" \
    -backend-config="lock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
    -backend-config="unlock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
    -backend-config="username=<YOUR-USERNAME>" \
    -backend-config="password=<YOUR-ACCESS-TOKEN>" \
    -backend-config="lock_method=POST" \
    -backend-config="unlock_method=DELETE" \
    -backend-config="retry_wait_min=5"

また、可変の値としてGitLabのプロジェクトのID、プロジェクト名、ユーザー名およびアクセストークンが必要だということに
なっています。gitlab.comを使わない場合(今回もですが)は、アクセス先も変わります。

プロジェクトIDとプロジェクト名は、今回はそれぞれ2、sample-projectとします。

今回は、このくらいは定義の方に書くことにしました。

terraform {
  required_version = "0.12.26"

  backend "http" {
    address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project"
    lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock"
    lock_method = "POST"
    unlock_method = "DELETE"
    retry_wait_max = 5
  }
}

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

  version = "1.9.0"
}

アクセス先はHTTPにしているので、転送中は思いきり平文ですね。

ユーザー名とパスワードだけは、terraform init時に指定することにします。

$ terraform init \
  -backend-config="username=[your-username]" \
  -backend-config="password=[your-access-token]"

チーム内で使うにしても、他の項目はメンバー単位に変わらないでしょう…。

パスワードは、GitLabのアクセストークンです。持っていなければ、「User Settings」→「Access Tokens」から発行できます。
アクセストークンのスコープとしては、「api」があれば大丈夫な気がします。

で、全体でこんな感じで定義してみました。MySQLデータベースを作成する、リソース定義です。
main.tf

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

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

  version = "1.9.0"
}

resource "mysql_database" "database" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

applyで、問題なくデータベースの作成ができます。

$ terraform apply

この時、アクセストークンがあればStateの保存内容をcurlなどで見ることができます。

$ curl -H 'Authorization: Bearer [your-access-token]' http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project
{
  "version": 4,
  "terraform_version": "0.12.26",
  "serial": 7,
  "lineage": "",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "mysql_database",
      "name": "database",
      "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=="
        }
      ]
    }
  ]
}

また、「terraform init」の時点で、ローカルにはバックエンドの情報を保存したこんなファイルができます。
.terraform/terraform.tfstate

{
    "version": 3,
    "serial": 1,
    "lineage": "fe17a8cb-a6c4-be4c-2b3c-2883f103a836",
    "backend": {
        "type": "http",
        "config": {
            "address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project",
            "lock_address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock",
            "lock_method": "POST",
            "password": "[your-access-token]",
            "retry_max": null,
            "retry_wait_max": 5,
            "retry_wait_min": null,
            "skip_cert_verification": null,
            "unlock_address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock",
            "unlock_method": "DELETE",
            "update_method": null,
            "username": "[your-username]"
        },
        "hash": 1929719276
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

Remote State+Data Sourceとして使う

続いて、GitLabをTerraformのRemote Stateとして使いつつも、Data Sourceとして参照にも使用してみましょう。

データベースの作成と、ユーザーの作成をそれぞれ別のリソース定義で行い、Data Sourceの参照で紐付けます。

まずは、データベース側のリソース定義。
main.tf

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/database"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

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

  version = "1.9.0"
}

resource "mysql_database" "database" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

output "database_name" {
  value = mysql_database.database.name
}

ドキュメントではプロジェクト名がBackendのアクセス先になっていましたが、このあたりの値は任意で良さそうなので、
変更しました。「Multiple named state files per project」というのが、リリースブログでもポイントとして書かれていましたしね。
今回は「database」としています。

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/database"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

このリソース定義の結果を次で使うので、Outputも定義。データベース名だけですが。

output "database_name" {
  value = mysql_database.database.name
}

で、apply。

$ terraform apply

続いて、ユーザーを作成するリソース定義を行いましょう。データベースを作成した時のリソース定義とは、別のディレクトリで
行ってください。

全体はこんな感じです。
main.tf

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/users"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

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

  version = "1.9.0"
}

variable "gitlab_username" {}
variable "gitlab_password" {}

data "terraform_remote_state" "database" {
  backend = "http"

  config = {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/database"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    username       = var.gitlab_username
    password       = var.gitlab_password
    retry_wait_max = 5
  }
}

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.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.database_name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]

  depends_on = [mysql_user.application_user]
}

Backendのパスは、先ほどのものとは変更し、用途に合わせて「users」としました。

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/users"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

また、先ほどのデータベース定義の結果を参照したいので、Data Sourceの定義を行います。

こんな感じで。参照先は、「database」のパスになります。

variable "gitlab_username" {}
variable "gitlab_password" {}

data "terraform_remote_state" "database" {
  backend = "http"

  config = {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/database"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    username       = var.gitlab_username
    password       = var.gitlab_password
    retry_wait_max = 5
  }
}

ここではユーザー名とパスワードを指定せざるをえないので、変数に切り出しました。

値は、環境変数で与えた方がいいのかな?と思います。

$ export TF_VAR_gitlab_username=[your-username]
$ export TF_VAR_gitlab_password=[your-access-token]

そして、Remote StateをData Sourceとして参照して、リソース定義を行います。

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

  depends_on = [mysql_user.application_user]
}

apply。

$ terraform apply

こんな感じで、確認完了です。

GitLab上でのStateの保存先について

GitLab側に保存されるTerraformのStateですが、保存先のパスなどはこちらのドキュメントに記載のある設定箇所で調整します。

Terraform state administration (alpha) | GitLab

設定ファイルは、「/etc/gitlab/gitlab.rb」です。

デフォルトの保存先は、「/var/opt/gitlab/gitlab-rails/shared/terraform_state」ディレクトリなので、こちらを確認してみます。

$ sudo ls -l /var/opt/gitlab/gitlab-rails/shared/terraform_state
total 8
drwxr-xr-x 2 git git 4096 Jun 13 11:01 2
drwxr-xr-x 4 git git 4096 Jun 13 09:22 tmp

プロジェクトID(今回は2)のディレクトリがありますね。

中身を見ると、3つStateと思しきファイルがあります。3つあるのは、今回3回実行したからですね。

$ sudo ls -l /var/opt/gitlab/gitlab-rails/shared/terraform_state/2
total 16
-rw-r--r-- 1 git git  150 Jun 13 10:53 16daa9081b1106c5c970411eaaec7e25.tfstate
-rw-r--r-- 1 git git  695 Jun 13 11:01 840999dc0e463bed8d360b4fdaac939f.tfstate
-rw-r--r-- 1 git git 4451 Jun 13 11:01 f0763fc6cc0691fcd13731b408ea0488.tfstate

ということを考えると、「/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>」といった指定のパスとの
マッピングがちょっと気になるところです。

中身を見ると、読めないようになっています。暗号化されているようですね。

$ sudo cat /var/opt/gitlab/gitlab-rails/shared/terraform_state/2/16daa9081b1106c5c970411eaaec7e25.tfstate
\ �Ջ��nUˤ1��Z�z3�����Cܵ�&c��&�IJ�v��M�)������u�2��<Nk$�t$��:�r2�?�&�]X�ou\��1���苮����=���UX�#���u�`�b�+�$���Ԟ�����D(��[��Ux��

というわけで、ちょっと気になることとして、アクセス先にサブディレクトリのようなものを追加したらどうなるのかな?と
思ったのですが

terraform {
  required_version = "0.12.26"

  backend "http" {
    address        = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo"
    lock_address   = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo/lock"
    unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo/lock"
    lock_method    = "POST"
    unlock_method  = "DELETE"
    retry_wait_max = 5
  }
}

こうすると、404になるようです。

$ terraform apply

Error: Error locking state: Error acquiring the state lock: Unexpected HTTP response code 404

Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

というわけで、「/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>」のパスルールで
コントロールしましょう、と。

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