0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform で prod / stg / dev をどう分けるか workspace・tfvars・ディレクトリ分割の 3 方式

0
Posted at

はじめに

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
modules/app_bucket/main.tf
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)。

a-workspace/main.tf
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 に置き、環境ごとに切り替えます。

b-tfvars/main.tf
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 のバケットが消えるところでした。

スクリーンショット 2026-10-04 16.46.21.png

手順 4:C. 環境ごとにディレクトリを分ける(c-dirs/)

最後は、環境ごとにディレクトリを分け、モジュールを呼ぶだけの小さな main.tf を置く 方式です。backend も環境ごとに持ちます。

c-dirs/dev/main.tf
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 ごと分ける方式 を選び、迷ったら ディレクトリ分割 が安全かと思います
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?