はじめに
Terraform で本番(prod)、検証(stg)、開発(dev)の 3 環境を管理するとき、「同じコードを、環境ごとに別々に apply したい」 という場面はよくあります。
よく使われるのは次の 3 つの方式です。
| A. workspace | B. tfvars + backend 切り替え | C. 環境ごとにディレクトリを分ける | |
|---|---|---|---|
| コード | 1 つ | 1 つ | 環境ごとに小さな main.tf
|
| 環境の切り替え | terraform workspace select |
-var-file と -backend-config
|
cd するだけ |
この記事では、3 方式で同じ S3 バケットを dev / stg / prod に作り、state がどこに分かれるか と、切り替えを忘れると何が起きるか を確かめます。state は実際の運用と同じく S3 に置きます(平文でシークレットが入ることもあるので、Git には含めません。Manage sensitive data)。
3 方式のいちばんの違いは、state を置くバケットの分け方です。
やってみた
手順 0:state 用の S3 バケットと共通のモジュールを作る(modules/app_bucket/main.tf)
まず、state を置く S3 バケットを AWS CLI で作ります。
方式 A 用の共用バケット(ws)1 つと、方式 B・C 用の環境ごとのバケット 3 つです。
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
for name in ws dev stg prod; do
aws s3api create-bucket --bucket tf-env-state-$name-$ACCOUNT_ID \
--region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1
done
次に、3 方式で作るバケットをモジュールにしておきます。prod のときだけバージョニングを有効にする という「環境ごとの差」を 1 つだけ持たせます。
mkdir -p tf-env-demo/modules/app_bucket && cd tf-env-demo
variable "name" {
type = string
}
variable "env" {
type = string
}
variable "enable_versioning" {
type = bool
default = false
}
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket" "this" {
bucket = "${var.name}-${var.env}-${data.aws_caller_identity.current.account_id}"
force_destroy = true
}
resource "aws_s3_bucket_versioning" "this" {
count = var.enable_versioning ? 1 : 0
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
以降の手順は tf-env-demo ディレクトリで、ACCOUNT_ID を設定した同じターミナルで実行します。
手順 1:A. workspace で分ける(a-workspace/main.tf)
workspace は、1 つの作業ディレクトリの中に state を複数持てる仕組みです。コードの中では、今の workspace 名を terraform.workspace で参照できます(Workspaces)。
terraform {
backend "s3" {
key = "a-workspace/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
module "app" {
source = "../modules/app_bucket"
name = "tf-env-ws"
env = terraform.workspace
enable_versioning = terraform.workspace == "prod"
}
cd a-workspace
terraform init -backend-config="bucket=tf-env-state-ws-$ACCOUNT_ID"
for env in dev stg prod; do
terraform workspace select -or-create $env
terraform apply -auto-approve
done
aws s3 ls s3://tf-env-state-ws-$ACCOUNT_ID --recursive
cd ..
バケット名はアカウント ID を含むので、backend には書かずに init で渡しています(Partial configuration)。
use_lockfile = true は、同時に apply されないようにロックをかける設定です。
aws s3 ls で、次のように 3 つの state が出れば OK です。 3 環境を作れましたが、1 つのバケットの中でフォルダが分かれているだけです。
tf-env-state-ws-<アカウントID>/
└── env:/
├── dev/a-workspace/terraform.tfstate
├── stg/a-workspace/terraform.tfstate
└── prod/a-workspace/terraform.tfstate
保存先が 1 つなので、dev を触れる人は prod の state にも触れます。 これが workspace の弱点です。
手順 2:B. tfvars と backend を切り替えて分ける(b-tfvars/)
コードは 1 つにして、環境ごとの値を envs/<環境名>/terraform.tfvars に置く 構成です(AWS の Terraform のベストプラクティスの例)。state のバケット名も envs/<環境名>/backend.hcl に置き、環境ごとに切り替えます。
terraform {
backend "s3" {
key = "b-tfvars/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
variable "env" {
type = string
}
variable "enable_versioning" {
type = bool
default = false
}
module "app" {
source = "../modules/app_bucket"
name = "tf-env-vars"
env = var.env
enable_versioning = var.enable_versioning
}
環境ごとのファイルは、次のコマンドでまとめて作ります。
cd b-tfvars
for env in dev stg prod; do
mkdir -p envs/$env
echo "bucket = \"tf-env-state-$env-$ACCOUNT_ID\"" > envs/$env/backend.hcl
echo "env = \"$env\"" > envs/$env/terraform.tfvars
done
echo "enable_versioning = true" >> envs/prod/terraform.tfvars
環境を切り替えるたびに、init -reconfigure で backend を、-var-file で値を 切り替えます。
for env in dev stg prod; do
terraform init -reconfigure -backend-config=envs/$env/backend.hcl
terraform apply -auto-approve -var-file=envs/$env/terraform.tfvars
done
for env in dev stg prod; do
aws s3 ls s3://tf-env-state-$env-$ACCOUNT_ID --recursive
done
3 つのバケットそれぞれに b-tfvars/terraform.tfstate が出れば OK です。 今度は state がバケットごと分かれました。
手順 3:切り替え忘れを試す(b-tfvars/)
方式 B は、init と -var-file を毎回そろえる必要があります。 手順 2 は prod で終わったので、今は prod の state につながっています。ここで dev の値だけを渡してみます。
terraform plan -var-file=envs/dev/terraform.tfvars
cd ..
prod のバケットに must be replaced(作り直し)と表示されます。 apply していたら、prod のバケットが消えるところでした。
手順 4:C. 環境ごとにディレクトリを分ける(c-dirs/)
最後は、環境ごとにディレクトリを分け、モジュールを呼ぶだけの小さな main.tf を置く 方式です。backend も環境ごとに持ちます。
terraform {
backend "s3" {
key = "c-dirs/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
module "app" {
source = "../../modules/app_bucket"
name = "tf-env-dirs"
env = "dev"
enable_versioning = false
}
stg と prod は dev をコピーして書き換え、prod だけ enable_versioning を true にします。
mkdir -p c-dirs/stg c-dirs/prod
sed 's/"dev"/"stg"/' c-dirs/dev/main.tf > c-dirs/stg/main.tf
sed 's/"dev"/"prod"/; s/= false/= true/' c-dirs/dev/main.tf > c-dirs/prod/main.tf
for env in dev stg prod; do
(cd c-dirs/$env && terraform init -backend-config="bucket=tf-env-state-$env-$ACCOUNT_ID" && terraform apply -auto-approve)
done
for env in dev stg prod; do
aws s3 ls s3://tf-env-state-$env-$ACCOUNT_ID --recursive
done
各バケットに c-dirs/terraform.tfstate が増えていれば OK です。 state と値の組み合わせがディレクトリごとに固定されるので、手順 3 のような食い違いは起きにくくなります。
手順 5:後片付け
作ったバケットをすべて削除します。state のバケットは、destroy できなくなるので最後に消します。
(cd a-workspace && for env in dev stg prod; do terraform workspace select $env && terraform destroy -auto-approve; done)
(cd b-tfvars && for env in dev stg prod; do terraform init -reconfigure -backend-config=envs/$env/backend.hcl && terraform destroy -auto-approve -var-file=envs/$env/terraform.tfvars; done)
for env in dev stg prod; do (cd c-dirs/$env && terraform destroy -auto-approve); done
for name in ws dev stg prod; do aws s3 rb s3://tf-env-state-$name-$ACCOUNT_ID --force; done
すべて Destroy complete! と表示され、最後に remove_bucket が 4 つ出れば完了です。
どれを選ぶべきか
公式ドキュメントでは、次のように書かれています。
- AWS の Backend best practices:環境ごとに backend を分ける ことを勧めている(workspace より分離が強いため)
- HashiCorp の Workspaces:環境ごとに権限を分けたいなら workspace は向かない とし、方式 C の形を案内している
これをふまえた使い分けは次のとおりです。
| 方式 | おすすめの場面 |
|---|---|
| A. workspace | 機能ブランチごとの一時的な検証環境など、作っては消す環境 |
| B. tfvars + backend | 環境の差が値だけで、CI/CD から決まった手順で実行する とき |
| C. ディレクトリ分割 | prod を含む本番運用。環境ごとに AWS アカウントや IAM ロールを変えたいとき |
まとめ
- 3 方式のいちばんの違いは state をどこまで分けるか です。workspace は 1 つのバケットの中で分かれるだけです
- 方式 B は手軽ですが、切り替えを忘れると別の環境が壊れる ことがあります
- 本番を含むなら backend ごと分ける方式 を選び、迷ったら ディレクトリ分割 が安全かと思います
