Generated banner for Terraform Stacks
Generated logo for Terraform Stacks

Terraform Stacks

HashiCorpOfficialPublished by HashiCorp

  • Skill
  • Tool integration
  • Cloud and DevOps
  • MPL-2.0
  • No issues found

From the author

Comprehensive guide for working with HashiCorp Terraform Stacks. Use when creating, modifying, or validating Terraform Stack configurations (.tfcomponent.hcl, .tfdeploy.hcl files), working with stack components and deployments from local modules, public registry, or private registry sources, managing multi-region or multi-environment infrastructure, or troubleshooting Terraform Stacks syntax and structure.
— the description by HashiCorp

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Terraform Stacks provides a declarative orchestration layer above traditional Terraform modules, enabling the management of complex infrastructure across multiple environments, regions, and cloud accounts. It introduces a specialized HCL-based configuration language using .tfcomponent.hcl and .tfdeploy.hcl files to define components, providers, and deployment instances. This skill covers the full lifecycle of Stacks, including configuration, dependency management, provider configuration, and deployment monitoring via the CLI or HCP Terraform API.

What it does

  • Defines infrastructure units as components referencing local, registry, or Git-based modules.
  • Orchestrates multi-region and multi-environment deployments within a single Stack.
  • Manages provider configurations with support for for_each and workload identity authentication.
  • Links multiple Stacks together using published outputs and upstream inputs.
  • Automates deployment workflows and monitoring through HCP Terraform integration.
  • Provides patterns for safe component removal and deferred changes.

When to use it

  • When you need to manage complex, multi-component infrastructure at scale.
  • When you require consistent infrastructure deployments across multiple environments or regions.
  • When you want to move beyond individual Terraform modules to a higher-level orchestration layer.
  • When implementing OIDC-based workload identity for secure, temporary cloud credentials.
  • When you need to link separate infrastructure stacks together in a dependency chain.

Advantages

  • Simplifies management of large-scale infrastructure by grouping related components.
  • Reduces reliance on long-lived static credentials through native workload identity support.
  • Provides clear separation between component definitions and deployment-specific inputs.
  • Enables automated deployment pipelines with built-in dependency inference.
  • Supports advanced patterns like multi-region deployments and cross-stack output sharing.

Usage examples

“How do I define a component in a Terraform Stack that uses a module from the public registry?”
The agent provides a component block example using the source argument with the registry path, specifying inputs and mapping providers.
“What is the best way to handle AWS credentials in a Stack without using static keys?”
The agent explains the workload identity pattern, showing how to configure an identity_token block and use assume_role_with_web_identity in the provider config.
“How can I monitor the status of a deployment run programmatically?”
The agent provides the API endpoints for retrieving deployment group summaries and run status, including the necessary headers for authentication.
“How do I remove a component from an existing Stack?”
The agent details the use of the removed block, explaining that it requires the component's providers to be defined to safely process the removal.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/hashicorp/terraform-stacks/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip terraform-stacks.zip -d .agents/skills

Documentation by HashiCorp

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.

my-stack/
├── .terraform-version               # The required Terraform version for this 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)
    ├── s3/
    └── 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

  • Git: git::https://github.com/org/repo.git//path?ref=v1.0.0

HCP Terraform processes all .tfcomponent.hcl and .tfdeploy.hcl files in dependency order.

Required Terraform version (.terraform-version)

Use Terraform v1.13.x or later to access the Stacks CLI plugin and to run terraform stacks CLI commands. Begin by adding a .terraform-version file to your Stack's root directory to specify the Terraform version required for your Stack. For example, the following file specifies Terraform v1.14.5:

1.14.5

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
}

Important: Use ephemeral = true for credentials and tokens (identity tokens, API keys, passwords) to prevent them from persisting in state files. Use stable for longer-lived values like license keys that need to persist across runs.

Required Providers Block
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 6.0"
  }
  random = {
    source  = "hashicorp/random"
    version = "~> 3.5.0"
  }
}
Provider Block

Provider blocks differ from traditional Terraform:

  1. Support for_each meta-argument

  2. Define aliases in the block header (not as an argument)

  3. Accept configuration through a config block

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

Authentication Best Practice: Use workload identity (OIDC) as the preferred authentication method for Stacks. This approach:

  • Avoids long-lived static credentials

  • Provides temporary, scoped credentials per deployment run

  • Integrates with cloud provider IAM (AWS IAM Roles, Azure Managed Identities, GCP Service Accounts)

  • Eliminates need for platform-managed environment variables

Configure workload identity using identity_token blocks and assume_role_with_web_identity in provider configuration. For detailed setup instructions for AWS, Azure, and GCP, see: https://developer.hashicorp.com/terraform/cloud-docs/dynamic-provider-credentials

