Terraform Stacks
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A single Terraform configuration is easy to reason about until the same system has to exist several times: a development
and a production environment, two regions each, perhaps a separate cloud account for production. The usual answers are
copies of a root module, one HCP Terraform workspace per copy, or a wrapper such as Terragrunt, and each of them leaves
you responsible for the order in which the copies are applied and for keeping their inputs aligned. Terraform Stacks
are HCP Terraform’s own answer: you describe the system once as a set of components (each wrapping a module), then say
how many times to deploy it, and HCP Terraform plans, orders and applies the deployments as one unit. This page
explains the model, both file types, authentication, deployment groups, linking Stacks to each other and the
terraform stacks command line, and builds a Bookshelf Stack with a network, a database and a service component
deployed to dev and prod in several regions.
The baseline is Terraform 1.17 (1.17.0 was released on October 21, 2026), checked against the Stacks documentation on
developer.hashicorp.com (the language pages, the terraform stacks CLI pages and the HCP Terraform Stacks pages). Stacks
are an HCP Terraform (and Terraform Enterprise) feature, so you cannot run them with the open-source CLI alone; see
Requirements, availability and status. The page builds on
Modules (a component sources a module),
Providers (Stacks declare providers differently) and
HCL Syntax and Configuration Files (the file types), and
it is the alternative to the layouts in
Workspaces, Environments and Project Structure.
The platform itself is described in HCP Terraform and Terraform Enterprise,
and the automation around it in CI/CD and Automation.
Source: Terraform Docs — Stacks overview.
The problem Stacks solve
The official overview names the situations in which Stacks are meant to help: infrastructure that is too large to manage as a single configuration or cannot be planned in a single operation, infrastructure that shares a common lifecycle, interconnected services that pass information back and forth without sharing a lifecycle, infrastructure repeated across environments, regions or accounts that must stay consistent and synchronized, and tightly orchestrated infrastructure that changes differently in each environment.
Seen from the Bookshelf scenario, three problems recur once catalog-api, reviews-api and web exist in more than one
place.
-
Many copies of the same configuration.
devandprod, each in two regions, are four instances of one design. With workspaces that is four workspaces whose variables must be kept aligned by hand or by thetfeprovider; with directories it is four directories that drift. -
Order and dependencies. The service needs the database address, the database needs the network. Inside one configuration Terraform orders this itself, but across workspaces it takes run triggers or a pipeline. Some changes cannot even be planned before others are applied (a Kubernetes custom resource needs its definition to exist first), and the overview describes Stacks as deferring such plans automatically until they can complete.
-
Rolling out a change safely. After editing the module,
devshould apply on its own andprodshould wait for a person, and a failure in the first region should stop the rest. That policy lives in pipeline code when you use workspaces; Stacks make it part of the configuration.
Stacks replace the traditional root module with a component-based configuration, and the documentation calls them an
alternative to workspaces that can coexist with them in the same project. Each deployment of a Stack has its own isolated
state, so a change applied to dev cannot alter prod.
The model: components, deployments and the two file types
A Stack is described by two kinds of files placed in the root directory of its repository. HCP Terraform reads all the files of each kind and processes their blocks in dependency order, so you can split them freely.
| File | Defines | Blocks |
|---|---|---|
|
What the Stack is: the components, their inputs and the providers they use. |
|
|
How the Stack is repeated: the deployments, their input values, authentication and rollout rules. |
|
The vocabulary is small.
-
A component is one module of the Stack, with its inputs and the provider configurations it receives. All components of a Stack share a lifecycle: they are planned and applied together.
-
A deployment is one instance of the whole Stack, created by a
deploymentblock that supplies the values of the Stack’s input variables.devandprodare two deployments of the same components. Each deployment has its own state. -
A deployment group gathers deployments so that rules (auto-approval, eager planning, failure tolerance) apply to all of them.
-
A deployment run is the sequence of plan and apply steps needed to make a deployment match its configuration. A deployment may need several plan-and-apply cycles when some resources cannot be planned before others exist.
-
A configuration version is a snapshot of the component and deployment files; each new version starts a deployment run for every deployment.
The diagram shows the shape of the Bookshelf Stack developed below: three components defined once, fanned out to deployments that each keep their own state.
Requirements, availability and status
What the documentation says, and nothing more, as of the baseline:
-
Where they run. Stacks require HCP Terraform. The
terraform stacks createpage and the run documentation are explicit that you can create a Stack from the CLI but cannot deploy it locally: a Stack is deployed remotely in HCP Terraform, or by a custom HCP Terraform agent. Terraform Enterprise is also documented (setTF_STACKS_HOSTNAMEfor the CLI), and the Enterprise documentation notes that using Stacks there requires the resources-under-management (RUM) pricing model. -
CLI. Terraform 1.13 or later provides the Stacks CLI plugin and the
terraform stackscommands. The Terraform 1.13 changelog introduces the command. The beta-era separateterraform-stacks-cliis deprecated and no longer supports current functionality. -
Maturity. The overview page describes Stacks as generally available and contains a migration guide, "Update for general availability", for users of the beta. The GA release changed the file extension, the deployment-group syntax and billing (see What changed recently). The documentation does not state a calendar date for general availability, so this page does not either.
-
Plans and pricing. Stacks are not available on legacy HCP Terraform team plans. Resources managed by Stacks count toward resources under management together with workspace resources, and the usage page shows billable Stack resources and applies. Running orchestration rules for deployment groups (the auto-approve rules, eager plan and failure tolerance) is marked as available in the HCP Terraform Plus Edition. The documentation points to the HCP Terraform pricing page for prices, and this page quotes none.
-
Limits. Up to 20 deployments, 100 components and 10,000 resources per Stack; a Stack can link to up to 20 upstream Stacks and expose values to up to 25 downstream Stacks. Concurrency is shared with workspaces: Stack operations use the same agent pool and queue, so the plan’s concurrency limit covers the sum of both.
-
Permissions. Stacks do not have separate permissions; they inherit the project’s permissions.
-
OpenTofu. The official Stacks documentation describes an HCP Terraform feature only. This page does not cover any OpenTofu equivalent.
Designing a Stack
The design guidance in the documentation is short and worth following before writing any file.
-
A Stack represents one system or application with a shared lifecycle. Split Stacks along technical boundaries: a shared networking Stack (VPCs, subnets, routing) and separate application Stacks that consume it.
-
Add a
componentblock for every top-level module. Child modules of those modules need no further blocks. -
Decide what varies between deployments (environment, region, account) and make exactly those things Stack variables.
-
Use
localsto share values andfor_eachon components and providers to scale to more regions. -
Put a
.terraform-versionfile in the Stack’s root directory with the Terraform version the Stack needs (the documentation’s example contains1.13.0).
For Bookshelf the system boundary is the platform of one environment: network, database and service. The covers bucket,
DNS and IAM that other pages manage could be further components or a separate Stack, and a shared network used by several
applications is the textbook case for splitting into two Stacks linked as shown in Linking Stacks: publish_output and upstream_input.
Source: Terraform Docs — Design a Stack.
Component configuration (*.tfcomponent.hcl)
The component configuration replaces the root module. It uses HCL like any other Terraform file, but its blocks differ from
those of a .tf file, and the supported functions are a subset of the built-in functions (modules referenced by a
component support all of them). Run terraform stacks fmt on these files, not plain terraform fmt.
The component block
component "network" {
source = "./modules/network"
inputs = {
name = "bookshelf-${var.environment}"
cidr_block = var.cidr_block
}
providers = {
aws = provider.aws.this
}
}
The reference lists these arguments, of which source, inputs and providers are required:
| Argument | Meaning |
|---|---|
|
The module to use, such as a local path, a registry module or a Git address. |
|
A version constraint when the source is a registry module. |
|
A map of module input variable names to values. Values may reference Stack variables, other components, locals and providers' outputs. |
|
A map from the provider names the module expects to provider configurations declared in the Stack
( |
|
A list of references to other components for dependencies that references do not reveal. |
|
A map or set that creates one instance of the component per element, reachable as |
Referring to an output of another component (component.network.vpc_id) creates a dependency, exactly as references
between resources do in a configuration. When a component needs an attribute that is not yet known, HCP Terraform defers its
plan until it can complete, and the run then needs more than one plan-and-apply cycle.
Variables, outputs and locals
variable blocks declare the inputs that each deployment sets. They look like ordinary variables and add one
Stacks-specific use: ephemeral = true marks values such as identity tokens that must not be stored in state.
output blocks expose values from the Stack to the HCP Terraform UI and make them available to the deployment
configuration, which is how a value reaches publish_output. locals define reusable expressions.
variable "environment" {
type = string
}
variable "regions" {
type = set(string)
}
variable "cidr_block" {
type = string
}
variable "role_arn" {
type = string
}
variable "identity_token" {
type = string
ephemeral = true
}
output "vpc_ids" {
description = "The VPC id of the Bookshelf network in each region."
type = map(string)
value = { for region, net in component.network : region => net.vpc_id }
}
Source: Terraform Docs — Define configuration and
Terraform Docs — component block reference.
The output expression is an illustration that assumes the network component uses for_each over var.regions, as in the
example later in this page.
Declaring providers
Provider handling differs from a normal configuration, and the differences are the most common source of mistakes.
-
Providers are declared in a top-level
required_providersblock (no enclosingterraformblock) in a.tfcomponent.hclfile. -
A provider configuration is a
providerblock with two labels, the provider name and an alias. The arguments of the provider go inside a nestedconfigblock. -
A
providerblock supportsfor_each, which creates one configuration per element, referenced asprovider.aws.configurations["eu-west-1"]. -
Components receive the configurations through their
providersmap; modules cannot configure their own. -
A Stack cannot run without a lock file.
terraform stacks initcreates or updates.terraform.lock.hclandterraform stacks init -upgraderefreshes it after a version change. -
Some Stack features need provider support; for example the documentation says deferred changes need version 2.32.0 or later of the Kubernetes provider. Check the provider’s registry page.
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
provider "aws" "configurations" {
for_each = var.regions
config {
region = each.value
assume_role_with_web_identity {
role_arn = var.role_arn
web_identity_token = var.identity_token
}
}
}
Source: Terraform Docs — Declare providers and
Terraform Docs — provider block reference.
The documentation’s examples use ~> 5.7.0 for the AWS provider; the version constraint here is the one the earlier pages of
this section use for the current major.
Removing a component with removed
Deleting a component block does not tell Terraform how to destroy what it created, because the component’s providers and
source module are needed to do that. The removed block carries those details.
removed {
source = "./modules/cache"
from = component.cache
providers = {
aws = provider.aws.this
}
}
Source: Terraform Docs — Manage components.
The procedure is: delete the component block, add the removed block with the same source module and the providers the
component used, push the configuration and apply it; after the apply you can delete the removed block. For components
created with for_each, repeat the for_each expression in the removed block (from = component.env[each.key]) to remove
all instances, or keep the component block and give removed the subset to drop. Never remove a provider while a
component that needs it still exists, because the provider is needed for the cleanup. The Stack removed block is distinct
from the removed block of normal configurations covered in
Refactoring with moved and removed.
Resources inside a component are managed in its module: to import a resource you add an import block to the module, pass
the identifier through a Stack variable and set it per deployment; to move or stop managing a resource you use moved and
removed in the module, as in a normal configuration.
Deployment configuration (*.tfdeploy.hcl)
A Stack needs at least one deployment block. The file lives at the top level of the repository with the component files.
The deployment block
deployment "dev" {
inputs = {
environment = "dev"
regions = ["eu-west-1"]
cidr_block = "10.10.0.0/16"
role_arn = "<DEV_ROLE_ARN>"
identity_token = identity_token.aws.jwt
}
}
The label is the deployment’s name; it must be unique, and it cannot be renamed, because changing the label creates a new deployment and destroys the old one. The reference lists these arguments:
| Argument | Meaning |
|---|---|
|
Required. A map from the names of Stack variables to values for this deployment. Values can reference locals,
|
|
Optional. A reference to the group of this deployment ( |
|
Optional. When |
|
Optional. Set automatically by the |
Adding a deployment block creates a new state after the run is applied; removing one (or destroy = true) deletes the
deployment and its state. HCP Terraform supports up to 20 deployments per Stack.
identity_token: OIDC without stored credentials
The documentation recommends authenticating Stacks with OIDC, because static credentials are a security risk even when
rotated. The identity_token block makes HCP Terraform generate a JSON Web Token (JWT) for the deployment; you pass it to
the provider’s assume_role_with_web_identity through a Stack variable.
identity_token "aws" {
audience = ["aws.workload.identity"]
}
The block has one required argument, audience (a set of strings), and one attribute, jwt, which you reference as
identity_token.aws.jwt. The token’s sub claim identifies the organization, project, Stack, deployment and operation
in the form organization:<ORG>:project:<PROJECT>:stack:<STACK>:deployment:<DEPLOYMENT>:operation:<OPERATION>, and custom
claims such as terraform_stack_deployment_name and terraform_operation let the cloud’s trust policy distinguish
deployments. The trust configuration is created once in the cloud account (an OIDC provider and a role whose trust policy
matches the aud and sub claims); the documentation provides a Terraform configuration for AWS and refers to a
repository of examples for other clouds. Use a separate role per deployment, so that dev cannot assume the production
role, and scope the sub pattern as narrowly as the deployments allow.
# Trust policy condition for the production role (AWS), as Terraform
condition {
test = "StringLike"
variable = "app.terraform.io:sub"
values = ["organization:<ORGANIZATION>:project:<PROJECT>:stack:bookshelf:deployment:prod:*"]
}
The store block: using a variable set
When OIDC is not an option, the store block reads values from an HCP Terraform variable set. varset is the only store
type. You give either id or name (mutually exclusive) and a category, terraform or env; one store reads one
category, so use two blocks to read both. The Stack must be able to see the variable set: it must be global or assigned to
the project or the Stack.
store "varset" "bookshelf_secrets" {
name = "bookshelf-secrets"
category = "terraform"
}
deployment "prod" {
inputs = {
db_admin_password = store.varset.bookshelf_secrets.db_admin_password
# ...
}
}
By default the values are used during the operation and not stored in the deployment state. To keep a value in state (the
documentation’s example is a license key), reference it as store.varset.<STORE>.stable.<VARIABLE>; the documentation
adds that a value marked stable cannot be changed afterwards and cannot also be referenced without stable. The
receiving Stack variables should be ephemeral (and sensitive where appropriate). Prefer OIDC over this route; see
Sensitive Data, Ephemeral and Write-Only for the
underlying concepts.
Deployment groups and auto-approve rules
By default every deployment is planned as soon as a configuration version is created, and each needs a person to approve its
apply, except plans that contain no changes (the default empty_plan rule approves those automatically). Deployment groups
change that.
The documentation marks orchestration rules for deployment groups as a Plus Edition feature.
-
A
deployment_auto_approveblock names a rule made of one or morecheckblocks, each with aconditionand areason(the message shown when the condition is false and manual approval is needed). -
The
conditionexpression reads acontextobject. Its documented fields includeoperation,success,plan,errorsandwarnings;context.planhasmode,applyable,changes(withadd,change,import,removeandtotal),component_changes,replansanddeployment. -
A
deployment_groupblock lists rules inauto_approve_checks; the plan applies automatically only if every rule passes. -
A deployment joins a group through its
deployment_groupargument. -
Setting
eager_planto the stringoffstops a group’s deployments from planning until a person starts the group in the UI or API, which also spares concurrency in large Stacks; the default ison. -
failure_toleranceis the number of failed deployments the group accepts before halting its rollout. It also limits concurrency: the documentation says Terraform applies one more deployment than the tolerance at a time, so a tolerance of 0 means deployments run one after another, and omitting it (ornull) means unlimited.
deployment_auto_approve "no_destroys" {
check {
condition = context.plan.changes.remove == 0
reason = "The plan destroys ${context.plan.changes.remove} resources."
}
}
deployment_auto_approve "successful_plan" {
check {
condition = context.success == true
reason = "The operation failed and needs manual intervention."
}
}
deployment_group "dev_group" {
auto_approve_checks = [
deployment_auto_approve.no_destroys,
deployment_auto_approve.successful_plan,
]
}
deployment_group "prod_group" {
eager_plan = "off"
failure_tolerance = 0
}
Source: Terraform Docs — Set conditions for deployment runs, Terraform Docs — deployment_group block reference and Terraform Docs — deployment_auto_approve block reference.
With these groups, a change to the shared module plans in dev immediately and applies there without a person when it
removes nothing; the prod deployment stays unplanned until someone starts its group, and, as it has no
auto_approve_checks, waits for approval. Because the group’s failure tolerance is 0, a failure in one production
deployment halts the others. The old orchestrate blocks (auto_approve, orchestrate, replan) of the beta are
deprecated and replaced by these.
The Bookshelf Stack
The complete example puts the pieces together. The repository holds three local modules (modules/network, modules/database
and modules/service, written as in Modules), a .terraform-version file and the
following files. The module inputs and outputs below belong to those Bookshelf modules and are illustrative.
components.tfcomponent.hclcomponent "network" {
for_each = var.regions
source = "./modules/network"
inputs = {
name = "bookshelf-${var.environment}-${each.value}"
cidr_block = var.cidr_block
}
providers = {
aws = provider.aws.configurations[each.value]
}
}
component "database" {
for_each = var.regions
source = "./modules/database"
inputs = {
name = "bookshelf-${var.environment}-${each.value}"
vpc_id = component.network[each.value].vpc_id
private_subnet_ids = component.network[each.value].private_subnet_ids
admin_password = var.db_admin_password
}
providers = {
aws = provider.aws.configurations[each.value]
}
}
component "service" {
for_each = var.regions
source = "./modules/service"
inputs = {
name = "bookshelf-${var.environment}-${each.value}"
vpc_id = component.network[each.value].vpc_id
subnet_ids = component.network[each.value].private_subnet_ids
database_endpoint = component.database[each.value].endpoint
}
providers = {
aws = provider.aws.configurations[each.value]
}
}
variables.tfcomponent.hclvariable "environment" {
type = string
}
variable "regions" {
type = set(string)
}
variable "cidr_block" {
type = string
}
variable "role_arn" {
type = string
}
variable "identity_token" {
type = string
ephemeral = true
}
variable "db_admin_password" {
type = string
sensitive = true
ephemeral = true
}
output "vpc_ids" {
description = "The VPC id of the Bookshelf network in each region."
type = map(string)
value = { for region, net in component.network : region => net.vpc_id }
}
providers.tfcomponent.hclrequired_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
provider "aws" "configurations" {
for_each = var.regions
config {
region = each.value
assume_role_with_web_identity {
role_arn = var.role_arn
web_identity_token = var.identity_token
}
default_tags {
tags = {
application = "bookshelf"
environment = var.environment
}
}
}
}
deployments.tfdeploy.hclidentity_token "aws" {
audience = ["aws.workload.identity"]
}
store "varset" "bookshelf_secrets" {
name = "bookshelf-secrets"
category = "terraform"
}
deployment_auto_approve "no_destroys" {
check {
condition = context.plan.changes.remove == 0
reason = "The plan destroys ${context.plan.changes.remove} resources."
}
}
deployment_group "dev_group" {
auto_approve_checks = [deployment_auto_approve.no_destroys]
}
deployment_group "prod_group" {
eager_plan = "off"
failure_tolerance = 0
}
deployment "dev" {
deployment_group = deployment_group.dev_group
inputs = {
environment = "dev"
regions = ["eu-west-1"]
cidr_block = "10.10.0.0/16"
role_arn = "<DEV_ROLE_ARN>"
identity_token = identity_token.aws.jwt
db_admin_password = store.varset.bookshelf_secrets.dev_db_admin_password
}
}
deployment "prod" {
deployment_group = deployment_group.prod_group
inputs = {
environment = "prod"
regions = ["eu-west-1", "us-east-1"]
cidr_block = "10.20.0.0/16"
role_arn = "<PROD_ROLE_ARN>"
identity_token = identity_token.aws.jwt
db_admin_password = store.varset.bookshelf_secrets.prod_db_admin_password
}
}
Source: Terraform Docs — Define configuration,
Terraform Docs — Define deployment configuration and
Terraform Docs — Stack use cases.
The <DEV_ROLE_ARN> and <PROD_ROLE_ARN> placeholders stand for roles created by the one-off trust configuration; the
password values live in a variable set, never in the repository.
Reading the example:
-
The same three components are deployed twice.
devhas one regional instance of each component,prodhas two, because theregionsvariable (a set of strings) drivesfor_eachon both the provider and the components. -
The dependencies are plain references (
component.network[each.value].vpc_id), so the network is applied before the database, and the database before the service, in every region. -
The provider configuration is created per region and each component instance picks its own with
provider.aws.configurations[each.value]. -
Each deployment has isolated state, and
devhas a role that cannot touchprod. -
Adding a third deployment (
staging) is one moredeploymentblock, subject to the limit of 20.
Creating the Stack and rolling it out
The configuration is created in HCP Terraform either from the UI, linking a repository, or from the CLI. The commands below use placeholders, and the output shape is the one shown on the official page (abbreviated, identifiers invented).
$ terraform stacks init
$ terraform stacks providers-lock
$ terraform stacks validate
$ terraform stacks create -organization-name <ORGANIZATION> -project-name <PROJECT> -stack-name bookshelf
$ terraform stacks configuration upload -organization-name <ORGANIZATION> -project-name <PROJECT> -stack-name bookshelf
Uploading stack configuration...
Configuration for Stack (id: '<STACK_ID>') was uploaded
Configuration ID: <CONFIGURATION_ID>
Sequence Number: 1
$ terraform stacks configuration watch -organization-name <ORGANIZATION> -project-name <PROJECT> -stack-name bookshelf
Source: Terraform Docs — Create a Stack.
In the UI you choose the project, a VCS provider and repository, a name that is unique within the project (letters, numbers, dashes and underscores; the documentation recommends 90 characters or fewer) and optional advanced settings: the working directory of the Stack inside the repository, the execution mode, and whether new runs start on every push (branch-based publishing with trigger patterns and speculative plans, or tag-based publishing with a regular expression). By default HCP Terraform fetches the configuration after creating the Stack. Each configuration version creates a deployment run for every deployment.
Linking Stacks: publish_output and upstream_input
Components that share a lifecycle belong in one Stack. Systems that do not share a lifecycle but exchange data, such as a shared network and several applications, are separate Stacks linked by two blocks in their deployment configurations.
-
The upstream Stack exposes a component
outputand publishes it with apublish_outputblock, which has a requiredvalueand an optionaldescription, and which reads from a deployment asdeployment.<NAME>.<OUTPUT>. -
The downstream Stack declares an
upstream_inputblock withtypeset tostack(the only supported type) and asourceURL of the formapp.terraform.io/<ORGANIZATION>/<PROJECT>/<STACK>, then reads values asupstream_input.<LABEL>.<OUTPUT>.
# network Stack: network.tfdeploy.hcl
publish_output "vpc_id_prod" {
description = "The VPC id of the production network."
value = deployment.prod.vpc_id
}
# application Stack: application.tfdeploy.hcl
upstream_input "network_stack" {
type = "stack"
source = "app.terraform.io/<ORGANIZATION>/<PROJECT>/network"
}
deployment "prod" {
inputs = {
vpc_id = upstream_input.network_stack.vpc_id_prod
# ...
}
}
Source: Terraform Docs — Pass data from one Stack to another,
Terraform Docs — publish_output block reference and
Terraform Docs — upstream_input block reference.
The deployment.prod.vpc_id reference assumes the upstream Stack has an output block named vpc_id in its component configuration, as
in the documentation’s own example.
The rules around the link:
-
The downstream Stack must be in the same project as the upstream one. Limits are 20 upstream Stacks per Stack and 25 downstream Stacks that can receive its values.
-
Published values are snapshots created when a configuration that contains
publish_outputis applied. If no snapshot exists, the downstream run fails, so apply the upstream Stack first. -
When a published value changes after a run, HCP Terraform automatically triggers runs in the downstream Stacks that use it, like run triggers between workspaces.
-
To remove the dependency, delete the
upstream_inputblock and every reference to it, then push and apply. -
The reference pages note that
publish_outputandupstream_inputneed Terraform 1.10.0-alpha20241009 or later.
The terraform stacks command line
Since Terraform 1.13 the Stacks commands are part of the main CLI as the terraform stacks command group, replacing the
beta’s separate binary. terraform stacks -help lists what the installed plugin offers. The command line only prepares,
creates and monitors Stacks: you cannot deploy locally.
| Command | Purpose and notable flags |
|---|---|
|
Downloads modules and providers into |
|
Creates or updates |
|
Validates local configuration without remote services. The documentation says to run |
|
Rewrites |
|
Creates a Stack. Requires |
|
Lists Stacks of an organization ( |
|
Subcommands |
|
Subcommands |
|
Subcommands |
|
Shows warnings and errors for a configuration (an ID with the |
|
Shows the version of the Stacks plugin. |
|
Subcommands |
Environment variables supply defaults: TF_STACKS_ORGANIZATION_NAME, TF_STACKS_PROJECT_NAME and
TF_STACKS_STACK_NAME stand for the matching flags (the flags override them), and TF_STACKS_HOSTNAME points the CLI at
a Terraform Enterprise instance. The Terraform 1.16 changelog adds that the command infers the hostname from the local
credentials file when neither TF_STACKS_HOSTNAME nor TF_CLOUD_HOSTNAME is set. Authenticate first with
terraform login, or configure credentials as for any other HCP Terraform command; see
CLI Essentials.
$ terraform stacks fmt -check -recursive
$ terraform stacks deployment-group list ...
$ terraform stacks deployment-run approve-all-plans -deployment-run-id <DEPLOYMENT_RUN_ID>
Source: Terraform Docs — terraform stacks commands,
Terraform Docs — terraform stacks configuration upload and
Terraform Docs — terraform stacks deployment-run approve-all-plans.
The second line is shortened: the flags of deployment-group list are on its own reference page.
Stacks, Terragrunt and workspaces
None of the three is universally better, and they can be combined (Stacks and workspaces can live in the same HCP Terraform project). The table compares what each documents.
| Aspect | HCP Terraform workspaces | Terraform Stacks | Terragrunt |
|---|---|---|---|
What it is |
A unit of state, variables and runs: one root module, one state. |
A set of components deployed several times; one isolated state per deployment. |
An open-source wrapper from Gruntwork around Terraform or OpenTofu; units are directories with a |
Where it runs |
Remote execution in HCP Terraform, or locally with the CLI. |
Only remotely: HCP Terraform or a custom HCP Terraform agent; the CLI creates and monitors but does not deploy. |
Wherever you run the |
Repeating a design |
One workspace per instance, with variables kept aligned by variable sets or the |
|
Implicit stacks (a directory per unit) or explicit stacks ( |
Ordering and dependencies |
Run triggers between workspaces or an external pipeline. |
References between components in a Stack; deferred planning; |
A dependency graph between units, applied with |
Rollout control |
Per-workspace approval and settings. |
Deployment groups: auto-approve checks, eager plan, failure tolerance (Plus Edition). |
What you build in the pipeline around the commands. |
Feature coverage |
The full workspace feature set. |
A subset: for example the comparison page lists run triggers, drift detection, run tasks and no-code provisioning as not supported, and config-driven import and team management as partial. |
Independent of HCP Terraform features; works with OpenTofu. |
Lock-in and cost |
HCP Terraform plan. |
HCP Terraform with RUM-based billing, not on legacy team plans. |
Open source; no platform needed. |
In practice the choice follows the constraints. If you are already on HCP Terraform and need many deployments of one design with rollout control, Stacks fit that need directly. If you need the workspace features Stacks do not support, or the infrastructure is a handful of root modules, workspaces remain the simpler path. If you run the open-source CLI or OpenTofu, in CI or on a platform other than HCP Terraform, Terragrunt (see the Terragrunt section of the structure page) or plain directories per environment are the options, since Stacks are unavailable there. The feature-support table of the comparison page also lists policy as code as unsupported for Stacks, while the policy documentation describes policy sets that target Stacks when they use Terraform policy as the evaluation engine; check both pages for the current state before relying on either, and see Policy as Code.
What changed recently
-
General availability. The Stacks documentation describes Stacks as generally available and carries an "Update for general availability" guide. The GA release moved the commands into
terraform stacks, deprecated the separateterraform-stacks-cli, renamed the component file extension from.tfstack.hclto.tfcomponent.hcl, and deprecated theauto_approve,orchestrateandreplanblocks in favour ofdeployment_groupwithauto_approve_checksanddeployment_auto_approve. Configurations and state history created in the beta are no longer available, so beta users must rewrite their files and fetch their configuration again. -
Billing. Stacks did not count toward resources under management during the public beta; since GA they are aggregated with workspace RUM, and the usage page shows billable Stack resources and applies.
-
Terraform 1.13. Introduces the
terraform stackscommand. Terraform 1.13 or later is required to use the Stacks CLI plugin. -
Terraform 1.14. Component registry source resolution for Stacks,
-helpsupport forterraform stacks, relativepath.moduleandpath.rootvalues, and a fix for relative module paths interraform stacks validate. -
Terraform 1.15. Input variable validation for Stacks, output values in the plan’s component instance change description and several fixes.
-
Terraform 1.16. The hostname is inferred from the credentials file when no hostname variable is set, provider versions in the lock file are validated against the configuration, and action configurations can use the
callersymbol. -
Terraform 1.17. The 1.17 changelog contains no Stacks entry that this page relies on.
-
Newer deployment-group arguments.
eager_planandfailure_toleranceappear in the current block references; they are not part of the beta syntax described in the GA update guide, and this page does not state the release that introduced them because the documentation does not. -
Terraform Enterprise. Terraform Enterprise 2.0 documents Stacks, with the RUM pricing requirement noted above.
Where the books are out of date
Neither of the two reference books for this section covers Stacks as the documentation now describes them, so this page
relies on the official documentation alone. Material about the Stacks beta, including snippets that use .tfstack.hcl
files, the orchestrate, auto_approve and replan blocks or the separate Stacks CLI binary, no longer matches the GA
syntax: use .tfcomponent.hcl, deployment_group with auto_approve_checks, and terraform stacks.
Pitfalls
-
Treating a Stack like a module. A component sources a module, but the Stack root is not a module: the usual
terraform init,planandapplydo not read.tfcomponent.hclfiles, and nothing applies until HCP Terraform runs it. -
Using the beta file extension or blocks.
.tfstack.hclfiles are ignored by the GA release; rename them. -
Declaring a
providerblock with one label. Stack providers need a name and an alias, and the arguments go inconfig. -
Putting a provider in a module. Modules sourced by a component cannot declare providers; pass them with
providers. -
Removing a component or a provider carelessly. Use
removedwith the source and providers, and keep the providers until the apply has finished. -
Renaming a deployment. It destroys the old deployment and creates a new one. Use
destroy = trueto retire one. -
Persisting secrets by accident. Variable set values are not stored in state unless referenced with
stable; once stable, they cannot change. -
Static credentials. Prefer
identity_tokenand a role per deployment; keep trust policies narrow. -
Counting on features workspaces have. Check the feature-support table before migrating a workspace.
-
Forgetting the order of cross-Stack links. A downstream Stack fails until the upstream Stack has applied a configuration that publishes its outputs.
-
Hitting limits. Twenty deployments per Stack is a hard ceiling for very wide fan-outs; split the design instead of forcing it.
-
Expecting a local apply. The CLI creates, uploads and watches, but the deployment happens in HCP Terraform.