PR-Based Preview Environments on Render with Terraform

The problem
If you use Render and manage infrastructure with Terraform, you've probably noticed that preview environments are locked behind Blueprints. There's no way to create them through the Terraform provider alone.
This came up in an issue on the terraform-provider-render repo. A Render contributor suggested a workaround: use a combination of Terraform and GitHub Actions to create and destroy resources based on PR lifecycle events, toggling plans and environment variables depending on whether the branch is main or not.
I tried it. It worked. So I packaged it into a reusable Terraform module: terraform-render-preview-module.
The core idea
The pattern is straightforward:
- When a PR is opened or updated, run
terraform applywithis_preview=trueandpr_number=<N>— this creates a preview instance with a suffixed name likemy-app-pr-123. - When the PR is closed, run
terraform destroyto tear it down. - When code is pushed to
main, runterraform applywithis_preview=falseto update the production environment.
Each preview gets its own Terraform state file (keyed by PR number), so multiple previews can coexist without stepping on each other.
PR opened => terraform apply (state: pr-123.tfstate) => preview up
PR updated => terraform apply (state: pr-123.tfstate) => preview updated
PR closed => terraform destroy (state: pr-123.tfstate) => preview gone
push main => terraform apply (state: prod.tfstate) => prod updated
The module
The module lives at modules/render-preview-stack and accepts inputs for toggling between production and preview:
module "render_preview_stack" {
source = "git::https://github.com/dviramontes/terraform-render-preview-module.git//modules/render-preview-stack?ref=v0.1.0"
is_preview = var.is_preview
pr_number = var.pr_number
name_prefix = "my-app"
region = "oregon"
web_plan_prod = "starter"
web_plan_preview = "starter"
start_command = "npm start"
runtime_source = {
native_runtime = {
auto_deploy = true
branch = "main"
build_command = "npm install"
repo_url = "https://github.com/your-org/your-repo"
runtime = "node"
}
}
env_vars = {
APP_ENV = {
value_prod = "production"
value_preview = "preview"
}
}
}
The is_preview flag controls naming, plan selection, and environment variable values. The pr_number variable gets appended to resource names so each PR gets isolated infrastructure.
GitHub Actions glue
The repo includes a workflow that wires everything together. On pull_request events (opened, synchronize, reopened), it runs terraform apply with the preview variables. On PR close, it runs terraform destroy. On push to main, it applies the production configuration.
State is stored in S3 with a per-PR key for previews and a separate key for production. The workflow supports both OIDC and static AWS credentials for authentication.
Why not just use Blueprints?
Blueprints work fine if your entire stack is defined in a render.yaml. But if you're already using Terraform to manage your Render infrastructure — maybe alongside other providers — maintaining a parallel Blueprint definition just for previews can be hard to keep in sync. This approach keeps everything in Terraform and lets you use the same IaC workflow for both production and preview environments.
A complete example
For a real-world example, check out this PR.
Wrapping up
This started as a quick experiment based on a suggestion in a GitHub issue. The workaround turned out to be clean enough to generalize into a module. If you're using Terraform with Render and want PR-based preview environments without Blueprints, give terraform-render-preview-module a try.