HCP Terraform の環境分離: stg と prod の workspace の設定

stg への変更はマージしたらそのまま反映し、prod への変更は人が確認してから反映したい。
HCP Terraform では、環境ごとに workspace を作り、同じリポジトリにつなぐ。マージで始まった run を apply まで進めるか、承認を待つかは、workspace ごとに決められる。
この記事では、stg の workspace がつながっているリポジトリに prod の workspace を追加して、次の 3 つを確かめる。
- 1 回のマージで stg は apply まで進み、prod は plan で止まること
- 2 つの workspace で同じ値を使う変数を variable set で共有すること
- 同じ
.tfのまま、変数の値で stg にだけリソースを作ること
前提
次の状態から始める。(「HCP Terraform の GitHub 連携: VCS-driven workflow の設定」を参照)
- workspace
tfc-study-stgが GitHub のリポジトリの main ブランチにつながっている - この workspace の auto-apply は有効
リポジトリの .tf は、S3 バケットを 1 つ定義している。
resource "random_pet" "name" {
length = 2
}
resource "aws_s3_bucket" "study" {
bucket = "tfc-study-${var.environment}-${random_pet.name.id}"
tags = {
Environment = var.environment
Owner = var.owner
ManagedBy = "terraform"
}
}
環境を workspace で分ける仕組み
.tf は環境ごとに分けない。同じリポジトリの同じブランチに、環境の数だけ workspace をつなぐ。
環境ごとに違うものは workspace が持つ。
| workspace が持つもの | stg の workspace | prod の workspace |
|---|---|---|
| state | stg のリソース | prod のリソース |
Terraform variable environment |
stg |
prod |
| auto-apply | 有効 | 無効 |
ブランチにマージすると、つながっている workspace のそれぞれで run が始まる。run は、その workspace の state と変数を使う。
設定するもの
HCP Terraform に 2 つ、AWS に 1 つ設定する。
- prod の workspace
- stg と同じリポジトリの同じブランチにつなぐ
- auto-apply は無効のままにする
- variable set
- 変数をまとめて名前を付けたもの。workspace の外にあり、複数の workspace に適用できる
- AWS の認証に使う変数を入れて、2 つの workspace に適用する
- IAM ロールの信頼ポリシー
- run が AWS を操作するときに引き受けるロールに付いている条件
- prod の workspace の run もロールを引き受けられるように、条件を追加する
prod 用の state と変数を持たせるために workspace を作る
HCP Terraform の画面で New → Workspace を押し、次の順に設定する。
- workflow に Version Control Workflow を選ぶ
- stg の workspace と同じリポジトリを選ぶ
- workspace の名前を
tfc-study-prodにして Create workspace を押す
作った直後に Configure Terraform variables のページが表示される。HCP Terraform がリポジトリの .tf を読み、default のない変数の入力欄を並べている。
この記事の .tf では environment と owner の 2 つ。environment には prod を入力する。
Terraform のバージョンを stg に合わせる
workspace が使う Terraform のバージョンは、workspace の設定で決まる。画面から作った workspace は、作った時点の最新のバージョンになる。
stg の workspace と違うバージョンになっていたので、Settings → General の Terraform Version を stg と同じ値に変えた。
auto-apply は同じページにある。既定は無効で、prod ではそのままにする。
AWS の認証の変数を共有するために variable set を作る
run が AWS を操作するための Environment variable は、どちらの workspace でも同じ値を使う。
TFC_AWS_PROVIDER_AUTHTFC_AWS_RUN_ROLE_ARN
workspace ごとに同じ値を置くと、ロールを変えるときに workspace の数だけ直すことになる。
variable set に置くと、値は 1 か所で済む。
organization の Settings → Variable sets から作る。決めるものは 3 つ。
- 名前
- この記事では
tfc-study-aws
- この記事では
- 適用する範囲
- organization のすべての workspace、project、選んだ workspace の 3 通りから選べる
- この記事では stg と prod の 2 つの workspace を選んだ
- 変数
- 上の Environment variable を 2 つ入れた
適用された variable set は、workspace の Variables のページの下に表示される。

