Terraform Provider
Declare workspaces, applications, variables and domains as infrastructure as code.
Cloady has a Terraform provider on the public registry, so workspaces, applications, environment variables and custom domains can be declared as code and reviewed, versioned and rolled out like the rest of your infrastructure.
terraform {
required_providers {
cloady = {
source = "cloady/cloady"
version = "~> 0.1"
}
}
}
provider "cloady" {} # reads CLOADY_TOKEN
resource "cloady_app" "api" {
workspace = "acme"
name = "API"
region = "eu1"
git = { repository_url = "https://github.com/acme/api" }
}
output "url" {
value = cloady_app.api.endpoints[0].url
}terraform init installs it from the registry. Nothing else to set up.
Authenticate
Create a token under Account → API tokens and export it:
export CLOADY_TOKEN=cldy_xxxxxxxxxxxxxxxxThe provider reads CLOADY_TOKEN by default; set token on the provider block
to override it, and base_url (or CLOADY_CONTROL_PLANE_URL) to point at a
Cloady installation other than https://cloady.com.
Creating workspaces and changing billing need a full-scope token. Everything
else works with deploy, and reads work with read. Your workspace role still
applies on top: a full token can't delete in a workspace where you're a
viewer.
Resources
| Resource | Manages |
|---|---|
cloady_app | An application from a Git repository or the catalog |
cloady_workspace | A workspace |
cloady_variable | One application environment variable |
cloady_domain | A custom domain on an application |
Two data sources read existing state without owning it: cloady_workspace, and
cloady_regions for the regions you can deploy to, including which ones accept
apps from a workspace that pays nothing.
The full attribute reference lives on the Terraform Registry.
A complete stack
data "cloady_regions" "all" {}
resource "cloady_workspace" "acme" {
slug = "acme"
name = "Acme"
}
resource "cloady_app" "db" {
workspace = cloady_workspace.acme.slug
name = "Postgres"
region = "eu1"
catalog = "postgres"
volume_sizes = { data = 20 }
lifecycle {
prevent_destroy = true
}
}
resource "cloady_app" "api" {
workspace = cloady_workspace.acme.slug
name = "API"
region = "eu1"
cpu_scale = 2
memory_scale = 2
git = {
repository_url = "https://github.com/acme/api"
branch = "main"
auto_deploy = true
}
}
resource "cloady_variable" "database_url" {
workspace = cloady_workspace.acme.slug
app = cloady_app.api.slug
region = cloady_app.api.region
key = "DATABASE_URL"
value = var.database_url
is_secret = true
}
resource "cloady_domain" "api" {
workspace = cloady_workspace.acme.slug
app = cloady_app.api.slug
region = cloady_app.api.region
host = "api.acme.com"
is_primary = true
}Each application runs in one region and one environment, and its name must be
unique within that workspace and environment. environment defaults to
production wherever it's accepted; set preview or development for the
others. Variable keys are SCREAMING_SNAKE_CASE.
Read this before your first apply
Terraform's model is "make reality match the configuration", and it will delete and recreate a resource whenever an attribute it can't change is changed. On an application holding data, a recreate is data loss.
- Destroying an application deletes its data, permanently. Its volumes go
with it and there is no undo. Put
prevent_destroyon anything holding state, as the Postgres app above does, and on the workspace — destroying a workspace destroys every application in it. - Some attributes force replacement. An application is created in one
region, in one environment, in one workspace, from one source, and stays
there. Changing
region,environment,workspaceor the Git repository URL is a destroy followed by a create: empty data, a new address, downtime. - Renaming happens in place — never destroy an application to rename it.
name,branch,cpu_scale,memory_scale,volume_sizesandauto_deployare all patched on the existing app. The one cost of a rename is the address: an app answers at<name>-<code>-<workspace>.cloady.io, for examplemy-blog-k7q2v9ze-acme.cloady.io, and that follows the name, so the old address stops working. Custom domains keep pointing at the app. - Volumes only grow. Lowering a value in
volume_sizesis rejected at plan time. To go smaller, back up and create a new application. - Variable changes reach the app on its next deploy. An apply that only
touches
cloady_variablerestarts nothing; follow it withcloady redeploy acme apiif the app needs the value now. - Variable values are stored in Terraform state in plaintext, including
secrets.
is_secrethides the value in Cloady; it does not encrypt your state file. Use a remote backend with encryption at rest, and prefervar.inputs over literals. - On
cloady_domain, onlyredirect_tochanges in place. Changinghostoris_primaryreplaces the record; the app keeps running, but the new host needs its DNS pointed at Cloady before HTTPS works on it. - A successful apply is not a healthy application. Create returns once the
deploy starts, so
statusreports what Cloady saw at that moment, not readiness. Watch the deploy in the dashboard or with the CLI.
Scale is a multiplier over what the application asks for, not an absolute size:
cpu_scale = 2 doubles it. Values run from 0.5 to 4 in steps of 0.25, and
everything running in the workspace — every region, every environment — counts
toward what the workspace pays.
While the applied stack fits the workspace's free allowance nothing is charged.
Beyond it, the card on file is charged for the difference as the apply runs; with
no card, or a declined one, Cloady answers payment_required and Terraform stops.
Add a card under Workspace settings → Billing and re-run the apply —
previewWorkspaceSubscription prices a change first if you want the number before
you apply.
Paid capacity needs a card on your account, added under Account → Billing.
Import existing applications
Everything created in the dashboard can be adopted without recreating it. The import ID is the resource's address, slash-separated:
terraform import cloady_workspace.acme acme
terraform import cloady_app.api acme/api/production/eu1
terraform import cloady_variable.db acme/api/production/eu1/<variable-id>
terraform import cloady_domain.api acme/api/production/eu1/<domain-id>Variable and domain ids come from listVars and listAppDomains in the
API, or from the CLI.
How it relates to the other interfaces
The provider calls the same REST API as the dashboard, the CLI, the SDKs and the MCP server, under the same scope and role checks. It differs in what it is for: the CLI and MCP server perform actions, while Terraform converges on a declared end state and will undo drift on the next apply.
That makes it a poor fit for one-off operations. Restarting a service, running a command in a container, rolling back a deploy or reading logs are actions with no desired state to converge on, and they stay with the CLI and the API.