Component Block

Each Stack requires at least one component block. Add a component for each module to include in the Stack. Components reference modules from local paths, registries, or Git.

component "vpc" {
  source  = "app.terraform.io/my-org/vpc/aws"  # Local, registry, or Git URL
  version = "2.1.0"          # For registry modules

  inputs = {
    cidr_block  = var.vpc_cidr
    name_prefix = var.name_prefix
  }

  providers = {
    aws = provider.aws.this
  }
}

See references/component-blocks.md for examples of dependencies, for_each, public registry modules, Git sources, and more.

Key Points:

  • Reference outputs: component.<name>.<output> or component.<name>[key].<output> for for_each

  • Dependencies inferred automatically from component references

  • Aggregate with for expressions: [for x in component.s3 : x.bucket_name]

  • For components with for_each, reference specific instances: component.<name>[each.value].<output>

  • Provider references are normal values: provider.<type>.<alias> or provider.<type>.<alias>[each.value]

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

Locals blocks work the same in both .tfcomponent.hcl and .tfdeploy.hcl files:

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

Store Block

Access HCP Terraform variable sets within Stack deployments:

store "varset" "aws_credentials" {
  id       = "varset-ABC123"  # Alternatively use: name = "varset_name"
  source   = "tfc-cloud-shared"
  category = "terraform"      # Alternatively use: category = "env" for environment variables
}

deployment "production" {
  inputs = {
    aws_access_key = store.varset.aws_credentials.AWS_ACCESS_KEY_ID
  }
}

Use to centralize credentials and share variables across Stacks. See references/deployment-blocks.md for details.

Deployment Block

Define deployment instances (minimum 1, maximum 20 per Stack):

deployment "production" {
  inputs = {
    aws_region     = "us-west-1"
    instance_count = 3
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Create multiple deployments for different environments
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
  }
}

To destroy a deployment: Set destroy = true, upload configuration, approve destroy run, then remove the deployment block. See references/deployment-blocks.md for details.

Deployment Group Block

Group deployments together for shared settings (HCP Terraform Premium tier feature). Free/standard tiers use default groups named {deployment-name}_default.

deployment_group "canary" {
  auto_approve_checks = [deployment_auto_approve.safe_changes]
}

deployment "dev" {
  inputs = { /* ... */ }
  deployment_group = deployment_group.canary
}

Multiple deployments can reference the same group. See references/deployment-blocks.md for details.

Deployment Auto-Approve Block

Define rules to automatically approve deployment plans (HCP Terraform Premium tier 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"
  }
}

Available context variables: context.plan.applyable, context.plan.changes.add/change/remove/total, context.success

Note: orchestrate blocks are deprecated. Use deployment_group and deployment_auto_approve instead.

See references/deployment-blocks.md for all context variables and patterns.

Publish Output and Upstream Input Blocks

Link Stacks together by publishing outputs from one Stack and consuming them in another:

# In network Stack - publish outputs
publish_output "vpc_id_network" {
  type  = string
  value = deployment.network.vpc_id
}

# In application Stack - consume outputs
upstream_input "network_stack" {
  type   = "stack"
  source = "app.terraform.io/my-org/my-project/networking-stack"
}

deployment "app" {
  inputs = {
    vpc_id = upstream_input.network_stack.vpc_id_network
  }
}

See references/linked-stacks.md for complete documentation and examples.

Terraform Stacks CLI

Note: Terraform Stacks is Generally Available (GA) as of Terraform CLI v1.13+. Stacks now count toward Resources Under Management (RUM) for HCP Terraform billing.

Initialize and Validate
terraform stacks init              # Download providers, modules, generate lock file
terraform stacks providers-lock    # Regenerate lock file (add platforms if needed)
terraform stacks validate          # Check syntax without uploading
Deployment Workflow

Important: No plan or apply commands. Upload configuration triggers deployment runs automatically.

# 1. Upload configuration (triggers deployment runs)
terraform stacks configuration upload

# 2. Monitor deployments
terraform stacks deployment-run list                          # List runs (non-interactive)
terraform stacks deployment-group watch -deployment-group=... # Stream status updates

# 3. Approve deployments (if auto-approve not configured)
terraform stacks deployment-run approve-all-plans -deployment-run-id=...
terraform stacks deployment-group approve-all-plans -deployment-group=...
terraform stacks deployment-run cancel -deployment-run-id=...  # Cancel if needed
Configuration Management
terraform stacks configuration list                    # List configuration versions
terraform stacks configuration fetch -configuration-id=...  # Download configuration
terraform stacks configuration watch                   # Monitor upload status
Other Commands
terraform stacks create              # Create new Stack (interactive)
terraform stacks fmt                 # Format Stack files
terraform stacks list                # Show all Stacks
terraform stacks version             # Display version
terraform stacks deployment-group rerun -deployment-group=...  # Rerun deployment

Monitoring Deployments with HCP Terraform API

For programmatic monitoring in automation, CI/CD, or non-interactive environments (like AI agents), use the HCP Terraform API instead of CLI watch commands. The API provides endpoints for:

  • Configuration status and validation

  • Deployment group summaries

  • Deployment run status

  • Deployment step details (plan/apply)

  • Error diagnostics with file locations and code snippets

  • Stack outputs via artifacts endpoint

Key points:

  • CLI watch commands stream indefinitely and don't work in automation

  • Use artifacts endpoint to retrieve Stack outputs: GET /api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description

  • Diagnostics endpoint requires stack_deployment_step_id query parameter

  • Artifacts endpoint returns HTTP 307 redirect (use curl -L)

For complete API workflow, authentication, polling best practices, and example scripts, see references/api-monitoring.md.

Common Patterns

Component Dependencies: Dependencies are automatically inferred when one component references another's output (e.g., subnet_ids = component.vpc.private_subnet_ids).

Multi-Region Deployment: Use for_each on providers and components to deploy across multiple regions. Each region gets its own provider configuration and component instances.

Deferred Changes: Stacks support deferred changes to handle dependencies where values are only known after apply. This enables complex multi-component deployments where some resources depend on runtime values from other components (cluster endpoints, generated passwords, etc.).

For complete examples including multi-region deployments, component dependencies, deferred changes patterns, and linked Stacks, see references/examples.md.

Best Practices

  1. Component Granularity: Create components for logical infrastructure units that share a lifecycle

  2. Module Compatibility:

    • Modules used with Stacks cannot include provider blocks (configure providers in Stack configuration)

    • Test public registry modules before using in production Stacks - some modules may have compatibility issues

    • Consider using raw resources for critical infrastructure if module compatibility is uncertain

    • Example: Some terraform-aws-modules versions have been found to have compatibility issues with Stacks (e.g., ALB and ECS modules)

  3. State Isolation: Each deployment has its own isolated state

  4. Input Variables: Use variables for values that differ across deployments; use locals for shared values

  5. Provider Lock Files: Always generate and commit .terraform.lock.hcl to version control

  6. Naming Conventions: Use descriptive names for components and deployments

  7. Deployment Groups: You can organize deployments into deployment groups. Deployment groups enable auto-approval rules, logical organization, and provide a foundation for scaling. Deployment groups are an HCP Terraform Premium tier feature

  8. Testing: Test Stack configurations in dev/staging deployments before production

Troubleshooting

Circular Dependencies: Refactor to break circular references or use intermediate components.

Deployment Destruction: Cannot destroy from UI. Set destroy = true in deployment block, upload configuration, and HCP Terraform creates a destroy run.

Empty Diagnostics: Add required stack_deployment_step_id query parameter to diagnostics API requests.

Module Compatibility: Test public registry modules before production use. Some modules may have compatibility issues with Stacks.

References

For detailed documentation, see:

  • references/component-blocks.md - Complete component block reference with all arguments and syntax

  • references/deployment-blocks.md - Complete deployment block reference with all configuration options

  • references/linked-stacks.md - Publish outputs and upstream inputs for linking Stacks together

  • references/examples.md - Complete working examples for multi-region and component dependencies

  • references/api-monitoring.md - Full API workflow for programmatic monitoring and automation

  • references/troubleshooting.md - Detailed troubleshooting guide for common issues and solutions

Files

references/examples.md

Terraform Stacks Complete Examples

Complete, working examples for common Terraform Stacks scenarios.

Table of Contents

  1. Simple Single-Region Stack

  2. Stack with Private Registry Modules

  3. Multi-Environment Stack

  4. Multi-Region Stack

  5. Linked Stacks (Cross-Stack Dependencies)

  6. Multi-Cloud Stack

  7. Complete AWS Production Stack

  8. Destroying Deployments

Simple Single-Region Stack

Basic Stack with a single environment deployment.

File Structure
simple-stack/
├── variables.tfcomponent.hcl
├── providers.tfcomponent.hcl
├── components.tfcomponent.hcl
├── deployments.tfdeploy.hcl
└── modules/
    └── webapp/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
variables.tfcomponent.hcl
variable "aws_region" {
  type    = string
  default = "us-west-1"
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}

variable "app_name" {
  type = string
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
}

provider "aws" "main" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }
  }
}
components.tfcomponent.hcl
component "webapp" {
  source = "./modules/webapp"

  inputs = {
    app_name = var.app_name
    region   = var.aws_region
  }

  providers = {
    aws = provider.aws.main
  }
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

deployment "production" {
  inputs = {
    aws_region     = "us-west-1"
    app_name       = "my-webapp"
    role_arn       = "arn:aws:iam::123456789012:role/terraform-stacks"
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "production" {
  deployments = [deployment.production]
}

Stack with Private Registry Modules

Example Stack using modules from a private HCP Terraform registry, combining both private and public registry sources.

File Structure
private-registry-stack/
├── variables.tfcomponent.hcl
├── providers.tfcomponent.hcl
├── components.tfcomponent.hcl
├── outputs.tfcomponent.hcl
└── deployments.tfdeploy.hcl
variables.tfcomponent.hcl
variable "aws_region" {
  type    = string
  default = "us-west-2"
}

variable "environment" {
  type = string
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "app_name" {
  type = string
}

variable "db_password" {
  type      = string
  sensitive = true
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
  random = {
    source  = "hashicorp/random"
    version = "~> 3.5.0"
  }
}

provider "aws" "main" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }

    default_tags {
      tags = {
        Environment = var.environment
        ManagedBy   = "Terraform Stacks"
        Application = var.app_name
      }
    }
  }
}

provider "random" "main" {
  config {}
}
components.tfcomponent.hcl
locals {
  name_prefix = "${var.app_name}-${var.environment}"
  common_tags = {
    Project     = var.app_name
    Environment = var.environment
  }
}

# Using a private registry module for VPC
component "vpc" {
  source  = "app.terraform.io/my-org/vpc/aws"
  version = "2.1.0"

  inputs = {
    name_prefix         = local.name_prefix
    cidr_block          = var.vpc_cidr
    availability_zones  = ["${var.aws_region}a", "${var.aws_region}b", "${var.aws_region}c"]
    enable_nat_gateway  = true
    single_nat_gateway  = var.environment != "prod"
    tags                = local.common_tags
  }

  providers = {
    aws = provider.aws.main
  }
}

# Using a private registry module for security groups
component "security_groups" {
  source  = "app.terraform.io/my-org/security-groups/aws"
  version = "1.5.2"

  inputs = {
    vpc_id      = component.vpc.vpc_id
    name_prefix = local.name_prefix
    environment = var.environment
  }

  providers = {
    aws = provider.aws.main
  }
}

# Using a public registry module for RDS
component "database" {
  source  = "terraform-aws-modules/rds/aws"
  version = "~> 6.0"

  inputs = {
    identifier           = "${local.name_prefix}-db"
    engine               = "postgres"
    engine_version       = "15.3"
    family               = "postgres15"
    major_engine_version = "15"
    instance_class       = var.environment == "prod" ? "db.t3.large" : "db.t3.micro"

    allocated_storage     = var.environment == "prod" ? 100 : 20
    db_name               = replace(var.app_name, "-", "_")
    username              = "dbadmin"
    password              = var.db_password
    port                  = 5432

    db_subnet_group_name   = component.vpc.database_subnet_group_name
    vpc_security_group_ids = [component.security_groups.database_sg_id]

    backup_retention_period = var.environment == "prod" ? 30 : 7
    skip_final_snapshot     = var.environment != "prod"
    deletion_protection     = var.environment == "prod"

    tags = local.common_tags
  }

  providers = {
    aws = provider.aws.main
  }
}

# Using a private registry module for application infrastructure
component "application" {
  source  = "app.terraform.io/my-org/ecs-application/aws"
  version = "3.2.1"

  inputs = {
    name_prefix           = local.name_prefix
    vpc_id                = component.vpc.vpc_id
    private_subnet_ids    = component.vpc.private_subnet_ids
    public_subnet_ids     = component.vpc.public_subnet_ids
    app_security_group_id = component.security_groups.app_sg_id

    container_image       = "my-org/my-app:latest"
    container_port        = 8080
    desired_count         = var.environment == "prod" ? 3 : 1

    environment_variables = {
      ENVIRONMENT    = var.environment
      DATABASE_HOST  = component.database.db_instance_endpoint
      DATABASE_NAME  = component.database.db_instance_name
    }

    tags = local.common_tags
  }

  providers = {
    aws = provider.aws.main
  }
}
outputs.tfcomponent.hcl
output "vpc_id" {
  type        = string
  description = "VPC ID"
  value       = component.vpc.vpc_id
}

output "application_url" {
  type        = string
  description = "Application load balancer URL"
  value       = component.application.load_balancer_dns
}

output "database_endpoint" {
  type        = string
  description = "Database endpoint"
  value       = component.database.db_instance_endpoint
  sensitive   = true
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"
}

deployment "development" {
  inputs = {
    aws_region     = "us-west-2"
    environment    = "dev"
    app_name       = "myapp"
    vpc_cidr       = "10.0.0.0/16"
    db_password    = "dev-password-change-me"
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

deployment "production" {
  inputs = {
    aws_region     = "us-east-1"
    environment    = "prod"
    app_name       = "myapp"
    vpc_cidr       = "10.1.0.0/16"
    db_password    = "prod-password-use-secrets-manager"
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "development" {
  deployments = [deployment.development]
}

deployment_group "production" {
  deployments = [deployment.production]
}
Key Points
  • Private registry modules use the format app.terraform.io/<org>/<module>/<provider>

  • Version constraints ensure consistent module versions across environments

  • Mixed sources: Combining private registry modules (VPC, security groups, application) with public registry modules (RDS)

  • Authentication: HCP Terraform workspaces automatically authenticate to private registries; CLI users need credentials configured

  • Terraform Enterprise: Replace app.terraform.io with your instance hostname

Multi-Environment Stack

Stack with development, staging, and production deployments.

variables.tfcomponent.hcl
variable "aws_region" {
  type = string
}

variable "environment" {
  type = string
}

variable "instance_count" {
  type = number
}

variable "instance_type" {
  type = string
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
}

provider "aws" "this" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }

    default_tags {
      tags = {
        Environment = var.environment
        ManagedBy   = "Terraform Stacks"
      }
    }
  }
}
components.tfcomponent.hcl
locals {
  name_prefix = "myapp-${var.environment}"
}

component "vpc" {
  source = "./modules/vpc"

  inputs = {
    name_prefix = local.name_prefix
    cidr_block  = "10.0.0.0/16"
  }

  providers = {
    aws = provider.aws.this
  }
}

component "compute" {
  source = "./modules/compute"

  inputs = {
    name_prefix    = local.name_prefix
    vpc_id         = component.vpc.vpc_id
    subnet_ids     = component.vpc.private_subnet_ids
    instance_count = var.instance_count
    instance_type  = var.instance_type
  }

  providers = {
    aws = provider.aws.this
  }
}
outputs.tfcomponent.hcl
output "vpc_id" {
  type  = string
  value = component.vpc.vpc_id
}

output "load_balancer_url" {
  type  = string
  value = component.compute.load_balancer_url
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"

  environments = {
    dev = {
      region         = "us-east-1"
      instance_count = 1
      instance_type  = "t3.micro"
    }
    staging = {
      region         = "us-west-1"
      instance_count = 2
      instance_type  = "t3.small"
    }
    prod = {
      region         = "us-west-1"
      instance_count = 5
      instance_type  = "t3.large"
    }
  }
}

deployment "development" {
  inputs = {
    aws_region     = local.environments.dev.region
    environment    = "dev"
    instance_count = local.environments.dev.instance_count
    instance_type  = local.environments.dev.instance_type
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

deployment "staging" {
  inputs = {
    aws_region     = local.environments.staging.region
    environment    = "staging"
    instance_count = local.environments.staging.instance_count
    instance_type  = local.environments.staging.instance_type
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

deployment "production" {
  inputs = {
    aws_region     = local.environments.prod.region
    environment    = "prod"
    instance_count = local.environments.prod.instance_count
    instance_type  = local.environments.prod.instance_type
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "development" {
  deployments = [deployment.development]
}

deployment_group "non_production" {
  deployments = [deployment.staging]
}

deployment_group "production" {
  deployments = [deployment.production]
}

# Auto-approve dev deployments
deployment_auto_approve "dev_auto" {
  deployment_group = deployment_group.development

  check {
    condition = context.plan.applyable
    reason    = "Development plans must be applyable"
  }
}

Multi-Region Stack

Stack that deploys identical infrastructure across multiple AWS regions.

variables.tfcomponent.hcl
variable "regions" {
  type = set(string)
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}

variable "app_name" {
  type = string
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
}

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
    }

    default_tags {
      tags = {
        Region    = each.value
        ManagedBy = "Terraform Stacks"
        AppName   = var.app_name
      }
    }
  }
}
components.tfcomponent.hcl
component "regional_infrastructure" {
  for_each = var.regions

  source = "./modules/regional-infra"

  inputs = {
    region      = each.value
    app_name    = var.app_name
    name_suffix = each.value
  }

  providers = {
    aws = provider.aws.regional[each.value]
  }
}

component "global_route53" {
  source = "./modules/route53"

  inputs = {
    app_name     = var.app_name
    domain_name  = "example.com"
    regional_lbs = {
      for region, comp in component.regional_infrastructure :
      region => comp.load_balancer_dns
    }
  }

  # Use one region's provider for global resources
  providers = {
    aws = provider.aws.regional["us-west-1"]
  }
}
outputs.tfcomponent.hcl
output "regional_endpoints" {
  type = map(string)
  value = {
    for region, comp in component.regional_infrastructure :
    region => comp.load_balancer_url
  }
}

output "global_domain" {
  type  = string
  value = component.global_route53.domain_name
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  regions = ["us-west-1", "us-east-1", "eu-west-1"]
}

deployment "multi_region_prod" {
  inputs = {
    regions        = toset(local.regions)
    app_name       = "my-global-app"
    role_arn       = "arn:aws:iam::123456789012:role/terraform-stacks"
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "production" {
  deployments = [deployment.multi_region_prod]
}

Linked Stacks (Cross-Stack Dependencies)

Two Stacks where the application Stack depends on the network Stack.

Network Stack
network-stack/variables.tfcomponent.hcl
variable "vpc_cidr" {
  type = string
}

variable "environment" {
  type = string
}

variable "aws_region" {
  type = string
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}
network-stack/providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
}

provider "aws" "this" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }
  }
}
network-stack/components.tfcomponent.hcl
component "vpc" {
  source = "./modules/vpc"

  inputs = {
    cidr_block  = var.vpc_cidr
    environment = var.environment
  }

  providers = {
    aws = provider.aws.this
  }
}

component "security_groups" {
  source = "./modules/security-groups"

  inputs = {
    vpc_id      = component.vpc.vpc_id
    environment = var.environment
  }

  providers = {
    aws = provider.aws.this
  }
}
network-stack/outputs.tfcomponent.hcl
output "vpc_id" {
  type  = string
  value = component.vpc.vpc_id
}

output "private_subnet_ids" {
  type  = list(string)
  value = component.vpc.private_subnet_ids
}

output "public_subnet_ids" {
  type  = list(string)
  value = component.vpc.public_subnet_ids
}

output "app_security_group_id" {
  type  = string
  value = component.security_groups.app_sg_id
}
network-stack/deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"
}

deployment "network" {
  inputs = {
    aws_region     = "us-west-1"
    environment    = "production"
    vpc_cidr       = "10.0.0.0/16"
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Publish outputs for other stacks
publish_output "vpc_id_network" {
  type  = string
  value = deployment.network.vpc_id
}

publish_output "private_subnet_ids" {
  type  = list(string)
  value = deployment.network.private_subnet_ids
}

publish_output "public_subnet_ids" {
  type  = list(string)
  value = deployment.network.public_subnet_ids
}

publish_output "app_security_group_id" {
  type  = string
  value = deployment.network.app_security_group_id
}

# Deployment groups
deployment_group "network" {
  deployments = [deployment.network]
}
Application Stack
application-stack/variables.tfcomponent.hcl
variable "vpc_id" {
  type = string
}

variable "subnet_ids" {
  type = list(string)
}

variable "security_group_id" {
  type = string
}

variable "instance_count" {
  type = number
}

variable "aws_region" {
  type = string
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}
application-stack/providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
}

provider "aws" "this" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }
  }
}
application-stack/components.tfcomponent.hcl
component "application" {
  source = "./modules/app"

  inputs = {
    vpc_id            = var.vpc_id
    subnet_ids        = var.subnet_ids
    security_group_id = var.security_group_id
    instance_count    = var.instance_count
  }

  providers = {
    aws = provider.aws.this
  }
}
application-stack/deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

