Skip to content

Phala-Network/terraform-provider-phala

Repository files navigation

Terraform Provider: Phala Cloud

Deploy confidential apps and CVMs on Phala Cloud with Terraform.

Start with phala_app. It deploys one CVM under a single app identity with a Docker Compose, optional encrypted environment, OS image, and instance size. That single CVM is the bootstrap — every Phala app is born with one. For stateful replica sets (Consul, Patroni, etcd, Kafka, …), pair phala_app with phala_app_instance resources to declare additional named slots that share the same app_id.

This provider is intentionally close to the Terraform ergonomics people expect from providers like DigitalOcean: catalog data sources, declarative compute resources, explicit power control, SSH key resources, and straightforward outputs such as app_id, cvm_ids, and endpoint.

Before You Start

You need:

  • Terraform installed.
  • A Phala Cloud account.
  • A Phala Cloud API key.
  • An SSH public key only if you want SSH access inside a deployment.

Get an API key from the Phala Cloud dashboard:

  1. Sign in to https://cloud.phala.com.
  2. Open Settings -> API Keys or your profile page.
  3. Create a key and export it:
export PHALA_CLOUD_API_KEY="phak_xxx"

Provider environment variables:

  • PHALA_CLOUD_API_KEY

Quick Start

This is the default path. It deploys one app with one CVM and gives you an app_id and public endpoint.

The example below uses concrete defaults:

  • size = "tdx.medium"
  • region = "US-WEST-1"
  • disk_size = 40 (GB)
  • image = "dstack-dev-0.5.7-9b6a5239"

These values were tested against real Phala Cloud on March 8, 2026. If you need different sizes, regions, or images, use the discovery examples in Common Tasks after you complete this first deploy.

terraform {
  required_providers {
    phala = {
      source  = "phala-network/phala"
      version = "0.2.0-beta.1" # or newer published version
    }
  }
}

provider "phala" {}

resource "phala_app" "hello" {
  name      = "hello-phala"
  size      = "tdx.medium"
  region    = "US-WEST-1"
  image     = "dstack-dev-0.5.7-9b6a5239" # use a full image slug from data.phala_images
  disk_size = 40 # GB

  docker_compose = <<-YAML
    services:
      web:
        image: nginx:stable
        ports:
          - "80:80"
  YAML

  wait_for_ready       = true
  wait_timeout_seconds = 900
}

output "app_id" {
  value = phala_app.hello.app_id
}

output "endpoint" {
  value = phala_app.hello.endpoint
}

Run:

terraform init
terraform apply -auto-approve
terraform output
terraform destroy -auto-approve

What success looks like:

  • Terraform prints an app_id.
  • Terraform prints a public endpoint.
  • The app appears in your Phala Cloud dashboard and reaches running state.

A complete copy of this example also lives in examples/app-quickstart.

If you want to test unreleased provider code from this repo, skip this path and use Developer Mode below.

License

This repository uses the Apache 2.0 license, matching the other open source Phala Cloud components. See LICENSE.

Common Tasks

Discover available sizes, regions, images, nodes, and workspace info

data "phala_sizes" "all" {}

data "phala_regions" "all" {}

data "phala_images" "all" {
  # optional region filter
  # region = "us-east"
}

data "phala_nodes" "west" {
  region = "us-west"
}

data "phala_account" "current" {}

data "phala_workspace" "current" {}

data "phala_attestation" "web" {
  cvm_id = "app_abc123"
}

For image selection, use the exact slug from data.phala_images. Do not shorten it to dstack-dev or dstack-dev-0.5.7.

data "phala_images" "west" {
  region = "us-west"
}

output "image_slugs" {
  value = data.phala_images.west.images[*].slug
}

Advanced: deploy a single app with SSH access and power control

resource "phala_ssh_key" "laptop" {
  name       = "laptop"
  public_key = file("~/.ssh/id_ed25519.pub")
}

locals {
  chosen_size   = data.phala_sizes.all.sizes[0].slug
  chosen_region = data.phala_regions.all.regions[0].slug
}

resource "phala_app" "web" {
  name           = "my-phala-web"
  size           = local.chosen_size
  region         = local.chosen_region
  env = {
    APP_SECRET = "replace-me"
  }
  docker_compose = <<-YAML
    services:
      web:
        image: nginx:stable
        ports:
          - "80:80"
  YAML

  wait_for_ready       = true
  wait_timeout_seconds = 900
}

resource "phala_cvm_power" "web_power" {
  cvm_id = phala_app.web.primary_cvm_id
  state  = "running" # or "stopped"

  wait_for_state       = true
  wait_timeout_seconds = 900
}

