<Home>
DocumentationSELF-HOSTED / DEVELOPMENT RELEASEView source ↗
GUIDE 08 / Build and deploy

Deploy with Terraform

Declare projects, networks, applications and secrets with the Hakopod Terraform provider, against the same API the CLI uses.

The Hakopod Terraform provider declares the same objects you create in the dashboard: projects and their environments, virtual networks, applications and secrets. It sends application TOML or equivalent JSON to the deployment API. The provider checks its resource settings, and the server validates the application and its permissions. Terraform records dependencies and state so you can review and apply changes across several resources.

Find the provider in the Terraform Registry and its source on GitHub. Version 0.1.0 is pre-1.0: resource schemas can still change between minor versions, so pin the version and read the release notes before upgrading.

Install the provider#

Declare the provider in your root module and pin it:

terraform {
  required_providers {
    hakopod = {
      source  = "hakopod/hakopod"
      version = "= 0.1.0"
    }
  }
}

provider "hakopod" {}

This selects exactly 0.1.0. A constraint such as ~> 0.1 also permits later 0.x releases. Commit the generated .terraform.lock.hcl file with your module.

Then download the provider:

terraform init

Configure credentials#

The provider reads the same two environment variables as the CLI:

Variable Value
HAKOPOD_API_URL Your API origin, such as https://deploy.example.com, without a trailing slash.
HAKOPOD_API_KEY A named, expiring API key scoped to the projects Terraform manages.
export HAKOPOD_API_URL="https://deploy.example.com"
export HAKOPOD_API_KEY="..."

You can also set the provider attributes url and api_key from values known before planning. Environment variables keep credentials out of your configuration files. Setting the provider's api_key attribute does not itself store that key as resource state; a variable, saved plan or resource used to supply it may still contain credentials. Literal hakopod_secret.value values and application env_files contents are stored in Terraform state. Protect state and plan files; marking an attribute sensitive hides display output, not its stored value.

A worked example#

This module creates a project, a virtual network with one segment, a generated secret, and two applications on the network. An application's name and services come from its configuration, not from a Terraform attribute: config takes the same application TOML the CLI deploys, and spec takes the equivalent JSON. Supply your own API and web images, pinned by digest. Both must listen on port 8080 and run as UID/GID 1000; change those settings to match your images. The API must read its session-signing key from SESSION_KEY, and the web server must use API_URL for server-side requests. A browser cannot reach this private hostname.

The web application reads the API's private address from the API application's computed service_hostnames map, which also gives Terraform the ordering between them without a depends_on.

variable "api_image" {
  type        = string
  description = "API image, including its @sha256 digest."
}

variable "web_image" {
  type        = string
  description = "Web server image, including its @sha256 digest."
}

resource "hakopod_project" "shop" {
  name         = "shop"
  environments = ["production"]
}

resource "hakopod_virtual_network" "internal" {
  project     = hakopod_project.shop.name
  environment = hakopod_project.shop.environments[0]
  spec = jsonencode({
    schema_version = 1
    name           = "internal"
    segments = {
      backend = { applications = ["web", "api"] }
    }
  })
}

# Secrets belong to one application and are referenced by name from its
# configuration as { ref = "session-key" }.
resource "hakopod_secret" "session_key" {
  project     = hakopod_project.shop.name
  environment = hakopod_project.shop.environments[0]
  application = "api"
  name        = "session-key"
  generate    = true
}

# The network name is passed into the TOML template, so the reference orders
# this application after the network.
resource "hakopod_application" "api" {
  project     = hakopod_project.shop.name
  environment = hakopod_project.shop.environments[0]
  config = templatefile("${path.module}/api/hakopod.toml.tftpl", {
    network        = hakopod_virtual_network.internal.name
    image          = var.api_image
    secret_name    = hakopod_secret.session_key.name
    secret_version = hakopod_secret.session_key.value_version
  })
}