ページの上の Workspace variables には、環境ごとに値が違う Terraform variable だけが残っている。
同じ Key の変数が workspace と variable set の両方にあると、workspace の変数が使われる。variable set に入れた変数は、workspace には置かない。
stg と prod で IAM ロールを分ける場合、TFC_AWS_RUN_ROLE_ARN は workspace ごとに違う値になる。
その場合は variable set に入れず、workspace の変数として置く。
prod の run が AWS を操作できるように信頼ポリシーに workspace を追加する
新しく作った VCS-driven workflow の workspace は、最初の run を画面から始める必要がある。
run が 1 つもない workspace は、PR やマージの通知が届いても run を始めない。
workspace tfc-study-prod で New run から run を始めると、plan がエラーで終わった。
Error: No valid credential sources found
with provider["registry.terraform.io/hashicorp/aws"]
on providers.tf line 1, in provider "aws":
Error: failed to refresh cached credentials, failed to retrieve credentials, operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 403, ... api error AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
AssumeRoleWithWebIdentity は、token を見せて IAM ロールを引き受ける AWS の操作。token は、HCP Terraform が run ごとに発行する身元の証明。
HCP Terraform はロールを引き受けようとして、AWS に断られている。
IAM ロールの信頼ポリシーが、stg の workspace の run だけを許しているため。
信頼ポリシーに prod の workspace を追加する
AWS のコンソールで IAM ロールの信頼ポリシーを開き、app.terraform.io:sub の条件に prod の workspace の行を追加する。
"StringLike": {
"app.terraform.io:sub": [
"organization:<organization>:project:Default Project:workspace:tfc-study-stg:run_phase:*",
"organization:<organization>:project:Default Project:workspace:tfc-study-prod:run_phase:*"
]
}
値を配列にすると、どれか 1 つに一致する token が許される。
workspace:tfc-study-* のように * で 1 行にまとめる書き方もある。
その書き方では、名前が tfc-study- で始まる workspace を誰かが作ると、その run もロールを引き受けられる。
この記事では workspace を 1 つずつ書いた。
prod の最初の run は plan で止まる
もう一度 run を始めると、plan が成功した。
- plan は
2 to add。random_pet.nameと S3 バケットを新しく作る - run は Planned で止まり、Confirm & Apply のボタンが表示される。auto-apply が無効なため
prod の workspace の state は空なので、同じ .tf から prod 用のリソースが新しく作られる。stg のバケットは plan に出てこない。
Confirm & Apply を押すと apply が実行され、prod のバケットができた。
$ aws s3 ls | grep tfc-study
2026-11-15 10:29:56 tfc-study-prod-engaging-buzzard
2026-11-08 11:31:53 tfc-study-stg-national-starfish
環境ごとの差分をマージ前に確かめるために PR を出す
バケットにタグを 1 つ追加する変更で、main 宛ての PR を作る。
tags = {
Environment = var.environment
Owner = var.owner
ManagedBy = "terraform"
Project = "tfc-study"
}
PR を作ると、plan だけの run が workspace ごとに 1 つずつ始まる。PR のチェックの欄のリンクを開くと、結果が workspace ごとに並んでいる。

- stg と prod のどちらも
0 to add, 1 to change, 0 to destroy - 行の右の View run details から、その workspace の run のページを開ける
マージしたときに何が起きるかを、環境ごとに確かめられる。
マージして stg と prod の run の進み方を比べる
PR をマージすると、2 つの workspace でそれぞれ run が始まる。
| workspace | auto-apply | run の進み方 |
|---|---|---|
tfc-study-stg |
有効 | plan のあと、そのまま apply に進んで Applied で終わる |
tfc-study-prod |
無効 | plan のあと、Planned で止まる |
prod の run のページは次のとおり。