Deploy a stateful replica set (MIG-style named slots)

For Consul, Patroni, etcd, or any cluster that needs stable per-member identity, declare members on phala_app and pair it with phala_app_instance resources keyed by name. The bootstrap CVM (created by phala_app itself) gets the name in phala_app.name; the matching phala_app_instance adopts it. The other slots are created via POST /apps/{app_id}/instances.

locals {
  consul_members = ["consul-0", "consul-1", "consul-2"]
}

resource "phala_app" "consul" {
  name    = local.consul_members[0]
  members = local.consul_members
  size    = data.phala_sizes.all.sizes[0].slug
  region  = data.phala_regions.all.regions[0].slug

  docker_compose = file("${path.module}/consul-compose.yaml")
}

resource "phala_app_instance" "consul" {
  for_each = toset(phala_app.consul.members)

  app_id = phala_app.consul.app_id
  name   = each.value
}

output "consul_member_uuids" {
  value = { for k, v in phala_app_instance.consul : k => v.vm_uuid }
}

The slot identity (name) is durable: if the cloud replaces the CVM occupying a slot, the next refresh rebinds vm_uuid while the Terraform resource key stays the same. App-level updates to docker_compose/env/image are blocked at plan time in members mode — destroy and recreate the app to roll new compose across slots.

Deploy one app and wire its outputs into another app

resource "phala_app" "api" {
  name           = "api-app"
  size           = data.phala_sizes.all.sizes[0].slug
  region         = data.phala_regions.all.regions[0].slug
  docker_compose = <<-YAML
    services:
      api:
        image: nginx:stable
        ports:
          - "80:80"
  YAML
}

resource "phala_app" "consumer" {
  name           = "consumer-app"
  size           = data.phala_sizes.all.sizes[0].slug
  region         = data.phala_regions.all.regions[0].slug
  docker_compose = <<-YAML
    services:
      app:
        image: nginx:stable
        ports:
          - "80:80"
  YAML
  env = {
    UPSTREAM_APP_ID   = phala_app.api.app_id
    UPSTREAM_ENDPOINT = phala_app.api.endpoint
  }
}

Pin placement to a specific node

data "phala_nodes" "west" {
  region = "us-west"
}

resource "phala_app" "pinned" {
  name    = "pinned-app"
  size    = data.phala_sizes.all.sizes[0].slug
  node_id = data.phala_nodes.west.nodes[0].node_id

  docker_compose = <<-YAML
    services:
      web:
        image: nginx:stable
        ports:
          - "80:80"
  YAML
}

Developer Mode (Contributors)

Use this only when developing the provider from source (dev overrides + local binary).

Advanced provider override

Most users should not set api_prefix or PHALA_CLOUD_API_PREFIX.

The provider defaults to the public Phala Cloud API. The base URL override exists for staging environments, local mocks, or temporary infrastructure migrations:

export PHALA_CLOUD_API_PREFIX="https://cloud-api.phala.com/api/v1"

The repo includes a smoke fixture and make targets under examples/smoke. This is for coverage and regression testing, not for first-time user onboarding.

Read-only smoke (catalog data sources only):

make smoke-plan PHALA_API_KEY="phak_xxx" CREATE_RESOURCES=false

Create + destroy smoke:

make smoke-apply \
  PHALA_API_KEY="phak_xxx" \
  CREATE_RESOURCES=true \
  APP_NAME="tf-smoke-app" \
  APP_REPLICAS=2 \
  CREATE_CONSUMER_APP=true \
  CONSUMER_APP_NAME="tf-smoke-consumer" \
  CONSUMER_APP_REPLICAS=1 \
  APP_ENV='{"APP_SECRET":"replace-me"}' \
  CVM_POWER_STATE="stopped" \
  WAIT_FOR_READY=false \
  SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"

make smoke-destroy \
  PHALA_API_KEY="phak_xxx" \
  CREATE_RESOURCES=true \
  APP_NAME="tf-smoke-app" \
  APP_REPLICAS=2 \
  CREATE_CONSUMER_APP=true \
  CONSUMER_APP_NAME="tf-smoke-consumer" \
  CONSUMER_APP_REPLICAS=1 \
  APP_ENV='{"APP_SECRET":"replace-me"}' \
  CVM_POWER_STATE="stopped" \
  WAIT_FOR_READY=false \
  SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"

