Skip to content

Terraform provider

Preview The official Terraform provider lets you declare your managed.dev fleet as code. It’s generated from the same OpenAPI 3.1 spec as the SDKs and the mf CLI, so its resources and arguments track the API exactly. You get the usual Terraform workflow — plan, apply, drift detection, state — over sites, environments, teams, and the scoped keys that drive your other automation.

The provider models the resource graph as Terraform resources. The runtime and scopes that the API treats as first-class are declared right in HCL.

Resource Manages Key arguments
forge_site A site/app name, project_id, runtime, repository
forge_environment An environment under a site site_id, name, branch, type (production/staging/preview)
forge_team A team name, members
forge_api_key A scoped key for other automation team_id, scopes, ttl, resource_constraint

A runtime of static advertises a different capability set than wordpress, so the provider validates that an argument you set is actually supported by the runtime before it ever calls the API.

This configuration provisions a WordPress site, a preview environment wired to a branch, and a least-privilege service key for a deploy bot — all in one apply.

main.tf
terraform {
required_providers {
forge = {
source = "managed-dev/forge"
version = "~> 0.1"
}
}
}
provider "forge" {
# Reads FORGE_TOKEN from the environment by default.
# Use an mfs_ service token here, never a personal key.
version = "2026-06-23"
}
resource "forge_site" "store" {
name = "acme-store"
project_id = "proj_01J6ABCD"
runtime = "wordpress"
repository = "git@github.com:acme/store.git"
}
resource "forge_environment" "checkout_preview" {
site_id = forge_site.store.id
name = "feature-checkout"
branch = "feature/checkout"
type = "preview"
}
# A scoped key for the deploy bot — the Deploy preset, expanded.
resource "forge_api_key" "deploy_bot" {
team_id = "team_01J5WXYZ"
scopes = [
"sites:read",
"deployments:write",
"environments:write",
"domains:read",
"jobs:read",
"observability:read",
]
ttl = "90d"
resource_constraint = forge_site.store.id
}
output "preview_url" {
value = forge_environment.checkout_preview.preview_url
}

Beyond terraform plan, the provider supports a server-side propose / validate step (DigitalOcean app-spec style): it submits the desired configuration to the API, which returns the set of jobs it would run and any capability or scope conflicts — without changing anything. This catches a bad runtime/argument combination or an insufficiently-scoped key before apply, not in the middle of it.

Validate the plan against the live platform
terraform plan
mf propose validate --from-plan tfplan

CI runners and the Terraform provider are hostile to long-lived SSE streams — connections get cut, proxies buffer, and there’s no terminal to tail. So when an apply triggers an async job, the provider waits with ETag long-poll instead: it polls GET /v1/jobs/{id} with If-None-Match, getting a cheap 304 Not Modified until the job’s state changes, then reads the new body. This is the same job, just consumed over a transport that survives CI. See async jobs for the full model and the other two consumption paths.