Skip to content

Latest commit

 

History

History
448 lines (329 loc) · 14.6 KB

getting-started.md

File metadata and controls

448 lines (329 loc) · 14.6 KB

Getting Started

This guide deploys Terrapod onto a Kubernetes cluster with the Helm chart, then creates your first workspace and runs your first plan and apply against it.

Terrapod runs only on Kubernetes — the runner uses the Jobs API to schedule plan/apply executions. You don't need a managed cloud cluster; a single-node k3s VM works. If you instead want to run Terrapod from source for development, see Local Development.

OpenTofu is the recommended execution backend. Terrapod supports both OpenTofu (tofu) and Terraform (terraform) as execution backends. OpenTofu is recommended because it is open-source under the MPL-2.0 license. All CLI examples in this guide use tofu, but terraform commands work identically.


Prerequisites

Need Notes
A Kubernetes cluster (1.27+) Any conformant cluster. No cluster? Spin up k3s on a single VM (below).
Helm 3.x brew install helm
PostgreSQL 14+ and Redis 7+ External — the chart does not bundle them. Provide a connection URL for each (a managed service, or run them on the cluster/VM). See Deployment → Database Setup.
Storage Defaults to a PVC-backed filesystem (no extra setup). For S3/Azure/GCS see Deployment → Storage Backend Setup.
tofu (recommended) or terraform Infrastructure CLI — opentofu.org or terraform.io.

Don't have a cluster? Use k3s

k3s is a fully conformant single-binary Kubernetes that runs on one VM (~512 MB RAM) and ships with an ingress controller (Traefik) and local-path storage:

curl -sfL https://get.k3s.io | sh -          # single-node cluster
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Run PostgreSQL and Redis as managed services or on a separate VM so your data survives cluster recreation.


Deploy Terrapod

Terrapod is published to GitHub Container Registry as an OCI Helm chart at oci://ghcr.io/mattrobinsonsre/terrapod.

Pick a hostname and install:

export TERRAPOD_HOST=terrapod.example.com

helm install terrapod oci://ghcr.io/mattrobinsonsre/terrapod \
  --namespace terrapod --create-namespace \
  --set ingress.enabled=true \
  --set ingress.hostname="$TERRAPOD_HOST" \
  --set ingress.className=traefik \
  --set postgresql.url="postgresql+asyncpg://terrapod:PASSWORD@PGHOST:5432/terrapod" \
  --set redis.url="redis://REDISHOST:6379" \
  --set bootstrap.adminEmail="admin@example.com" \
  --set bootstrap.adminPassword="change-me-now"

What the defaults give you: filesystem storage on a PVC, local password auth enabled, a pre-install migrations job, and a post-install bootstrap job that creates the admin user from bootstrap.adminEmail / bootstrap.adminPassword. Set ingress.className to your cluster's ingress controller (traefik on k3s, often nginx on managed clusters). Pin a version with --version X.Y.Z.

For production, put the admin password in a Kubernetes Secret (bootstrap.existingSecret) rather than on the command line, terminate TLS at the ingress (e.g. cert-manager), and set api.config.external_url so outbound links (VCS status, notifications) are correct. The full value reference — storage backends, external DB, SSO, scaling, TLS, runners — is in Deployment, and Production Checklist covers hardening.

Watch it come up:

kubectl -n terrapod get pods -w

Access

Point your hostname's DNS at the ingress controller's external IP (on k3s, the VM's IP — Traefik listens on ports 80/443). For a quick local test you can add a hosts entry instead:

sudo sh -c 'echo "<INGRESS_IP> terrapod.example.com" >> /etc/hosts'

Then open https://terrapod.example.com and log in with the bootstrap admin email + password.

TLS: the ingress expects a certificate by default (ingress.tls=true). For a quick HTTP-only evaluation, add --set ingress.tls=false; for real TLS see Deployment → Ingress.

No DNS available at all? Port-forward the web service for a quick look: kubectl -n terrapod port-forward svc/terrapod-web 8080:3000, then open http://localhost:8080.

Get an API token

In the web UI, go to Settings → API Tokens, create a token, and copy the value (shown once):

export TERRAPOD_TOKEN="<your-token-value>"

Or let the CLI mint one for you via the browser login flow:

tofu login terrapod.example.com

Creating Your First Workspace

Via the API

curl -s -X POST https://terrapod.example.com/api/v2/organizations/default/workspaces \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "workspaces",
      "attributes": {
        "name": "demo",
        "auto-apply": true
      }
    }
  }' | jq .

Note the workspace ID from the response (e.g., ws-abc123...).

Via the Web UI

  1. Navigate to Workspaces
  2. Click New Workspace
  3. Enter a name (e.g., demo)
  4. Optionally enable auto-apply
  5. Click Create

Workspaces


Configuring the CLI

Create a configuration that uses Terrapod as its backend:

# main.tf
terraform {
  cloud {
    hostname     = "terrapod.example.com"
    organization = "default"

    workspaces {
      name = "demo"
    }
  }
}

# A simple example resource
resource "null_resource" "hello" {
  triggers = {
    timestamp = timestamp()
  }

  provisioner "local-exec" {
    command = "echo 'Hello from Terrapod!'"
  }
}

Note: The terraform {} block syntax is used by both OpenTofu and Terraform. No changes are needed when switching between them.

Do not set project = "..." in the cloud block. Terrapod is single-organization and has no project concept — workspaces live directly under the organization. Setting project causes tofu init to fail with a 422 Projects are not supported error from the API. Omit the argument.