resource "hakopod_application" "web" {
  project     = hakopod_project.shop.name
  environment = hakopod_project.shop.environments[0]
  spec = jsonencode({
    schema_version = 1
    name           = "web"
    networks = {
      backend = {
        internal        = true
        virtual_network = hakopod_virtual_network.internal.name
        segment         = "backend"
      }
    }
    services = {
      web = {
        image        = var.web_image
        port         = 8080
        run_as_user  = 1000
        run_as_group = 1000
        public       = true
        networks     = ["backend"]
        env = {
          API_URL = "http://${hakopod_application.api.service_hostnames["api"]}:8080"
        }
      }
    }
  })
}

Save the API template as api/hakopod.toml.tftpl next to your module:

schema_version = 1
name = "api"

[networks.backend]
internal = true
virtual_network = "${network}"
segment = "backend"

[services.api]
image = "${image}"
port = 8080
run_as_user = 1000
run_as_group = 1000
networks = ["backend"]

[services.api.network_access]
from = []
from_applications = ["web/web"]

[services.api.secrets]
SESSION_KEY = { ref = "${secret_name}" }

[services.api.env]
SECRET_VERSION = "${secret_version}"

The secret references order the API deployment after secret creation. Including value_version in the template changes the application configuration when Terraform changes the secret, so it redeploys and reads the new value. The secret value itself stays on the server and is injected into the API container. Your API must implement session signing with this key; setting the environment variable alone does not add that behavior.

Set api_image and web_image in a terraform.tfvars file to your published image references, each ending in its real @sha256:… digest. Configure any other settings your applications need in the same definitions.

Run terraform plan to review the change and terraform apply to deploy it. Creating or updating a hakopod_application deploys it as one revision, exactly as a CLI deployment does, and, by default, the provider waits for the rollout with a 20-minute timeout. A completed apply does not verify database migrations or application behavior; check those separately.

The provider repository carries longer examples: a static site, a six-application stack of web, api, worker, scheduler, PostgreSQL and Redis on a virtual network, a five-service mesh generated from a single map, one module per environment, and a directory tree of hakopod.toml files consumed as configuration.

Resources#

Resource What it manages
hakopod_project A project and its environments. Environments can be added; removing one from the list is not supported.
hakopod_application An application and all of its services, from application TOML via config or JSON via spec. Services are not separate resources. Exposes the computed service_hostnames map of private DNS names.
hakopod_virtual_network A virtual network and the segment grants on it.
hakopod_secret One application's secret, either a literal value or one the server generates with generate = true. A generated value never enters Terraform state. Include value_version in the application configuration to trigger a redeployment when Terraform changes the secret. Literal secret values are stored in state.

An application is one resource because it is one deployable unit. Splitting services into separate resources would let Terraform apply half of a release; keeping them together means each apply is a single reviewable revision.

Names are retired permanently#

A destroyed application's or project's name can never be reused. Hakopod retires the name when the object is destroyed, so running terraform destroy followed by terraform apply cannot recreate the same name. A replacement plan that tries to reuse it also fails.

Before destroying or replacing a resource:

  • Read every plan for # forces replacement on a hakopod_project or hakopod_application. Choose a new name if replacement is necessary.
  • Do not use throwaway workspaces that create and destroy the same names. Give ephemeral environments generated names, or keep long-lived environments on a project you do not destroy.
  • For a rename within one state, use a Terraform moved block or terraform state mv. When transferring between state files, back up both states and coordinate state removal and import so only one state manages the application. Do not destroy it to move it.

Secrets and virtual networks are not affected; only project and application names are retired.

CLI or Terraform?#

Use the CLI for individual deployments, logs and rollbacks. Use Terraform when you want one plan for projects, networks, secrets and applications with dependencies between them.

Choose one deployment owner for each application. If Terraform owns an application, have CI update the image digest in Terraform's input and apply the module. If CI deploys the application directly, Terraform can still manage its project, network and secrets, while CI owns its complete application definition.

Do not omit image from a container service to make room for CI: the container service definition still requires an image. The provider's config and spec attributes are whole strings, so Terraform cannot ignore just an image nested inside them. Ignoring the entire attribute also hides other configuration changes. If both tools deploy the same application, a later Terraform apply can restore an older image or configuration.

Continue with Deploy from CI with the API, Deployments and rollback or API keys.