Notes:

  • make writes a local Terraform CLI config at /tmp/phala-tf-dev/terraformrc with dev_overrides so your global ~/.terraformrc is unchanged.
  • Smoke variables can be overridden with SIZE, REGION, and IMAGE.
  • Set CVM_POWER_STATE=running|stopped to exercise phala_cvm_power in smoke tests.
  • Set CREATE_CONSUMER_APP=true to exercise cross-app wiring (UPSTREAM_APP_ID, UPSTREAM_ENDPOINT).
  • WAIT_FOR_READY=false can be useful for infrastructure lifecycle tests when runtime boot latency is variable.

Behavior and Lifecycle Notes

phala_app

  • phala_app is the app + bootstrap CVM. Each app is born with exactly one CVM via Phala's two-step API: POST /cvms/provision then POST /cvms.
  • For stateful replica sets, declare members = [<slot names>] on phala_app and pair with phala_app_instance resources keyed by name. See Deploy a stateful replica set.
  • Key outputs: app_id, primary_cvm_id (the bootstrap CVM), cvm_ids (every CVM under the app), endpoint.
  • Create-time identity/placement fields:
    • kms (currently phala only; ethereum/base planned)
    • custom_app_id + nonce (deterministic identity flow for PHALA KMS)
    • node_id (maps to provision teepod_id; discover via data.phala_nodes)
  • In-place updates on the bootstrap CVM: size, disk, OS image, docker compose, pre-launch script, encrypted env. In members mode these app-level updates are blocked at plan time (they only mutate the bootstrap CVM, not the named slots) — destroy and recreate to roll new compose across the set.
  • Compose-file runtime settings are exposed as first-class attributes:
    • public_logs
    • public_sysinfo
    • public_tcbinfo
    • gateway_enabled
    • secure_time
    • storage_fs
  • Changing compose-file runtime settings triggers compose provision/apply flow and CVM restart.
  • SSH access is managed at the account level with the phala_ssh_key resource. The keys registered on your account are injected into CVMs at launch; there is no per-app SSH key field.
  • storage_fs is immutable after initial deployment (zfs or ext4); changing it forces replacement.
  • disk_size can only grow (shrink is rejected).
  • CPU/RAM changes are supported through size updates.
  • Encrypted secret modes:
    • env (recommended): provider auto-derives env_keys and encrypts values before API calls.
    • encrypted_env + env_keys (manual): pass-through encrypted payload mode.
  • State caveat:
    • env is sensitive but still stored in Terraform state; use manual encrypted_env mode if plaintext state storage is unacceptable.
  • Optional phase-2 fields for on-chain KMS env updates:
    • env_compose_hash
    • env_transaction_hash
  • Mode rule:
    • env cannot be combined with encrypted_env/env_keys in the same resource.
  • Manual encrypted fields:
    • encrypted_env (sensitive, pass-through hex blob)
    • env_keys (allowed env keys)
  • Force-new fields: name, region, listed, storage_fs.

phala_attestation (data source)

  • Read-only attestation fetch by cvm_id.
  • Returns:
    • is_online, is_public, error, compose_file
    • tcb_info_json, app_certificates_json
    • raw_json (full response)

phala_cvm_power

  • Backed by:
    • POST /cvms/{id}/start
    • POST /cvms/{id}/stop
    • GET /cvms/{id} (read/drift detection)
  • state accepts running or stopped.
  • Delete is no-op (removes Terraform state only; does not change CVM runtime).

phala_ssh_key

  • Backed by:
    • POST /user/ssh-keys
    • GET /user/ssh-keys
    • DELETE /user/ssh-keys/{id}
  • name and public_key are immutable (replace on change), similar to DO-style patterns.

Project Status

Roadmap

  • On-chain KMS create/update flows (BASE/ETHEREUM).
  • Add richer filtering for data sources (images, sizes, regions).

Maintainers

Release process and gates: RELEASE.md

Release Quick Path

make ci
make package-release VERSION=0.2.0

Then push a release tag such as v0.2.0 or v0.2.0-beta.1 to trigger the GitHub release workflow.

OpenAPI-generated Client

This provider includes an OpenAPI-generated Go client in internal/phalaapi, sourced from:

  • https://cloud-api.phala.network/openapi.json

Regenerate it with:

go generate ./internal/phalaapi

Notes:

  • The upstream OpenAPI is currently 3.1.0, and codegen compatibility is improved by a normalization step in openapi/normalize-openapi.jq.
  • Some SDK endpoints used by this provider (/instance-types, /user/ssh-keys) are not currently present in the public OpenAPI schema; those remain on fallback HTTP path handling for now.

About

Terraform for Phala Cloud

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors