
Terraform Stacks
- 32 installs
- 781 repo stars
- Updated August 4, 2026
- hashicorp/terraform-agent-kit
This is a copy of terraform-stacks by hashicorp - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
terraform-stacks is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- terraform-stacks
- AI & Agent Building
- AI-coding skill
Terraform Stacks by the numbers
- 32 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hashicorp/terraform-agent-kit --skill terraform-stacksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 32 |
|---|---|
| repo stars | ★ 781 |
| Last updated | August 4, 2026 |
| Repository | hashicorp/terraform-agent-kit ↗ |
What it does
Helps with ai & agent building tasks.
Files
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/
├── .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.5Component 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>orcomponent.<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>orprovider.<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 uploadingDeployment 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 neededConfiguration Management
terraform stacks configuration list # List configuration versions
terraform stacks configuration fetch -configuration-id=... # Download configuration
terraform stacks configuration watch # Monitor upload statusOther 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 deploymentMonitoring 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_idquery 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 syntaxreferences/deployment-blocks.md- Complete deployment block reference with all configuration optionsreferences/linked-stacks.md- Publish outputs and upstream inputs for linking Stacks togetherreferences/examples.md- Complete working examples for multi-region and component dependenciesreferences/api-monitoring.md- Full API workflow for programmatic monitoring and automationreferences/troubleshooting.md- Detailed troubleshooting guide for common issues and solutions
API Monitoring Reference
Complete guide for monitoring Terraform Stack deployments using the HCP Terraform API. Use this approach for automation, CI/CD pipelines, and non-interactive environments like AI agents.
Table of Contents
1. When to Use the API 2. Authentication 3. API Monitoring Workflow 4. Detailed Endpoint Reference 5. Notes for AI Agents and Automation
When to Use the API
Use the HCP Terraform API instead of CLI commands when:
- Running in non-interactive environments (CI/CD, automation scripts)
- Building tools or integrations that need programmatic access
- Monitoring multiple Stacks simultaneously
- Implementing custom retry logic or error handling
- Working in environments where streaming CLI commands don't work
CLI commands that don't work in automation:
terraform stacks deployment-run watch- Streams output, blocks indefinitelyterraform stacks deployment-group watch- Streams output, blocks indefinitelyterraform stacks configuration watch- Streams output, blocks indefinitely
Authentication
Extract API Token from Credentials File
TOKEN=$(jq -r '.credentials["app.terraform.io"].token' ~/.terraform.d/credentials.tfrc.json)Alternative: Use Environment Variable
export TFC_TOKEN="your-token-here"
TOKEN=$TFC_TOKENAPI Request Headers
All API requests require these headers:
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/vnd.api+json"API Monitoring Workflow
After uploading a configuration with terraform stacks configuration upload, follow this sequence to monitor deployment progress:
Step 1: Get Configuration Status
Endpoint: GET /api/v2/stack-configurations/{configuration-id}
Purpose: Verify configuration upload completed successfully and get the configuration details.
Request:
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/stack-configurations/{configuration-id}" | jq '.'Response Fields:
attributes.status- Configuration processing status (pending/completed)attributes.sequence-number- Version number of this configurationattributes.components-detected- Number of components foundattributes.deployments-detected- Number of deployments found
Example Response:
{
"data": {
"id": "stc-ABC123",
"type": "stack-configurations",
"attributes": {
"status": "completed",
"sequence-number": 5,
"components-detected": 3,
"deployments-detected": 2,
"created-at": "2024-01-15T10:30:00.000Z",
"updated-at": "2024-01-15T10:30:45.000Z"
}
}
}Step 2: Get Deployment Group Summaries
Endpoint: GET /api/v2/stack-configurations/{configuration-id}/stack-deployment-group-summaries
Purpose: Get list of deployment groups, their IDs, and current status summary.
Request:
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/stack-configurations/{configuration-id}/stack-deployment-group-summaries" | jq '.'Response Fields:
id- Deployment group ID (needed for next step)attributes.name- Deployment group name (e.g.,dev_default)attributes.status- Overall status (running/succeeded/failed)attributes.status-counts- Breakdown of deployment statuses
Example Response:
{
"data": [
{
"id": "sdg-XYZ789",
"type": "stack-deployment-group-summaries",
"attributes": {
"name": "dev_default",
"status": "running",
"status-counts": {
"pending": 0,
"running": 1,
"succeeded": 1,
"failed": 0
}
}
}
]
}Step 3: Get Deployment Runs
Endpoint: GET /api/v2/stack-deployment-groups/{group-id}/stack-deployment-runs
Purpose: Get list of deployment runs for a specific group with their current status.
Request:
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/stack-deployment-groups/{group-id}/stack-deployment-runs" | jq '.'Response Fields:
id- Deployment run ID (needed for next step)attributes.status- Current status (planning/planned/applying/applied/failed)attributes.created-at- Run start timeattributes.updated-at- Last update time
Example Response:
{
"data": [
{
"id": "sdr-123ABC",
"type": "stack-deployment-runs",
"attributes": {
"status": "planning",
"created-at": "2024-01-15T10:31:00.000Z",
"updated-at": "2024-01-15T10:31:15.000Z"
}
}
]
}Step 4: Get Deployment Steps
Endpoint: GET /api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps
Purpose: Get detailed information about individual plan and apply steps.
Request:
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps" | jq '.'Response Fields:
id- Step ID (needed for diagnostics and outputs)attributes.operation-type- Type of operation (plan/apply)attributes.status- Step status (running/completed/failed)attributes.component-name- Which component is being processed
Example Response:
{
"data": [
{
"id": "sds-PlanStep123",
"type": "stack-deployment-steps",
"attributes": {
"operation-type": "plan",
"status": "completed",
"component-name": "vpc",
"created-at": "2024-01-15T10:31:05.000Z",
"completed-at": "2024-01-15T10:31:30.000Z"
}
},
{
"id": "sds-ApplyStep456",
"type": "stack-deployment-steps",
"attributes": {
"operation-type": "apply",
"status": "running",
"component-name": "vpc",
"created-at": "2024-01-15T10:32:00.000Z"
}
}
]
}Step 5: Get Error Diagnostics (When Deployment Fails)
Endpoint: GET /api/v2/stack-deployment-steps/{step-id}/stack-diagnostics
Purpose: Retrieve detailed error messages when a deployment step fails.
Critical: The stack_deployment_step_id query parameter is required. Without it, the API returns empty results.
Request:
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | jq '.'Response Fields:
attributes.severity- Diagnostic level (error/warning)attributes.summary- Brief error descriptionattributes.detail- Detailed error messageattributes.diags- Array of diagnostic objects with file locations and code snippets
Example Response (Error with Details):
{
"data": [
{
"id": "stf-ErrorExampleId",
"type": "stack-diagnostics",
"attributes": {
"severity": "error",
"summary": "Diagnostics reported",
"detail": "2 errors",
"diags": [
{
"summary": "Unsupported attribute",
"detail": "This object does not have an attribute named \"target_id\".",
"range": {
"filename": "main.tf",
"start": {
"line": 634,
"column": 33
},
"end": {
"line": 634,
"column": 43
},
"source": "registry.terraform.io/terraform-aws-modules/alb/aws@9.17.0//main.tf"
},
"snippet": {
"code": " target_id = each.value.target_id",
"context": "resource \"aws_lb_target_group_attachment\" \"this\""
}
},
{
"summary": "Invalid reference",
"detail": "A reference to a resource type must be followed by at least one attribute access.",
"range": {
"filename": "main.tf",
"start": {
"line": 142,
"column": 15
},
"end": {
"line": 142,
"column": 28
},
"source": "local-module//main.tf"
},
"snippet": {
"code": " vpc_id = aws_vpc.main",
"context": "resource \"aws_subnet\" \"private\""
}
}
],
"acknowledged": false,
"created-at": "2024-01-15T10:32:15.000Z"
}
}
]
}Parsing Diagnostics:
Extract error information with jq:
# Get error summaries
curl -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | \
jq -r '.data[].attributes.diags[]? | "\(.summary): \(.detail)"'
# Get file locations
curl -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}" | \
jq -r '.data[].attributes.diags[]? | "\(.range.filename):\(.range.start.line)"'Step 6: Get Stack Outputs (After Successful Deployment)
Endpoint: GET /api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description
Purpose: Retrieve Stack outputs after a successful deployment completes.
Important Notes:
- This endpoint returns HTTP 307 redirect - use
curl -Lto follow redirects automatically - This is currently the only way to retrieve Stack outputs programmatically
- This endpoint is not documented in public API documentation
- You need the final apply step ID from Step 4
Request:
curl -L -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description"Response Structure:
The artifact response includes an .outputs object where each output contains a change.after property with the actual output value:
{
"outputs": {
"alb_url": {
"change": {
"actions": ["no-op"],
"before": "http://my-alb-123456789.us-west-2.elb.amazonaws.com",
"after": "http://my-alb-123456789.us-west-2.elb.amazonaws.com",
"after_unknown": false,
"before_sensitive": false,
"after_sensitive": false
},
"type": "string"
},
"ecr_repository_url": {
"change": {
"actions": ["no-op"],
"before": "123456789.dkr.ecr.us-west-2.amazonaws.com/my-repo",
"after": "123456789.dkr.ecr.us-west-2.amazonaws.com/my-repo",
"after_unknown": false,
"before_sensitive": false,
"after_sensitive": false
},
"type": "string"
}
}
}Extract Only Output Values:
curl -L -s --header "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{final-apply-step-id}/artifacts?name=apply-description" | \
jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"'Example Output:
alb_url: http://my-alb-123456789.us-west-2.elb.amazonaws.com
ecr_repository_url: 123456789.dkr.ecr.us-west-2.amazonaws.com/my-repoDetailed Endpoint Reference
Available Artifact Types
The artifacts endpoint accepts these name parameter values:
plan-description- Terraform plan output in JSON formatplan-debug-log- Detailed debug logs from plan operationapply-description- Terraform apply output including outputs (JSON format)apply-debug-log- Detailed debug logs from apply operation
Polling Best Practices
Recommended polling intervals:
- Configuration status: Check every 5 seconds until status is "completed"
- Deployment runs: Check every 10 seconds during active deployment
- Deployment steps: Check every 10 seconds for individual step status
Implement exponential backoff:
# Example polling script with backoff
RETRY_COUNT=0
MAX_RETRIES=30
BACKOFF=5
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
STATUS=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}" | \
jq -r '.data.attributes.status')
if [ "$STATUS" = "applied" ] || [ "$STATUS" = "failed" ]; then
echo "Deployment finished with status: $STATUS"
break
fi
echo "Current status: $STATUS. Waiting ${BACKOFF}s..."
sleep $BACKOFF
RETRY_COUNT=$((RETRY_COUNT + 1))
doneNotes for AI Agents and Automation
CLI Command Limitations
These CLI commands DO NOT work in automation:
terraform stacks deployment-run watch- Streams output, blocks indefinitelyterraform stacks deployment-group watch- Streams output, blocks indefinitelyterraform stacks configuration watch- Streams output, blocks indefinitely
Solution: Use API polling instead of watch commands.
No Direct Output Command
There is currently no CLI command to retrieve Stack outputs. You must: 1. Use API to get deployment steps 2. Find the final apply step ID 3. Request the apply-description artifact 4. Parse JSON to extract outputs
Handling Redirects
The artifacts endpoint returns HTTP 307 redirect to the actual artifact location. Ensure your HTTP client follows redirects:
curl: Use -L flag Python requests: Set allow_redirects=True (default) Node.js fetch: Set redirect: 'follow' (default)
Error Handling
Common API errors:
- 401 Unauthorized: Invalid or expired token - refresh credentials
- 404 Not Found: Invalid ID or resource doesn't exist yet - retry with backoff
- 429 Too Many Requests: Rate limited - implement exponential backoff
- Empty diagnostics: Missing required
stack_deployment_step_idquery parameter
Complete Monitoring Script Example
#!/bin/bash
# Configuration
TOKEN=$(jq -r '.credentials["app.terraform.io"].token' ~/.terraform.d/credentials.tfrc.json)
CONFIG_ID="stc-ABC123"
BASE_URL="https://app.terraform.io/api/v2"
# Helper function
api_get() {
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/vnd.api+json" \
"$1"
}
# 1. Wait for configuration to complete
echo "Checking configuration status..."
while true; do
STATUS=$(api_get "$BASE_URL/stack-configurations/$CONFIG_ID" | jq -r '.data.attributes.status')
[ "$STATUS" = "completed" ] && break
echo "Configuration status: $STATUS. Waiting..."
sleep 5
done
# 2. Get deployment groups
echo "Getting deployment groups..."
GROUP_ID=$(api_get "$BASE_URL/stack-configurations/$CONFIG_ID/stack-deployment-group-summaries" | \
jq -r '.data[0].id')
# 3. Get deployment run
echo "Getting deployment run..."
RUN_ID=$(api_get "$BASE_URL/stack-deployment-groups/$GROUP_ID/stack-deployment-runs" | \
jq -r '.data[0].id')
# 4. Monitor deployment run
echo "Monitoring deployment run: $RUN_ID"
while true; do
STATUS=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID" | jq -r '.data.attributes.status')
echo "Deployment status: $STATUS"
if [ "$STATUS" = "applied" ]; then
echo "Deployment succeeded!"
# 5. Get outputs from final apply step
APPLY_STEP=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID/stack-deployment-steps" | \
jq -r '.data[] | select(.attributes["operation-type"] == "apply") | .id' | tail -1)
echo "Retrieving outputs from step: $APPLY_STEP"
curl -L -s -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/stack-deployment-steps/$APPLY_STEP/artifacts?name=apply-description" | \
jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"'
break
fi
if [ "$STATUS" = "failed" ]; then
echo "Deployment failed!"
# Get error diagnostics
FAILED_STEP=$(api_get "$BASE_URL/stack-deployment-runs/$RUN_ID/stack-deployment-steps" | \
jq -r '.data[] | select(.attributes.status == "failed") | .id' | head -1)
echo "Error diagnostics from step: $FAILED_STEP"
api_get "$BASE_URL/stack-deployment-steps/$FAILED_STEP/stack-diagnostics?stack_deployment_step_id=$FAILED_STEP" | \
jq -r '.data[].attributes.diags[]? | "\(.summary): \(.detail)"'
exit 1
fi
sleep 10
doneThis script demonstrates a complete monitoring workflow from configuration upload to output retrieval with error handling.
Component Configuration Block Reference
Complete reference for all blocks available in Terraform Stack component configuration files (.tfcomponent.hcl).
Table of Contents
1. Variable Block 2. Required Providers Block 3. Provider Block 4. Component Block 5. Output Block 6. Locals Block 7. Removed Block
Variable Block
Declares input variables for Stack configuration.
Syntax
variable "variable_name" {
type = <type>
description = "<description>"
default = <value>
sensitive = <bool>
nullable = <bool>
ephemeral = <bool>
}Arguments
- type (required): Data type (string, number, bool, list, map, object, set, tuple, any)
- description (optional): Variable description
- default (optional): Default value
- sensitive (optional, default false): Mark as sensitive to redact from logs
- nullable (optional, default true): Whether null is allowed
- ephemeral (optional, default false): Do not persist to state file
Differences from Traditional Terraform
- type is required (not optional)
- validation argument is not supported
Examples
variable "aws_region" {
type = string
description = "AWS region for infrastructure"
default = "us-west-1"
}
variable "identity_token" {
type = string
description = "OIDC identity token"
ephemeral = true
}
variable "subnet_config" {
type = object({
cidr_block = string
availability_zone = string
map_public_ip = bool
})
}For complete variable examples in context, see examples.md.
Required Providers Block
Declares provider dependencies.
Syntax
required_providers {
<provider_name> = {
source = "<source>"
version = "<version_constraint>"
}
}Arguments
- source (required): Provider source address (e.g., "hashicorp/aws")
- version (optional): Version constraint (e.g., "~> 5.0")
Examples
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.7.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0"
}
}Provider Block
Configures provider instances.
Syntax
provider "<provider_type>" "<alias>" {
for_each = <map_or_set> # Optional
config {
<provider_arguments>
}
}Arguments
- provider_type (label 1, required): Provider type (e.g., "aws", "azurerm")
- alias (label 2, required): Unique identifier for this provider configuration
- for_each (optional): Create multiple provider instances from a map or set
- config (required): Nested block containing provider-specific configuration
Key Differences from Traditional Terraform
1. Alias is defined in block header, not as an argument 2. Configuration goes in a nested config block 3. Supports for_each meta-argument 4. Provider configurations are treated as first-class values
Example
provider "aws" "main" {
config {
region = var.aws_region
assume_role_with_web_identity {
role_arn = var.role_arn
web_identity_token = var.identity_token
}
}
}For complete provider examples including for_each and multi-cloud patterns, see examples.md.
Component Block
Defines infrastructure components to include in the Stack.
Syntax
component "<component_name>" {
for_each = <map_or_set> # Optional
source = "<module_source>"
inputs = {
<input_name> = <value>
}
providers = {
<provider_local_name> = provider.<type>.<alias>[<key>]
}
}Arguments
- component_name (label, required): Unique identifier for this component
- for_each (optional): Create multiple component instances
- source (required): Module source (see Source Argument below)
- version (optional): Version constraint for registry-based sources only
- inputs (required): Map of input variables for the module
- providers (required): Map of provider configurations
Source Argument
The source argument accepts the same module sources as traditional Terraform configurations.
Local File Path:
source = "./modules/vpc"
source = "../shared-modules/networking"Public Terraform Registry:
source = "terraform-aws-modules/vpc/aws"
source = "hashicorp/consul/aws"Format: <NAMESPACE>/<NAME>/<PROVIDER>
Private HCP Terraform Registry:
source = "app.terraform.io/my-org/vpc/aws"
source = "app.terraform.io/example-corp/networking/azurerm"Format: <HOSTNAME>/<ORGANIZATION>/<MODULE_NAME>/<PROVIDER_NAME>
- HCP Terraform (SaaS): Use hostname
app.terraform.io - Terraform Enterprise: Use your instance hostname (e.g.,
terraform.mycompany.com) - Generic hostname: Use
localterraform.comfor deployments spanning multiple Terraform Enterprise instances
Git Repository:
source = "git::https://github.com/org/repo.git//modules/vpc?ref=v1.0.0"
source = "git::ssh://git@github.com/org/repo.git//modules/vpc?ref=main"HTTP/HTTPS Archive:
source = "https://example.com/modules/vpc-module.tar.gz"Version Argument
The version argument is supported only for registry-based sources (public and private registries). Local file paths and Git sources do not support the version argument.
component "vpc" {
source = "app.terraform.io/my-org/vpc/aws"
version = "~> 2.0" # Semantic versioning constraint
inputs = {
cidr_block = var.vpc_cidr
}
providers = {
aws = provider.aws.main
}
}Note: Modules sourced from local file paths always share the same version as their caller and cannot have independent version constraints.
Component References
Access component outputs using: component.<name>.<output>
For components with for_each: component.<name>[<key>].<output>
Examples
Basic Component:
component "vpc" {
source = "app.terraform.io/my-org/vpc/aws"
version = "2.1.0"
inputs = {
cidr_block = var.vpc_cidr
name_prefix = var.name_prefix
}
providers = {
aws = provider.aws.main
}
}Component with Dependencies:
component "database" {
source = "./modules/rds"
inputs = {
vpc_id = component.vpc.vpc_id
subnet_ids = component.vpc.private_subnet_ids
security_group_ids = [component.security.database_sg_id]
engine_version = var.db_engine_version
}
providers = {
aws = provider.aws.main
}
}For complete component examples including for_each, multi-region, public registry, and multi-provider patterns, see examples.md.
Output Block
Exposes values from Stack configuration.
Syntax
output "<output_name>" {
type = <type>
description = "<description>"
value = <expression>
sensitive = <bool>
ephemeral = <bool>
}Arguments
- output_name (label, required): Unique identifier for this output
- type (required): Data type of the output
- description (optional): Output description
- value (required): Expression to output
- sensitive (optional, default false): Mark as sensitive
- ephemeral (optional, default false): Ephemeral value
Differences from Traditional Terraform
- type is required
- precondition block is not supported
Examples
output "vpc_id" {
type = string
description = "VPC ID"
value = component.vpc.vpc_id
}
output "instance_details" {
type = object({
id = string
public_ip = string
private_ip = string
})
description = "EC2 instance details"
value = {
id = component.compute.instance_id
public_ip = component.compute.public_ip
private_ip = component.compute.private_ip
}
}For complete output examples including sensitive outputs and for expressions, see examples.md.
Locals Block
Defines local values for reuse within the Stack configuration.
Syntax
locals {
<name> = <expression>
}Example
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform Stacks"
Project = var.project_name
}
name_prefix = "${var.project_name}-${var.environment}"
region_config = {
for region in var.regions : region => {
name_suffix = region
instance_count = var.environment == "prod" ? 3 : 1
}
}
}Removed Block
Declares components to be removed from the Stack.
Syntax
removed {
from = component.<component_name>
source = "<original_module_source>"
providers = {
<provider_name> = provider.<type>.<alias>
}
}Arguments
- from (required): Reference to the component being removed
- source (required): Original module source
- providers (required): Provider configurations needed for removal
Important Notes
- Required for safe component removal
- Must include all providers the component used
- Do not remove providers before removing components that use them
Examples
removed {
from = component.old_component
source = "./modules/deprecated-module"
providers = {
aws = provider.aws.main
}
}
removed {
from = component.legacy_regional
source = "registry.terraform.io/example/legacy/aws"
providers = {
aws = provider.aws.main
random = provider.random.main
}
}Provider References in Component Blocks
Single Provider
providers = {
aws = provider.aws.main
}Multiple Providers
providers = {
aws = provider.aws.main
random = provider.random.main
tls = provider.tls.main
}Provider from for_each
providers = {
aws = provider.aws.regional[each.value]
}Aliased Providers in Module
If module requires specific provider aliases:
providers = {
aws.source = provider.aws.us_east
aws.dest = provider.aws.eu_west
}Deployment Configuration Block Reference
Complete reference for all blocks available in Terraform Stack deployment configuration files (.tfdeploy.hcl).
Table of Contents
1. Identity Token Block 2. Locals Block 3. Deployment Block 4. Deployment Group Block 5. Deployment Auto-Approve Block
Note: For Publish Output and Upstream Input blocks (linked Stacks), see linked-stacks.md.
Identity Token Block
Generates JWT tokens for OIDC authentication with cloud providers.
Syntax
identity_token "<token_name>" {
audience = [<audience_strings>]
}Arguments
- token_name (label, required): Unique identifier for this token
- audience (required): List of audience strings for the JWT
Accessing Token
Reference the JWT using: identity_token.<n>.jwt
Cloud Provider Audiences
AWS:
identity_token "aws" {
audience = ["aws.workload.identity"]
}Azure:
identity_token "azure" {
audience = ["api://AzureADTokenExchange"]
}Google Cloud:
identity_token "gcp" {
audience = ["//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>"]
}Setup Documentation: For detailed instructions on configuring OIDC/workload identity for each cloud provider (including IAM roles, trust policies, and federated credentials), see: https://developer.hashicorp.com/terraform/cloud-docs/dynamic-provider-credentials
Examples
Single Token:
identity_token "aws" {
audience = ["aws.workload.identity"]
}
deployment "production" {
inputs = {
identity_token = identity_token.aws.jwt
role_arn = var.role_arn
}
}For complete working examples including multi-region identity token usage, see examples.md.
Locals Block
Defines local values for reuse within deployment configuration.
Syntax
locals {
<n> = <expression>
}Example
locals {
aws_regions = ["us-west-1", "us-east-1", "eu-west-1"]
role_arn = "arn:aws:iam::123456789012:role/hcp-terraform-stacks"
common_inputs = {
project_name = "my-app"
environment = "production"
}
}Deployment Block
Defines deployment instances of the Stack.
Syntax
deployment "<deployment_name>" {
inputs = {
<input_name> = <value>
}
}Arguments
- deployment_name (label, required): Unique identifier for this deployment
- inputs (required): Map of input variable values
- destroy (optional, default: false): Boolean flag to destroy this deployment
Constraints
- Minimum 1 deployment per Stack
- Maximum 20 deployments per Stack
- No meta-arguments supported (no
for_each,count)
Destroying a Deployment
To safely remove a deployment from your Stack:
1. Set destroy = true in the deployment block 2. Apply the plan through HCP Terraform 3. After successful destruction, remove the deployment block from your configuration
Important: Using the destroy argument ensures your configuration has the provider authentication necessary to properly destroy the deployment's resources.
Example:
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 this plan and the deployment is destroyed, remove the entire deployment "old_environment" block from your configuration.
Examples
Single Deployment:
deployment "production" {
inputs = {
aws_region = "us-west-1"
instance_count = 5
instance_type = "t3.large"
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}Using Locals for Multiple Deployments:
locals {
common_inputs = {
role_arn = "arn:aws:iam::123456789012:role/terraform"
identity_token = identity_token.aws.jwt
project_name = "my-app"
}
}
deployment "dev" {
inputs = merge(local.common_inputs, {
aws_region = "us-east-1"
instance_count = 1
environment = "dev"
})
}
deployment "prod" {
inputs = merge(local.common_inputs, {
aws_region = "us-west-1"
instance_count = 5
environment = "prod"
})
}For complete multi-environment and multi-region deployment examples, see examples.md.
Deployment Group Block
Groups deployments together to configure shared settings and auto-approval rules (HCP Terraform Premium tier feature).
Syntax
deployment_group "<group_name>" {
deployments = [<deployment_references>]
}Arguments
- group_name (label, required): Unique identifier for this deployment group
- deployments (required): List of deployment references to include in this group
Purpose
Deployment groups allow you to:
- Organize deployments logically (by environment, team, region, etc.)
- Configure shared auto-approval rules for multiple deployments
- Manage deployments more effectively at scale
- Establish consistent configuration patterns across all Stacks
Examples
Single Deployment Group (Best Practice):
deployment "production" {
inputs = {
aws_region = "us-west-1"
instance_count = 5
role_arn = local.role_arn
identity_token = identity_token.aws.jwt
}
}
deployment_group "production" {
deployments = [deployment.production]
}Multiple Deployment Groups:
deployment_group "non_production" {
deployments = [
deployment.development,
deployment.staging
]
}
deployment_group "production" {
deployments = [
deployment.prod_us_east,
deployment.prod_us_west,
deployment.prod_eu_west
]
}Deployment Auto-Approve Block
Defines rules that automatically approve deployment plans based on specific conditions (HCP Terraform Premium feature).
Syntax
deployment_auto_approve "<rule_name>" {
deployment_group = deployment_group.<group_name>
check {
condition = <boolean_expression>
reason = "<failure_message>"
}
}Arguments
- rule_name (label, required): Unique identifier for this auto-approve rule
- deployment_group (required): Reference to the deployment group this rule applies to
- check (required, one or more): Condition that must be met for auto-approval
Context Variables
Access plan information through context object:
context.plan.applyable- Boolean: plan succeeded without errorscontext.plan.changes.add- Number: resources to addcontext.plan.changes.change- Number: resources to changecontext.plan.changes.remove- Number: resources to removecontext.plan.changes.import- Number: resources to import
Important Notes
- All checks must pass for auto-approval to occur
- If any check fails, manual approval is required
- HCP Terraform displays the failure reason from failed checks
- Auto-approve rules only apply to deployments in the specified deployment group
Examples
Auto-approve Successful Plans:
deployment_group "canary" {
deployments = [
deployment.dev,
deployment.staging
]
}
deployment_auto_approve "applyable_plans" {
deployment_group = deployment_group.canary
check {
condition = context.plan.applyable
reason = "Plan must be applyable without errors"
}
}Auto-approve Non-Destructive Changes:
deployment_group "production" {
deployments = [
deployment.prod_primary,
deployment.prod_secondary
]
}
deployment_auto_approve "safe_production_changes" {
deployment_group = deployment_group.production
check {
condition = context.plan.changes.remove == 0
reason = "Production deletions require manual approval"
}
check {
condition = context.plan.applyable
reason = "Plan must be successful"
}
}Graduated Rollout Pattern:
deployment_group "canary" {
deployments = [deployment.canary]
}
deployment_group "production" {
deployments = [
deployment.prod_us,
deployment.prod_eu,
deployment.prod_asia
]
}
# Canary auto-approves with strict checks
deployment_auto_approve "canary_strict" {
deployment_group = deployment_group.canary
check {
condition = context.plan.changes.remove == 0
reason = "Canary cannot delete resources"
}
check {
condition = context.plan.changes.change <= 5
reason = "Canary limited to 5 resource changes"
}
check {
condition = context.plan.applyable
reason = "Plan must be applyable"
}
}
# Production requires manual approval after canary validationFor complete deployment configuration examples with all blocks, see 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.tfvariables.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.hclvariables.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.iowith 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 validatePlan Specific Deployment
terraform stacks plan --deployment=development
terraform stacks plan --deployment=productionApply Deployment
terraform stacks apply --deployment=stagingDestroying 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=developmentHCP 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
destroyargument 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 = trueis the recommended approach for maintaining infrastructure-as-code practices
Linked Stacks Reference
Complete reference for linking Terraform Stacks together using published outputs and upstream inputs.
Publish Output Block
Exports outputs from a Stack for consumption by other Stacks (linked Stacks).
###Syntax
publish_output "<output_name>" {
type = <type>
value = <expression>
}Arguments
- output_name (label, required): Unique identifier for this published output
- type (required): Data type of the output
- value (required): Expression to export
Accessing Deployment Outputs
Reference deployment outputs using: deployment.<deployment_name>.<output_name>
Important Notes
- Must apply the Stack's deployment configuration before downstream Stacks can reference outputs
- Published outputs create a snapshot that other Stacks can read
- Changes to published outputs automatically trigger runs in downstream Stacks
Examples
Basic Published Output:
publish_output "vpc_id" {
type = string
value = deployment.network.vpc_id
}
publish_output "subnet_ids" {
type = list(string)
value = deployment.network.private_subnet_ids
}Multiple Deployment Outputs:
publish_output "regional_vpc_ids" {
type = map(string)
value = {
us_east = deployment.us_east.vpc_id
us_west = deployment.us_west.vpc_id
eu_west = deployment.eu_west.vpc_id
}
}Complex Output:
publish_output "database_config" {
type = object({
endpoint = string
port = number
name = string
})
value = {
endpoint = deployment.production.db_endpoint
port = deployment.production.db_port
name = deployment.production.db_name
}
}Regional Endpoints:
publish_output "api_endpoints" {
type = map(object({
url = string
region = string
}))
value = {
for env in ["dev", "staging", "prod"] : env => {
url = deployment[env].api_url
region = deployment[env].region
}
}
}Upstream Input Block
References published outputs from another Stack (linked Stacks).
Syntax
upstream_input "<input_name>" {
type = "stack"
source = "<stack_address>"
}Arguments
- input_name (label, required): Local name for this upstream input
- type (required): Must be "stack"
- source (required): Full Stack address in format:
app.terraform.io/<org>/<project>/<stack-name>
Accessing Upstream Outputs
Reference upstream outputs using: upstream_input.<input_name>.<output_name>
Important Notes
- Creates a dependency on the upstream Stack
- Upstream Stack must have applied its deployment configuration
- Changes in upstream Stack automatically trigger downstream Stack runs
- Only works with Stacks in the same HCP Terraform project
Examples
Basic Upstream Reference:
upstream_input "network" {
type = "stack"
source = "app.terraform.io/my-org/my-project/networking-stack"
}
deployment "application" {
inputs = {
vpc_id = upstream_input.network.vpc_id
subnet_ids = upstream_input.network.subnet_ids
}
}Multiple Upstream Stacks:
upstream_input "network" {
type = "stack"
source = "app.terraform.io/my-org/my-project/network-stack"
}
upstream_input "database" {
type = "stack"
source = "app.terraform.io/my-org/my-project/database-stack"
}
deployment "application" {
inputs = {
vpc_id = upstream_input.network.vpc_id
subnet_ids = upstream_input.network.private_subnet_ids
database_endpoint = upstream_input.database.endpoint
database_credentials = upstream_input.database.credentials
}
}Regional Upstream Dependencies:
upstream_input "regional_network" {
type = "stack"
source = "app.terraform.io/my-org/my-project/regional-networks"
}
deployment "us_east_app" {
inputs = {
region = "us-east-1"
vpc_id = upstream_input.regional_network.regional_vpc_ids["us_east"]
subnet_ids = upstream_input.regional_network.regional_subnet_ids["us_east"]
}
}Complete Working Example
For a complete example showing full Stack configurations with all files (variables, providers, components, outputs, deployments) for both upstream and downstream Stacks, see the "Linked Stacks (Cross-Stack Dependencies)" section in examples.md.
Troubleshooting Reference
Common issues and solutions when working with Terraform Stacks.
Table of Contents
1. Configuration Issues 2. Deployment Issues 3. Provider and Authentication Issues 4. Module Compatibility Issues 5. State and Dependency Issues 6. API and CLI Issues
Configuration Issues
Circular Dependencies
Issue: Component A references Component B, and Component B references Component A.
Error Message:
Error: Cycle detected in component dependenciesSolutions:
1. Break the circular reference by refactoring components:
# Before (circular dependency)
component "vpc" {
source = "./modules/vpc"
inputs = {
security_group_id = component.app.security_group_id # References app
}
}
component "app" {
source = "./modules/app"
inputs = {
vpc_id = component.vpc.vpc_id # References vpc
}
}
# After (broken circular reference)
component "vpc" {
source = "./modules/vpc"
inputs = {
# Remove reference to app
}
}
component "security_group" {
source = "./modules/security-group"
inputs = {
vpc_id = component.vpc.vpc_id
}
}
component "app" {
source = "./modules/app"
inputs = {
vpc_id = component.vpc.vpc_id
security_group_id = component.security_group.id
}
}2. Use intermediate components to break the dependency chain 3. Refactor modules to remove the circular dependency at the module level
Validation Errors on Variables
Issue: Variable block validation errors during terraform stacks validate.
Error Message:
Error: Unsupported argument
on variables.tfcomponent.hcl line 5:
5: validation {
Validation blocks are not supported in Stack configurationsSolution: Remove validation blocks from variable declarations. Stacks do not support validation blocks:
# Incorrect
variable "instance_count" {
type = number
validation {
condition = var.instance_count > 0
error_message = "Instance count must be positive"
}
}
# Correct
variable "instance_count" {
type = number
description = "Number of instances (must be positive)"
}Move validation logic into the underlying modules if needed.
Missing Type in Variable Declarations
Issue: Variables fail validation when type is not specified.
Error Message:
Error: Missing required argument
on variables.tfcomponent.hcl line 3:
3: variable "region" {
The argument "type" is required in Stack variable declarationsSolution: Always specify type for variables - it's required in Stacks (unlike traditional Terraform):
# Incorrect
variable "region" {
default = "us-west-1"
}
# Correct
variable "region" {
type = string
default = "us-west-1"
}Provider Configuration in Modules
Issue: Modules with embedded provider blocks cause errors.
Error Message:
Error: Provider configuration not allowed in module
Modules used with Terraform Stacks cannot contain provider blocksSolution:
1. Remove provider blocks from modules - configure providers in Stack configuration instead 2. Use modules that don't contain provider blocks (most public registry modules are compatible) 3. Fork and modify modules if necessary to remove provider blocks
Deployment Issues
Cannot Destroy Deployment from UI
Issue: The HCP Terraform UI doesn't provide an option to destroy Stack deployments.
Why: Stack deployment destruction is only available through configuration, not the UI.
Solution: Set destroy = true in the deployment block and upload the configuration:
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 # Marks deployment for destruction
}Workflow:
1. Add destroy = true to the deployment block 2. Run terraform stacks configuration upload 3. HCP Terraform creates a destroy run automatically 4. Approve the destroy run (if auto-approve is not configured) 5. After destruction completes, remove the deployment block entirely 6. Upload configuration again to clean up the deployment definition
Important: You cannot destroy deployments from the UI. This is by design to prevent accidental destruction.
Deployment Stuck in "Planning" State
Issue: Deployment remains in "planning" state indefinitely.
Possible Causes:
1. Provider authentication failed - Check OIDC configuration and IAM roles 2. Module download failed - Verify module sources are accessible 3. Provider version conflict - Check .terraform.lock.hcl matches required providers
Diagnosis:
# Get deployment step diagnostics
terraform stacks deployment-run list
# Note the run ID, then:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-runs/{run-id}/stack-deployment-steps" | \
jq '.data[] | {id, status: .attributes.status, component: .attributes["component-name"]}'Solutions:
1. Check diagnostics for the stuck step 2. Verify provider authentication is configured correctly 3. Ensure all module sources are accessible 4. Check provider lock file matches required providers
Deployment Requires Approval But No Approval Prompt
Issue: Deployment is waiting for approval but CLI doesn't show approval prompt.
Why: CLI monitoring commands are non-blocking and don't automatically prompt for approval.
Solution:
Option 1: Approve via CLI
# Approve all pending plans in a deployment run
terraform stacks deployment-run approve-all-plans -deployment-run-id=sdr-ABC123
# Or approve all plans in a deployment group
terraform stacks deployment-group approve-all-plans -deployment-group=canaryOption 2: Configure auto-approve (Premium feature)
deployment_auto_approve "safe_changes" {
deployment_group = deployment_group.canary
check {
condition = context.plan.applyable
reason = "Plan must be successful"
}
}Provider and Authentication Issues
OIDC Authentication Failing
Issue: Provider authentication fails with OIDC/workload identity.
Error Messages:
Error: Error assuming role with web identity
Error: Failed to retrieve credentials
Error: Invalid identity tokenDiagnosis Steps:
1. Verify identity token configuration:
# Check identity_token block exists
identity_token "aws" {
audience = ["aws.workload.identity"]
}
# Check deployment references the token
deployment "production" {
inputs = {
identity_token = identity_token.aws.jwt
}
}2. Verify 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
}
}
}3. Check IAM role trust policy:
AWS - Verify trust policy includes HCP Terraform:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<account-id>:oidc-provider/app.terraform.io"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"app.terraform.io:aud": "aws.workload.identity"
},
"StringLike": {
"app.terraform.io:sub": "organization:<org-name>:project:<project-name>:stack:<stack-name>:deployment:<deployment-name>"
}
}
}
]
}Azure - Verify federated credential:
- Application ID matches the one in provider configuration
- Subject matches:
organization:<org>:project:<project>:stack:<stack>:deployment:<deployment> - Issuer is
https://app.terraform.io
GCP - Verify workload identity pool:
- Provider configuration includes correct workload identity provider
- Service account has necessary IAM permissions
- Attribute mapping includes
google.subjectfrom token claims
Solutions:
1. Fix IAM role trust policy to include correct HCP Terraform OIDC provider 2. Ensure audience matches between identity_token block and IAM trust policy 3. Verify subject pattern matches your organization/project/stack/deployment names 4. Check that the role_arn is correct in provider configuration
Provider Version Lock File Issues
Issue: Provider version conflicts or "could not retrieve provider" errors.
Error Messages:
Error: Failed to install provider
Error: Provider version not found
Error: Checksum mismatch for providerSolutions:
1. Regenerate provider lock file:
terraform stacks providers-lock2. Add additional platforms (if deploying from different OS):
terraform stacks providers-lock \
-platform=linux_amd64 \
-platform=darwin_amd64 \
-platform=darwin_arm643. Verify required_providers block:
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.7.0" # Ensure version constraint is valid
}
}4. Commit `.terraform.lock.hcl` to version control
Module Compatibility Issues
Public Registry Module Errors
Issue: Modules from the Terraform public registry cause errors during plan or apply.
Common Errors:
Error: Unsupported attribute
Error: Invalid reference
Error: Missing required argumentKnown Problematic Modules:
terraform-aws-modules/alb/aws- Some versions have compatibility issuesterraform-aws-modules/ecs-service/aws- May have issues with certain configurations
Solutions:
1. Test modules in dev deployment first before using in production
2. Check module compatibility by reviewing recent issues on the module repository
3. Use specific module versions rather than latest:
component "alb" {
source = "terraform-aws-modules/alb/aws"
version = "8.7.0" # Use specific version known to work
# ...
}4. Consider using raw resources for critical infrastructure:
# Instead of using a module that has issues
component "alb" {
source = "./modules/alb" # Create local module with raw resources
# ...
}5. Fork and fix modules if you have the resources to maintain them
6. Report compatibility issues to module maintainers
Local Module Not Found
Issue: Stack can't find local module sources.
Error Message:
Error: Module not found
Could not load module ./modules/vpcSolutions:
1. Verify module path is relative to Stack root:
# Correct
component "vpc" {
source = "./modules/vpc"
}
# Incorrect (absolute paths don't work)
component "vpc" {
source = "/Users/username/project/modules/vpc"
}2. Ensure module directory exists with proper structure:
my-stack/
├── components.tfcomponent.hcl
└── modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf3. Check file permissions on module directories
State and Dependency Issues
Component Output Not Available
Issue: Component output is not available to referencing component.
Error Message:
Error: Reference to unknown component
Component "vpc" has not been definedSolutions:
1. Verify component exists in configuration:
component "vpc" {
source = "./modules/vpc"
# Must define component before referencing it
}
component "app" {
source = "./modules/app"
inputs = {
vpc_id = component.vpc.vpc_id # Now valid
}
}2. Check output is defined in module:
# In modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}3. For components with for_each, reference specific instance:
component "regional" {
for_each = var.regions
# ...
}
component "app" {
inputs = {
# Correct - reference specific instance
vpc_id = component.regional["us-west-1"].vpc_id
# Incorrect - can't reference for_each component directly
# vpc_id = component.regional.vpc_id
}
}Deferred Changes Not Converging
Issue: Deployment with deferred changes doesn't complete after multiple iterations.
Error Message:
Error: Maximum deferred change iterations reachedCause: Dependency cycle or values that never stabilize.
Solutions:
1. Review component dependencies for logical cycles 2. Check for computed values that change on every run 3. Refactor to break dependency chain 4. Consider multi-stage deployments if resources truly can't be created together
API and CLI Issues
Empty Diagnostics Response
Issue: API request for diagnostics returns empty results.
Request:
curl "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics"Response:
{
"data": []
}Solution: Add required stack_deployment_step_id query parameter:
curl "https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/stack-diagnostics?stack_deployment_step_id={step-id}"Cannot Retrieve Stack Outputs
Issue: No CLI command to retrieve Stack outputs after deployment.
Why: Currently no direct CLI command for outputs retrieval.
Solution: Use the artifacts API endpoint:
# Get final apply step ID first
APPLY_STEP=$(terraform stacks deployment-run list --json | \
jq -r '.[0].deployment_steps[] | select(.operation_type == "apply") | .id' | tail -1)
# Get outputs
curl -L -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/$APPLY_STEP/artifacts?name=apply-description" | \
jq -r '.outputs | to_entries | .[] | "\(.key): \(.value.change.after)"'CLI Watch Commands Hang in CI/CD
Issue: Commands like terraform stacks deployment-run watch never return in CI/CD pipelines.
Why: Watch commands stream output indefinitely and are designed for interactive use.
Solution: Use API polling instead of watch commands. See api-monitoring.md for complete workflow.
Artifacts Endpoint Returns 404
Issue: Request to artifacts endpoint returns 404 Not Found.
Possible Causes:
1. Step hasn't completed yet - wait for step status to be "completed" 2. Wrong artifact name - use one of: plan-description, plan-debug-log, apply-description, apply-debug-log 3. Invalid step ID - verify step ID from deployment-steps endpoint
Solution:
# Check step status first
curl -s -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}" | \
jq '.data.attributes.status'
# Only request artifacts when status is "completed"
if [ "$STATUS" = "completed" ]; then
curl -L -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description"
fiHTTP 307 Redirect Not Followed
Issue: Artifacts endpoint returns redirect response instead of artifact content.
Why: The endpoint returns HTTP 307 redirect to the actual artifact URL.
Solution: Configure HTTP client to follow redirects:
# curl: Use -L flag
curl -L -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description"
# Python requests: allow_redirects=True (default)
import requests
response = requests.get(url, headers=headers, allow_redirects=True)
# Node.js fetch: redirect: 'follow' (default)
const response = await fetch(url, {
headers: headers,
redirect: 'follow'
});Getting Additional Help
Enable Debug Logging
For more detailed error information, enable debug logging:
# CLI commands
TF_LOG=DEBUG terraform stacks validate
TF_LOG=DEBUG terraform stacks configuration upload
# API artifacts
# Request the debug-log artifact instead of description
curl -L -H "Authorization: Bearer $TOKEN" \
"https://app.terraform.io/api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-debug-log"Check HCP Terraform Status
If experiencing widespread issues, check HCP Terraform status page:
- https://status.hashicorp.com
Review Configuration Version
List recent configurations to identify when issues started:
terraform stacks configuration listContact Support
For issues not covered here: 1. Gather relevant error messages and diagnostics 2. Note the configuration sequence number 3. Include deployment run IDs 4. Contact HashiCorp Support with details