# Reference the network stack
upstream_input "network" {
  type   = "stack"
  source = "app.terraform.io/my-org/my-project/network-stack"
}

deployment "application" {
  inputs = {
    aws_region        = "us-west-1"
    vpc_id            = upstream_input.network.vpc_id_network
    subnet_ids        = upstream_input.network.private_subnet_ids
    security_group_id = upstream_input.network.app_security_group_id
    instance_count    = 3
    role_arn          = "arn:aws:iam::123456789012:role/terraform-stacks"
    identity_token    = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "application" {
  deployments = [deployment.application]
}

Multi-Cloud Stack

Stack that deploys to both AWS and Azure.

variables.tfcomponent.hcl
variable "aws_region" {
  type = string
}

variable "azure_location" {
  type = string
}

variable "aws_identity_token" {
  type      = string
  ephemeral = true
}

variable "aws_role_arn" {
  type = string
}

variable "azure_identity_token" {
  type      = string
  ephemeral = true
}

variable "azure_subscription_id" {
  type = string
}

variable "azure_tenant_id" {
  type = string
}

variable "azure_client_id" {
  type = string
}

variable "app_name" {
  type = string
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
  azurerm = {
    source  = "hashicorp/azurerm"
    version = "~> 3.0"
  }
}

provider "aws" "this" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.aws_role_arn
      web_identity_token = var.aws_identity_token
    }
  }
}

