Terraformで、同じ構成を複数プロビジョニングしたい: Terragruntでbackendを動的設定編

2022.09.21

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

先日 HashiTalks Japanで「シングルテナント構成のSaaSのIaCにTerraform Workspacesを導入してみた」というビデオ登壇をしました。その中で時間の都合でご紹介できなかった、「Workspacesを使う以外の、同じ構成(リソースセット)を複数個プロビジョニングする方法案」を、複数回に分けてご紹介していきます。

関連エントリ

詳細

まずはTerragruntをインストールします。

terraform.backendブロックは定義しません。

terraform {
  required_version = "= 1.2.7"

  # backend "s3" {
  #   bucket = "hogehoge-terraform"
  #   key    = "dev.tfstate"
  #   region = "us-east-1"
  # }

  required_providers {
    aws = {
      version = "~> 4.26.0"
    }
  }
}

同じディレクトリ上にterragrunt.hclを作成し、以下のように書きます。

terragrunt.hcl

locals {
  prefix = get_env("PREFIX")
}

generate "backend" {
  path      = "backend.tf"
  if_exists = "overwrite"
  contents =  <<EOT
terraform {
  backend "s3" {
    bucket         = "hogehoge-terraform"
    key            = "${local.prefix}_main.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
  }
}
EOT
}

generate "tfvars" {
  path = "terraform.tfvars"
  if_exists = "overwrite"
  contents =  <<EOT
prefix = "${local.prefix}"
EOT
}

見ていただくと大体何をしているファイルかおわかりいただけるのではないかと思いますが、説明します。

  • まず、prefixというTerragruntのローカル変数を定義しています。get_env()関数はその名の通り環境変数を取得する関数です。
  • 上記prefixローカル変数を使って、generateブロックで2つのファイルを動的に作成しています。 generateブロックは、contents以下の内容のファイルをpathのファイル名で作成するものです。
  • ひとつはバックエンドの設定を書いたファイルです。
  • もう一つは変数値のファイルです。terraform.tfvarsという名前にしてTerraformコマンド実行ディレクトリと同じディレクトリに置いておくと、自動で読み込んでくれます。

次に、上記で参照している環境変数を設定します。

% export PREFIX=hoge

また、プロビジョニング対象となるリソースはS3バケットひとつだけとします。前述のterraform.tfvars内で記述しているprefix variableを定義して参照しています。

s3.tf

variable "prefix" {
  type = string
}

resource "aws_s3_bucket" "main" {
  bucket_prefix = var.prefix
}

この状態でterragrunt planします。(Terragruntの場合initは必要があれば内部的に自動でやってくれます)

すると、backendの設定が書かれたファイルと変数値のファイルが作成されます。さらにその設定を踏まえてplan結果が出力されます。

backend.tf

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
  backend "s3" {
    bucket         = "hogehoge-terraform"
    key            = "hoge_main.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
  }
}

terraform.tfvars

# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
prefix = "hoge"

S3バケットひとつ作るだけのplan

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.main will be created
  + resource "aws_s3_bucket" "main" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "hoge"
      + 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)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

terragrunt applyすればS3バケットが作成されます。

backend.tf terraform.tfvarsはGit管理対象外にしておくのが良いでしょう。

別プロビジョニング先の設定

環境変数を更新します。

% export PREFIX=fuga

バックエンドの設定を変えたので、今回はinitが必要です。(CI環境のような毎回捨てる環境なら不要です)

% terragrunt init -reconfigure

もしくはワーキングディレクトリの.terraformディレクトリを削除してください。

applyします。

% terragrunt apply

先程作ったバケット(hoge〜)は残ったまま、新しいバケット(fuga〜)もできていますね。 hogeandfugabacket stateファイルも別々に作成されています。 hogeandfugastatefiles

こんな感じで、環境変数値を切り替えることで同じ構成を複数プロビジョニングすることが可能になります。

動的生成部分もTerragrunt内でやっちゃう

前述の方法だと、プロビジョニング先を切り替える際に、事前に環境変数の更新が必要ですね。できればこの手間も無くしたいです。

run_cmd関数を使えば Terragrunt内でシェルスクリプトを実行することも可能です。これを利用してみます。

terragrunt.hcl

  locals {
-  prefix = get_env("PREFIX")
+  prefix = run_cmd("./bash/get_prefix.sh")
  }

./bash/get_prefix.sh

#!/bin/bash

echo "ppp"

面倒だったので 今回は単純に固定値を返すスクリプトにしていますが、このシェルスクリプトを作り込めばTerragruntだけで複数環境の切り替えができますね。

この構成の良い点

展開先の情報がコードに出ない、Gitに残らない

Module案などと異なり展開先の情報がコードに現れないので、その情報を隠蔽化したい際には適しています。例えばシングルテナント構成のSaaSを作る際などには向いているでしょう。

backendの設定が自在

Terraform Workspacesに対する優位点です。

Workspacesを使う場合、Stateファイルの格納場所は固定です。S3をbackendに使う場合、Stateファイルはすべて同じバケット内のenv:/(workspace名)/(key名)に格納されます。

例えばWorkspace案でクロスアカウントでプロビジョニングする場合、Stateファイル格納先は1アカウントに集中することになります。展開先のアカウントに別々に格納することはできません。 workspaces-cross-account

対してこの構成だともっと自由にStateファイル格納場所を選ぶことができます。オブジェクトパスはもちろん、バケットを別々にすることも、違うリージョンにすることも、別アカウントにすることもできますね。

Terragruntだけで完結できる

backend-configオプション案だとBackendの設定をTerraform外で行なう都合上、その外出し先との連携を徹底する(例えばシェルスクリプト化して 外部処理→Terraformコマンドを一気通貫で行なうようにするなど)必要があります。が、前述のとおり Terragruntだとその外出しの部分もTerragrunt内のシェルスクリプトとして内部化できるので、普段扱うコマンドはTerragruntだけで済みます。

Terraformコマンドも使える

TerragruntでやっているのはTerraformの設定ファイルを2つ作ることだけなので、そのファイルが作成済の場合、Terragruntコマンドの代わりにTerraformコマンドを使っても同じ結果になります。つまりプロビジョニング先を変えたいとき以外は慣れ親しんだTerraformコマンドを使うことができます。

この構成のイマイチな点

Terragrunt学習コスト

当たり前ですがTerragruntについて学ぶ必要があります。この構成をそのまま使うのであれば問題ないと思います。が、ちょっと踏み込んだことを足そうと思ったときに、「できそうだけどできないなー」みたいなことが沢山起きる、かもしれません。私は今回この構成を検証するにあたり最初Hooksを使えば良さそうと考えて試したのですが断念しました。Terragruntの処理の流れ、順序を理解するのが使いこなす近道のような気がします。(道半ば)

展開先ごとの差分の実現方法が限られる

展開先の情報が全くコードに出てこないので、もし展開先毎にプロビジョニングしたいリソースが異なるような要件があった場合、それを実現する方法は変数ファイル(.tfvars)の値を変えるくらいしかありません。

ユースケース

シングルテナント構成のSaaSのような、展開先毎のプロビジョニング内容にほぼほぼ差分がない、かつ展開先が多い場合に向いていると思います。 Workspaces案のほうがシンプルですが、それでは対応できない要件がある場合に検討するのが良いでしょう。

参考情報