# Manage ZCP with Terraform or OpenTofu

Start with an empty directory and finish with a running virtual machine managed as code, using the official `zsoftly/zcp` provider. The provider ships on the [OpenTofu registry](https://search.opentofu.org/provider/zsoftly/zcp) and the [Terraform Registry](https://registry.terraform.io/providers/zsoftly/zcp), and it behaves the same with either tool, so pick whichever you already use.

By the end you have:

- The provider installed and authenticated with your account
- A network and a virtual machine defined in one configuration file
- The full lifecycle under your control: plan, apply, change, and destroy

Plan for about 15 minutes. Most of it is the VM booting.

The slugs in this tutorial (region `yul-1`, project `default-9`, template `ubuntu-2204-lts-4`, plans, and so on) are examples from one account. Yours will differ. Each step shows the `zcp` CLI command to print the right value for your account and region. Always use those, don’t copy the examples verbatim.

## Before you start

- A ZSoftly Public Cloud account. [Sign up](https://docs.zcp.zsoftly.ca/public-cloud/getting-started/account-signup) first if you do not have one.
- [Terraform](https://developer.hashicorp.com/terraform/install) 1.0 or later, or [OpenTofu](https://opentofu.org/docs/intro/install/) 1.6 or later.
- Optional but handy: the [`zcp` CLI](https://docs.zcp.zsoftly.ca/public-cloud/cli/installation) to look up slugs for your account.

## Step 1: Get an API token

The provider authenticates with the same bearer token as the CLI and the API. In the [console](https://cloud.zcp.zsoftly.ca/), go to Account → API Keys and create a token, then export it:

```bash
export ZCP_BEARER_TOKEN="<your-token>"
```

The provider reads it from the environment, so the token never appears in your configuration or in your state file.

## Step 2: Declare the provider

Create an empty directory with one file, `main.tf`, and declare the provider. The same source address works for both tools:

```hcl
terraform {
  required_providers {
    zcp = {
      source  = "zsoftly/zcp"
      version = "~> 0.1"
    }
  }
}


provider "zcp" {
  default_project = "default-9"
}
```

`default_project` plays the role of a default region setting in other providers: resources without an explicit project use it. Every account starts with a project whose slug ends in a number, like `default-9`. Find yours with `zcp project list`.

Then initialize the directory:

#### OpenTofu

```bash
tofu init
```

#### Terraform

```bash
terraform init
```

The tool downloads the provider from its registry and verifies the release signature.

## Step 3: Describe a network and a VM

Append the resources to `main.tf`. The region data source feeds the correct `cloud_provider` value to everything else, so you hardcode nothing twice:

```hcl
data "zcp_region" "yul" {
  slug = "yul-1"
}


resource "zcp_network" "app" {
  name           = "app-network"
  cloud_provider = data.zcp_region.yul.cloud_provider
  region         = data.zcp_region.yul.slug
  network_plan   = "pnet-yul"
  billing_cycle  = "hourly"
}


resource "zcp_instance" "web" {
  name             = "web-01"
  template         = "ubuntu-2204-lts-4"
  plan             = "ca2sxs"
  billing_cycle    = "hourly"
  network          = zcp_network.app.id
  storage_category = "premium-ssd"
  cloud_provider   = data.zcp_region.yul.cloud_provider
  region           = data.zcp_region.yul.slug
}


output "public_ip" {
  value = zcp_instance.web.public_ip
}


output "private_ip" {
  value = zcp_instance.web.private_ip
}
```

Look up the slugs for your account with the CLI: `zcp region list`, `zcp plan vm`, `zcp plan network`, `zcp template list`, and `zcp storage-category list`.

Declare the network as its own resource and attach the instance to it with `network`, as above. The instance also accepts a `network_plan` shortcut. The platform then creates a network for you, but it stays invisible to your configuration, so `destroy` leaves it behind. A tracked network comes and goes with your stack.

A network you declare yourself starts with no egress rules and blocks all outbound traffic, so the VM above cannot reach package archives or any other internet host. A network auto-created through `network_plan` does not have this problem. Owning the network means owning its egress rules, so add what the workload needs:

```hcl
resource "zcp_egress_rule" "https_out" {
  network    = zcp_network.app.id
  protocol   = "tcp"
  cidr       = "0.0.0.0/0"
  start_port = "443"
  end_port   = "443"
}
```

Repeat for tcp/80 and udp/53 if the instance installs packages or resolves names. Each rule you add here is another resource, so the counts in Step 4 below go up accordingly. Inbound is a separate matter: see [Back Up Vaultwarden to Object Storage with restic](https://docs.zcp.zsoftly.ca/tutorials/backup-vaultwarden-restic-object-storage/) for the firewall and port-forward pair that opens a port on the network’s source-NAT address.

## Step 4: Plan and apply

#### OpenTofu

```bash
tofu plan
tofu apply
```

#### Terraform

```bash
terraform plan
terraform apply
```

The plan shows two resources to add. Confirm with `yes` and wait: the apply blocks until the VM reports Running, then prints the outputs. Expect about two to three minutes.

```plaintext
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.


Outputs:


private_ip = "10.0.0.194"
public_ip = "23.229.49.88"
```

Run the plan again and it reports No changes: your file and the platform agree.

Plans are compute-only since October 11, 2026. The provider still models the root disk size only for custom plans. An update that adds root disk sizing for fixed plans is coming in a later release. It is not available yet.

## Step 5: Change something

Edit the instance’s `plan` to a bigger size (pick one from `zcp plan vm`) and apply again. The provider resizes the VM in place: it stops the instance, changes the compute offering, and starts it again, the same way an instance type change works on other clouds. Attributes shown as forces replacement in the plan output (the template, the region) recreate the VM instead: read the plan before you confirm.

## Step 6: Import existing resources

Bring resources you created in the console or with the CLI into the configuration rather than recreating them. Write the matching resource block, then import by slug:

#### OpenTofu

```bash
tofu import zcp_instance.existing <instance-slug>
```

#### Terraform

```bash
terraform import zcp_instance.existing <instance-slug>
```

Every resource in the provider supports import. Each resource’s page on the registry documents its import ID format: some are a plain slug, others are composite, like the SSH key’s `<slug>/<region>`.

## Clean up

#### OpenTofu

```bash
tofu destroy
```

#### Terraform

```bash
terraform destroy
```

Both resources go: the VM first, then the network, together with the network’s public address. Nothing keeps billing after a destroy of a fully tracked stack.

## Next steps

- Browse all 38 resources and 12 data sources in the provider documentation on the [Terraform Registry](https://registry.terraform.io/providers/zsoftly/zcp/latest/docs) or the [OpenTofu registry](https://search.opentofu.org/provider/zsoftly/zcp).
- Add an [SSH key resource](https://docs.zcp.zsoftly.ca/public-cloud/compute/settings/ssh-keys) to your configuration to log in to the instances you create.
- Watch the [changelog](https://docs.zcp.zsoftly.ca/changelog/) for new provider releases.