If you used TFC projects to scope RBAC (e.g. "team X has write on project Y"), Terrapod's label-based RBAC covers the same use case and more flexibly: roles match workspaces by label allow/deny rules (e.g. team: payments + env: prod → write), so a workspace can belong to any combination of dimensions instead of a single project. The same labels also drive filtering in the web UI — workspace lists, registry modules, and other resources can be filtered by any label key/value, so labels double as the navigation hierarchy projects provided in TFC. See docs/rbac.md for examples.

If you'd rather select a workspace at run time (typical when a single repo backs several environments) replace name with tags. Tags are matched against the workspace's labels — see the RBAC docs for details.

workspaces {
  tags = ["network"]                 # any workspace with the label key "network"
  # or, for an exact key:value match:
  tags = ["repo:aws-infra"]          # workspaces whose `repo` label is `aws-infra`
}

OpenTofu only accepts the set-of-strings tags form, so use the key:value string above for an exact match. The Terraform 1.10+ map form (tags = { repo = "aws-infra" }) is rejected by tofu at config validation with set of string required.

Then pick a specific workspace per invocation:

TF_WORKSPACE=network-staging tofu plan
# or
tofu workspace select network-staging

If you have not already run tofu login:

tofu login terrapod.example.com

Then initialize:

tofu init

You should see output confirming that OpenTofu is using Terrapod as its backend:

Initializing Terraform Cloud...
...
Terraform Cloud has been successfully initialized!

Running Your First Plan

Local Execution Mode

In local execution mode, tofu runs on your machine and pushes state to Terrapod:

tofu plan

The plan output appears in your terminal as usual. State locking is handled by Terrapod (lock/unlock API calls).

tofu apply

State is uploaded to Terrapod after a successful apply. You can view state versions in the web UI under the workspace's State tab.

Agent Execution Mode

For agent execution, the runner listener creates a K8s Job that runs tofu (or terraform) on the server via an agent pool.

Agent workspaces support two workflows depending on whether VCS is connected:

VCS-connected workspaces — VCS is the source of truth. CLI can only plan; applies must come through VCS.

Non-VCS workspaces (CLI-driven) — the CLI is the source of truth. tofu plan and tofu apply both work, with the apply phase running on the server after plan confirmation.

Source VCS-Connected Non-VCS (CLI-driven)
tofu plan (CLI) Plan-only on server Plan-only on server
tofu apply (CLI) Blocked (use VCS) Plan + confirm + apply on server
VCS push to tracked branch Full plan + apply N/A
VCS pull request / merge request Speculative plan-only N/A
  1. Set the workspace execution mode to agent:
curl -X PATCH https://terrapod.example.com/api/v2/workspaces/ws-{id} \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "workspaces",
      "attributes": {
        "execution-mode": "agent"
      }
    }
  }'
  1. Create a configuration version and upload your code:
# Create configuration version
CV_RESPONSE=$(curl -s -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/configuration-versions \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data": {"type": "configuration-versions", "attributes": {"auto-queue-runs": true}}}')

# Extract upload URL
UPLOAD_URL=$(echo "$CV_RESPONSE" | jq -r '.data.attributes."upload-url"')

# Create tarball and upload
tar -czf config.tar.gz -C /path/to/terraform/dir .
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @config.tar.gz
  1. A run is automatically queued (plan-only). Monitor it:
curl -s https://terrapod.example.com/api/v2/workspaces/ws-{id}/runs \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" | jq '.data[0].attributes.status'

Setting Up Workspace Variables

Variables can be set per-workspace via the API or web UI.

Workspace Variables

Terraform Variables

curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "vars",
      "attributes": {
        "key": "instance_type",
        "value": "t3.micro",
        "category": "terraform",
        "sensitive": false,
        "description": "EC2 instance type"
      }
    }
  }'

Environment Variables

curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "vars",
      "attributes": {
        "key": "AWS_REGION",
        "value": "eu-west-1",
        "category": "env",
        "sensitive": false
      }
    }
  }'

Sensitive Variables

Set "sensitive": true for secrets. The value is protected by database encryption-at-rest and never returned in API responses:

curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "vars",
      "attributes": {
        "key": "AWS_SECRET_ACCESS_KEY",
        "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "category": "env",
        "sensitive": true
      }
    }
  }'

Note: Sensitive variables are protected by database encryption-at-rest and never returned in API responses.

For managing variables across multiple workspaces, see variable sets (admin-only) in the web UI under Admin > Variable Sets.

Variable Sets


Setting Up the Private Registry

Module Registry

Publishing a Module

  1. Create the module:
curl -X POST https://terrapod.example.com/api/terrapod/v1/registry-modules \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "registry-modules",
      "attributes": {
        "name": "vpc",
        "provider": "aws"
      }
    }
  }'
  1. Create a version:
curl -X POST https://terrapod.example.com/api/terrapod/v1/registry-modules/private/default/vpc/aws/versions \
  -H "Authorization: Bearer $TERRAPOD_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "registry-module-versions",
      "attributes": {
        "version": "1.0.0"
      }
    }
  }'
  1. Upload the module tarball using the presigned URL from the response.

  2. Use the module in Terraform:

module "vpc" {
  source  = "terrapod.example.com/default/vpc/aws"
  version = "1.0.0"
}

For full registry documentation, see registry.md.


Connecting to VCS

To automatically trigger runs when you push to a Git repository:

  1. Create a VCS connection (GitHub or GitLab)
  2. Link a workspace to a repository

See vcs-integration.md for detailed setup instructions.


Uninstall

helm uninstall terrapod --namespace terrapod

PersistentVolumeClaims (filesystem storage, ephemeral runner scratch) are not removed by helm uninstall — delete them explicitly if you want to reclaim the storage, and drop the namespace with kubectl delete namespace terrapod.

Building and testing Terrapod from source — Tilt, live-reload, make test — is covered in Local Development. You don't need any of that to run Terrapod.


Next Steps