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.

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. dev and prod, 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 the tfe provider; 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, dev should apply on its own and prod should 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

*.tfcomponent.hcl

What the Stack is: the components, their inputs and the providers they use.

component, required_providers, provider, removed, variable, output, locals

*.tfdeploy.hcl

How the Stack is repeated: the deployments, their input values, authentication and rollout rules.

deployment, deployment_group, deployment_auto_approve, identity_token, store, publish_output, upstream_input, locals

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 deployment block that supplies the values of the Stack’s input variables. dev and prod are 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.

flowchart LR subgraph Stack["Stack bookshelf"] direction TB N["component: network"] --> D["component: database"] N --> S["component: service"] D --> S end Stack --> DEV["deployment: dev - 1 region"] Stack --> PROD["deployment: prod - 2 regions"] DEV --> SD["state: dev"] PROD --> SP["state: prod"] DEV --> G1["group: dev_group - auto-approve"] PROD --> G2["group: prod_group - manual approval"]

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 create page 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 (set TF_STACKS_HOSTNAME for 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 stacks commands. The Terraform 1.13 changelog introduces the command. The beta-era separate terraform-stacks-cli is 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 component block 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 locals to share values and for_each on components and providers to scale to more regions.

  • Put a .terraform-version file in the Stack’s root directory with the Terraform version the Stack needs (the documentation’s example contains 1.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.

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

source

The module to use, such as a local path, a registry module or a Git address.

version

A version constraint when the source is a registry module.

inputs

A map of module input variable names to values. Values may reference Stack variables, other components, locals and providers' outputs.

providers

A map from the provider names the module expects to provider configurations declared in the Stack (provider.aws.this). Modules sourced by a component cannot declare their own providers.

depends_on

A list of references to other components for dependencies that references do not reveal.

for_each

A map or set that creates one instance of the component per element, reachable as each.key and each.value.

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_providers block (no enclosing terraform block) in a .tfcomponent.hcl file.

  • A provider configuration is a provider block with two labels, the provider name and an alias. The arguments of the provider go inside a nested config block.

  • A provider block supports for_each, which creates one configuration per element, referenced as provider.aws.configurations["eu-west-1"].

  • Components receive the configurations through their providers map; modules cannot configure their own.

  • A Stack cannot run without a lock file. terraform stacks init creates or updates .terraform.lock.hcl and terraform stacks init -upgrade refreshes 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
  }
}

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

inputs

Required. A map from the names of Stack variables to values for this deployment. Values can reference locals, identity_token, store and upstream_input.

deployment_group

Optional. A reference to the group of this deployment (deployment_group.dev_group). Without it, Terraform creates an independent group named <DEPLOYMENT_NAME>_default, on which you cannot define custom rules.

destroy

Optional. When true, HCP Terraform destroys the deployment. After the apply, delete the block. The documentation recommends this over simply deleting the block, because the configuration then still holds the authentication needed to destroy.

import

Optional. Set automatically by the tf-migrate CLI when it imports the state of an existing workspace into the deployment.

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_approve block names a rule made of one or more check blocks, each with a condition and a reason (the message shown when the condition is false and manual approval is needed).

  • The condition expression reads a context object. Its documented fields include operation, success, plan, errors and warnings; context.plan has mode, applyable, changes (with add, change, import, remove and total), component_changes, replans and deployment.

  • A deployment_group block lists rules in auto_approve_checks; the plan applies automatically only if every rule passes.

  • A deployment joins a group through its deployment_group argument.

  • Setting eager_plan to the string off stops 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 is on.

  • failure_tolerance is 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 (or null) 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
}

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.hcl
component "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.hcl
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
}

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.hcl
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
    }

    default_tags {
      tags = {
        application = "bookshelf"
        environment = var.environment
      }
    }
  }
}
deployments.tfdeploy.hcl
identity_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. dev has one regional instance of each component, prod has two, because the regions variable (a set of strings) drives for_each on 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 dev has a role that cannot touch prod.

  • Adding a third deployment (staging) is one more deployment block, 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

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.

  1. The upstream Stack exposes a component output and publishes it with a publish_output block, which has a required value and an optional description, and which reads from a deployment as deployment.<NAME>.<OUTPUT>.

  2. The downstream Stack declares an upstream_input block with type set to stack (the only supported type) and a source URL of the form app.terraform.io/<ORGANIZATION>/<PROJECT>/<STACK>, then reads values as upstream_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_output is 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_input block and every reference to it, then push and apply.

  • The reference pages note that publish_output and upstream_input need 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

