Repository navigation
Description
Aspire issue link
No response
Overview
Applications often need Terraform-managed configuration or infrastructure alongside their Aspire resources: identity provider configuration, external services, or infrastructure required before deployment.
Today, integrating an existing Terraform module requires manually composing executable resources, propagating input variables and backend settings, ordering commands, and implementing deployment pipeline steps. The local development workflow and CI deployment workflow can easily diverge, particularly when applying a previously reviewed plan.
There is no dedicated Terraform hosting resource that:
- Models a module with shared variables, backend configuration, and workspace selection
- Executes Terraform commands as observable Aspire resources with completion dependencies
- Uses Aspire parameters and reference expressions as Terraform inputs
- Exposes init, plan, apply, and destroy as Aspire deployment pipeline steps
- Applies the same saved plan across separate planning and approval stages
A CommunityToolkit.Aspire.Hosting.Terraform integration with the following resource types:
| Resource | Type | Description |
|---|---|---|
TerraformEnvironmentResource |
Resource |
Logical Terraform module with shared configuration and declared operations; starts no process |
TerraformCliResource |
ExecutableResource |
Runs one Terraform command from a configured module directory |
AddTerraformEnvironment creates the logical module. WithInit, WithPlan, WithApply, and WithDestroy declare its operations, creating child TerraformCliResource instances through AddTerraformCli and registering corresponding pipeline steps.
AddTerraformCli is also available directly for standalone commands and explicitly composed workflows.
Usage example
Terraform environment
var serviceUrl = builder.AddParameter("service-url");
var serviceToken = builder.AddParameter("service-token", secret: true);
var infrastructure = builder.AddTerraformEnvironment("infrastructure", "./terraform")
.WithTfVar("service_url", serviceUrl)
.WithTfVar("service_token", serviceToken)
.WithTfVarsFile("./configuration/production.tfvars")
.WithBackendConfigFile("./configuration/backend.hcl")
.WithWorkspace("production")
.WithInit()
.WithPlan(outputPath: "./artifacts/infrastructure.tfplan")
.WithApply(apply => apply.WithExplicitStart())
.WithDestroy();Each operation accepts a callback receiving its IResourceBuilder<TerraformCliResource>:
infrastructure.WithPlan(configure: plan => plan
.WithArgs("-lock-timeout=60s")
.WithTfVar("validation_mode", "strict"));Additional arguments and operation-specific environment values are used by both the local resource and its pipeline step. Repeating an operation declaration reuses its existing resource.
Standalone CLI resources
var init = builder.AddTerraformCli("module-init", "./terraform")
.WithBackendConfigFile("./configuration/backend.hcl")
.WithInit();
var plan = builder.AddTerraformCli("module-plan", "./terraform")
.WithTfVar("service_token", serviceToken)
.WithPlan("-out=review.tfplan")
.WaitForCompletion(init);
builder.AddTerraformCli("module-apply", "./terraform")
.WithApply(args: ["review.tfplan"])
.WaitForCompletion(plan)
.WithExplicitStart();
builder.AddTerraformCli("module-validate", "./terraform")
.WithArgs("validate", "-no-color")
.WaitForCompletion(init);Standalone operation helpers select the command on the existing resource. They do not create sibling resources, infer dependencies, or register Terraform pipeline steps.
WithApply(bool autoApprove = false, params string[] args) and WithDestroy(bool autoApprove = false, bool explicitStart = true, params string[] args) support explicitly requested auto-approval. WithExplicitStart controls process startup, not Terraform's interactive approval prompt.
Variables and state backend
Configuration helpers target ITerraformConfigurableResource, implemented by both resource types:
var stateConnection = builder.AddParameter("terraform-state-connection", secret: true);
var stateSchema = builder.AddParameter("terraform-state-schema");
infrastructure
.WithPostgresBackend(ReferenceExpression.Create($"{stateConnection}"))
.WithBackendConfig("schema_name", stateSchema)
.WithTfVar("service_url", serviceUrl)
.WithTfVar("service_token", serviceToken)
.WithTfVar("deployment_name", "production");WithTfVaraccepts strings, parameter builders, and Aspire values implementingIValueProviderandIManifestExpressionProvider, including endpoint references and reference expressions. It setsTF_VAR_<name>.WithTfVarsFile(path)adds a variable file for plan, apply without a saved plan, and destroy. Multiple calls accumulate in call order.WithBackendConfig(key, value)accepts strings, parameters, or reference expressions;WithBackendConfigFile(path)supplies a backend configuration file. Both apply only to init.WithPostgresBackend(connectionString, schema: null)setsPG_CONN_STRand optionallyPG_SCHEMA_NAME. The module must already declarebackend "pg" {}.- Other backends and provider credential chains use backend configuration and standard
WithEnvironmentcalls.
Deployment pipeline
Every declared environment operation registers a pipeline step:
| Declaration | Local child resource | Pipeline step |
|---|---|---|
WithInit() |
<name>-init |
terraform-init-<name> |
WithPlan() |
<name>-plan |
terraform-plan-<name> |
WithApply() |
<name>-apply |
terraform-apply-<name> |
WithDestroy() |
<name>-destroy |
terraform-destroy-<name> |
Pipeline attachment and chaining are configurable:
infrastructure.WithPipelineOptions(options =>
{
options.ChainSteps = false;
options.RequiredByDeploy = true;
options.RequiredByDestroy = true;
});These are the defaults. Operation steps are independent unless ChainSteps is enabled; declared apply and destroy steps participate in aspire deploy and aspire destroy, respectively.
A CI workflow with a separate approval stage can:
- Run
aspire do terraform-init-infrastructure, thenaspire do terraform-plan-infrastructure. - Publish the saved plan as a restricted-access artifact and obtain approval.
- Restore the module, provider lock file, and saved plan at the configured path on the apply runner.
- Run init on that runner, then
aspire deployoraspire do terraform-apply-infrastructure.
With ChainSteps = true, plan depends on init, apply on plan (or init without a declared plan), and destroy on init. Applying with chaining creates a fresh plan rather than consuming only the previously approved artifact.
Additional context
- CLI resources as the execution primitive - the environment creates actual
TerraformCliResourcechildren. Their command, arguments, and environment configuration are also the source for pipeline execution, avoiding separate configuration paths. - Shared configuration evaluated at execution time - environment settings apply to all declared operations regardless of configuration order. Operation-specific environment values override shared values; parameters and expressions resolve when the command executes.
- Local completion dependencies - plan waits for declared init; apply waits for declared plan, or init without a plan; destroy waits for declared init. These local dependencies are separate from optional pipeline chaining.
- Saved-plan apply - environment
WithPlansaves a binary plan, defaulting to<module>/<name>.tfplan. Environment apply consumes that exact artifact and fails if it is missing; it never silently replans. Variable-file arguments are omitted for saved-plan apply, while unrelated apply settings are preserved. Terraform rejects stale plans. - Explicit destructive behavior - environment apply starts automatically after its prerequisites unless configured with
WithExplicitStart; without a declared plan it uses-auto-approve. Environment destroy uses-auto-approvebut always requires explicit local start. No destroy runs on AppHost shutdown. - Pipeline invocation is explicit execution, not dashboard startup - local
WithExplicitStartis not a CI approval gate. A declared operation can still execute through its pipeline step or enabled deploy/destroy hook. With default independent steps, CI must initialize the runner and supply the saved plan before invoking apply. - Automation and failures - pipeline execution uses
-input=false,-no-color,TF_INPUT=false, andTF_IN_AUTOMATION=true. Plan additionally uses-detailed-exitcode, treating 0 and 2 as success. Other nonzero exit codes fail the step; output is reported and cancellation terminates the process tree. - AppHost-relative paths - module directories, configuration files, and explicit environment plan paths resolve relative to the AppHost directory. Absolute paths are supported. The plan output directory is created before planning.
- Local tooling - Terraform must be installed on the AppHost or pipeline runner.
WithTerraformExecutablePathselects the environment executable and propagates it to every operation; standalone resources use standardWithCommand. - Execution-only resources - the environment and generated CLI children are excluded from deployment manifests. Deployment runs Terraform on the pipeline runner, not as a published application service. Local endpoints are not automatically translated into production endpoints.
- State and credential ownership - existing modules, workspaces, backends, and provider authentication remain under Terraform's control. The integration does not generate HCL, install Terraform, migrate state, or provision backend storage. State and plan artifacts can contain secrets; backend configuration may be persisted in
.terraform. Concurrent operations require isolated working copies and backend state locking. - Polyglot AppHosts - compatible resources and methods are exported through Aspire ATS, with adapters for variable unions and optional settings. Operation callbacks and pipeline option exports must be validated against the targeted Aspire version.
Help us help you
Yes, I'd like to be assigned to work on this item
Activity
manuelelucchi commentedon Nov 14, 2026
Does anyone have any suggestion or comment?