Terraformが依存関係を理解できるようにコードを書こう

2022.10.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

問題

S3バケットを作成するTerraformのコードを書きました。命名のためにlocal変数を使っています。

locals {
  bucket_name = "kazue-hogehoge"
}

resource "aws_s3_bucket" "sample" {
  bucket = local.bucket_name
}

このバケットに、さらにバージョニングの設定を足したいと思います。ここで問題です。以下2つのコード、どちらがより良いコードと言えるでしょうか?

A: bucket attributeの参照元としてlocal変数を使う

resource "aws_s3_bucket_versioning" "sample" {
  bucket = local.bucket_name
  versioning_configuration {
    status = "Enabled"
  }
}

B: bucket attributeの参照元としてaws_s3_bucketのattributeを使う

resource "aws_s3_bucket_versioning" "sample" {
  bucket = aws_s3_bucket.sample.bucket
  versioning_configuration {
    status = "Enabled"
  }
}

私は、「B: bucket attributeの参照元としてaws_s3_bucketのattributeを使う」の方がより良いコードだと考えます。理由は、エラーが起きにくいからです。

解説

aws_s3_bucket_versioningリソースは、設定対象となるS3バケットに依存しています。当然ですね。バケットの設定項目の一つですから。

ですが、Aのコードだとその依存関係をTerraformは理解していません。故に、「バケットをプロビジョニングする前にバージョニングのプロビジョニングを行おうとしてエラーになる」というパターンが発生し得ます。

Aでターゲットを絞ってプロビジョニングしてみる

例を挙げます。さっきのAのコードで、-targetオプションを使って、aws_s3_bucket_versioningリソースだけプロビジョニングしてみましょう。

プラン結果は以下です。当然 aws_s3_bucket_versioningだけが作成予定です。