provider "azurerm" "this" {
  config {
    features {}

    subscription_id = var.azure_subscription_id
    tenant_id       = var.azure_tenant_id
    client_id       = var.azure_client_id

    use_oidc = true
    oidc_token = var.azure_identity_token
  }
}
components.tfcomponent.hcl
component "aws_infrastructure" {
  source = "./modules/aws-infra"

  inputs = {
    region   = var.aws_region
    app_name = var.app_name
  }

  providers = {
    aws = provider.aws.this
  }
}

component "azure_infrastructure" {
  source = "./modules/azure-infra"

  inputs = {
    location = var.azure_location
    app_name = var.app_name
  }

  providers = {
    azurerm = provider.azurerm.this
  }
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

identity_token "azure" {
  audience = ["api://AzureADTokenExchange"]
}

deployment "multi_cloud" {
  inputs = {
    aws_region             = "us-west-1"
    azure_location         = "westus2"
    app_name               = "my-multi-cloud-app"
    aws_role_arn           = "arn:aws:iam::123456789012:role/terraform-stacks"
    aws_identity_token     = identity_token.aws.jwt
    azure_subscription_id  = "12345678-1234-1234-1234-123456789012"
    azure_tenant_id        = "87654321-4321-4321-4321-210987654321"
    azure_client_id        = "11111111-1111-1111-1111-111111111111"
    azure_identity_token   = identity_token.azure.jwt
  }
}

# Deployment groups
deployment_group "multi_cloud" {
  deployments = [deployment.multi_cloud]
}

Complete AWS Production Stack

Full production-grade Stack with VPC, RDS, ECS, and monitoring.

variables.tfcomponent.hcl
variable "aws_region" {
  type        = string
  description = "AWS region"
}

variable "environment" {
  type        = string
  description = "Environment name"
}

variable "vpc_cidr" {
  type        = string
  description = "VPC CIDR block"
}

variable "app_name" {
  type        = string
  description = "Application name"
}

variable "db_instance_class" {
  type        = string
  description = "RDS instance class"
}

variable "ecs_desired_count" {
  type        = number
  description = "Desired ECS task count"
}

variable "identity_token" {
  type      = string
  ephemeral = true
}

variable "role_arn" {
  type = string
}
providers.tfcomponent.hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.7.0"
  }
  random = {
    source  = "hashicorp/random"
    version = "~> 3.5.0"
  }
}

