How to Integrate Terraform with Your Headless CMS
Connect published infrastructure configuration in your headless CMS to Terraform Cloud runs, so redirects, environment settings, region launches, and workspace variables don’t depend on copy-paste tickets.
What is Terraform?
Terraform is HashiCorp’s infrastructure as code tool for defining cloud resources, SaaS settings, networking rules, and platform configuration in declarative files. Platform, DevOps, and SRE teams use it with providers for AWS, Google Cloud, Azure, Cloudflare, Vercel, Datadog, and hundreds of other services. Terraform Cloud and Terraform Enterprise add remote runs, state storage, policy checks, variables, and approval workflows through an HTTP API.
Why integrate Terraform with a headless CMS?
Infrastructure changes often start as content decisions. A new country page needs DNS, CDN routing, region-specific environment variables, redirects, analytics settings, and preview environments. If those decisions live in a headless CMS but Terraform owns the infrastructure, someone usually copies values into a ticket, edits tfvars by hand, asks for review, and waits for a plan. That works for one change a month. It gets messy when you publish 20 localized launches in a week.
Architecture overview
A typical flow starts when an editor publishes a Sanity document like terraformConfig, marketLaunch, or edgeRedirectMap. A Sanity webhook is filtered to publish events for that type, for example _type == "terraformConfig" and !(_id in path("drafts.**")). The webhook sends the document ID to a Sanity Function, Next.js route, or other small listener. That handler uses @sanity/client and GROQ to fetch only the fields Terraform needs, including joined references such as region codes, environment names, or redirect targets. The handler then calls the Terraform Cloud API with a team or user token. It can list workspace variables with GET /api/v2/workspaces/:workspace_id/vars, create or update variables with POST or PATCH on the vars endpoint, and queue a run with POST /api/v2/runs. Terraform Cloud runs the plan against the selected workspace, applies normal policy and approval rules, and then updates the real infrastructure. The end user sees the result through the channel affected by that infrastructure, such as a new regional site route, a preview environment, a CDN redirect, or a docs portal deployment.
Common use cases
Regional launch infrastructure
Publish a market launch document in Sanity, then queue a Terraform Cloud run that provisions CDN routes, DNS records, storage buckets, and region-specific variables.
Edge redirects from structured rules
Model redirect rules as typed entries, then sync them into Terraform variables used by Cloudflare, Fastly, or AWS CloudFront modules.
Preview environments for releases
Create preview workspace variables when a content release moves to review, so Terraform can provision temporary infrastructure for product, docs, or campaign testing.
Access configuration for docs portals
Use Sanity for nonsecret access tiers and product metadata, then pass allowed group IDs or route scopes into Terraform-managed IAM, Auth0, or Okta resources.
Step-by-step integration
- 1
Set up Terraform Cloud
Create a Terraform Cloud organization and workspace, connect the workspace to your Terraform configuration, and confirm a normal plan runs from the UI. Create a team token or user API token with access to the target workspace. Save the workspace ID, not just the workspace name, because Terraform Cloud run creation uses the workspace ID.
- 2
Install the integration dependencies
In your webhook listener or Sanity Function project, install the Sanity client with npm install @sanity/client. Terraform Cloud exposes an HTTP API, so a Node 18+ runtime can use fetch without an extra SDK. If you’re building a custom Terraform provider, use HashiCorp’s Terraform Plugin Framework in Go instead, but most content-triggered integrations only need the Terraform Cloud API.
- 3
Model Terraform-safe content in Sanity Studio
Create a schema for the data you want Terraform to consume. A practical terraformConfig type might include title, workspaceId, environment, variables[], runMessage, and applyPolicy. Avoid storing secrets in the Content Lake. Store secret names, resource identifiers, or nonsecret config values, and keep real secrets in Terraform Cloud variables or your secret manager.
- 4
Create the publish trigger
Add a Sanity webhook filtered to the document type that should affect infrastructure, such as _type == "terraformConfig". Send a compact payload like {"id": _id}. For server-side processing without a separate worker, use Sanity Functions. For an app-owned route, use a Next.js, Express, or Cloudflare Workers endpoint and verify the webhook signature before making Terraform calls.
- 5
Map GROQ results to Terraform Cloud API calls
Fetch the published document from the Content Lake with GROQ, transform the selected fields into Terraform Cloud variable payloads, create or update those variables, and queue a run with POST /api/v2/runs. Start with plan-only workflows and require manual apply in Terraform Cloud until the team trusts the mapping.
- 6
Test the frontend and the infrastructure result
Use a sandbox Terraform workspace first. Publish one Sanity document, confirm the webhook fires once, inspect the Terraform Cloud run, and verify the end user result, such as a working redirect, new preview URL, or correct regional route. Then expose run status in your internal tool or Sanity Studio so editors can see whether the infrastructure plan is pending, failed, or applied.
Code example
import {createClient} from '@sanity/client';
import {NextRequest, NextResponse} from 'next/server';
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-02-19',
token: process.env.SANITY_READ_TOKEN,
useCdn: false,
});
async function tf(path: string, init: RequestInit = {}) {
return fetch(`https://app.terraform.io/api/v2${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.TFC_TOKEN}`,
'Content-Type': 'application/vnd.api+json',
...init.headers,
},
});
}
export async function POST(req: NextRequest) {
const {id} = await req.json();
const doc = await sanity.fetch(
`*[_id == $id][0]{title, workspaceId, "vars": variables[]{key, value, category, hcl, sensitive}}`,
{id},
);
if (!doc?.workspaceId) {
return NextResponse.json({error: 'Missing Terraform workspace'}, {status: 400});
}
const varsRes = await tf(`/workspaces/${doc.workspaceId}/vars`);
const existing = (await varsRes.json()).data ?? [];
for (const item of doc.vars ?? []) {
const category = item.category || 'terraform';
const current = existing.find(
(v: any) => v.attributes.key === item.key && v.attributes.category === category,
);
const body = JSON.stringify({
data: {
type: 'vars',
attributes: {
key: item.key,
value: String(item.value),
category,
hcl: Boolean(item.hcl),
sensitive: Boolean(item.sensitive),
},
},
});
await tf(
current
? `/workspaces/${doc.workspaceId}/vars/${current.id}`
: `/workspaces/${doc.workspaceId}/vars`,
{method: current ? 'PATCH' : 'POST', body},
);
}
const runRes = await tf('/runs', {
method: 'POST',
body: JSON.stringify({
data: {
type: 'runs',
attributes: {message: `Sanity publish: ${doc.title}`},
relationships: {
workspace: {data: {type: 'workspaces', id: doc.workspaceId}},
},
},
}),
});
return NextResponse.json(await runRes.json(), {status: runRes.status});
}How Sanity + Terraform works
Build your Terraform integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect published content decisions with Terraform Cloud plans and runs.
Start building free →CMS approaches to Terraform
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Infrastructure-safe content modeling | Often stores routing or launch details inside pages, plugins, or HTML fields, which makes Terraform mappings fragile. | Uses schema-as-code in Sanity Studio, so fields that affect Terraform can be typed, reviewed, versioned, and tested. |
| Triggering Terraform runs | Typically depends on cron exports, custom plugins, or manual tickets after publishing. | Uses webhooks or Functions to react to publish events and call Terraform Cloud without polling. |
| Field-level payload control | May require parsing full pages or plugin-specific data before sending values to Terraform. | Uses GROQ to select exact fields, follow references, and return a Terraform-ready projection in one query. |
| Secrets, state, and approvals | Teams may be tempted to paste secrets or infrastructure values into editorial fields. | Keeps nonsecret structured config in the Content Lake while Terraform Cloud remains responsible for secrets, state, plans, policies, and apply approval. |
| Multi-channel reuse | Infrastructure-related values are often tied to one website or page tree. | Feeds websites, mobile apps, Terraform workflows, and AI agents from the same structured content model. |
Keep building
Explore related integrations to complete your content stack.
Sanity + GitHub
Connect schema code, Terraform modules, pull requests, and content-triggered workflows in one developer loop.
Sanity + GitLab
Use GitLab CI/CD with Sanity webhooks to test content-driven infrastructure changes before Terraform apply.
Sanity + Linear
Create Linear issues from failed Terraform plans or route content requests to platform teams with document context attached.