Skip to content
DevOps AI ToolKit
Newsletter
All prompts
AI for Terraform Difficulty: Advanced ClaudeChatGPTCursor

Terraform Ephemeral Preview Environments Prompt

Design per-PR ephemeral preview environments with Terraform — unique naming/tagging, TTL auto-destroy in CI, DNS/subdomain per env, cost caps, and safe teardown on PR close without nuking the wrong workspace.

Target user
Platform engineers building PR preview infra for app teams
Difficulty
Advanced
Tools
Claude, ChatGPT, Cursor

The prompt

You are a platform engineer who has run PR-scoped ephemeral environments at scale and knows exactly how they leak money and collide on names when done naively.

I will provide:
- Cloud + provider(s) and the app shape (containers, DB, DNS zone)
- CI system (GitHub Actions, GitLab CI, etc.) and where PR events fire
- Backend config (S3/DynamoDB, GCS, or HCP Terraform) and current workspace strategy
- Any TTL / budget constraints

Your job:

1. **Isolation model** — decide between workspace-per-PR (`terraform workspace new pr-${PR_NUMBER}`) and dir-per-PR (copied tfvars + separate state key). State the tradeoff: workspaces share provider/backend config and are cheaper to spin up; dir-per-PR gives stronger blast-radius isolation. Recommend one for MY setup and justify it.

2. **Deterministic, collision-proof naming** — every resource name, tag, DNS record, and state key must derive from the PR number (and repo) so two PRs can NEVER target the same resource. Show the `locals` block that builds `name_prefix = "pr-${var.pr_number}"` and a `resource_suffix` guaranteed unique. Call out cloud name-length limits (S3 bucket 63 chars, RDS identifier 63, etc.).

3. **DNS / subdomain per env** — provision `pr-${var.pr_number}.preview.example.com` via a Route53/Cloud DNS record scoped to the env, and output the URL for the CI to comment on the PR.

4. **TTL auto-destroy** — apply a `ttl-expires` tag/label with an ISO timestamp, and describe BOTH teardown triggers: (a) the PR-close CI job that runs `terraform destroy`, and (b) a scheduled reaper that destroys any env whose `ttl-expires` is in the past (covers abandoned/stuck PRs). Never rely on PR-close alone.

5. **Cost control** — smallest viable instance sizes, spot/preemptible where safe, `skip_final_snapshot = true` on ephemeral DBs, and a hard budget alarm. Estimate monthly cost if N envs run concurrently.

6. **Safe teardown** — the destroy job must (a) select the EXACT workspace/state for that PR, (b) refuse to run against `default`/prod workspaces, (c) `terraform state list` to confirm scope before destroy, and (d) delete the workspace only after a clean destroy. This is a DESTRUCTIVE operation: destroying the wrong workspace wipes another PR's env or prod.

7. **Orphan detection** — a query (by tag) that finds resources with no matching open PR so cost leaks are caught.

Output: (a) the isolation recommendation, (b) naming/tagging `locals`, (c) the CI create + destroy job outlines, (d) the reaper design, (e) a teardown safety checklist.

Bias toward: deterministic naming, TTL reaper as the safety net, and guard rails that make destroying prod impossible.

---
Input template:
```
Cloud/provider:
App shape (containers/DB/DNS):
CI system + PR event source:
Backend (S3+DynamoDB / GCS / HCP TF):
DNS zone:
TTL requirement:
Concurrent-env budget ceiling:
```

Run this prompt with AI

Test it, get an AI-improved version, or compare models — live in the Prompt Workspace. No copy-paste.

Why this prompt works

Ephemeral preview environments fail in two predictable ways: they collide (two PRs racing to own the same S3 bucket or DNS record) and they leak (an abandoned PR leaves a running RDS instance billing for weeks). This prompt forces the model to attack both up front — deterministic PR-derived naming eliminates collisions, and a dual teardown design (PR-close job plus an independent TTL reaper) eliminates leaks. It also treats terraform destroy as the load-bearing destructive operation it is, demanding workspace guards before any teardown runs.

How to use it

  1. Fill in the input template with your cloud, CI, and backend details.
  2. Paste the whole prompt into Claude or Cursor.
  3. Start with workspace-per-PR unless you need hard tenant isolation — it is the cheapest correct default.
  4. Wire the generated create job into your pull_request: [opened, synchronize] trigger and the destroy job into pull_request: [closed].
  5. Deploy the reaper as a scheduled workflow (hourly is plenty) so nothing survives its TTL.

Useful commands

# Create/select an isolated workspace for the PR
terraform workspace new "pr-${PR_NUMBER}" || terraform workspace select "pr-${PR_NUMBER}"

# Plan/apply with the PR number threaded through as a variable
terraform apply -auto-approve \
  -var "pr_number=${PR_NUMBER}" \
  -var "ttl_expires=$(date -u -d '+48 hours' +%Y-%m-%dT%H:%M:%SZ)"

# SAFE teardown: confirm scope, refuse prod/default, then destroy
CURRENT=$(terraform workspace show)
case "$CURRENT" in
  default|prod|prod-*) echo "REFUSING to destroy $CURRENT" && exit 1 ;;
esac
terraform state list                                   # eyeball the blast radius
terraform destroy -auto-approve -var "pr_number=${PR_NUMBER}"
terraform workspace select default
terraform workspace delete "pr-${PR_NUMBER}"

# Reaper: find AWS resources whose ttl-expires tag is in the past
aws resourcegroupstaggingapi get-resources \
  --tag-filters Key=managed-by,Values=pr-preview \
  --query "ResourceTagMappingList[].[ResourceARN, Tags[?Key=='ttl-expires'].Value | [0]]" \
  --output text

Patterns

Deterministic naming and TTL tagging, all derived from the PR number so two PRs can never collide:

variable "pr_number" {
  type = string
}

variable "ttl_expires" {
  type    = string
  default = ""
}

locals {
  name_prefix = "pr-${var.pr_number}"

  common_tags = {
    "managed-by"   = "pr-preview"
    "pr-number"    = var.pr_number
    "ttl-expires"  = var.ttl_expires
    "environment"  = "ephemeral"
  }
}

resource "aws_s3_bucket" "assets" {
  # 63-char limit: prefix + account-scoped suffix keeps it unique and short
  bucket = "${local.name_prefix}-assets-${data.aws_caller_identity.me.account_id}"
  tags   = local.common_tags
}

resource "aws_route53_record" "preview" {
  zone_id = var.preview_zone_id
  name    = "${local.name_prefix}.preview.example.com"
  type    = "A"

  alias {
    name                   = aws_lb.app.dns_name
    zone_id                = aws_lb.app.zone_id
    evaluate_target_health = true
  }
}

output "preview_url" {
  value = "https://${aws_route53_record.preview.name}"
}

GitHub Actions teardown job with a hard guard against destroying anything that is not a PR workspace:

name: preview-teardown
on:
  pull_request:
    types: [closed]

jobs:
  destroy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Destroy PR environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          terraform init -input=false
          terraform workspace select "pr-${PR_NUMBER}"
          CURRENT=$(terraform workspace show)
          if [[ "$CURRENT" == "default" || "$CURRENT" == prod* ]]; then
            echo "::error::Refusing to destroy protected workspace $CURRENT"
            exit 1
          fi
          terraform state list
          terraform destroy -auto-approve -var "pr_number=${PR_NUMBER}"
          terraform workspace select default
          terraform workspace delete "pr-${PR_NUMBER}"

Related prompts

More Terraform prompts & error guides

Browse every Terraform prompt and troubleshooting guide in one place.

Free download · 368-page PDF

Reading prompts? Get all 500 in one free PDF

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.