# 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

1. 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 a `SHA256SUMS` list.
2. Check the zip for your platform against `SHA256SUMS`: `sha256sum -c SHA256SUMS --ignore-missing`.
3. Tell OpenTofu (or Terraform) to take the provider from that folder. In `~/.tofurc` (`%APPDATA%\tofu.rc` on Windows; for Terraform `~/.terraformrc` or `%APPDATA%\terraform.rc`):

```hcl
provider_installation {
  filesystem_mirror {
    path    = "/path/to/terraform-provider/0.1.0"
    include = ["deplane-cloud/deplane"]
  }
  direct {
    exclude = ["deplane-cloud/deplane"]
  }
}
```

4. Run `tofu init` in 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`:

```sh
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

```hcl
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](errors.md).

## Images and deploys

- With `image`, the provider deploys that public image and waits for it to be live. Change `image` and it deploys again. With an image, a new `port`, `health_check` or `release_command` also 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 shows `image` as `(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 apply` again.
- 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), then `tofu apply`. The error says so too.
- `tofu import` cannot read which image an app runs, so the first apply after an import deploys the file's image once more.

## Variables

- `values` are for what is not secret: they show in plans and stay in Terraform's state.
- `sensitive` is 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 with `sensitive = true` and `ephemeral = true`: otherwise OpenTofu and Terraform write the variable's own value into a saved plan file (`tofu plan -out`).
- `refs` are 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_env` per 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](stacks.md) 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.
