AI-narrated article. Prose is preserved with pronunciation adjustments; code and image cues refer to the written article.
Terraform Stacks: Components Share Code, Deployments Own the Boundary
This is the AI-narrated article. The prose follows the written post; pronunciation is adjusted, and code and images are announced separately.
Define one Stack component structure with separate deployment state, AWS workload identities, explicit plan starts and manual approval boundaries.
TL;DR: Terraform Stacks separate reusable infrastructure components from deployment-specific inputs, state and workflow controls. This article provides complete component, deployment and local module files for an S3 pattern repeated across dev and prod. Distinct AWS roles and bucket names preserve intended boundaries; ephemeral workload tokens avoid persistent credential inputs. Explicit plan starts and manual approvals remain operator controls, not automatic promotion dependencies. The configuration was reviewed against primary documentation without cloud execution. Deferred plans, role-trust failures and deployment renames require lifecycle-aware review rather than assuming isolated state provides permission isolation or transactional rollback.
A platform team often standardizes a storage or application module, then discovers that each environment still carries its own root configuration, credentials and rollout conventions. Copying the root creates drift. Sharing one powerful identity creates a different problem: every environment becomes reachable through the same permission boundary.
Terraform Stacks separate those concerns. Components define the infrastructure structure; deployments instantiate that structure with their own inputs. The useful outcome is a repeatable configuration whose state, identity and approval decisions remain explicit.
This article provides a complete two-deployment Stack around an S3 module. It is a configuration and workflow example reviewed against primary documentation. No HCP run, AWS authentication or provisioning was executed.
Choose the lifecycle before the abstraction
Stacks are generally available in HCP Terraform and replace the traditional root-module arrangement with a component configuration. Existing Terraform modules remain the building blocks. A deployment repeats the component structure with different inputs and has isolated state. That isolation separates resource tracking; it does not establish IAM isolation or guarantee that two deployments cannot target the same external resource. Stacks overview.
Use a Stack when the components share a coherent deployment lifecycle. A product environment with coordinated infrastructure is a reasonable boundary. Combining an independently maintained corporate network and every application merely because they all use Terraform creates unnecessary ownership coupling.
For this example, the shared structure is one storage component. Dev and prod have different bucket names, AWS accounts and roles. A single repository revision changes their common definition, while each deployment still has its own run and approval decision.
Image. Shared component configuration with separate deployment boundaries
The diagram shows these declared boundaries and operator controls. It represents the configuration, not observed HCP execution or an automatic promotion pipeline.
Prerequisites and repository layout
Use Terraform CLI 1.16.0 for this example and record terraform stacks version, because the Stacks plugin has a separate version. The provider is pinned to hashicorp/aws 6.0.0, deliberately an exact version rather than a claim about the latest release.
You need an HCP Terraform organization and project with Stacks access, permission to create a Stack, and a connected VCS repository for the workflow below. An AWS administrator must already have configured the HCP OIDC provider and separate approved roles in the two target accounts. Replace every sample account ID, role ARN and globally unique bucket name before deployment.
Those roles need the S3 management and read permissions used by the module and provider, scoped to the intended bucket and relevant account policies. No IAM bootstrap or bucket deployment is performed by the local validation commands. This example does not configure application access, object retention or an organizational encryption policy.
Create this layout:
Code example. Refer to the written article for the complete code.
The Stack files belong at its configuration root. The module contains ordinary Terraform resource configuration and receives its provider from the Stack.
Define the structure and provider explicitly
Save the following as main.tfcomponent.hcl:
Code example. Refer to the written article for the complete code.
The component passes the configured provider into the module rather than letting the module create another provider configuration. Local module sources share the repository revision; they do not use a registry version argument. Component input references can also consume another component's outputs, expressing an infrastructure dependency when the design grows. Component reference.
Save modules/storage/main.tf:
Code example. Refer to the written article for the complete code.
The module declares an S3 bucket, public-access blocking and explicit AES256 default encryption. These configuration resources reference the bucket ID. Public-access blocking is not an application authorization policy, and encryption configuration is not proof of the organization's entire data-protection contract. The resource fields were checked against the pinned provider documentation. Public-access block, Encryption configuration.
Repeat the structure without sharing privileges
Save main.tfdeploy.hcl:
Code example. Refer to the written article for the complete code.
The token declaration is shared, but HCP generates a workload token for the deployment operation. Passing its JWT through an ephemeral variable avoids persisting that credential as a normal Stack input. HCP's documented subject includes organization, project, Stack, deployment and operation:
Code example. Refer to the written article for the complete code.
Configure the prod role's trust to accept the intended issuer, audience and exact deployment subject for the required operations. A role accepting every deployment in the organization would undermine the separation drawn in the diagram. Keep role trust, which decides who may assume it, distinct from role permissions, which decide what the resulting session can do. Workload identity token reference.
This sample passes one role per deployment for both planning and applying. It therefore does not implement separate read-only plan and write-capable apply identities. If that separation is required, design it explicitly rather than assuming the token's operation claim automatically reduces an AWS role's privileges.
Validate locally, then review the hosted workflow
From catalog-stack, run:
Code example. Refer to the written article for the complete code.
Initialization downloads dependencies; review and commit the generated provider lock file while excluding downloaded caches. The documented validation workflow requires initialization and provider locking first. Validation checks configuration without consulting remote state or provider APIs; a successful result would not establish AWS role trust, bucket availability or deployment entitlement. These commands are supplied for the reader and were not executed for this article. Stacks validation reference.
For hosted review, commit the files and lock file to an approved repository. In HCP Terraform, select the organization and project, create a Stack, connect the repository and select the configuration directory. Review fetch and VCS-trigger settings before creation: fetching configuration and triggering new runs can happen automatically. Create a Stack.
Both groups use eager_plan = "off" and an empty auto-approval list. Start dev planning explicitly, inspect the actual plan, and approve only an authorized change. Start prod planning after the required dev evidence is available, inspect prod's own plan and approve it separately. These controls require operator discipline; the supplied configuration does not encode a dependency requiring dev success before prod starts. Deployment-group reference.
Treat deferred plans as part of the review boundary
A deployment run can contain multiple plan/apply cycles when later infrastructure cannot be planned until an earlier dependency exists. Approving the initial plan is therefore not evidence that every later change is already visible. Inspect subsequent steps and any applicable automatic approval rules. This S3 example demonstrates the configuration split; it does not deliberately require or demonstrate deferral.
A group can limit tolerated failures and halt further rollout. That is containment, not transaction rollback across deployments. Already changed infrastructure can remain changed. Failure tolerance and concurrency behavior need deliberate review before grouping many accounts under one mass approval. Stack run lifecycle.
Diagnose a failed identity without widening the boundary
A realistic failure is dev planning successfully while prod cannot assume its role because trust names the wrong Stack or omits the plan subject. Diagnose the failed operation first: compare deployment name, audience, organization/project/Stack path and plan-versus-apply subject against the configured trust. Do not paste the raw JWT into logs or a ticket.
Correct the narrowly scoped trust or deployment input, then rerun the failed deployment through the approved workflow. If role assumption succeeds but S3 reads fail, examine the role's permissions and account guardrails instead. Treating both failures as “OIDC broken” encourages overly broad policies.
This is a documented diagnosis scenario, not an injected or observed AWS failure. Preserve the actual run ID, operation and sanitized provider diagnostic when exercising it in an authorized environment.
Local review creates no managed infrastructure. If a reader deploys the example, set destroy = true on the intended deployment, retain its authentication inputs, and review and approve the destruction plan before removing its block. Nonempty buckets remain protected from forced deletion here. Renaming a deployment label creates a new deployment and destroys the old one; it is not a cosmetic edit. Deployment reference.
Frequently asked questions
Does separate deployment state isolate AWS permissions? No. Isolation requires appropriate account, trust and role-permission boundaries as well as distinct resource names.
Does this configuration automatically promote dev to prod? No. Planning and approval are separate operator actions; a promotion prerequisite needs additional enforced orchestration.
Can I approve once and ignore deferred plans? No. Later plan/apply cycles may expose changes unavailable during the initial review.
Can I rename a deployment to improve readability? Treat it as a lifecycle change. The documented behavior creates a new deployment and destroys the previous deployment.