> For the complete documentation index, see [llms.txt](https://docs.platform9.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.platform9.com/private-cloud-director/automation-and-cli/terraform-provider.md).

# Terraform Provider

Manage Private Cloud Director infrastructure as code with the first-party PCD Terraform provider. Install, authenticate, and provision your first resources.

The <code class="expression">space.vars.product\_name</code> (<code class="expression">space.vars.product\_acronym</code>) Terraform provider (`platform9/pcd`) is the first-party way to manage <code class="expression">space.vars.product\_acronym</code> as code. It is published on the public [Terraform Registry](https://registry.terraform.io/providers/platform9/pcd/latest) and works with any standard Terraform (or OpenTofu) workflow.

{% hint style="warning" %}
**Information**

The <code class="expression">space.vars.product\_acronym</code> Terraform provider is currently in `beta`. It is published at a `0.x` version, so resources and attributes can change between releases. Pin a provider version in your configuration and review the release notes before you upgrade.
{% endhint %}

## Overview

The provider manages the services that <code class="expression">space.vars.product\_acronym</code> exposes: Identity Service, Compute Service, Networking Service (including QoS and quotas), Persistent Storage Service, Image Library Service, Load Balancing (Octavia/OVN), DNS (Designate), and Key Management (Barbican). It also manages <code class="expression">space.vars.product\_acronym</code>'s own cluster blueprints and host configuration and roles, declared as code.

In this guide, you'll install the provider, authenticate against your <code class="expression">space.vars.product\_acronym</code> environment, and create your first resources with Terraform.

## Requirements

| Component                                                   | Version                                                                                 |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| <code class="expression">space.vars.product\_acronym</code> | <code class="expression">space.vars.product\_acronym</code> 2026 April release or later |
| Terraform                                                   | 1.0 or later (provider protocol 6)                                                      |
| OpenTofu                                                    | Any release supporting protocol 6                                                       |

## Install the Provider

Declare the provider in your Terraform configuration and pin a compatible version:

```hcl
terraform {
  required_providers {
    pcd = {
      source  = "platform9/pcd"
      version = "~> 0.1"
    }
  }
}
```

Run `terraform init`. Terraform downloads the provider from the registry and verifies its GPG signature automatically.

## Authenticate

Password, token, and application-credential authentication are all supported, as is sourcing credentials from a `clouds.yaml` file (`cloud` / `OS_CLOUD`).

```hcl
provider "pcd" {
  auth_url    = "https://pcd.example.com/keystone/v3"
  region      = "Infra"
  user_name   = "admin@example.localnet"
  password    = var.pcd_password
  tenant_name = "service"

  user_domain_id    = "default"
  project_domain_id = "default"

  # PCD Community Edition ships a self-signed certificate.
  insecure = true
}
```

Every argument can instead be supplied through the standard `OS_*` environment variables (`OS_AUTH_URL`, `OS_USERNAME`, `OS_PASSWORD`, `OS_PROJECT_NAME`, `OS_REGION_NAME`, `OS_INSECURE`, and so on), so an existing RC file works unchanged.

{% hint style="info" %}
**Self-signed TLS**

Against a Community Edition or lab endpoint, set `insecure = true` (or `OS_INSECURE=true`). For production, prefer pinning the CA with `cacert_file` instead of disabling verification.
{% endhint %}

## Quickstart

This minimal example authenticates, then creates a private network and subnet:

```hcl
terraform {
  required_providers {
    pcd = {
      source  = "platform9/pcd"
      version = "~> 0.1"
    }
  }
}

provider "pcd" {
  # Reads OS_AUTH_URL / OS_USERNAME / OS_PASSWORD / OS_REGION_NAME from the environment.
  insecure = true
}

resource "pcd_networking_network" "app" {
  name           = "app-net"
  admin_state_up = true
}

resource "pcd_networking_subnet" "app" {
  name       = "app-subnet"
  network_id = pcd_networking_network.app.id
  cidr       = "192.168.100.0/24"
  ip_version = 4
}

output "network_id" {
  value = pcd_networking_network.app.id
}
```

```bash
terraform init      # downloads platform9/pcd and verifies its signature
terraform plan
terraform apply
```

Runnable, self-contained examples for every resource and data source, including `import.sh` files for importable resources, are in the provider repository under [`examples/`](https://github.com/platform9/terraform-provider-pcd/tree/main/examples).

## Day 1 Configuration

Day 1 configuration stands up a new region: a cluster blueprint, host networking, a host cluster, and the roles that onboard a host into it. These resources configure <code class="expression">space.vars.product\_acronym</code>'s own control plane. The order below matters: most steps either name a resource created earlier or otherwise depend on one being ready first. The complete, runnable version of these examples, applied and destroyed end to end on Community Edition, is the provider's [Community Edition guide](https://registry.terraform.io/providers/platform9/pcd/latest/docs/guides/community-edition), with its source in [`examples/complete/community-edition/`](https://github.com/platform9/terraform-provider-pcd/tree/main/examples/complete/community-edition).

These examples assume the host is already prepared and authorized in <code class="expression">space.vars.product\_acronym</code>. See [pcdctl](/private-cloud-director/automation-and-cli/pcdctl-command-line.md) if it isn't yet; the `host_id` variable below is that host's resmgr UUID. The host records that UUID in `/etc/pf9/host_id.conf`. The other ways to find it, and every other ID the provider asks for, are in [Look Up IDs for Import](#look-up-ids-for-import) below.

### Create a Volume Type

A volume type is the name tenants choose when they create a volume. Its `volume_backend_name` extra spec routes those volumes to a storage backend that the cluster blueprint declares in `storage_backends_json`, and the blueprint's `image_library_storage` names a volume type as well. <code class="expression">space.vars.product\_acronym</code> validates that the type exists when the blueprint is saved, so create the type first. The `nfs` in `volume_backend_name` below is the backend name: it matches the top-level key of the backend declared in the blueprint that follows, and if you rename one, rename both.

```hcl
resource "pcd_blockstorage_volume_type" "nfs" {
  name        = "nfs-storage"
  description = "NFS-backed persistent storage"
  is_public   = true

  extra_specs = {
    volume_backend_name = "nfs"
  }
}
```

### Define the Cluster Blueprint

<code class="expression">space.vars.product\_acronym</code> keeps one cluster blueprint per region. `storage_backends_json` declares the region's Persistent Storage Service backends. The example below declares a single NFS backend.

```hcl
resource "pcd_cluster_blueprint" "region" {
  name            = "region-1"
  dns_domain_name = "pcd.local."

  virtual_networking = {
    enabled       = true
    underlay_type = "vlan"
    vnid_range    = "1000:2000"
  }

  image_library_storage        = pcd_blockstorage_volume_type.nfs.name
  image_library_shared_storage = true
  instance_shared_storage      = false
  vm_storage                   = "/opt/data/instances"

  storage_backends_json = jsonencode({
    nfs = {
      "nfs-primary" = {
        driver = "NFS"
        config = {
          nfs_shares_config           = "/opt/pf9/etc/pf9-cindervolume-base/conf.d/nfs_shares"
          nfs_mount_points            = "192.0.2.50:/srv/nfs/pcd"
          nfs_mount_point_base        = "/opt/pf9/etc/pf9-cindervolume-base/volumes/"
          nfs_snapshot_support        = true
          nas_secure_file_permissions = false
          nas_secure_file_operations  = false
        }
      }
    }
  })
}
```

`storage_backends_json` has two levels of keys. The top-level key is the backend name (`nfs` here): on the host it becomes `volume_backend_name`, which is what a volume type's `volume_backend_name` must equal. The key under it names one driver configuration for that backend (`nfs-primary` here; both names are yours to choose): it is what the `persistent-storage` role's `backends` list selects, and it becomes the backend section on the host. That configuration carries `driver` and `config`. Write boolean options as booleans, not quoted strings; a quoted `"true"` never validates and the host keeps converging. `driver` is one of the built-in driver identifiers (`NFS`, `LVM`, `HitachiISCSI`, and so on) or the full class path of a custom driver, and `config` holds that driver's own keys, the same ones the UI shows under **Infrastructure > Cluster Blueprint > Persistent Storage Connectivity > Add Volume Backend Configuration**. For other drivers, see [Certified Block Storage Drivers & Configurations](/private-cloud-director/storage/block-storage/volume-backend-configuration-examples.md).

{% hint style="warning" %}
**Sensitive State**

`storage_backends_json` carries the backend driver's credentials and is stored in Terraform state as plain text. Use a remote backend with encryption at rest and restrict who can read it, rather than a local state file or plain version control.
{% endhint %}

If the region already has a blueprint, don't create a new one: see [the section below on managing an existing blueprint](#manage-pcd-native-infrastructure) to import and manage it instead.

### Configure and Assign Host Networking

`pcd_host_config` maps each traffic type on a host to a network interface, and `network_labels` maps a physical-network label to one of those same interfaces. A later provider network binds to the host through that label, so pick a name (`physnet1` below) and reuse it consistently.

```hcl
# The resmgr UUID of the host being onboarded.
variable "host_id" {
  type = string
}

resource "pcd_host_config" "single_nic" {
  name         = "hc-single-nic"
  cluster_name = pcd_cluster_blueprint.region.name

  mgmt_interface           = "enp1s0"
  vm_console_interface     = "enp1s0"
  host_liveness_interface  = "enp1s0"
  tunneling_interface      = "enp1s0"
  imagelib_interface       = "enp1s0"
  live_migration_interface = "enp1s0"

  network_labels = {
    physnet1 = "enp1s0"
  }
}

resource "pcd_host_config_assignment" "host1" {
  host_id        = var.host_id
  host_config_id = pcd_host_config.single_nic.id
}
```

### Create the Cluster and Onboard the Host

`pcd_cluster` is the host cluster a hypervisor joins; VM high availability and auto-rebalancing are declared on it, so they're part of the region's definition from the start rather than a setting you turn on later. `pcd_host_cluster_role` then onboards the host by assigning it a cluster role: <code class="expression">space.vars.product\_acronym</code> expands each cluster role into its granular roles and computes their settings from the blueprint and the host configuration above, instead of you setting each one by hand. That makes it a different, higher-level resource than `pcd_host_role` in the table below, which assigns one granular role directly and suits only roles that need no computed settings.

Setting `wait_until_converged = true` blocks each role's apply until the host reports it converged. Do this for every role here: the Day 2 resources that follow (images, instances, volumes) need a hypervisor, image library, and storage backend that are actually ready.

```hcl
resource "pcd_cluster" "main" {
  name = "cluster-1"

  vm_high_availability = {
    enabled = true
  }

  auto_resource_rebalancing = {
    enabled                    = true
    rebalancing_strategy       = "vm_workload_consolidation"
    rebalancing_frequency_mins = 10
  }
}

resource "pcd_host_cluster_role" "hypervisor" {
  host_id              = var.host_id
  role                 = "hypervisor"
  host_cluster         = pcd_cluster.main.name
  wait_until_converged = true

  depends_on = [pcd_host_config_assignment.host1]
}

resource "pcd_host_cluster_role" "image_library" {
  host_id              = var.host_id
  role                 = "image-library"
  wait_until_converged = true

  depends_on = [pcd_host_config_assignment.host1]
}

resource "pcd_host_cluster_role" "storage" {
  host_id              = var.host_id
  role                 = "persistent-storage"
  backends             = ["nfs-primary"]
  wait_until_converged = true

  depends_on = [pcd_host_config_assignment.host1]
}
```

Each role explicitly depends on the host configuration assignment. Nothing above references it by attribute, so Terraform can't infer the order on its own, and assigning a cluster role before the host has its network configuration produces an inconsistent host. The `persistent-storage` role's `backends` list names a driver configuration from the blueprint's `storage_backends_json`: the second-level key (`nfs-primary` here), not the backend name (`nfs`) above it. Assigning the role is what turns that configuration into a running storage service on the host. Naming the top-level key instead leaves the service with no backend, and the host cannot converge.

## Day 2 Configuration

With the region converged, Day 2 configuration adds the resources a workload runs on: a network, an image, a flavor, an instance, and a volume. These are the tenant-facing services described in the Overview above.

### Configure Tenant Networking

This example creates a flat provider network on the `physnet1` label from the host configuration above, so instances land directly on that network.

```hcl
resource "pcd_networking_network" "workload" {
  name   = "workload-net"
  shared = true

  segments = [{
    network_type     = "flat"
    physical_network = "physnet1"
  }]

  depends_on = [pcd_host_cluster_role.hypervisor]
}

resource "pcd_networking_subnet" "workload" {
  network_id  = pcd_networking_network.workload.id
  name        = "workload-subnet"
  cidr        = "203.0.113.0/24"
  ip_version  = 4
  gateway_ip  = "203.0.113.1"
  enable_dhcp = true

  allocation_pools = [
    {
      start = "203.0.113.10"
      end   = "203.0.113.200"
    }
  ]

  dns_nameservers = ["8.8.8.8"]
}
```

The network depends on the hypervisor role directly, because the `physnet1` label only exists on a host once that role has converged. Nothing in the network's own attributes references the role, so Terraform needs the explicit dependency.

A security group with SSH and ICMP access completes the tenant-facing setup:

```hcl
resource "pcd_networking_secgroup" "workload" {
  name        = "workload-secgroup"
  description = "SSH and ICMP for the workload instance"
}

resource "pcd_networking_secgroup_rule" "ssh" {
  security_group_id = pcd_networking_secgroup.workload.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
}

resource "pcd_networking_secgroup_rule" "icmp" {
  security_group_id = pcd_networking_secgroup.workload.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "icmp"
  remote_ip_prefix  = "0.0.0.0/0"
}
```

### Add an Image and a Flavor

This example uses CirrOS, a minimal test image commonly used to validate an image library end to end. Point `image_source_url` (or `local_file_path`) at your own image for anything you plan to run for real.

```hcl
resource "pcd_images_image" "cirros" {
  name             = "cirros"
  container_format = "bare"
  disk_format      = "qcow2"
  image_source_url = "https://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img"
  min_disk_gb      = 1
  visibility       = "public"

  depends_on = [pcd_host_cluster_role.image_library]
}

resource "pcd_compute_flavor" "small" {
  name  = "small"
  vcpus = 1
  ram   = 512
  disk  = 5
}
```

The image depends on the `image-library` role directly: uploads need a working image-library host to receive them, and `image_source_url` isn't a reference Terraform can order on its own.

The upload goes to the image-library host itself, on port 9494, which is how the UI uploads images too. The machine running Terraform must reach the host on that port; if it cannot, set `endpoint_overrides = { image = "https://<reachable-address>:9494/v2/" }` in the provider block.

### Boot an Instance with an Attached Volume

The instance boots from the image and flavor above onto the workload subnet, then gets a Persistent Storage Service volume attached to it:

```hcl
resource "pcd_compute_instance" "workload" {
  name        = "workload-vm"
  image_name  = pcd_images_image.cirros.name
  flavor_name = pcd_compute_flavor.small.name

  security_groups = [pcd_networking_secgroup.workload.name]

  network {
    uuid = pcd_networking_network.workload.id
  }

  depends_on = [pcd_networking_subnet.workload]
}

resource "pcd_blockstorage_volume" "data" {
  name        = "workload-data"
  size        = 1
  volume_type = pcd_blockstorage_volume_type.nfs.name

  depends_on = [pcd_host_cluster_role.storage]
}

resource "pcd_compute_volume_attach" "data" {
  instance_id = pcd_compute_instance.workload.id
  volume_id   = pcd_blockstorage_volume.data.id
}
```

The instance only references the network by ID, not the subnet, so it depends on the subnet directly: <code class="expression">space.vars.product\_acronym</code> needs an actual subnet, not just a bare network, to assign the instance an address. The volume depends on the `persistent-storage` role the same way: nothing in its attributes points at that role, but the backend needs a running service on the host before it can accept a new volume.

## Manage PCD-Native Infrastructure

<code class="expression">space.vars.product\_acronym</code> keeps one cluster blueprint per region, so a region that already runs has a blueprint to import rather than create. The import ID is the blueprint's name.

```hcl
resource "pcd_cluster_blueprint" "region" {
  name = "region-1"
  # The other attributes are read back on import. Leave storage_backends_json
  # unset to keep the backends the region already has.
}
```

```bash
terraform import pcd_cluster_blueprint.region region-1
terraform plan   # reconcile until the plan is empty, then manage changes as code
```

With Terraform 1.5 or later, an `import` block and `terraform plan -generate-config-out=generated.tf` write the resource block for you, which is the quickest way to learn the shape of a blueprint you have only configured in the UI. The sensitive `storage_backends_json` comes out as `null`, which is what you want: the provider keeps the backends the region already has.

```hcl
import {
  to = pcd_cluster_blueprint.region
  id = "region-1"
}
```

Destroying an imported blueprint deletes it from <code class="expression">space.vars.product\_acronym</code>. To stop managing it without deleting it, run `terraform state rm pcd_cluster_blueprint.region`.

| Resource                     | Manages                                                                                                                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pcd_cluster_blueprint`      | The region's cluster blueprint: virtual-network segmentation, DNS domain, image library and VM storage, and the Persistent Storage Service backends. One per region.                                                                                    |
| `pcd_cluster`                | A cluster, the unit hypervisors join. VM high availability and auto-rebalancing are set here.                                                                                                                                                           |
| `pcd_host_config`            | A host's mapping of each traffic type (management, VM console, tunneling, image library, live migration) to a network interface, plus physical-network labels.                                                                                          |
| `pcd_host_config_assignment` | Attaches a host configuration to a host.                                                                                                                                                                                                                |
| `pcd_host_cluster_role`      | Onboards a host by assigning it a cluster role (`hypervisor`, `image-library`, `persistent-storage`, `dns`); <code class="expression">space.vars.product\_acronym</code> computes the granular role settings from the blueprint and host configuration. |
| `pcd_host_role`              | Assigns one granular role (for example `pf9-ostackhost-neutron`) directly. The low-level API under `pcd_host_cluster_role`; reserve it for roles that take no settings.                                                                                 |

A read-only `pcd_cluster_blueprint` data source is also available.

### Look Up IDs for Import

Import IDs are assigned by <code class="expression">space.vars.product\_acronym</code>. `pcdctl` prints them; source the `pcdctl RC` file from **Settings > API Access** first, and on Community Edition also set `OS_INSECURE=true` for its self-signed certificate. (`pcdctl config set` configures the host onboarding commands; it does not authenticate these.)

```bash
source pcdctlrc
export OS_INSECURE=true
pcdctl volume type show nfs-storage -f value -c id
```

| Resource                                                               | Import ID                      | Where to Find It                            |
| ---------------------------------------------------------------------- | ------------------------------ | ------------------------------------------- |
| `pcd_blockstorage_volume_type`                                         | Volume type UUID               | `pcdctl volume type list`                   |
| `pcd_blockstorage_volume`                                              | Volume UUID                    | `pcdctl volume list`                        |
| `pcd_images_image`                                                     | Image UUID                     | `pcdctl image list`                         |
| `pcd_compute_flavor`                                                   | Flavor UUID                    | `pcdctl flavor list`                        |
| `pcd_compute_instance`                                                 | Instance UUID                  | `pcdctl server list`                        |
| `pcd_networking_network`, `pcd_networking_subnet`                      | Network or subnet UUID         | `pcdctl network list`, `pcdctl subnet list` |
| `pcd_networking_secgroup`                                              | Security group UUID            | `pcdctl security group list`                |
| `pcd_cluster_blueprint`                                                | Blueprint name                 | `GET /resmgr/v2/blueprint` (below)          |
| `pcd_cluster`                                                          | Cluster name                   | `pcdctl aggregate list`                     |
| `pcd_host_config`                                                      | Host configuration UUID        | `GET /resmgr/v2/hostconfigs` (below)        |
| `pcd_host_config_assignment`, `pcd_host_cluster_role`, `pcd_host_role` | `<host-uuid>/<config-or-role>` | Host UUID, as below                         |

The host UUID is not the ID that `pcdctl hypervisor list` shows. The host records it in `/etc/pf9/host_id.conf`; once the host has the hypervisor role, `pcdctl hypervisor show <hypervisor-id> -c service_host` and `pcdctl compute service list --service nova-compute -c Host -c Zone` print it too.

Host configurations, blueprints, and clusters are read from the resource manager API with a token from `pcdctl`:

```bash
TOKEN=$(pcdctl token issue -f value -c id)
curl -sk -H "X-Auth-Token: $TOKEN" https://pcd.example.com/resmgr/v2/hostconfigs | python3 -m json.tool
curl -sk -H "X-Auth-Token: $TOKEN" https://pcd.example.com/resmgr/v2/blueprint | python3 -m json.tool
curl -sk -H "X-Auth-Token: $TOKEN" https://pcd.example.com/resmgr/v1/hosts | python3 -m json.tool
```

The full per-resource table is in the provider's [Importing guide](https://registry.terraform.io/providers/platform9/pcd/latest/docs/guides/importing).

## Full Resource Reference

Per-resource and per-data-source documentation, including every argument, attribute, and import ID, is generated from the provider schema and published on the Terraform Registry:

[**registry.terraform.io/providers/platform9/pcd/latest/docs**](https://registry.terraform.io/providers/platform9/pcd/latest/docs)

Two guides there cover this page's workflows end to end:

* [Community Edition guide](https://registry.terraform.io/providers/platform9/pcd/latest/docs/guides/community-edition): a runnable zero-to-VM configuration for a Community Edition region.
* [Importing guide](https://registry.terraform.io/providers/platform9/pcd/latest/docs/guides/importing): the import ID and lookup command for every importable resource.

The Registry is the source of truth for the reference; this page covers concepts and getting started.

## Support and Source

* **Source and issues:** [github.com/platform9/terraform-provider-pcd](https://github.com/platform9/terraform-provider-pcd)
* **Examples:** [`examples/` in the repository](https://github.com/platform9/terraform-provider-pcd/tree/main/examples)
* **License:** Mozilla Public License 2.0. Portions are ported from `terraform-provider-openstack` (MPL-2.0) and carry provenance comments.

## Related Pages

* [PCD CLI - pcdctl](/private-cloud-director/automation-and-cli/pcdctl-command-line.md): the pcdctl equivalent for imperative, scripted operations.
* [Certified Block Storage Drivers & Configurations](/private-cloud-director/storage/block-storage/volume-backend-configuration-examples.md): the driver keys for `storage_backends_json`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.platform9.com/private-cloud-director/automation-and-cli/terraform-provider.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
