Terraform で本番やステージングなどの変数定義

Terraform で本番やステージングなどの複数の環境を管理するとき、環境ごとに異なる変数を管理する方法について。

Terraform のバージョンは 0.14.8 で確認しています。

案 1. -var-file で tfvars ファイルを指定

Workspaces は tfstate を分けるためだけに使い、-var-file で環境ごとの変数定義ファイルを指定します。

terraform workspace select prod
terraform plan -var-file=envs/prod.tfvars

都度都度 -var-file を指定するのが煩雑なのと、間違った組み合わせを(workspace が本番で tfvars がステージングとか)指定してしまうと悲惨な障害になりかねません。

特に terraform workspace select prod が必要なために二手になるので事故りやすそうです。

次のように workspace と tfvars が一致していることをチェックするアイデアもあるようです。

あるいは、いまは workspace は環境変数でも指定できるので次のように一手にするとか。

env TF_WORKSPACE=prod terraform plan -var-file=envs/prod.tfvars

案 2. locals から環境名で変数定義をルックアップ

locals で環境ごとのキーの下に変数を定義し、terraform.workspace でルックアップします。

locals {
  envs = {
    prod = {
      setting = "this is production"
    }
    stg = {
      setting = "this is staging"
    }
  }
  env = local.envs[terraform.workspace]
}

output "setting" {
  value = local.env.setting
}

変数定義ファイルを分けたいなら次のようにもできます。

// main.tf
locals {
  env = merge(
    { prod = local.prod },
    { stg = local.stg },
  )[terraform.workspace]
}

output "setting" {
  value = local.env.setting
}

// prod.tf
locals {
  prod = {
    setting = "this is production"
  }
}

// stg.tf
locals {
  stg = {
    setting = "this is staging"
  }
}

案 3. YAML ファイルを読み込み

workspace 名の YAML ファイルで変数を定義し、fileyamldecodelocals に読み込みます。

locals {
  env = yamldecode(file("envs/${terraform.workspace}.yml"))
}

output "setting" {
  value = local.env.setting
}

シンプルで良いですね。

案 4. 環境名のモジュール

環境名でモジュールを作成して、そのアウトプットを変数定義として使います。

例えば次のようなディレクトリ構造。

main.tf
prod/
    output.tf
stg/
    output.tf

main.tf で次のようにモジュールを読みます。

module "prod" {
  count = terraform.workspace == "prod" ? 1 : 0
  source = "./prod"
}

module "stg" {
  count = terraform.workspace == "stg" ? 1 : 0
  source = "./stg"
}

locals {
  env = concat(module.prod, module.stg)[0]

}

output "setting" {
  value = local.env.setting
}

この方法は、共通化しにくいリソース定義でも環境名のモジュールに入れれば countfor_each を用いたハックが必要ない、というメリットもあります。

前述の locals からルックアップする方法と併用して、変数定義は基本的に locals で、共通化しにくいリソースが含まれるならそれだけ環境名のモジュールに入れる、などの使い方ができそうです。

案.5 Workspaces を使わずにディレクトリで分ける

例えば次のようなディレクトリ構造。

common/
    aaa/
        aaa.tf
    bbb/
        aaa.tf
prod/
    main.tf
    vars.tf
    ccc/
        ccc.tf
stg/
    main.tf
    vars.tf
    ccc/
        ccc.tf

共通で使いたいモジュールは common/ に入れて、共通化が難しいリソース定義は環境ごとのディレクトリの中のモジュールに入れます。環境ごとの main.tf で各モジュールを読み込みます。

locals {
  env_name = "prod"
}

module "aaa" {
  source = "../common/aaa"
  env_name = local.env_name
}

module "bbb" {
  source = "../common/bbb"
  env_name = local.env_name
}

module "ccc" {
  source = "./ccc"
  env_name = local.env_name
}

さいごに

環境によってあったりなかったりするリソースが含まれると、Workspaces を使う方法だと count やら for_each やらでめんどくさいことをする必要があるため、なんだかんだ汎用性のある Workspaces を使わずにディレクトリで分ける方法を使う方法が無難な気がします。

また、Workspaces はいちいち terraform workspace select する必要がひと手間あるのが煩雑なような・・環境変数で TF_WORKSPACE=prod terraform plan とかできますけど、コマンドラインで --workspace=prod みたいに指定できるのが一番良いと思うんですけどどうなの。

Terraform の Workspaces は git ブランチをモデルにしているとのことです、なるほど。ただ git みたいに常時使うようなものでもないので、そのためにプロンプトに workspace 名を表示するとかまではやりたくないですね。。。