terraform-stacks
Terraform Stacks
Terraform Stacks simplify infrastructure provisioning and management at scale by providing a configuration layer above traditional Terraform modules. Stacks enable declarative orchestration of multiple components across environments, regions, and cloud accounts.
Core Concepts
Stack: A complete unit of infrastructure composed of components and deployments that can be managed together.
Component: An abstraction around a Terraform module that defines infrastructure pieces. Each component specifies a source module, inputs, and providers.
Deployment: An instance of all components in a stack with specific input values. Use deployments for different environments (dev/staging/prod), regions, or cloud accounts.
Stack Language: A separate HCL-based language (not regular Terraform HCL) with distinct blocks and file extensions.
File Structure
Terraform Stacks use specific file extensions:
- Component configuration:
.tfcomponent.hcl - Deployment configuration:
.tfdeploy.hcl - Provider lock file:
.terraform.lock.hcl(generated by CLI)
All configuration files must be at the root level of the Stack repository. HCP Terraform processes all files in dependency order.
Recommended File Organization
my-stack/
├── variables.tfcomponent.hcl # Variable declarations
├── providers.tfcomponent.hcl # Provider configurations
├── components.tfcomponent.hcl # Component definitions
├── outputs.tfcomponent.hcl # Stack outputs
├── deployments.tfdeploy.hcl # Deployment definitions
├── .terraform.lock.hcl # Provider lock file (generated)
└── modules/ # Local modules (optional - only if using local modules)
├── vpc/
└── compute/
Note: The modules/ directory is only required when using local module sources. Components can reference modules from:
- Local file paths:
./modules/vpc - Public registry:
terraform-aws-modules/vpc/aws - Private registry:
app.terraform.io/<org-name>/vpc/aws
When validating Stack configurations, check component source declarations rather than assuming a local modules/ directory must exist.
Component Configuration (.tfcomponent.hcl)
Variable Block
Declare input variables for the Stack configuration. Variables must define a type field and do not support the validation argument.
variable "aws_region" {
type = string
description = "AWS region for deployments"
default = "us-west-1"
}
variable "identity_token" {
type = string
description = "OIDC identity token"
ephemeral = true # Does not persist to state file
}
variable "instance_count" {
type = number
nullable = false
}
Required Providers Block
Works the same as traditional Terraform configurations:
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.7.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.0"
}
}
Provider Block
Provider blocks differ from traditional Terraform:
- Support
for_eachmeta-argument - Define aliases in the block header (not as an argument)
- Accept configuration through a
configblock
Single Provider Configuration:
provider "aws" "this" {
config {
region = var.aws_region
assume_role_with_web_identity {
role_arn = var.role_arn
web_identity_token = var.identity_token
}
}
}
Multiple Provider Configurations with for_each:
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
}
}
}
Component Block
Each Stack requires at least one component block. Add a component for each module to include in the Stack.
Component Source: Each component's source argument must specify one of the following source types:
- Local file path:
./modules/vpc - Public registry:
terraform-aws-modules/vpc/aws - Private registry:
app.terraform.io/my-org/vpc/aws - Git repository:
git::https://github.com/org/repo.git//modules/vpc?ref=v1.0.0
component "vpc" {
source = "./modules/vpc"
inputs = {
cidr_block = var.vpc_cidr
name_prefix = var.name_prefix
}
providers = {
aws = provider.aws.this
}
}
component "networking" {
source = "app.terraform.io/my-org/vpc/aws"
version = "2.1.0"
inputs = {
cidr_block = var.vpc_cidr
environment = var.environment
}
providers = {
aws = provider.aws.this
}
}
component "compute" {
source = "./modules/compute"
inputs = {
vpc_id = component.vpc.vpc_id
subnet_ids = component.vpc.private_subnet_ids
instance_type = var.instance_type
}
providers = {
aws = provider.aws.this
}
}
Component with for_each for Multi-Region:
component "s3" {
for_each = var.regions
source = "./modules/s3"
inputs = {
region = each.value
tags = var.common_tags
}
providers = {
aws = provider.aws.configurations[each.value]
}
}
Key Points:
- Reference component outputs using
component.<name>.<output> - All inputs are provided as a single
inputsobject - Provider references are normal values:
provider.<type>.<alias> - Dependencies are automatically inferred from component references
Output Block
Outputs require a type argument and do not support preconditions:
output "vpc_id" {
type = string
description = "VPC ID"
value = component.vpc.vpc_id
}
output "endpoint_urls" {
type = map(string)
value = {
for region, comp in component.api : region => comp.endpoint_url
}
sensitive = false
}
Locals Block
Works exactly as in traditional Terraform:
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform Stacks"
Project = var.project_name
}
region_config = {
for region in var.regions : region => {
name_suffix = "${var.environment}-${region}"
}
}
}
Removed Block
Use to safely remove components from a Stack. HCP Terraform requires the component's providers to remove it.
removed {
from = component.old_component
source = "./modules/old-module"
providers = {
aws = provider.aws.this
}
}
Deployment Configuration (.tfdeploy.hcl)
Identity Token Block
Generate JWT tokens for OIDC authentication with cloud providers:
identity_token "aws" {
audience = ["aws.workload.identity"]
}
identity_token "azure" {
audience = ["api://AzureADTokenExchange"]
}
Reference tokens in deployments using identity_token.<name>.jwt
Locals Block
Define local values for deployment configuration:
locals {
aws_regions = ["us-west-1", "us-east-1", "eu-west-1"]
role_arn = "arn:aws:iam::123456789012:role/hcp-terraform-stacks"
}
Deployment Block
Define deployment instances. Each Stack requires at least one deployment (maximum 20 per Stack).
Single Environment Deployment:
deployment "production" {
inputs = {
aws_region = "us-west-1"
instance_count = 3
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}
Multiple Environment Deployments:
deployment "development" {
inputs = {
aws_region = "us-east-1"
instance_count = 1
name_suffix = "dev"
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}
deployment "staging" {
inputs = {
aws_region = "us-east-1"
instance_count = 2
name_suffix = "staging"
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}
deployment "production" {
inputs = {
aws_region = "us-west-1"
instance_count = 5
name_suffix = "prod"
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}
Destroying a Deployment:
To safely remove a deployment:
deployment "old_environment" {
inputs = {
aws_region = "us-west-1"
instance_count = 2
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
destroy = true # Mark for destruction
}
After applying the plan and the deployment is destroyed, remove the deployment block from your configuration.
Deployment Group Block
Group deployments together to configure shared settings (Premium feature). Best Practice: Always create deployment groups for all deployments, even single deployments, to enable future auto-approval rules and maintain consistent configuration patterns.
deployment_group "canary" {
deployments = [
deployment.dev,
deployment.staging
]
}
deployment_group "production" {
deployments = [
deployment.prod_us_east,
deployment.prod_us_west
]
}
Deployment Auto-Approve Block
Define rules that automatically approve deployment plans based on specific conditions (Premium feature):
deployment_auto_approve "safe_changes" {
deployment_group = deployment_group.canary
check {
condition = context.plan.changes.remove == 0
reason = "Cannot auto-approve plans with resource deletions"
}
check {
condition = context.plan.applyable
reason = "Plan must be applyable"
}
}
deployment_auto_approve "applyable_only" {
deployment_group = deployment_group.production
check {
condition = context.plan.applyable
reason = "Plan must be successful"
}
}
Available Context Variables:
context.plan.applyable- Plan succeeded without errorscontext.plan.changes.add- Number of resources to addcontext.plan.changes.change- Number of resources to changecontext.plan.changes.remove- Number of resources to remove
Note: orchestrate blocks are deprecated. Use deployment_group and deployment_auto_approve instead.
Publish Output Block
Export outputs from a Stack for use in other Stacks (linked Stacks):
publish_output "vpc_id_network" {
type = string
value = deployment.network.vpc_id
}
publish_output "subnet_ids" {
type = list(string)
value = deployment.network.private_subnet_ids
}
Upstream Input Block
Reference published outputs from another Stack:
upstream_input "network_stack" {
type = "stack"
source = "app.terraform.io/my-org/my-project/networking-stack"
}
deployment "application" {
inputs = {
vpc_id = upstream_input.network_stack.vpc_id_network
subnet_ids = upstream_input.network_stack.subnet_ids
}
}
Terraform Stacks CLI
Initialize and Validate
Generate provider lock file:
terraform stacks providers-lock
Validate Stack configuration:
terraform stacks validate
Plan and Apply
Plan a specific deployment:
terraform stacks plan --deployment=production
Apply a deployment:
terraform stacks apply --deployment=production
Common Patterns
Multi-Region Deployment
# variables.tfcomponent.hcl
variable "regions" {
type = set(string)
default = ["us-west-1", "us-east-1", "eu-west-1"]
}
# providers.tfcomponent.hcl
provider "aws" "regional" {
for_each = var.regions
config {
region = each.value
assume_role_with_web_identity {
role_arn = var.role_arn
web_identity_token = var.identity_token
}
}
}
# components.tfcomponent.hcl
component "regional_infra" {
for_each = var.regions
source = "./modules/regional"
inputs = {
region = each.value
}
providers = {
aws = provider.aws.regional[each.value]
}
}
Component Dependencies
Dependencies are automatically inferred when one component references another's output:
component "database" {
source = "./modules/rds"
inputs = {
subnet_ids = component.vpc.private_subnet_ids # Creates dependency
}
providers = {
aws = provider.aws.this
}
}
Best Practices
- Component Granularity: Create components for logical infrastructure units that share a lifecycle
- Module Compatibility: Modules used with Stacks cannot include provider blocks (configure providers in Stack configuration)
- State Isolation: Each deployment has its own isolated state
- Input Variables: Use variables for values that differ across deployments; use locals for shared values
- Provider Lock Files: Always generate and commit
.terraform.lock.hclto version control - Naming Conventions: Use descriptive names for components and deployments
- Deployment Groups: Always organize deployments into deployment groups, even if you only have one deployment. Deployment groups enable auto-approval rules, logical organization, and provide a foundation for scaling. While deployment groups are a Premium feature, organizing your configurations to use them is a best practice for all Stacks
- Testing: Test Stack configurations in dev/staging deployments before production
Troubleshooting
Circular Dependencies
Issue: Component A references Component B, and Component B references Component A Solution: Refactor to break the circular reference or use intermediate components
Deployment Limit
HCP Terraform supports maximum 20 deployments per Stack. For more instances, use multiple Stacks or for_each within components.
References
For detailed block specifications and advanced features, see:
references/component-blocks.md- Complete component block referencereferences/deployment-blocks.md- Complete deployment block referencereferences/examples.md- Complete working examples for common scenarios