Terraform and OpenTofu
Manage your apps, databases and variables from .tf files, with tofu apply or terraform apply, through the Deplane provider.
The provider works with OpenTofu 1.11 or newer and Terraform 1.11 or newer. It is not in the public registries yet: you install it from a folder (below).
Install
- Get the provider folder for your version (for example
terraform-provider/0.1.0/) from your Deplane contact. It holds one zip per platform and aSHA256SUMSlist. - Check the zip for your platform against
SHA256SUMS:sha256sum -c SHA256SUMS --ignore-missing. - Tell OpenTofu (or Terraform) to take the provider from that folder. In
~/.tofurc(%APPDATA%\tofu.rcon Windows; for Terraform~/.terraformrcor%APPDATA%\terraform.rc):
provider_installation {
filesystem_mirror {
path = "/path/to/terraform-provider/0.1.0"
include = ["deplane-cloud/deplane"]
}
direct {
exclude = ["deplane-cloud/deplane"]
}
}
- Run
tofu initin your folder.
Your .tf files name the provider as deplane-cloud/deplane, which will not change when it is published.
Sign in
Create a CI token with deplane tokens create (it asks for your passkey) and give it to Terraform as DEPLANE_TOKEN:
export DEPLANE_TOKEN=... # from a secret store, not a file in the repository
tofu plan
The provider block can also take token and api_url. A token is only ever sent to the address it was given with: a token from DEPLANE_TOKEN goes to DEPLANE_API_URL (or the default), never to an api_url written only in the file.
Example
terraform {
required_providers {
deplane = { source = "deplane-cloud/deplane", version = "~> 0.1" }
}
}
provider "deplane" {}
variable "stripe_key" {
type = string
sensitive = true
ephemeral = true # kept out of saved plan files too
}
resource "deplane_postgres" "db" {
name = "shop-db"
version = "18"
}
resource "deplane_app" "web" {
name = "shop-web"
image = "ghcr.io/acme/shop-web:1.4.2"
port = 3000
health_check = "/healthz"
instances = 2
}
resource "deplane_attachment" "web_db" {
app = deplane_app.web.name
database = deplane_postgres.db.name
}
resource "deplane_app_env" "web" {
app = deplane_app.web.name
values = { NODE_ENV = "production" }
sensitive = { STRIPE_KEY = var.stripe_key }
refs = { DB_HOST = "$${postgres.host}" }
depends_on = [deplane_attachment.web_db]
}
resource "deplane_worker" "jobs" {
app = deplane_app.web.name
name = "jobs"
command = "node worker.js"
}
resource "deplane_domain" "www" {
app = deplane_app.web.name
hostname = "www.acme.example"
}
Resources
| Resource | Import id | What it is |
|---|---|---|
deplane_app | shop-web | An app: image (optional), port, health_check, release_command, size, instances, max_instances, sleep_after_minutes, auto_rollback, private_network. Reads url and private_url. |
deplane_app_env | shop-web, or shop-web/build | Some of an app's variables, in one scope. |
deplane_postgres | shop-db | A Postgres database: version (17 or 18), size. |
deplane_mysql | shop-mysql | A MySQL database: size (medium or large). |
deplane_redis | shop-cache | A Redis. |
deplane_attachment | shop-web/postgres/shop-db | Gives an app a database (database, mysql or redis): DATABASE_URL, MYSQL_URL or REDIS_URL. |
deplane_worker | shop-web/jobs | A worker: command, instances, size. |
deplane_cron_job | shop-web/nightly | A cron job: command, schedule (UTC), size. |
deplane_domain | shop-web/www.acme.example | A custom domain. Reads the DNS records to set: cname_target, txt_name, txt_value. |
Data sources: deplane_app (an app by name) and deplane_database (by name and engine: postgres, mysql or redis). Neither ever returns a password or a variable's value.
Bring an existing object under Terraform with tofu import deplane_app.web shop-web, or an import block.
Fields you leave out of deplane_app keep what the app has. Every limit (sizes, instances, names, your plan's limits) comes from Deplane, and its error says what to change, with the same code as in Troubleshooting and error codes.
Images and deploys
- With
image, the provider deploys that public image and waits for it to be live. Changeimageand it deploys again. With an image, a newport,health_checkorrelease_commandalso deploys again, because they take effect with a new release. - If someone deploys the app another way (a git push,
deplane deploy), the next plan showsimageas(deployed outside Terraform)and the apply deploys your image again. - Without
image, the provider manages the app's settings only, and deploys come from GitHub or the CLI as before. - A failed deploy ends the apply with the build's reason and fix. Fix the image, then run
tofu applyagain. - If a new app or database was created but its first deploy or start failed, OpenTofu marks it tainted and would replace it, which stops at a delete Terraform cannot do. Keep it instead:
tofu untaint deplane_app.web(your resource's address), thentofu apply. The error says so too. tofu importcannot read which image an app runs, so the first apply after an import deploys the file's image once more.
Variables
valuesare for what is not secret: they show in plans and stay in Terraform's state.sensitiveis for secrets. It is write-only: the value is sent to Deplane and never kept in plans or the state. The state keeps a hash of each, so a changed secret shows as a change. Pass secrets through a variable withsensitive = trueandephemeral = true: otherwise OpenTofu and Terraform write the variable's own value into a saved plan file (tofu plan -out).refsare filled in on Deplane's side from the databases attached to the app, such as"$${postgres.password}"(the$$keeps Terraform from reading it). Runtime variables only.- Only the names you list are set or removed. Variables set elsewhere (DATABASE_URL from an attachment, the dashboard, a variable group) are left alone. Use one
deplane_app_envper app and scope. - A variable changed or removed outside Terraform is set again on the next apply.
Deleting
Deleting an app or a database needs a person's passkey, which a CI token cannot give. When a plan deletes one (it left the file, or a change such as a new Postgres version needs a new database), the apply stops at the delete and names the command:
Error: Terraform cannot delete shop-web
deplane_app: deleting an app needs a person's passkey, which a CI token cannot give.
Run "deplane apps destroy shop-web", then run tofu apply again: it will see shop-web is gone.
Run that command (it asks for your passkey), then tofu apply again. Nothing is created before the delete, so you never have two databases or none.
Variables, attachments, workers, cron jobs and domains are removed by the provider itself.
From a stack
deplane stack export NAME --format hcl > main.tf writes a stack as a .tf file with import blocks, so the first tofu apply takes over its existing apps and databases. What the provider cannot manage yet (a disk, ClickHouse, a GitHub or folder source) and values Deplane generated are left out and stay as they are; the file and the command list them. That first apply also deploys each app's image once more and sets its variables again with the same values. After it, run deplane stack forget NAME.
Not yet
- Disks, ClickHouse, GitHub connections, previews and variable groups.
- The public Terraform and OpenTofu registries, and signed releases.
- Showing Terraform-made stacks on the canvas.
This page is also available as Markdown. AI agents can start at /llms.txt.