- 状態は Planned。plan は終わり、apply は Apply pending のまま始まっていない
- Confirm & Apply と Discard run のボタンが表示される
この時点で、2 つのバケットのタグの Key を確かめる。
$ aws s3api get-bucket-tagging --bucket tfc-study-stg-national-starfish --query 'TagSet[].Key'
[
"Project",
"Environment",
"Owner",
"ManagedBy"
]
$ aws s3api get-bucket-tagging --bucket tfc-study-prod-engaging-buzzard --query 'TagSet[].Key'
[
"Environment",
"Owner",
"ManagedBy"
]
stg のバケットには Project が追加されている。prod のバケットにはまだない。
main には同じコミットが入っているが、prod のバケットは Confirm & Apply を押すまで変わらない。押すと apply が実行され、prod のバケットにも Project が追加される。
Discard run は、止まっている run を apply せずに終わらせる。
main のコミットは残るので、次の run の plan に同じ変更がもう一度出る。変更を取りやめるときは、リポジトリでコミットを revert する。
環境ごとにリソースを出し分けるために bool の変数を使う
新しいリソースを、まず stg にだけ作りたいことがある。
.tf を環境ごとに分けずにこれを行うには、作るかどうかを Terraform variable で受け取る。値は workspace ごとに決める。
変数とリソースの定義
アーカイブ用の S3 バケットを追加する。作るかどうかは、bool の変数 enable_archive_bucket で受け取る。
variable "enable_archive_bucket" {
description = "アーカイブ用の S3 バケットを作るかどうか"
type = bool
default = false
}
resource "aws_s3_bucket" "archive" {
count = var.enable_archive_bucket ? 1 : 0
bucket = "tfc-study-${var.environment}-${random_pet.name.id}-archive"
tags = {
Environment = var.environment
Owner = var.owner
ManagedBy = "terraform"
Project = "tfc-study"
}
}
countは、リソースを作る数。0のときは作らないvar.enable_archive_bucket ? 1 : 0は、変数がtrueなら1、falseなら0になるdefaultがfalseなので、値を置いていない workspace ではバケットは作られない
マージしただけではどの環境にも作られない
この変更を PR でマージすると、2 つの workspace で run が始まる。
どちらの run も 0 to add, 0 to change, 0 to destroy で、Planned and finished で終わった。
apply するものがないので、prod の run も承認を待たない。
バケットの定義は main に入ったが、どちらの workspace も enable_archive_bucket に値を置いていない。
stg の workspace の変数を true にする
stg の workspace の Variables に、Terraform variable を 1 つ追加する。
| Key | Value |
|---|---|
enable_archive_bucket |
true |
変数を追加しただけでは run は始まらない。stg の workspace で New run から run を始める。
![stg の workspace の run のページ。名前は Triggered via UI、状態は Applied。Plan finished は 1 to add で aws_s3_bucket.archive[0] が作成対象、Apply finished は 1 added で Created と表示されている](/images/posts/369/04.webp)
- plan は
1 to add。aws_s3_bucket.archive[0]が作られる - リポジトリの
.tfは変えていない。変えたのは workspace の変数だけ - stg は auto-apply が有効なので、そのまま Applied で終わる
AWS のバケットを確かめると、stg にだけアーカイブ用のバケットができている。
$ aws s3 ls | grep tfc-study
2026-11-15 10:29:56 tfc-study-prod-engaging-buzzard
2026-11-08 11:31:53 tfc-study-stg-national-starfish
2026-11-15 11:02:58 tfc-study-stg-national-starfish-archive
prod にも作るときは、prod の workspace に同じ変数を追加して run を始め、Confirm & Apply を押す。
全体像
2 つの workspace の違いと、その違いを決めているものを並べる。
| stg の workspace | prod の workspace | 決めているもの | |
|---|---|---|---|
run が使う .tf |
リポジトリの main | リポジトリの main | VCS connection |
| マージで始まる run | apply まで進む | plan で止まり、承認を待つ | auto-apply |
| バケットの名前 | tfc-study-stg-... |
tfc-study-prod-... |
Terraform variable environment |
| アーカイブ用のバケット | 作る | 作らない | Terraform variable enable_archive_bucket |
| AWS の認証 | IAM ロール | 同じ IAM ロール | variable set と信頼ポリシー |
1 回のマージで動いたものは次のとおり。
flowchart LR
me[自分]
subgraph gh[GitHub のリポジトリ]
main["main ブランチ"]
end
subgraph hcp[HCP Terraform]
vs["variable set<br>TFC_AWS_PROVIDER_AUTH<br>TFC_AWS_RUN_ROLE_ARN"]
subgraph stg["workspace: tfc-study-stg<br>auto-apply 有効"]
runS[["run<br>plan → apply"]]
end
subgraph prod["workspace: tfc-study-prod<br>auto-apply 無効"]
runP[["run<br>plan → 承認待ち → apply"]]
end
end
subgraph aws[AWS]
role["IAM ロール<br>信頼ポリシーに 2 つの workspace"]
bS[("stg の S3 バケット")]
bP[("prod の S3 バケット")]
end
me -- "マージする" --> main
main -- "通知" --> runS
main -- "通知" --> runP
me -- "Confirm & Apply を押す" --> runP
vs -- "変数" --> runS
vs -- "変数" --> runP
runS -- "引き受ける" --> role
runP -- "引き受ける" --> role
runS -- "タグを追加する" --> bS
runP -- "タグを追加する" --> bP
図から読み取れることは 3 つ。
- リポジトリは 1 つで、1 回のマージから run が workspace の数だけ始まる
- HCP Terraform の画面を操作するのは、prod の run を承認するときだけ
- stg と prod の違いは
.tfにはない。workspace の auto-apply と変数にある
まとめ
- 環境ごとに workspace を作り、同じリポジトリの同じブランチにつなぐ。state と変数は workspace ごとに別になる
- マージで始まる run は、auto-apply が有効な workspace では apply まで進む
- auto-apply が無効な workspace では plan で止まり、Confirm & Apply を押すと apply が実行される
- 複数の workspace で同じ値を使う変数は variable set に置く。同じ Key の変数が workspace にあると、workspace の変数が使われる
- workspace を追加したら、IAM ロールの信頼ポリシーにもその workspace を追加する
boolの変数とcountを組み合わせると、同じ.tfのまま、リソースを作るかどうかを workspace の変数で決められる
参考 URL
- [official] Create workspaces in HCP Terraform | Terraform | HashiCorp Developer
- [official] UI and VCS-driven run workflow in HCP Terraform | Terraform | HashiCorp Developer
- [official] Workspaces settings in HCP Terraform | Terraform | HashiCorp Developer
- [official] Variables in HCP Terraform | Terraform | HashiCorp Developer
- [official] Manage variables and variable sets in HCP Terraform | Terraform | HashiCorp Developer
- [official] Use dynamic credentials with the AWS provider in HCP Terraform | Terraform | HashiCorp Developer
- [official] count meta-argument reference | Terraform | HashiCorp Developer