You manage ZCP infrastructure as code now. The official provider is live on the
OpenTofu registry and the Terraform Registry, with 38 resources and 12
data sources covering everything our zcp CLI does: instances, volumes, snapshots and backups,
VPCs and networks, firewall and egress rules, load balancers, auto-scaling, Kubernetes clusters,
DNS, object storage, VPN, and account governance.
terraform {
required_providers {
zcp = {
source = "zsoftly/zcp"
version = "~> 0.1"
}
}
}
The same source address works with both tools. Here is what building it taught us.
One SDK, two clients
The provider does not reimplement our API. It imports the same Go SDK the zcp CLI uses. Every request the provider sends is a request the CLI already sends. Authentication, retries, and platform quirks live in one place. When we fix a platform edge case in the SDK, both clients inherit the fix in their next release.
This forced a licensing decision. The CLI repository carried a source-available license. A Go binary statically links its dependencies. An MIT provider embedding a restricted SDK would have promised users rights we had not granted. We relicensed the CLI and its SDK under Apache-2.0. The dependency chain is now clean. Contributions carry a patent grant. Both registries serve a provider anyone is free to rebuild and redistribute.
Declarative semantics are the hard part
Wrapping an API in resource schemas is the easy half of a provider. The hard half is honoring Terraform’s contract: a plan must tell the truth, an apply must converge, and a second plan must show no changes.
This contract surfaced platform behaviors a CLI user never notices:
- Cached state lies. Our control plane refreshes instance state lazily, so a VM reports
Startingfor minutes after it is running. A CLI user shrugs and rerunslist. A provider blocking onRunningwould hang. We added a live state endpoint to the platform, and the provider polls it instead, reconciling against the hypervisor. Creates now return in about two minutes, exactly when the VM is up. - DNS has no record IDs. Our DNS backend models records as sets addressed by name and type,
the PowerDNS way. Faking per-record IDs would have produced resources Terraform has no reliable way to refresh or delete. Instead,
zcp_dns_recordembraces the model: one resource per name and type pair, with a synthetic ID built from both. - Write-only fields need a plan. The API never returns an SSH key’s material or a sub-user’s
password. Naive handling makes every
terraform importplan a destroy-and-recreate. The provider adopts the configured value on the first apply after import, and only forces replacement when a known prior value changes.
Live testing beats unit testing
Every resource has unit tests against fakes, but the findings we cared about came from applying real configurations against production. Two examples from one afternoon:
First, orphaned networks. Our instance resource accepts a network_plan shortcut, and the platform
creates a network for the VM as a side effect. Terraform never sees it, so destroy removes the VM
and leaves the network and its public address billing forever. The fix was not code, it was
guidance: declare the network as its own resource and attach the instance to it. Our examples and
tutorial now use the tracked pattern everywhere, and a full destroy leaves nothing behind. We
verified the whole cycle live: apply, check the platform, destroy, confirm zero leftovers.
Second, the empty public_ip. On source-NAT networks the public address belongs to the network,
not the VM, so the VM object never carries it and the attribute came back blank. The provider now
resolves it from the account’s IP list by VM identifier, preferring static assignments. Outputs
show the address the moment apply completes.
We also ran three adversarial review rounds over the codebase and verified every finding against the live API before fixing it. Several were platform bugs exposed by the provider’s strictness, not provider bugs. Those became backend tickets. The provider now fails loudly rather than recording state the platform did not honor.
Signed releases, two registries, zero ceremony
CI builds and GPG-signs every release. The private key lives only in the release pipeline and
in our vault. Both registries verify the signature against our published key before serving a
version, and your init verifies it again on install.
We published to the OpenTofu registry first. The whole process is two issue forms on a public GitHub repository. A bot validates the release artifacts, then a maintainer merges the request.

The Terraform Registry followed the same afternoon. Shipping a new version now takes one git tag. Both registries pick it up automatically.

Try it
The tutorial takes you from an empty directory to a running VM and back to a clean account in about fifteen minutes, with OpenTofu and Terraform commands side by side. The provider reference lives on either registry, and the source is on GitHub under MIT. Issues and contributions are welcome. Our first community contribution shipped in the CLI last week. We would be glad to see the same happen here.
If you are new to ZCP, create an account and put your
first terraform apply on Canadian infrastructure today.