CLOVER🍀

That was when it all began.

Terraformのcount、for_eachに作成前のリソースの情報を指定できないという話

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

Terraformでcountを使っていた時にこんなエラーに当たったので、「これはなんだろう?」と思い。

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

ちょっと調べてみることにしました。

結論を書くと、countやfor_eachで指定する値の中に、作成前のリソースの情報(属性)が含まれているとダメみたいです。

ドキュメントにも書いてありました。

Resources / Using Expressions in count

The count meta-argument accepts numeric expressions. However, unlike most resource arguments, the count value must be known before Terraform performs any remote resource actions. This means count can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).

Resources / Using Expressions in for_each

The for_each meta-argument accepts map or set expressions. However, unlike most resource arguments, the for_each value must be known before Terraform performs any remote resource actions. This means for_each can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).

これは、その他のリソースに関する引数とは異なる、countやfor_eachに関する性質です。

環境

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

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

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

サンプル

では、サンプルを書いていきます。

お題はこうしましょう。

  • MySQLデータベースのユーザーおよび権限を作成するモジュールを作る
  • 権限などで利用するデータベースは、モジュールのInput Variablesで指定する
  • データベースの情報が指定されなかった場合は、モジュール側でデフォルトのデータベースを作成する

このお題だと、データベースが条件によって作成されたり、作成されなかったりします。

では、最初にモジュールの呼び出し側となるリソース定義ファイルを載せます。
main.tf

terraform {
  required_version = "0.12.28"
}

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

  version = "1.9.0"
}

module "user" {
  source = "./modules/user"
}

この段階では、単純にモジュールを呼び出しているだけです。

次に、モジュール側の定義ファイル。
modules/user/main.tf

variable "database_id" {
  type    = string
  default = null
}

variable "database_name" {
  type    = string
  default = null
}