provider "aws" "this" {
  config {
    region = var.aws_region

    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }

    default_tags {
      tags = {
        Environment = var.environment
        Application = var.app_name
        ManagedBy   = "Terraform Stacks"
      }
    }
  }
}

provider "random" "this" {
  config {}
}
components.tfcomponent.hcl
locals {
  name_prefix = "${var.app_name}-${var.environment}"
}

component "vpc" {
  source = "./modules/vpc"

  inputs = {
    name_prefix = local.name_prefix
    cidr_block  = var.vpc_cidr
    azs_count   = 3
  }

  providers = {
    aws = provider.aws.this
  }
}

component "security_groups" {
  source = "./modules/security-groups"

  inputs = {
    name_prefix = local.name_prefix
    vpc_id      = component.vpc.vpc_id
  }

  providers = {
    aws = provider.aws.this
  }
}

component "rds" {
  source = "./modules/rds"

  inputs = {
    name_prefix        = local.name_prefix
    instance_class     = var.db_instance_class
    subnet_ids         = component.vpc.private_subnet_ids
    security_group_ids = [component.security_groups.database_sg_id]
  }

  providers = {
    aws    = provider.aws.this
    random = provider.random.this
  }
}

component "ecs_cluster" {
  source = "./modules/ecs-cluster"

  inputs = {
    name_prefix = local.name_prefix
  }

  providers = {
    aws = provider.aws.this
  }
}

component "ecs_service" {
  source = "./modules/ecs-service"

  inputs = {
    name_prefix      = local.name_prefix
    cluster_id       = component.ecs_cluster.cluster_id
    desired_count    = var.ecs_desired_count
    subnet_ids       = component.vpc.private_subnet_ids
    security_group_id = component.security_groups.app_sg_id
    database_endpoint = component.rds.endpoint
  }

  providers = {
    aws = provider.aws.this
  }
}

component "alb" {
  source = "./modules/alb"

  inputs = {
    name_prefix       = local.name_prefix
    vpc_id            = component.vpc.vpc_id
    subnet_ids        = component.vpc.public_subnet_ids
    security_group_id = component.security_groups.alb_sg_id
    target_group_arn  = component.ecs_service.target_group_arn
  }

  providers = {
    aws = provider.aws.this
  }
}

