Terraform / OpenTofu
Use the Zerops provider to manage projects, services, deployments and routing as code. It works with both Terraform and OpenTofu.
Install
- Create a token in token management
(see the guide) and export it as
ZEROPS_TOKEN. - If you use OpenTofu, set
source = "zeropsio/zerops". The provider is listed on the OpenTofu Registry, andtofu initinstalls the signed provider. - Explicit provider attributes override environment variables.
You can configure the provider with the following settings:
| Setting | Environment variable | Default / behavior |
|---|---|---|
token | ZEROPS_TOKEN | Required. |
organization_id | ZEROPS_ORGANIZATION_ID | Auto-selected when the token has one organization. |
endpoint | ZEROPS_ENDPOINT | https://api.app-prg1.zerops.io |
postgresql_min_disk_gb | ZEROPS_POSTGRESQL_MIN_DISK_GB | 2 GB. PostgreSQL disk default and floor. |
| None, environment only | ZEROPS_CLICKHOUSE_MIN_RAM_GB | 0.375 GB. ClickHouse RAM floor. |
| None, environment only | ZEROPS_ELASTICSEARCH_MIN_RAM_GB | 2 GB. Elasticsearch RAM floor. |
routing_force_sync | ZEROPS_ROUTING_FORCE_SYNC | false. Synchronizes foreign staged routing changes only when enabled. |
Resource Map
The following table maps Zerops concepts to provider resources and data sources.
Minimal Example
main.tf
terraform {
required_providers {
zerops = {
source = "zeropsio/zerops"
version = "~> 0.1"
}
}
}
provider "zerops" {}
resource "zerops_project" "example" {
name = "terraform-example"
}
resource "zerops_service" "app" {
project_id = zerops_project.example.id
hostname = "app"
type = "nodejs@22"
}
resource "zerops_service" "db" {
project_id = zerops_project.example.id
hostname = "db"
type = "postgresql:single@16"
}
resource "zerops_service_env" "database_url" {
service_id = zerops_service.app.id
key = "DATABASE_URL"
content = "$${db_connectionString}"
}
resource "zerops_service_deploy" "app" {
service_id = zerops_service.app.id
source_dir = "${path.module}/app"
zerops_yaml = <<-YAML
zerops:
- setup: app
build:
base: nodejs@22
buildCommands:
- npm ci
deployFiles: ./
run:
base: nodejs@22
ports:
- port: 3000
httpSupport: true
start: npm start
YAML
depends_on = [zerops_service_env.database_url, zerops_service.db]
}
resource "zerops_service_subdomain_access" "app" {
service_id = zerops_service.app.id
depends_on = [zerops_service_deploy.app]
}
app/holds the Node.js app (package.json,package-lock.json, andnpm startlistening on port 3000). Keep Terraform files and state outsideapp/.$$escapes Terraform interpolation. Zerops resolves${db_connectionString}. See referencing variables.- Subdomain access requires a deployed service with HTTP ports.
Deploy
| Source | Configuration | Use when |
|---|---|---|
| Local directory | source_dir | Upload local source. .git directories are excluded. |
| Archive | archive_path; build = false for prebuilt archives | Upload a local .tar.gz source or ready-to-run archive. |
| Provider Git | git = { mode = "provider", url = "...", ref = "..." } | Pin a branch, tag, or full commit SHA. Use ZEROPS_GIT_TOKEN for private HTTPS repositories. |
| Server Git | git = { mode = "server", url = "...", ref = "..." } | Build a public repository's branch HEAD on Zerops. |
- Set exactly one source:
source_dir,archive_path, orgit. - Set
zerops_yamlin every mode. See the zerops.yaml specification. - Server Git also requires a matching
zerops.yamlin the repository. - Server Git builds the branch HEAD at build time, not the planned commit.
- Deployments are immutable: input changes create a new application version. Destroying a deployment only forgets the invocation. Import is unsupported.
Import
| Resource | Import ID |
|---|---|
zerops_project | PROJECT_ID |
zerops_service | SERVICE_ID or PROJECT_ID/HOSTNAME |
zerops_object_storage | STORAGE_SERVICE_ID |
zerops_project_env | PROJECT_ID/KEY or canonical ENV_ID |
zerops_service_env | SERVICE_ID/KEY |
zerops_public_http_routing, zerops_public_port_routing | ROUTING_ID |
zerops_project_dedicated_ipv4 | PROJECT_ID (existing allocation only) |
zerops_service_backup_config, zerops_service_git_integration | SERVICE_ID |
zerops_service_subdomain_access | SERVICE_ID (access already enabled) |
zerops_project_vpn_peer | PROJECT_ID/PUBLIC_KEY |
zerops_project_l7_balancer_config, zerops_project_remote_logging, zerops_project_prometheus_config | PROJECT_ID |
zerops_integration_token | ORGANIZATION_ID/TOKEN_ID (secret not recovered) |
zerops_service_deploy,zerops_service_restart, andzerops_project_importcannot be imported.- Routing imports do not synchronize staged changes.
Behavior and Limitations
- Resources wait for asynchronous Zerops processes. Set per-resource
timeouts. Operations and defaults are listed in each resource schema. - Routing mutations synchronize staged routes. A per-route
force_syncoverrides the provider'srouting_force_sync, and forcing permits foreign staged changes. - Non-sudo tokens cannot detect secret-content drift when a refresh returns masked values. Importing masked secrets requires sudo credentials. Secret contents remain in state, so protect state and backend access.
- Managed services that offer both single-node and highly available modes need an
explicit mode in
type, for examplepostgresql:single@16orpostgresql:ha@16. The shorthandpostgresql@16creates an HA service, and provider v0.1.2 and later reject it at plan time. - The PostgreSQL disk floor is set by
postgresql_min_disk_gb. Adjust it when the catalog changes. - Project
max_credit_limitis write-only: it cannot be read back or cleared. - Server-side Git supports only public repositories and branches.
- A dedicated IPv4 address cannot be released. Destroying it only drops Terraform ownership, and billing continues until project deletion.
- The Zerops public API is in beta, and the provider is pre-1.0. Breaking changes are possible in 0.x releases.
- Avoid the reserved service hostnames
coreandl7httpbalancer. - Failed API calls may leave partial resources that require dashboard cleanup.