Using Terraform Cloud with CDKTF Python Projects
Integrating CDK for Terraform with Terraform Cloud requires strict adherence to remote state protocols and secure credential boundaries. This task is the managed-backend variant of State Backend Configuration for CDKTF: instead of self-hosting S3 and DynamoDB, you push state and execution to Terraform Cloud workspaces. Python 3.9+ type hints must survive synthesis without triggering runtime failures. This guide establishes a production-ready workflow for authentication, workspace isolation, and safe deployment cycles.
Prerequisites
Terraform Cloud couples two things that are separate on a self-hosted backend: a workspace, which owns the state file, the variable set, and the run history; and a run environment, the ephemeral container in which terraform init, plan, and apply actually execute. CDKTF sits upstream of both. It never talks to a provider directly — it synthesizes JSON into cdktf.out/stacks/<stack>/cdk.tf.json, and that JSON is what the run environment consumes. Everything in this guide is about making that handoff deterministic.
| Requirement | Minimum | Why it matters |
|---|---|---|
cdktf-cli |
0.20 | Emits a cloud {} block instead of the older backend "remote" stanza |
| Node.js | 20.9 | The CDKTF CLI is a Node process even when the app is Python |
| Python | 3.9 | The jsii runtime bindings will not import below this |
| Terraform binary | 1.6 | Used locally only for validate and fmt; TFC pins its own version per workspace |
| TFC role | Workspace admin | Required to create workspaces and write run variables via the API |
CLI: Confirm the toolchain before touching a workspace.
cdktf --version && node --version && terraform version && python --version
You also need one AWS credential pair (or an OIDC trust relationship) that will live in the workspace, not on your laptop, once execution moves remote. Decide that boundary now — retrofitting it after the first apply means rotating whatever you leaked into a local shell history.
1. Project Initialization & Authentication Setup
Begin by scaffolding a Python CDKTF project with remote execution explicitly disabled at the CLI level. This prevents accidental local state initialization. Terraform Cloud API authentication must flow exclusively through environment variables. Never embed tokens in configuration files or version control.
CLI: Initialize the project scaffold.
cdktf init --template=python --local=false
Export the Terraform Cloud API token into your shell session. This credential authorizes all subsequent synthesis and deployment operations against the TFC API.
CLI: Securely inject the TFC token into the execution environment.
export TFE_TOKEN=""
Terraform resolves a Terraform Cloud credential from three places, in a fixed precedence order, and getting this wrong is the single most common first-run failure. Highest priority is the host-specific environment variable TF_TOKEN_app_terraform_io — the hostname with dots replaced by underscores. Next is the credentials file written by terraform login at ~/.terraform.d/credentials.tfrc.json. Last is any credentials block in a CLI configuration file. The CDKTF CLI additionally reads TFE_TOKEN and injects it into the run environment it shells out to, which is why both variable names appear in CDKTF documentation. If none resolve, the run aborts before the plan even starts:
Error: Required token could not be found
Run the following command to generate a token for app.terraform.io:
terraform login
Prefer a team API token over a personal one for anything a pipeline touches. A personal token inherits every permission its owner has across the whole organization, so a leaked laptop token can destroy workspaces the pipeline was never meant to see. A team token is scoped to the workspaces that team is granted, and revoking it does not lock out a human.
CLI: Write a credentials file interactively for local work, then verify what it resolved to.
terraform login app.terraform.io terraform -chdir=cdktf.out/stacks/CloudStack providers
Enforce strict static typing before synthesis. mypy catches structural mismatches early and prevents synthesis-time failures that corrupt remote workspace configurations. CDKTF is a code generator, so a wrong Python type does not surface as a Terraform error — it surfaces as a valid but wrong JSON document that Terraform Cloud happily plans against.
CLI: Validate type boundaries against the project entry point.
python -m mypy main.py --strict
2. State Backend Mapping & Workspace Isolation
Map your CDKTF project to a dedicated Terraform Cloud workspace. Configure the remote backend in cdktf.json to control state locking behavior and remote backend routing:
{
"language": "python",
"app": "python main.py",
"terraformProviders": ["aws@~> 6.0"],
"terraformCloud": {
"hostname": "app.terraform.io",
"organization": "your-org",
"workspaces": {
"name": "cdktf-prod-vpc"
}
},
"context": {
"excludeStackIdFromLogicalIds": "true",
"allowSepCharsInLogicalIds": "true"
}
}
The cdktf.json form above is convenient but global — it applies to every stack in the app. Once the app holds more than one stack, declare the backend inside the stack instead, using the CloudBackend construct. That keeps the workspace binding next to the resources it governs and lets each stack target a different workspace from the same synthesis run:
# backend.py
# CLI: cdktf synth --output cdktf.out
from typing import Final
from constructs import Construct
from cdktf import CloudBackend, NamedCloudWorkspace, TerraformStack
TFC_ORG: Final[str] = "your-org"
def bind_workspace(stack: TerraformStack, workspace: str) -> CloudBackend:
"""Attach one CDKTF stack to exactly one Terraform Cloud workspace."""
# State implication: this emits a `cloud {}` block into cdk.tf.json. Terraform
# will migrate local state into the workspace on the next `init` and prompt
# for confirmation unless -migrate-state is passed.
return CloudBackend(
stack,
hostname="app.terraform.io",
organization=TFC_ORG,
workspaces=NamedCloudWorkspace(workspace),
)
class NetworkStack(TerraformStack):
def __init__(self, scope: Construct, id: str, *, workspace: str) -> None:
super().__init__(scope, id)
bind_workspace(self, workspace)
NamedCloudWorkspace pins the stack to one workspace by name. The alternative, TaggedCloudWorkspaces(["network", "prod"]), selects a set of workspaces by tag and requires the operator to pick one with terraform workspace select — useful when the same stack is deployed across many accounts, but it makes CI non-deterministic unless TF_WORKSPACE is set. For a single-environment stack, name it explicitly and move on.
A subtlety that bites teams migrating from an S3 backend: the cloud {} block does not merge with a pre-existing backend block. If cdktf.out still contains a synthesized S3 backend from an earlier run, terraform init reports a backend change and refuses to continue non-interactively. Delete cdktf.out before the first synthesis against Terraform Cloud rather than trying to reconcile the two.
Generate provider bindings and synthesize infrastructure definitions:
CLI: Generate provider bindings and synthesize.
cdktf get cdktf synth --output cdktf.out
Verify workspace routing before pushing configuration:
CLI: Inspect active workspace bindings in the synthesized output directory.
terraform -chdir=cdktf.out/stacks/workspace list
Cross-reference advanced backend override patterns in State Backend Configuration for CDKTF when managing multi-environment state dependencies.
3. Remote Execution & Pipeline Handoff
Transition execution control to Terraform Cloud remote runners. Local CLI operations synthesize artifacts and trigger remote plans. Configure VCS triggers, policy-as-code gates, and workspace run variables to enforce compliance boundaries before any apply operation.
Execution mode is a workspace setting, not something CDKTF controls, and it changes what cdktf deploy actually does on your machine. In remote mode the CLI uploads the synthesized configuration directory as a configuration version, then streams the run log back; your laptop never holds an AWS credential. In local mode the workspace is used purely as a state store and lock, and the plan executes wherever you invoked it. Agent mode is the escape hatch for private networks — a self-hosted agent polls Terraform Cloud for work, so the run environment sits inside your VPC and can reach private endpoints.
Run variables are the other half of the handoff. Terraform Cloud distinguishes Terraform variables, which map to variable blocks in the configuration, from environment variables, which are exported into the run container. Provider credentials belong in the second category: set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (or, better, wire up dynamic provider credentials so TFC exchanges a workload identity token for a short-lived role) as sensitive environment variables on the workspace. CDKTF stacks rarely declare variable blocks at all — configuration usually arrives through Python constructor arguments resolved at synthesis time — so the Terraform-variable tab is often empty and that is correct.
Mark every credential variable sensitive. Once set, Terraform Cloud will not read it back through the API, and the run log redacts it. A non-sensitive variable is visible to anyone with read access to the workspace and is echoed in plan output.
CLI: Trigger a remote plan and apply cycle with explicit stack targeting.
cdktf deploy --stack--auto-approve
Verify active workspace context and preview remote drift:
CLI: Check workspace context and preview pending changes.
terraform -chdir=cdktf.out/stacks/workspace show cdktf diff --stack
# main.py
# CLI: cdktf deploy --stack CloudStack --auto-approve
from typing import Optional
from constructs import Construct
from cdktf import TerraformStack, TerraformOutput
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.vpc import Vpc
class CloudStack(TerraformStack):
def __init__(
self,
scope: Construct,
id: str,
*,
region: str,
cidr_block: str,
) -> None:
super().__init__(scope, id)
AwsProvider(self, "aws", region=region)
network = Vpc(
self,
"main",
cidr_block=cidr_block,
enable_dns_hostnames=True,
)
TerraformOutput(
self,
"vpc_id",
value=network.id,
description="Primary VPC identifier",
)
Align synthesis artifacts with standardized pipeline expectations. Artifact versioning must remain deterministic across CI/CD runs. Review CDKTF Workflows & Terraform Synthesis for pipeline integration patterns that guarantee reproducible remote execution.
4. Drift Detection & Safe Rollback Protocols
Monitor configuration drift through Terraform Cloud's workspace run history and state version list. Use lifecycle blocks (prevent_destroy = true) on critical resources to block accidental deletion during automated runs.
CLI: Export current state for offline analysis and backup.
terraform -chdir=cdktf.out/stacks/state pull > state_backup.json
Recovery operations require strict validation before state restoration. Never push unverified state snapshots to production workspaces. Always pair state restoration with drift verification commands.
CLI: Restore a verified state snapshot and reconcile configuration.
terraform -chdir=cdktf.out/stacks/state push state_backup.json cdktf diff --stack
5. Testing Boundaries & Validation Gates
Isolate unit tests from remote state using cdktf.Testing and unittest.mock. Validate synthesized Terraform JSON against terraform validate before triggering remote execution. Strict type boundaries prevent runtime synthesis failures that bypass CI/CD policy gates.
CLI: Execute isolated unit tests with coverage reporting.
pytest tests/ -m 'not integration' --cov=src
Run deterministic validation checks against the synthesized output directory. Formatting and structural validation must pass before any remote plan execution.
CLI: Validate and format synthesized infrastructure definitions.
cdktf synth terraform -chdir=cdktf.out/stacks/validate terraform fmt -check -recursive cdktf.out
#!/usr/bin/env bash
set -euo pipefail
STACK_NAME="${1:-CloudStack}"
cdktf synth --output cdktf.out
terraform -chdir="cdktf.out/stacks/${STACK_NAME}" validate
cdktf diff --stack "${STACK_NAME}"
Operational Notes
A Terraform Cloud workspace pins its own Terraform version, independent of whatever binary sits on your machine or in the CI image. CDKTF synthesizes JSON that declares required_version from the terraformVersion hint in cdktf.json, and when the workspace is older than that constraint the run fails during initialization with Error: Unsupported Terraform Core version. Set the workspace version explicitly rather than leaving it on "latest" — an automatic upgrade to a new minor release can change plan output for a stack nobody touched that week.
Run concurrency is per-organization, not per-workspace. A monorepo that synthesizes twelve stacks and fires twelve cdktf deploy invocations in parallel will queue eleven of them behind the concurrency limit of the plan tier. The practical fix is to deploy in dependency waves rather than all at once, using cdktf deploy --stack for each wave and letting cross-stack references resolve through TerraformRemoteState between waves.
Token rotation deserves a scheduled job rather than an incident. Team tokens have no expiry by default; create a replacement, update the CI secret, run one plan to prove the new token works, then revoke the old one. Because CDKTF reads the token only at CLI invocation time, there is no daemon to restart and no cached session to invalidate.
State version retention matters for the rollback story. Terraform Cloud keeps every state version a workspace has ever produced, and each cdktf deploy that changes anything produces one. That history is what makes the rollback protocol above viable, but it also means an accidentally applied secret stays retrievable by anyone with state-read permission on the workspace. Move genuinely sensitive values out of resource arguments and into a secrets manager referenced by data source — see Managing IaC State for Python Projects for the tool-agnostic version of that argument.
Finally, watch the size of the configuration version CDKTF uploads. The CLI archives the whole synthesized stack directory, and if .terraform/ or a downloaded provider binary ends up inside cdktf.out, the upload balloons from kilobytes to hundreds of megabytes and the run times out fetching it. Keep cdktf.out in .gitignore and regenerate it in CI rather than caching it between jobs.
Common Pitfalls
- Omitting the
TFE_TOKENenvironment variable triggers401 Unauthorizederrors duringcdktf deploy. - Using local state backend defaults while expecting Terraform Cloud remote execution causes state desynchronization.
- Failing to pin provider versions in
cdktf.jsonintroduces non-deterministic synthesis across pipeline runs. - Running
cdktf deploywithout--auto-approvein non-interactive CI/CD environments causes indefinite pipeline hangs awaiting interactive input. - Mixing local
terraform planincdktf.out/with remote workspace state triggers state lock conflicts and corrupts version history. - Leaving
cdktf.outon disk from an S3-backend run makes the first Terraform Cloudinitreport a backend change and stop, because thecloud {}block and abackend "s3"block cannot coexist. - Setting provider credentials as Terraform variables instead of environment variables leaves them unset in the run container; the plan then fails with the AWS provider's
No valid credential sources found. - Storing the token in
cdktf.jsonor a committed.envpublishes an organization-wide credential to every clone of the repository.
FAQ
How do I resolve state locked by another process errors in Terraform Cloud?
Force unlock only after verifying no active runs exist: terraform force-unlock -force <LOCK_ID> in the synthesized stack directory. Always audit workspace run history in the TFC UI before unlocking to prevent state corruption.
Can I use Python 3.9+ type hints with CDKTF remote execution?
Yes. Type hints are stripped during synthesis. Ensure cdktf synth succeeds locally before pushing to Terraform Cloud. Use mypy in CI to enforce strict typing pre-synthesis.
How do I safely rollback a failed CDKTF deployment on Terraform Cloud?
Use Terraform Cloud's state version history in the workspace UI to restore a prior snapshot, or pull the state locally with terraform state pull, then push the previous version with terraform state push. Always pair with cdktf diff to verify drift before re-deploying.
Why does cdktf deploy fail with workspace not found despite correct cdktf.json?
The cloud {} block will create a missing workspace on init, but only if the token carries permission to manage workspaces in that organization. A read-scoped team token cannot, so the create silently fails and the subsequent lookup reports the workspace as absent. Either grant the team "Manage workspaces" or pre-create the workspace in the UI — and check the organization name, which is case-sensitive.
Should I use CloudBackend in Python or the terraformCloud key in cdktf.json?
Use the terraformCloud key while the app has one stack; it is less code and the CLI understands it without synthesis. Move to CloudBackend as soon as a second stack appears, because the JSON key applies to every stack in the app and cannot target different workspaces per stack.
Can I still run cdktf destroy when the workspace is in remote execution mode?
Yes. The CLI uploads a configuration version and queues a destroy run, exactly as it does for an apply. If the workspace has prevent_destroy lifecycle blocks on any resource, the plan fails with Instance cannot be destroyed and names the resource address — remove the lifecycle block in code, synthesize, and re-run rather than overriding it in the UI.
Key Takeaways
Terraform Cloud with CDKTF gives you managed state, remote execution, and policy-as-code gates without running your own Terraform backend infrastructure. The critical discipline is keeping TFE_TOKEN out of source control and enforcing workspace isolation per environment. With those in place, the remote execution model is significantly more reliable than local apply for team environments.
Related
- State Backend Configuration for CDKTF — the parent guide covering S3, GCS, and Azure backends alongside the Terraform Cloud option.
- Managing IaC State for Python Projects — tool-agnostic state locking, encryption, and isolation concepts that apply to managed backends too.
- CDKTF Workflows & Terraform Synthesis — how synthesis artifacts hand off to remote execution.