component "cloudwatch" {
  source = "./modules/cloudwatch"

  inputs = {
    name_prefix  = local.name_prefix
    cluster_name = component.ecs_cluster.cluster_name
    service_name = component.ecs_service.service_name
  }

  providers = {
    aws = provider.aws.this
  }
}
outputs.tfcomponent.hcl
output "load_balancer_url" {
  type        = string
  description = "Application load balancer URL"
  value       = component.alb.dns_name
}

output "database_endpoint" {
  type        = string
  description = "RDS endpoint"
  value       = component.rds.endpoint
  sensitive   = true
}

output "vpc_id" {
  type  = string
  value = component.vpc.vpc_id
}

output "ecs_cluster_name" {
  type  = string
  value = component.ecs_cluster.cluster_name
}
deployments.tfdeploy.hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"
}

deployment "staging" {
  inputs = {
    aws_region        = "us-west-1"
    environment       = "staging"
    app_name          = "myapp"
    vpc_cidr          = "10.1.0.0/16"
    db_instance_class = "db.t3.small"
    ecs_desired_count = 2
    role_arn          = local.role_arn
    identity_token    = identity_token.aws.jwt
  }
}

deployment "production" {
  inputs = {
    aws_region        = "us-west-1"
    environment       = "production"
    app_name          = "myapp"
    vpc_cidr          = "10.0.0.0/16"
    db_instance_class = "db.r5.large"
    ecs_desired_count = 5
    role_arn          = local.role_arn
    identity_token    = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "staging" {
  deployments = [deployment.staging]
}

deployment_group "production" {
  deployments = [deployment.production]
}

# Auto-approve staging with safety checks
deployment_auto_approve "staging_safe" {
  deployment_group = deployment_group.staging

  check {
    condition = context.plan.changes.remove == 0
    reason    = "Cannot auto-approve deletions in staging"
  }

  check {
    condition = context.plan.applyable
    reason    = "Plan must be applyable"
  }
}

Testing Configurations

Validate Stack Configuration
terraform stacks providers lock
terraform stacks validate
Plan Specific Deployment
terraform stacks plan --deployment=development
terraform stacks plan --deployment=production
Apply Deployment
terraform stacks apply --deployment=staging

Destroying Deployments

Example of safely removing a deployment from your Stack.

Scenario

You want to decommission the "development" deployment while keeping staging and production active.

Step 1: Mark Deployment for Destruction

Update your deployments.tfdeploy.hcl file to set destroy = true:

identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"
}

# Mark this deployment for destruction
deployment "development" {
  inputs = {
    aws_region     = "us-east-1"
    environment    = "dev"
    instance_count = 1
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
  destroy = true  # This tells HCP Terraform to destroy all resources
}

# Keep these deployments active
deployment "staging" {
  inputs = {
    aws_region     = "us-west-1"
    environment    = "staging"
    instance_count = 2
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

deployment "production" {
  inputs = {
    aws_region     = "us-west-1"
    environment    = "prod"
    instance_count = 5
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "staging" {
  deployments = [deployment.staging]
}

deployment_group "production" {
  deployments = [deployment.production]
}
Step 2: Plan and Apply
# Review the destruction plan
terraform stacks plan --deployment=development

# Apply the destruction
terraform stacks apply --deployment=development

HCP Terraform will destroy all resources in the development deployment.

Step 3: Remove the Deployment Block

After the deployment is successfully destroyed, remove the entire deployment block from your configuration:

identity_token "aws" {
  audience = ["aws.workload.identity"]
}

locals {
  role_arn = "arn:aws:iam::123456789012:role/terraform-stacks"
}

# deployment "development" block has been removed

deployment "staging" {
  inputs = {
    aws_region     = "us-west-1"
    environment    = "staging"
    instance_count = 2
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

deployment "production" {
  inputs = {
    aws_region     = "us-west-1"
    environment    = "prod"
    instance_count = 5
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Deployment groups
deployment_group "staging" {
  deployments = [deployment.staging]
}

deployment_group "production" {
  deployments = [deployment.production]
}
Important Notes
  • Provider Authentication: The destroy argument ensures your configuration retains the provider authentication needed to destroy resources

  • Do Not Remove Immediately: Don't remove the deployment block until after the destruction is complete

  • Verify Before Removing: Check HCP Terraform UI to confirm all resources are destroyed before removing the block

  • Alternative: You could manually destroy resources through HCP Terraform UI, but using destroy = true is the recommended approach for maintaining infrastructure-as-code practices

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs a command-line tool

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
HashiCorp
Publisher
HashiCorp (Official)
Source
hashicorp/agent-skills · plugins/terraform/skills/terraform-stacks · commit 5e30fa5
Licence
Mozilla Public License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by HashiCorp.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download terraform-stacks.zip