% terraform plan -target="aws_s3_bucket_versioning.sample"
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with
the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket_versioning.sample will be created
  + resource "aws_s3_bucket_versioning" "sample" {
      + bucket = "kazue-hogehoge"
      + id     = (known after apply)

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
(略)

applyするとエラーになります。そりゃバケットがないのでそうですよね。

% terraform apply -target="aws_s3_bucket_versioning.sample" -auto-approve

(略)

│ Error: error creating S3 bucket versioning for kazue-hogehoge: NoSuchBucket: The specified bucket does not exist
│       status code: 404, request id: MA1QPNDWZSAP0PZX, host id: WCEsy4o7hf4w7i8vJSuQ6QmFvCGFIvz0r8tCvlJ7ej7qubpkNvPyCWI1PaMUxmNMZ2icb4sneqM=
│ 
│   with aws_s3_bucket_versioning.sample,
│   on main.tf line 25, in resource "aws_s3_bucket_versioning" "sample":
│   25: resource "aws_s3_bucket_versioning" "sample" {
│

Bでターゲットを絞ってプロビジョニングしてみる

では、同じことをBのコードでもやってみましょう。

% terraform plan -target="aws_s3_bucket_versioning.sample"

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with
the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.sample will be created
  + resource "aws_s3_bucket" "sample" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "kazue-hogehoge"
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule {
          + allowed_headers = (known after apply)
          + allowed_methods = (known after apply)
          + allowed_origins = (known after apply)
          + expose_headers  = (known after apply)
          + max_age_seconds = (known after apply)
        }

      + grant {
          + id          = (known after apply)
          + permissions = (known after apply)
          + type        = (known after apply)
          + uri         = (known after apply)
        }

      + lifecycle_rule {
          + abort_incomplete_multipart_upload_days = (known after apply)
          + enabled                                = (known after apply)
          + id                                     = (known after apply)
          + prefix                                 = (known after apply)
          + tags                                   = (known after apply)

          + expiration {
              + date                         = (known after apply)
              + days                         = (known after apply)
              + expired_object_delete_marker = (known after apply)
            }

          + noncurrent_version_expiration {
              + days = (known after apply)
            }

          + noncurrent_version_transition {
              + days          = (known after apply)
              + storage_class = (known after apply)
            }

          + transition {
              + date          = (known after apply)
              + days          = (known after apply)
              + storage_class = (known after apply)
            }
        }

      + logging {
          + target_bucket = (known after apply)
          + target_prefix = (known after apply)
        }

      + object_lock_configuration {
          + object_lock_enabled = (known after apply)

          + rule {
              + default_retention {
                  + days  = (known after apply)
                  + mode  = (known after apply)
                  + years = (known after apply)
                }
            }
        }

      + replication_configuration {
          + role = (known after apply)

          + rules {
              + delete_marker_replication_status = (known after apply)
              + id                               = (known after apply)
              + prefix                           = (known after apply)
              + priority                         = (known after apply)
              + status                           = (known after apply)

              + destination {
                  + account_id         = (known after apply)
                  + bucket             = (known after apply)
                  + replica_kms_key_id = (known after apply)
                  + storage_class      = (known after apply)

                  + access_control_translation {
                      + owner = (known after apply)
                    }

                  + metrics {
                      + minutes = (known after apply)
                      + status  = (known after apply)
                    }

                  + replication_time {
                      + minutes = (known after apply)
                      + status  = (known after apply)
                    }
                }

              + filter {
                  + prefix = (known after apply)
                  + tags   = (known after apply)
                }

              + source_selection_criteria {
                  + sse_kms_encrypted_objects {
                      + enabled = (known after apply)
                    }
                }
            }
        }

      + server_side_encryption_configuration {
          + rule {
              + bucket_key_enabled = (known after apply)

              + apply_server_side_encryption_by_default {
                  + kms_master_key_id = (known after apply)
                  + sse_algorithm     = (known after apply)
                }
            }
        }

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }

      + website {
          + error_document           = (known after apply)
          + index_document           = (known after apply)
          + redirect_all_requests_to = (known after apply)
          + routing_rules            = (known after apply)
        }
    }

  # aws_s3_bucket_versioning.sample will be created
  + resource "aws_s3_bucket_versioning" "sample" {
      + bucket = "kazue-hogehoge"
      + id     = (known after apply)

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.
(略)

aws_s3_bucketもplanに含まれましたね。(attribute多すぎでとても縦長…)
これは-targetオプションは、そこに指定されたリソース(今回だとaws_s3_bucket_versioning)に加えて、そのリソースが依存している、つまりそのリソースを作るのに必要になる別のリソース(今回だとaws_s3_bucket)も併せてプロビジョニングするからです。aws_s3_bucket_versioning.samplebucketarrtibute値にaws_s3_bucket.sampleのattributeを使ったのでこういう結果になりました。この後applyも成功しました。applyのログを見ていると以下のようになっており、バケットの作成完了を待ってからバージョニングの設定が始まっていることがわかります。

aws_s3_bucket.sample: Creating...
aws_s3_bucket.sample: Creation complete after 6s [id=kazue-hogehoge]
aws_s3_bucket_versioning.sample: Creating...
aws_s3_bucket_versioning.sample: Creation complete after 3s [id=kazue-hogehoge]

でも-targetオプションなんてそうそう使わへんやん

-targetなんてほとんど使わないオプション持ち出して、『こっちの方が良いコードです(`・ω・´)キリッ』なんて言われてもねぇ…」とシラけたあなた。おっしゃるとおりです。私もそう思います。

ですが、-targetオプションを使わずとも、先に挙げたような「バケットをプロビジョニングする前にバージョニングのプロビジョニングを行おうとしてエラーになる」ケースは結構発生すると考えています。依存関係をTerraformが理解していない以上、どちらが先にプロビジョニングされるかは、完全にTerraform任せになります。

  • 通常私達はTerraformで複数リソースを同時並行的にプロビジョニングします。デフォルト並列数は10です。依存関係にあるリソースが、同時に作成開始するケースが発生します。それどころか、他のリソースももっと大量にプロビジョニングするコードである場合は、他のリソースの作成に紛れて依存しているリソース(バージョニング)→依存されている(バケット)の順番で実行される場合もあります。
  • 今回のS3バケットのような、すぐに作成されるリソースの場合、たとえそれに依存しているリソース(バージョニング)と同時に作成開始したとしてもエラーになる確率は低いでしょう。(実際上記Aのコードも-targetを外せばapplyは正常完了しました。) ですが、もっと作成に時間がかかるリソース、例えばEKSクラスター等の場合はより注意が必要です。

おまけ: グラフで確認

terraform graphコマンドでAとBのコードのリソースの依存関係を図示してみました。

% terraform graph | dot -Tpng > graph.png

Aのグラフ

graph-a aws_s3_bucketaws_s3_bucket_versioningに依存関係はありませんね。

Bのグラフ

graph-b aws_s3_bucket_versioningaws_s3_bucketに依存しています。

まとめ

以上、リソース間の依存関係をTerraformが理解できるように書きましょう、という話でした。細かい話ですが、これが原因でエラーが出たとき問題特定が辛かったなぁと思いだして書きました(S3バケットではなかったですが)。日々applyしているような環境ではあまり起きないんですが、久しぶりに別環境を一から作成する時に発生しがちです。お気をつけください。また、明示的に依存関係が表せない場合は、depends_onの使用も検討しましょう。

参考情報