🌐

Terraform の複数環境運用の比較

2024/08/16に公開

概要

  • Terraform テンプレートを複数の環境で運用する方法についての比較と検討の記事です。
  • この記事では以下の3つの方法を比較します:
    • ルートモジュールの切り替え
    • パラメーターファイルの切り替え
    • ワークスペースの切り替え
  • AWS、 GCP のインフラ管理を対象にします。
  • 構築するインフラや、ステートファイルの管理について以下の前提を置きます。この前提と異なる環境では各方法の評価は変わってきます。
    • 環境ごとに異なる AWS アカウント / GCP プロジェクトを使用する。
    • ステートファイルを置くバックエンドは S3 または GCS とする。また、インフラを構築する AWS アカウント / GCP プロジェクトと同じ場所に配置する。

背景

  • ある程度以上の規模のプロジェクトであれば、複数の環境をセットアップすることになります。例えば開発環境、ステージング環境(検証環境、検品環境とも)、本番環境(製品環境とも)など。この記事では、 dev, stg, prd の 3 環境を構築することを想定します。
  • Terraform を用いてインフラを管理する場合、複数の環境について以下の要件を満たす必要が出てきます。
    • 複数の環境で同一構成のインフラを構築できること。
    • 必要に応じて、インフラ構成の一部を環境ごとに変更できること。度合いはまちまちだけれど、パラメーターでちょっと変える程度の変更から、「この環境でだけこのリソースを作成する」などの変更まで様々。
    • コードが共通化されていること。DRY (Don't Repeat Yourself) の実現。
  • Terraform でそれをどのように実現するか、各実現方法にどのような違いがあるかなどをまとめます。

切り替えにおける課題事項

AWS アカウント / GCP プロジェクトの切り替え

リソースの作成場所は AWS であれば AWS アカウント、 GCP であれば GCP プロジェクトです。
(これを一般化した呼び方ってあるの?)

「開発環境用の AWS アカウント」「本番環境用の AWS アカウント」「開発環境用の GCP プロジェクト」「本番環境用の GCP プロジェクト」というように、環境ごとに AWS アカウント / GCP プロジェクトを別にする場合が多く、本記事ではその状況を前提とします。

このため環境を切り替えて Terraform を適用する場合には、適用先の AWS アカウント / GCP プロジェクトも切り替わる必要があります。
これをパラメーターとして切り替えの一部に含める必要があります。

バックエンドの切り替え

Terraform で構築したインフラの情報を保持するファイルを ステートファイル と言い、これを保存する場所を バックエンド と呼びます。

AWS では S3 + DynamoDB をバックエンドとして、GCP では GCS をバックエンドとして使用することができ、この記事ではそれを前提とします。
この他に HCP Terraform (Terraform Cloud) を使うこともでき、そうすると運用の様相は一変すると思いますが、使ったことないので本記事ではそれについては考慮しないことにします。

バックエンドの置き場所と環境をどう対応付けるかはプロジェクトによってまちまちですが、おそらく以下のどちらかになるでしょう:

  • 各環境にバックエンドを作る。
    • 環境のインフラ構築先と、バックエンドの置き場所を一緒にする。
    • つまり開発環境の AWS アカウント上のインフラを構築するときのステートファイルは開発環境の S3 バケットに置き、本番環境の AWS アカウント上のインフラを構築するときのステートファイルは本番環境の S3 バケットに置く。
  • 特定環境にだけバックエンドを作る。
    • バックエンドの置き場所を常に特定の環境にする。
    • 例えば開発環境の AWS アカウント上のインフラを構築するときも、本番環境の AWS アカウント上のインフラを構築するときも、ステートファイルは開発環境の S3 バケットに置く、など。
    • 開発環境にまとめるか、本番環境にまとめるかのバリエーションはある。

「各環境にバックエンドを作る」場合、環境を切り替えて Terraform を適用する際には、使用するバックエンドも切り替わる必要があります。

一方、「特定環境にだけバックエンドを作る」場合は、インフラ構築先とは別の AWS アカウント GCP プロジェクトへのアクセスが発生することとなり、それをどうやって実現するかの検討が必要となります。GCP であれば別プロジェクトへの権限設定が簡単なのであまり問題にならないのですが、AWS ではクロスアカウントのアクセス設定が複雑だったり限定的だったりするため扱いづらい問題になります。

本記事では、「各環境にバックエンドを作る」を前提とすることにします。
インフラの構築のためのアクセス権と、ステートファイル更新のためのアクセス権を一括で管理することができる運用的なメリットがあります。

バックエンドの指定は例えば以下のように行います (s3 を使用する場合):

terraform {
  backend "s3" {
    bucket         = "myproject-tfstate-dev"
    key            = "infra.tfstate"
    dynamodb_table = "tfstate-lock"
    
  }
}

S3 バケット名、 GCS バケット名はグローバルユニークなので、環境ごとに変更することになります。
一方、バックエンドの指定はパラメーター化することができません (参考: Using variables in terraform backend config block #13022 ):

terraform {
  backend "s3" {
    # これはできない(エラーになる)
    bucket         = "myproject-tfstate-${var.env}"
    key            = "infra.tfstate"
    dynamodb_table = "tfstate-lock"
    
  }
}

Terraform の環境切り替えの不便は基本的にはすべてこの仕様に由来します。
バックエンド設定と構築するインフラのパラメーター設定を共通化することが原理的に不可能であるということでもあるため、(今回の前提の範囲では) 環境切り替えは原理的に単なるワークアラウンドであり、完全な方法が存在しない という結果になります。
その想定を持っておいていただくと、どうやってもビミョーな Terraform の環境切り替えの方法を広い心で受け入れやすくなると思います。

認証情報の切り替え

Terraform を実行するにあたっては、クラウドサービスに対する権限を持った認証情報を設定しておく必要があります。
AWS であれば IAM ユーザーや IAM ロール、 GCP であればサービスアカウントに当たります。

環境を切り替えて Terraform を適用する際には、使用する認証情報も切り替えることが多いでしょう。
特に、 AWS では AWS アカウントを切り替える場合には認証情報の切り替えは実質必要不可欠になります。

認証情報は Terraform の実行以前に設定するものになるため、本記事で扱う環境切り替えの中に認証情報の切り替えについては含みません。
ただし実際に Terraform の運用を設計する際には、この部分も設計の検討項目に含まれることでしょう。

環境切替方法一覧

この記事では、以下の3つの切り替えの方法を対象に比較します:

各方法の比較表 (早見表)

ルートモジュールの切り替え パラメーターファイルの切り替え ワークスペースの切り替え
バックエンドの切り替え ○(種類の切り替えも可) △(バケットの切り替え不可)
バックエンドとパラメーターの整合性 ×(事故が起きうる)
環境ごとのワークディレクトリー × ―(不要)
DRY の達成
シンプルなリソース名 ×
シンプルなoutput ×
単一のロックファイル ×
  • ワークスペースの切り替えは今回の要件 (バケットの切り替え) を満たさないため、最終的に選択肢に入らない。
  • ルートモジュールの切り替え、パラメーターファイルの切り替えともに問題点があり、影響度合いの比較や、仕組みでカバーする方法の検討が必要。

各切り替え方法詳細

ここでは例として、AWS・GCP それぞれで以下のようなインフラを構築する Terraform を作成します
(VPC とかの付随する設定が必要なくて、作ってコストがかからないリソースを適当に選んだ):

  • AWS: DynamoDB テーブルを作成する。
  • GCP: Artifact Registry を作成する。

バックエンドに使用する S3 バケット、 GCS バケットは事前に手動で作成しておきます。
S3 バケット、GCS バケットについては、原則環境ごとに別にします (「各環境にバックエンドを作る」の方針)。ただしそれができない場合(ワークスペースの切り替えのこと)は dev 環境のバケットを複数の環境で共有します。

サンプルコードでは、AWS アカウント ID、 GCP プロジェクト ID、 S3 バケット、GCP バケット などのパラメーターを設定していないため、そのままでは動作しないことに注意してください。

ルートモジュールの切り替え

ルートモジュールの切り替えの実装方法

以下のようなディレクトリー構成にします (カッコ内はリポジトリーにはコミットされないファイル/ディレクトリー):

|
+- env/
|   +- dev/
|   |   +- main.tf
|   |   +- .terraform.lock.hcl
|   |   +- (.terraform/)
|   |
|   +- prd/
|   |   +- main.tf
|   |   +- .terraform.lock.hcl
|   |   +- (.terraform/)
|   |
|   +- stg/
|       +- main.tf
|       +- .terraform.lock.hcl
|       +- (.terraform/)
|
+- main.tf
+- outputs.tf
+- variables.tf
+- Makefile

env/ENV/main.tf ファイルは以下のような実装になっています:

module "main" {
  source = "../.."  # リポジトリールートを参照

  # 環境ごとのパラメーターをモジュールのパラメーターとして設定する。
  basename = "terraform-rootmodule"
  env      = "dev"
}

output "main" {
  value = module.main
}

この実装により、以下のように DRY を実現しています:

  • リポジトリールートにある Terraform テンプレートを main モジュールとして参照することで、Terraform テンプレートを環境間で共有する。
  • output で main モジュールの output 全体を参照させることで、新しい output の追加時には main モジュールだけ変更すればよいようにしている。
    • output は以下のようにネストした構造化データとして出力される:

      Outputs:
      
      main = {
        "dynamodb_table_arn" = "arn:aws:dynamodb:ap-northeast-1:ACCOUNTID:table/terraform-rootmodule-dev"
      }
      

ルートモジュールの切り替えの運用方法

Terraform の実行の際には、以下のように -chdir オプションを指定します:

terraform -chdir=env/dev apply

Makefile などのタスクランナーを使用して、以下のようにコマンドの呼び出しを簡略化するのが良いでしょう:

make apply ENV=dev

ルートモジュールの切り替えのメリット

  • バックエンド設定を完全に切り替えることができます。
    • 異なる S3 バケット・GCS バケットを使用できます。
    • 必要であれば、環境によって異なる種類のバックエンドを使用することもできます。dev 環境だけオンプレミスの共有ファイルサーバーを使用するなど。
      • そんな必要が生じるケースを見たことはないけれど。
  • ディレクトリーの変更でバックエンドとパラメーターの両方の切り替えを同時に行えます。このため、異なる環境のバックエンドの設定とパラメーターの設定の組み合わせで Terraform を実行してしまうことがありません。
    • -chdir パラメーターを使用しなくても、なんとなれば cd env/ENV して terraform apply などを呼び出すだけなので、実行方法としてもシンプル。
  • 各環境に terraform init で作成されるワークディレクトリーを維持できます。一度ローカル環境を dev, stg, prd それぞれについて terraform init で初期化しておけば、それ以降は各環境の terraform planterraform apply を任意のタイミングで実行できます。

ルートモジュールの切り替えのデメリット

  • バージョン関係の設定を何度も記述することになり、 DRY を達成できない。環境で変わることがない、ルートモジュールの以下の部分を環境ごとに繰り返し書くことになり、更に main モジュールでも同一の内容を書くことになります:

    terraform {
      required_version = ">= 1.9.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.56.0"
        }
      }
      ...
    
    • 環境ごとに設定の変更が必要な backend ブロックもこの terraform ブロックに一緒に含まれるため、「全環境で同一のパート」「環境ごとに異なるパート」をファイルで分けて分かりやすくする…ということもできないという困った問題もある。
  • リソース名が冗長になります。リソース名が module.main.aws_dynamodb_table.table のようになり、先頭に module.main が付加されます。

  • output が構造化されてしまう。output を参照する処理を書くときに jq などの JSON パーサーを使用するか、プログラムから参照する値については各環境に個別に output ブロックを書く必要が出てきます。

    • terraform output -json main を実行すると以下のような JSON フォーマットでの出力になる:

      {"dynamodb_table_arn":"arn:aws:dynamodb:ap-northeast-1:ACCOUNTID:table/terraform-rootmodule-dev"}
      
      • terraform output -raw main はエラーになる。
      • terraform output main.dynamodb_table_arn はエラーになる。
  • .terraform.lock.hcl が環境ごとに作成される。

    • プロバイダーのロックファイルは環境ごとに変わることがないので共通化したいですが、 env/ENV/.terraform.lock.hcl という具合に環境ごとに作成されます。
    • ロックファイルの位置を指定する機能はないので、これは制限として受け入れることになります。
    • ロックファイル自体どう扱うのが適切か自体が難しい問題ではありますが、タスクランナーで全環境の terraform providers lock を呼び出せるようにしておくなどの工夫で対処することになるでしょう。

パラメーターファイルの切り替え

パラメーターファイルの切り替えの実装方法

以下のようなディレクトリー構成にします (カッコ内はリポジトリーにはコミットされないファイル/ディレクトリー):

|
+- env/
|   +- dev/
|   |   +- backend.tfbackend
|   |   +- terraform.tfvars
|   |
|   +- prd/
|   |   +- backend.tfbackend
|   |   +- terraform.tfvars
|   |
|   +- stg/
|       +- backend.tfbackend
|       +- terraform.tfvars
|
+- (.terraform/)
+- .terraform.lock.hcl
+- main.tf
+- outputs.tf
+- variables.tf
+- Makefile

または、以下のように env/ ディレクトリー配下にフラットにファイルを置く配置も考えられます:

+- env/
|   +- dev.tfbackend
|   +- dev.tfvars
|   +- prd.tfbackend
|   +- prd.tfvars
|   +- stg.tfbackend
|   +- stg.tfvars
|

/env/ENV/backend.tfbackend の内容は、 /main.tf のバックエンド設定と対応づきます:

  • /main.tf:

    terraform {
      ...
      backend "s3" {
        # 具体的な設定パラメーターは init 時に与える
      }
    }
    
  • /env/ENV/backend.tfbackend:

    bucket         = "ACCOUNDID-tfstate-dev"
    key            = "parameterfile.tfstate"
    dynamodb_table = "tfstate-lock"
    

/env/ENV/terraform.tfvars の内容は、 /variables.tfvars の変数定義と対応づきます:

  • /variables.tfvars:

    variable "basename" {
      type        = string
      description = "構築するリソースの共通prefix"
    }
    
    variable "env" {
      type        = string
      description = "構築する環境名"
    }
    
    variable "account_id" {
      type        = number
      description = "使用するAWSアカウントID"
    }
    
  • /env/ENV/terraform.tfvars:

    basename   = "terraform-parameterfile"
    env        = "dev"
    account_id = ACCOUNTID
    

パラメーターファイルだけ環境別に作成することで、 DRY を実現しています。

パラメーターファイルの切り替えの運用方法

Terraform の実行の際には、各種コマンドの引数でパラメーターファイルを引き渡します:

  • terraform init

    terraform init -backend-config="env/dev/backend.tfbackend"
    
  • terraform plan / terraform apply

    terraform apply -var-file="env/dev/terraform.tfvars"
    

Makefile などのタスクランナーを使用して、以下のようにコマンドの呼び出しを簡略化するのが良いでしょう:

make init ENV=dev
make apply ENV=dev

パラメーターファイルの切り替えのメリット

  • バックエンド設定を完全に切り替えることができます。

  • 環境ごとに指定するのはパラメーターだけなので、高い度合いで DRY を実現できます。

  • リソース名・output 名がルートの Terraform テンプレートの記述どおりになり、シンプルな名称になります。

    • 「ルートモジュールの切り替え」との比較の観点。

    • 特に terraform output によって取得する値が簡単な構造になるので、他のプログラムを Terraform と連携させるのが簡単です。例:

      $ erraform output -raw dynamodb_table_arn
      arn:aws:dynamodb:ap-northeast-1:ACCOUNTID:table/terraform-parameterfile-dev
      $
      
  • 作成される .terraform.lock.hcl が 1 つだけです。

パラメーターファイルの切り替えのデメリット

  • 誤って別環境のバックエンド設定のまま terraform apply を実行してしまう事故が起こせてしまいます。
    • dev 環境の terraform init terraform apply をしたあとに、 (terraform init をせずに) stg 環境の terraform apply を行ってしまうなど。
    • 環境ごとに認証情報の切り替えが必要な AWS の場合は認証で失敗するので事故が起きづらいですが、環境横断で権限を持つことができる Google Cloud (GCP) では比較的事故を起こしやすくなります。
    • また、ある環境の terraform init 実行後に別環境の terraform init を実行すると、特にオプションを指定しないとバックエンドの設定の変更と認識されてエラーになります。-reconfigure オプションを指定するか、 .terraform/ ディレクトリーを削除してから terraform init を実行する必要があります。
      • エラーメッセージで -migrate-state-reconfigure かのどちらかを指定する必要がある旨が表示されますが、誤って -migrate-state を指定するとステートファイルを上書き削除が起きるためちょっとした事件になります。
      • うっかりに対して起きる被害が大きすぎるため、だいたいいつも手動で .terraform/ ディレクトリーを削除するようにしています。(特に、シェルスクリプトや Makefile で実行コマンドのテンプレ化が行われていないプロジェクトの場合)
  • Terraform のワークディレクトリーが同じ場所になるため、別環境の terraform planterraform apply を実行する前には必ず terraform init を実行する必要があります。
    • 複数環境の作業をまとめてローカルから行う場合に煩わしい。
    • 前項の設定切り替え忘れによる事故と一体の関係にあります。

ワークスペースの切り替え

ワークスペース機能の概要

ワークスペース機能 は Terraform 自体で提供されている環境切り替えのための機能です。
ワークスペース名を TF_WORKSPACE 環境変数で設定するか、 terraform workspace newterraform workspace select コマンドで指定することでワークスペースを切り替えることができます。

ワークスペースが切り替わると以下の変化が置きます:

  • バックエンドでのステートの保存先が変わる。
  • ${terraform.workspace} 変数にワークスペース名が設定される。

ステートの保存先は例として以下のように変わります:

バックエンド デフォルト ワークスペース設定時 備考
s3 s3://BUCKET/terraform.tfstate s3://BUCKET/:env/WORKSPACE/terraform.tfstate :env は設定で変更可能
gcs gs://BUCKET/default.tfstate gs://BUCKET/WORKSPACE.tfstate

ワークスペースの切り替えの実装方法

環境ごとのディレクトリーが必要なくなり、フラットな Terraform テンプレートになります (もちろん、さらにサブモジュールなどを追加しても良い)。
このため、以下のようなディレクトリー構成になります (カッコ内はリポジトリーにはコミットされないファイル/ディレクトリー):

|
+- (.terraform/)
+- .terraform.lock.hcl
+- main.tf
+- outputs.tf
+- variables.tf
+- Makefile

バックエンドの設定は以下のようになります:

  • AWS の場合

    terraform {
      ...
    
      backend "s3" {
        bucket = "ACCOUNTID-tfstate-dev"
        key    = "workspace.tfstate"
        dynamodb_table = "tfstate-lock"
      }
    }
    
    • ステートファイルは以下に作成されます:
      • dev: s3://ACCOUNTID-tfstate-dev/env:/dev/workspace.tfstate
      • stg: s3://ACCOUNTID-tfstate-dev/env:/stg/workspace.tfstate
      • prd: s3://ACCOUNTID-tfstate-dev/env:/prd/workspace.tfstate
  • GCP の場合

    terraform {
      ...
    
    
      backend "gcs" {
        bucket = "PROJECT-tfstate-dev"
        prefix = "workspace"
      }
    }
    
    • ステートファイルは以下に作成されます:
      • dev: gs://PROJECT-tfstate-dev/workspace/dev.tfstate
      • stg: gs://PROJECT-tfstate-dev/workspace/stg.tfstate
      • prd: gs://PROJECT-tfstate-dev/workspace/prd.tfstate

環境ごとのパラメーターを以下のようにローカル変数で用意しておき、ワークスペース名で切り替えを行います。環境ごとの相違をこのローカル変数のみに集約することで DRY を実現します:

locals {
  env_configs = {
    dev = {
      account_id = ACCOUNTID1
    }
    stg = {
      account_id = ACCOUNTID2
    }
    prd = {
      account_id = ACCOUNTID3
    }
  }
}

locals {
  env        = terraform.workspace
  account_id = lookup(local.env_configs, local.env, local.env_configs["dev"]).account_id
}

provider "aws" {
  allowed_account_ids = [local.account_id]
}

ワークスペースの切り替えの運用方法

Terraform の apply / plan 実行の際には、 TF_WORKSPACE 環境変数を設定して呼び出します:

TF_WORKSPACE=dev terraform apply

Makefile などのタスクランナーを使用して、以下のようにコマンドの呼び出しを簡略化するのが良いでしょう:

make apply ENV=dev

ワークスペースの切り替えのメリット

  • TF_WORKSPACE の指定でバックエンドとパラメーターの両方を同時に切り替えられる(ように設計できる)ため、異なる環境のバックエンドの設定とパラメーターの設定の組み合わせで Terraform を実行してしまうことがありません。
  • terraform init で作成されるワークディレクトリーを共有できます。一度ローカル環境を dev, stg, prd のどれかで terraform init で初期化しておけば、それ以降は各環境の terraform planterraform apply を任意のタイミングで実行できます。
  • 環境ごとに指定するのはパラメーターだけなので、かなり高い度合いで DRY を実現できます。
  • リソース名・output 名がルートの Terraform テンプレートの記述どおりになり、シンプルな名称になります。
  • 作成される .terraform.lock.hcl が 1 つだけです。

ワークスペースの切り替えのデメリット

  • バックエンド設定の切り替えが限定的。
    • s3 や gcs の場合、切り替わるのはステートファイルのバケット内のパス(キー)のみで、バケットそのものを切り替えることができません。
    • 多くの場合、これは運用の要件を満たさないでしょう。
    • 特に AWS の場合、インフラの構築場所の AWS アカウントと S3 バケットのある AWS アカウントでのクロスアカウントでの運用になるため認証周りはかなり面倒です。
      • assume_role の設定を行った認証を行うのが一番簡単だと思う。
      • dynamodb_table パラメーターがアカウントを指定する方法を提供しないため、 assume_role を使わない場合はアカウントごとにテーブルを作成するという妙なセットアップになる。
    • インフラの構築場所とバックエンドの場所を完全に別にする設計であれば、この制限は悪影響を及ぼさないのでワークスペースでの切り替えはかなり現実的な選択肢になります。
      • インフラの構築は AWS アカウント上、バックエンドは GCP 上の GCS にするなど。
      • HCP Terraform (Terraform Cloud) を使えば自然にこの状態になりそうな気がしますが、使ったことがないのでよく知らない。

Discussion