Part 3: Multi-Environment Promotion, Drift Detection, and Policy-as-Code in Terraform Cloud
AuthorEmmanuel Secretaria
Published Jan 11, 2026
A comprehensive guide to promoting Terraform changes across environments, scheduling drift detection, and enforcing policy-as-code in Terraform Cloud using workspace design, CI pipelines, and Sentinel or OPA guardrails.
If Part 1 introduced the Terraform tooling in this repo and Part 2 built a full CI pipeline. In Part 3 we take it to a production-grade operating model inside Terraform Cloud by adding:
- Multi-environment promotion (dev → staging → prod)
- Drift detection schedules (recurring plans with alerts)
- Policy-as-code enforcement (Sentinel or OPA guardrails)
This is a complete, detailed playbook for turning Terraform Cloud into a safe, auditable release system rather than just a state backend.
1) Multi-Environment Promotion: The Core Model
The canonical Terraform Cloud pattern is one workspace per environment (dev, staging, prod). The promotion flow is:
- Develop and test in dev
- Promote to staging after approval
- Promote to prod with stricter policy + manual approval
1.1 Workspace structure and naming
A clear workspace naming scheme makes automation predictable:
myapp-devmyapp-stagingmyapp-prod
If you run multiple regions or tenants, expand the suffix:
myapp-dev-us-east-1myapp-prod-eu-west-1
This makes it trivial to enumerate and update using the repo’s helper scripts:
terraform/terraform_cloud_workspaces.sh terraform/terraform_cloud_workspace_vars.sh <workspace_id>
1.2 Environment-specific variables
Use workspace variables for environment-specific values, and variable sets for shared values.
Workspace vars (environment-specific):
terraform/terraform_cloud_workspace_set_vars.sh <workspace_id> \ environment=dev \ replica_count=1 \ enable_deletion_protection=false
Variable sets (shared across environments):
terraform/terraform_cloud_varset_set_vars.sh <varset_id> \ TF_LOG=ERROR \ LOG_LEVEL=info
This prevents drift in shared configuration while still allowing per-env tuning.
1.3 Promotion paths: three viable patterns
There are three common promotion models. Pick the one that fits your org.
Pattern A: Branch-based promotion (simple + intuitive)
→ dev workspacemain
→ staging workspacerelease/staging
→ prod workspacerelease/prod
This works well when you treat Git branches as promotion gates.
Pattern B: Tag-based promotion (audit-friendly)
tag → devv1.2.3-dev
tag → stagingv1.2.3-rc
tag → prodv1.2.3
This provides immutable promotion artifacts, which are easier to audit.
Pattern C: Artifact promotion (advanced)
You generate a plan (or module package) in dev and reuse the artifact for staging/prod. This keeps the plan deterministic but needs more tooling.
2) Promotion Pipeline in GitHub Actions (Terraform Cloud as State + Execution)
Below is a practical pipeline to promote from dev to staging to prod. It uses environment approvals plus Terraform Cloud for state and audit.
name: terraform-promotion on: push: branches: [ main ] env: TF_IN_AUTOMATION: "true" TF_INPUT: "false" TERRAFORM_ORGANIZATION: my-org jobs: plan-dev: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Plan (dev) env: TERRAFORM_WORKSPACE: myapp-dev run: | terraform init terraform plan -out=tfplan apply-dev: runs-on: ubuntu-latest needs: plan-dev steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Apply (dev) env: TERRAFORM_WORKSPACE: myapp-dev run: | terraform init terraform apply -auto-approve tfplan plan-staging: runs-on: ubuntu-latest needs: apply-dev steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Plan (staging) env: TERRAFORM_WORKSPACE: myapp-staging run: | terraform init terraform plan -out=tfplan apply-staging: runs-on: ubuntu-latest needs: plan-staging environment: name: staging steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Apply (staging) env: TERRAFORM_WORKSPACE: myapp-staging run: | terraform init terraform apply -auto-approve tfplan plan-prod: runs-on: ubuntu-latest needs: apply-staging steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Plan (prod) env: TERRAFORM_WORKSPACE: myapp-prod run: | terraform init terraform plan -out=tfplan apply-prod: runs-on: ubuntu-latest needs: plan-prod environment: name: production steps: - uses: actions/checkout@v4 - name: Install Terraform run: install/install_terraform.sh - name: Terraform login (Terraform Cloud) env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} run: | mkdir -p ~/.terraform.d cat <<EOF > ~/.terraform.d/credentials.tfrc.json { "credentials": { "app.terraform.io": { "token": "${TERRAFORM_TOKEN}" } } } EOF - name: Apply (prod) env: TERRAFORM_WORKSPACE: myapp-prod run: | terraform init terraform apply -auto-approve tfplan
Why this works
- Workspaces isolate environments while keeping code identical.
- Promotion uses GitHub environment approvals for staging/prod.
- Terraform Cloud keeps state, run history, and audit logs.
3) Drift Detection Schedules: Catching Changes Outside Terraform
Drift happens when something changes outside Terraform (manual changes, auto-scaling, provider defaults). Drift detection should be continuous, automated, and visible.
3.1 The simplest schedule: periodic plan-only runs
Set up a scheduled plan using either:
- Terraform Cloud UI (workspace settings → Runs → Schedule)
- CI cron + API (queue a plan on a schedule)
A basic GitHub Actions cron approach:
name: terraform-drift-check on: schedule: - cron: "0 3 * * *" # daily at 03:00 UTC jobs: drift: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Queue drift plan env: TERRAFORM_TOKEN: ${{ secrets.TERRAFORM_TOKEN }} TERRAFORM_ORGANIZATION: my-org TERRAFORM_WORKSPACE: myapp-prod run: | payload='{ "data": { "attributes": { "message": "drift detection (scheduled)" }, "type": "runs", "relationships": { "workspace": { "data": { "type": "workspaces", "id": "'${TERRAFORM_WORKSPACE}'" } } } } }' terraform/terraform_cloud_api.sh /runs -X POST -d "$payload" | jq .
This queues a remote plan-only run in Terraform Cloud. You can then:
- alert on non-empty plans
- send notifications to Slack/email
- block promotion if drift exists
3.2 Drift as a policy gate
Treat drift like a failing test. If a scheduled run finds changes, you can:
- auto-open a ticket
- block prod applies until drift is reconciled
- require manual review
This keeps your infrastructure consistent and prevents surprises.
3.3 Workspace-level visibility
Terraform Cloud shows scheduled runs in the workspace history. This provides an audit trail and a forensic timeline of drift events.
4) Policy-as-Code Enforcement in Terraform Cloud
Policies are how you guarantee guardrails, regardless of who triggers the run. In Terraform Cloud, policies apply to all runs in a workspace—CI, CLI, or UI.
4.1 Sentinel policies (Terraform Cloud native)
Sentinel is Terraform Cloud’s built-in policy language. A simple example that blocks public S3 buckets might look like this:
import "tfplan/v2" as tfplan public_buckets = filter tfplan.resource_changes as _, rc { rc.type is "aws_s3_bucket" and rc.change.after.acl is "public-read" } main = rule { length(public_buckets) is 0 }
Apply this policy to prod workspaces only using a policy set.
4.2 OPA (Open Policy Agent)
If you already use OPA or want Rego-based policies, Terraform Cloud supports it as a policy framework. This is useful when you want to share policy logic across Terraform and Kubernetes.
Example OPA rule (conceptual):
package terraform deny[msg] { input.resource_changes[_].type == "aws_iam_user" msg := "IAM users must be provisioned via SSO" }
4.3 Organizing policy sets by environment
A strong approach is:
- Global policies: security baselines (no public buckets, encryption required)
- Staging policies: warnings-only or soft fail
- Production policies: hard fail + manual approval
Terraform Cloud lets you assign policy sets by workspace or tag. This supports per-environment enforcement without duplicating logic.
4.4 Guardrails for promotion pipelines
Use policies to enforce:
- No destructive changes in prod (block if
detected)destroy - Tagging requirements (every resource must include
,owner
,env
)cost_center - Region constraints (production only in allowed regions)
Policies are enforced before apply, which is the ideal time to block risky or non-compliant changes.
5) Putting It All Together: A Mature Terraform Cloud Operating Model
When you combine promotion, drift schedules, and policy-as-code, you get:
✅ predictable releases across environments
✅ continuous drift detection
✅ automated security and compliance enforcement
✅ complete audit trails in Terraform Cloud
The result is a Terraform system that behaves like a real infrastructure delivery platform rather than a set of scripts.
Final Thoughts
Part 3 completes the Terraform Cloud production journey. You now have:
- Promotion workflows that scale across environments
- Scheduled drift detection to catch out-of-band changes
- Policy-as-code guardrails to enforce compliance
The next evolution is run-task integrations, module registries, and automated cost governance.