init

Downloads modules and providers into .terraform (keep it out of version control) and writes the lock file. Flags: -upgrade, -json.

providers-lock

Creates or updates .terraform.lock.hcl. By default checksums are requested for the platform used by HCP Terraform (linux_amd64) and the current machine; repeat -platform=os_arch to add more. Flag: -json.

validate

Validates local configuration without remote services. The documentation says to run init and providers-lock first. Flag: -json.

fmt

Rewrites .tfcomponent.hcl and .tfdeploy.hcl files to canonical form. Takes an optional target (file, directory or -). Flags: -list, -write, -diff, -check, -recursive.

create

Creates a Stack. Requires -organization-name, -project-name and -stack-name; optional -with-template generates boilerplate and -json formats the output.

list

Lists Stacks of an organization (-organization-name required) and optionally a project (-project-name).

configuration

Subcommands list, upload, fetch and watch. upload takes -organization-name, -project-name and -stack-name or -stack-id, plus -speculative (upload without triggering deployments) and -json; fetch pulls the latest configuration from the VCS source; watch follows a configuration (-configuration-id).

deployment-group

Subcommands list, approve-all-plans, rerun and watch. rerun takes -deployment-names and a group by -deployment-group-id or by name with the Stack identifiers.

deployment-run

Subcommands list, approve-all-plans (with the required -deployment-run-id), cancel and watch.

diagnostics

Shows warnings and errors for a configuration (an ID with the stc- prefix) or a deployment step (an ID with the sds- prefix), passed with -id.

version

Shows the version of the Stacks plugin.

migrate

Subcommands prepare-manifest, validate-manifest, translate-config and translate-state to prepare the migration of HCP Terraform workspaces to Stacks locally. Experimental and disabled unless the environment variable TF_STACKS_MIGRATE_EXPERIMENTAL is true; it does not create the Stack or move remote state.

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>

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 terragrunt.hcl.

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 terragrunt binary: laptops and CI.

Repeating a design

One workspace per instance, with variables kept aligned by variable sets or the tfe provider.

deployment blocks over one component configuration; for_each on components and providers for regions.

Implicit stacks (a directory per unit) or explicit stacks (terragrunt.stack.hcl) that generate units from a template.

Ordering and dependencies

Run triggers between workspaces or an external pipeline.

References between components in a Stack; deferred planning; publish_output and upstream_input between Stacks.

A dependency graph between units, applied with terragrunt run --all.

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 separate terraform-stacks-cli, renamed the component file extension from .tfstack.hcl to .tfcomponent.hcl, and deprecated the auto_approve, orchestrate and replan blocks in favour of deployment_group with auto_approve_checks and deployment_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 stacks command. Terraform 1.13 or later is required to use the Stacks CLI plugin.

  • Terraform 1.14. Component registry source resolution for Stacks, -help support for terraform stacks, relative path.module and path.root values, and a fix for relative module paths in terraform 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 caller symbol.

  • Terraform 1.17. The 1.17 changelog contains no Stacks entry that this page relies on.

  • Newer deployment-group arguments. eager_plan and failure_tolerance appear 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, plan and apply do not read .tfcomponent.hcl files, and nothing applies until HCP Terraform runs it.

  • Using the beta file extension or blocks. .tfstack.hcl files are ignored by the GA release; rename them.

  • Declaring a provider block with one label. Stack providers need a name and an alias, and the arguments go in config.

  • 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 removed with 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 = true to 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_token and 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.