Terraform provider
The official Terraform provider lets you declare your managed.dev fleet as
code — sites, environments, custom domains, teams, and the scoped keys that drive
your other automation. It’s a thin declarative adapter over the
Go SDK: every call goes through the SDK, so
idempotency keys, retries, and the pinned
Forge-Version are handled for you. Because it speaks
the standard plugin protocol, it works with both Terraform and OpenTofu.
shape, not motion
Section titled “shape, not motion”Terraform manages resource shape — the sites, environments, and domains that
exist. Imperative motion — restart, cache purge, backup, deploy, promote,
rollback — is deliberately out of scope; drive that from CI or the
mf CLI. There is no “deployment as a resource”.
install
Section titled “install”The provider isn’t listed on the Terraform Registry yet. Build the plugin and point Terraform at the local binary with a dev override. First build it from the provider repo:
make install # produces ./terraform-provider-forgeThen add a dev override to ~/.terraformrc that points at the built binary:
provider_installation { dev_overrides { "managed-dev/forge" = "/absolute/path/to/terraform-provider-forge" } direct {}}authenticate
Section titled “authenticate”The provider reads a Forge API key — an mfk_ token — from the
FORGE_TOKEN environment variable, or the token provider argument (the argument
wins when both are set). FORGE_TOKEN is the same variable the
Go SDK and mf CLI read, so one credential
works across every client surface.
export FORGE_TOKEN="mfk_live_…"The provider takes two optional arguments:
| Argument | Description |
|---|---|
token |
Forge API token (Sensitive). Falls back to FORGE_TOKEN. |
endpoint |
Base URL of the API, including the /v1 prefix. Defaults to the production endpoint; override it for a staging fleet. |
resources and data sources
Section titled “resources and data sources”The provider models the resource graph as Terraform resources. State is keyed by the canonical UUID in every case — never the domain — so a domain that later moves doesn’t retarget a resource.
| Type | Name | Manages |
|---|---|---|
| Resource | forge_site |
A site/app. Async provision, teardown, and in-place PHP-version switch. domain/profile/team_id force replacement. |
| Resource | forge_environment |
A staging or per-branch preview environment. Async create/delete; every input forces replacement. |
| Resource | forge_domain |
A custom hostname with DNS-TXT verification; exports the record to publish. |
| Resource | forge_api_key |
A scoped, reveal-once key for other automation. The secret lands in state. |
| Resource | forge_team |
A team (rename in place). Needs an account-level key. |
| Data source | forge_site |
Resolve an existing site by id or domain to its canonical id. |
key arguments
Section titled “key arguments”forge_site—domain(required),profile(bedrock|vanilla|static, required, immutable),php_version(8.2–8.5, required unless the profile isstatic, mutable in place via the dedicated switch), plus optionalregion,team_id,git_repo, andgit_branch. Exportsid,status,slug, andruntime.forge_environment—site_idandtype(staging|branch) required;branchrequired whentype = branch; optionalcopy_uploadsto seed from production. Exportsid,status,subdomain, andenv_slug.forge_domain—site_idandhostnamerequired; optionalenv_id(front a preview environment instead of production),verify(defaulttrue), andprimary. Exports thedns_record_type/dns_record_host/dns_record_valueyou publish, plusverification_state.forge_api_key—scopesrequired (may only be narrowed on update); optionalname,expires_in_days(default 90), and aresource_type/resource_idpin. Exportsid,prefix,last4, and the Sensitivesecret.forge_team—name(editable in place). Exportsid,created_by,plan, andsuspended.
a worked example
Section titled “a worked example”This provisions a WordPress site, a staging environment, and a verified custom
domain; mints a least-privilege key for a deploy bot; and
resolves an existing site by domain — all in one apply.
terraform { required_providers { forge = { source = "managed-dev/forge" } }}
provider "forge" { # token read from FORGE_TOKEN when omitted; # endpoint defaults to the production API root.}
resource "forge_site" "blog" { domain = "blog.example.com" profile = "bedrock" php_version = "8.3" git_repo = "git@github.com:acme/blog.git" git_branch = "main"
# Provisioning is an async job the provider waits on — bound the wait. timeouts { create = "30m" delete = "15m" }}
resource "forge_environment" "staging" { site_id = forge_site.blog.id type = "staging"}
# Attach a custom domain; publish the exported TXT record to pass verification.resource "forge_domain" "www" { site_id = forge_site.blog.id hostname = "www.example.com" primary = true}
# A least-privilege key for a deploy bot, pinned to this one site.resource "forge_api_key" "deploy_bot" { name = "blog-deploy-bot" scopes = ["sites:read", "deployments:write", "environments:write", "jobs:read"]
expires_in_days = 90 resource_type = "site" resource_id = forge_site.blog.id}
# Reference an EXISTING site by its domain; key other resources off the resolved# .id, never the domain, so a moved domain cannot silently retarget them.data "forge_site" "shop" { domain = "acme-store.com"}
output "verify_record" { description = "Publish this TXT record to verify the custom domain." value = { type = forge_domain.www.dns_record_type host = forge_domain.www.dns_record_host value = forge_domain.www.dns_record_value }}
output "deploy_bot_key" { value = forge_api_key.deploy_bot.secret sensitive = true}async operations and timeouts
Section titled “async operations and timeouts”Site and environment creates and deletes, and the site PHP-version switch, are
async jobs. The provider writes the new resource id into
state immediately, then blocks until the job reaches a terminal state — so a
terraform apply doesn’t return until the site is actually ready, and a failed or
timed-out job leaves a tracked (tainted) resource rather than an orphaned billing
site. Bound each wait with the per-resource timeouts block.