resource "mysql_database" "default" {
  count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

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

resource "mysql_grant" "grant" {
  user       = mysql_user.user.user
  host       = "%"
  database   = var.database_name != null ? var.database_name : mysql_database.default[0].id
  privileges = ["ALL"]
}

Input Variableに、mysql_databaseリソースのidとnameを指定しています。

variable "database_id" {
  type    = string
  default = null
}

variable "database_name" {
  type    = string
  default = null
}

特に、idはリソース作成後でないと得られない値です。

MySQL: mysql_database - Terraform by HashiCorp

ここで、idが指定されていなければデフォルトのデータベースを作成し、指定された場合はデータベースを作成しない、という
条件でmysql_databaseリソースの定義を行います。

resource "mysql_database" "default" {
  count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

ちなみに、mysql_databaseリソースの情報はmysql_grantで使用します。

resource "mysql_grant" "grant" {
  user       = mysql_user.user.user
  host       = "%"
  database   = var.database_name != null ? var.database_name : mysql_database.default[0].id
  privileges = ["ALL"]
}

これを実行します。

$ terraform apply -auto-approve

問題なく、うまくいきます。

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

1度、リソースは破棄しておきます。

$ terraform destroy -force

モジュールの外側でmysql_databaseを定義した場合

次に、モジュールの外側でデータベースを事前に作成するように変更してみましょう。 main.tf

terraform {
  required_version = "0.12.28"
}

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

  version = "1.9.0"
}

resource "mysql_database" "predefined" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

module "user" {
  source = "./modules/user"

  database_id   = mysql_database.predefined.id
  database_name = mysql_database.predefined.name
}

mysql_databaseリソース定義を追加して

resource "mysql_database" "predefined" {
  name = "my_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

モジュールのid、nameには、このリソースの属性を指定します。

module "user" {
  source = "./modules/user"

  database_id   = mysql_database.predefined.id
  database_name = mysql_database.predefined.name
}

これがどういう結果になるか、ですが、Terraformが実行できなくなります。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

Error: Invalid count argument

  on modules/user/main.tf line 13, in resource "mysql_database" "default":
  13:   count = var.database_id == null ? 1 : 0

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

エラーメッセージを読むと、「countは、applyを実行するまで決定できない値には依存できない」と言っています。
インスタンスをいくつ作っていいか予測できないから、だと。

これを回避するには、エラーメッセージにあるように事前にモジュールが依存している(countを使うリソースが依存している)
リソースを作成しておきます。

$ terraform apply -target=mysql_database.predefined -auto-approve

まあ、完全には終わってないよ、って言われますけど。

mysql_database.predefined: Creating...
mysql_database.predefined: Creation complete after 0s [id=my_database]

Warning: Resource targeting is in effect

You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
        
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.


Warning: Applied changes may be incomplete

The plan was created with the -target option in effect, so some changes
requested in the configuration may have been ignored and the output values may
not be fully updated. Run the following command to verify that no other
changes are pending:
    terraform plan
    
Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes, or
when Terraform specifically suggests to use it as part of an error message.


Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

このあとは、「terraform apply」すれば残りのリソースを作成してくれます。

$ terraform apply -auto-approve
mysql_database.predefined: Refreshing state... [id=my_database]
module.user.mysql_user.user: Creating...
module.user.mysql_user.user: Creation complete after 0s [id=user@%]
module.user.mysql_grant.grant: Creating...
module.user.mysql_grant.grant: Creation complete after 0s [id=user@%:`my_database`]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

ちなみに、この事象はcountではなく、for_eachを使っても発生します。

resource "mysql_database" "default" {
  for_each = var.database_id == null ? toset([var.database_id]) : toset([])
  #count = var.database_id == null ? 1 : 0

  name = "default_database"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

こんな感じのエラーになります。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

Error: Invalid for_each argument

  on modules/user/main.tf line 12, in resource "mysql_database" "default":
  12:   for_each = var.database_id == null ? toset([var.database_id]) : toset([])

The "for_each" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the for_each depends on.

というわけで、countやfor_eachを使う値に、terraform applyなどが同じタイミングで適用、生成される値が使用されている場合は、
こういう結果になるので事前に作成しておきましょう、と。

ちょっと惜しい感じもするのですが、今回のような単一の値ではなく、リソースの作成結果がリストのような複数の値を返し、
それを元に別のリソースを作成する…といったようなケースを考えると、確かに作成するリソースの数が定まらなくなるので
仕方ないですね、と。

参考

https://github.com/hashicorp/terraform/blob/v0.12.28/terraform/eval_count.go#L39

https://github.com/hashicorp/terraform/blob/v0.12.28/terraform/eval_for_each.go#L23

Conditional expression assigned to local cannot be evaluated during plan · Issue #21450 · hashicorp/terraform · GitHub

リモートホストで動作しているDockerデーモンを使用する

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

リモートホストで動作しているDockerデーモンを扱ったことがなかったので、1度試してみようかな、と。

環境

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

$ uname -srvmpio
Linux 4.15.0-108-generic #109-Ubuntu SMP Fri Jun 19 11:33:10 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux


$ docker version
Client: Docker Engine - Community
 Version:           19.03.12
 API version:       1.40
 Go version:        go1.13.10
 Git commit:        48a66213fe
 Built:             Mon Jun 22 15:45:36 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.12
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.10
  Git commit:       48a66213fe
  Built:            Mon Jun 22 15:44:07 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Dockerをインストールしたサーバーは、2つ用意します。

  • 192.168.33.10 … host1
  • 192.168.33.11 … host2

このうち、host1をクライアント側、host2をサーバー側とします。コマンドの記載も、コメントでhost1とhost2を書いて見分ける
ようにしていきます。

他のDockerデーモンを使うには?

DOCKER_HOST環境変数を使うか、コンテキストの切り替えを行うみたいです。

Use the Docker command line | Docker Documentation

Docker Context | Docker Documentation

それぞれ試してみましょう。

DOCKER_HOST環境変数を使う

最初は、DOCKER_HOST環境変数を試してみます。この環境変数を使うことで、接続先のDockerデーモンを指定できます。

Use the Docker command line | Docker Documentation

ちなみに、コマンド都度で書くことになりますが、「-H」オプションで指定することも可能です。

今回は、クライアント側からTCP接続できるようにします。

サーバー側の設定としては、Dockerデーモンのソケット指定を行います。

dockerd / Daemon socket option

dockerコマンドに対して、「-H」でどのような方法で接続を待つのかを指定します。パッケージインストールした場合は、
systemdのUnitファイルでの指定になります。

デフォルトの状態を確認してみます。
/etc/systemd/system/multi-user.target.wants/docker.service

# host2
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

「fd://」のみですね。この状態だと、「/var/run/docker.sock」というUnixドメインソケットが使用されます。

ここで、TCP接続可能にしてみましょう。今回は「0.0.0.0」で全インターフェースにバインド、2375ポートでリッスンします。

# host2
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H fd:// --containerd=/run/containerd/containerd.sock

Dockerデーモンを再起動。

# host2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

TCPポートをリッスンするようになりました。

# host2
$ ss -tnl | grep 2375
LISTEN   0         128                       *:2375                   *:* 

ここで、クライアント側でDOCKER_HOST環境変数の指定を行ってみます。

# host1
$ export DOCKER_HOST=tcp://192.168.33.11:2375

nginxのイメージをPullしてみましょう。

# host1
$ docker image pull nginx:latest

確認。

# host1
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              2622e6cca7eb        2 weeks ago         132MB

ここで、サーバー側でも確認してみます。同じイメージが取得できています。

# host2
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              2622e6cca7eb        2 weeks ago         132MB

クライアント側に戻って、コンテナを起動してみましょう。この時に、ポート80をローカルにもバインドさせてみます。

# host1
$ docker container run -d -p 80:80 nginx:latest

どこでコンテナが動作しているか、確認してみます。ポート80は、クライアント側ではバインドされません。

# host1
$ ss -tnl | grep 80

この状態なので、当然、接続もできません。

# host1
$ curl -I localhost
curl: (7) Failed to connect to localhost port 80: Connection refused

サーバー側のIPアドレスを指定すると、接続できます。

# host1
$ curl -I 192.168.33.11
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Tue, 30 Jun 2020 12:50:43 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 May 2020 15:00:20 GMT
Connection: keep-alive
ETag: "5ecd2f04-264"
Accept-Ranges: bytes

「docker container ps」で、実行中のコンテナを確認してみます。

# host1
$ docker container ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
e70a043d60c0        nginx:latest        "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp   sweet_sutherland

サーバー側でも、同じように見えます。

# host2
$ docker container ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
e70a043d60c0        nginx:latest        "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp   sweet_sutherland

つまり、この状態でDockerコンテナが動作するのは、サーバー側(Dockerデーモンが動作している側)となります。

# host2
$ ss -tnl | grep 80
LISTEN   0         128                       *:80                     *:* 

この動きをちゃんと確認したのは、初めてだったりします。

確認できたので、コンテナの停止と削除。

# host1
$ docker container stop sweet_sutherland 
$ docker container rm sweet_sutherland 

なお、最初にも記載しましたが、DOCKER_HOST環境変数の代わりに、「-H」オプションで接続先のDockerデーモンを指定しても
同じ動作になります。

# host1
$ docker -H tcp://192.168.33.11:2375 container run -it --rm nginx:latest

コマンド都度の指定になるので、面倒といえば面倒ですが…。

コンテキストを使う

次に、コンテキストの切り替えを行ってみます。

Docker Context | Docker Documentation

現在のコンテキストは、「docker context ls」で確認できます。
※DOCKER_HOST環境変数は、今回は未設定です

# host1
$ docker context ls
NAME                DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default *           Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm

docker context ls | Docker Documentation

デフォルトのUnixドメインソケットが使われています。

ここで、新しくコンテキストを作成します。名前は「remote」にしました。

# host1
$ docker context create remote --docker host=tcp://192.168.33.11:2375
remote
Successfully created context "remote"

docker context create | Docker Documentation

確認。

# host1
$ docker context ls
NAME                DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default *           Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm
remote                                                        tcp://192.168.33.11:2375

「docker context use」で、指定のコンテキストに切り替えることができます。

# host1
$ docker context use remote
remote
Current context is now "remote"

docker context use | Docker Documentation

あとは、DOCKER_HOST環境変数で指定したように、接続先のDockerデーモンを切り替えた状態で操作することができます。

# host1
$ docker image pull httpd:latest

コンテナの起動。

# host1
$ docker container run -d -p 80:80 httpd:latest

確認。

# host1
$ curl -I 192.168.33.11
HTTP/1.1 200 OK
Date: Tue, 30 Jun 2020 13:01:40 GMT
Server: Apache/2.4.43 (Unix)
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Content-Type: text/html

また、コンテキストの作成後であれば、「docker context use」ではなく、DOCKER_CONTEXT環境変数でコンテキスト名を
指定することで切り替えることもできます。

# host1
$ export DOCKER_CONTEXT=remote
$ docker context ls
NAME                DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default             Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm
remote *                                                      tcp://192.168.33.11:2375 

確認したいことはできたので、OKとしましょう。