teriac server Helm chart

Helm chart

Run teriac server on Kubernetes with the teriac-server chart: install, routing with Gateway API or Ingress, sign-in, cloud credentials, values and how the chart runs it.

The teriac-server chart runs teriac server on Kubernetes 1.27+. It's published at oci://ghcr.io/marcdomain/charts/teriac-server (package).

Install

kubectl create namespace teriac
kubectl -n teriac create secret generic teriac-github \
  --from-file=private-key=app.pem --from-literal=webhook-secret="$WEBHOOK_SECRET"

helm install teriac oci://ghcr.io/marcdomain/charts/teriac-server --version 0.2.1 -n teriac -f values.yaml
# values.yaml
github:
  appId: "123456"
  existingSecret: teriac-github
config:
  repoAllowlist: [github.com/acme/*]
  requireApplyProject: true           # one project per apply: teriac apply -p <project>
httpRoute:
  enabled: true
  parentRefs: [{ name: public-gateway, namespace: gateway-system }]
  hostnames: [teriac.acme.dev]
dashboard:
  oidc:
    enabled: true
    issuer: https://acme.okta.com/oauth2/default
    clientId: 0oa1b2c3d4
    existingSecret: teriac-okta          # key: client-secret
    adminGroups: [platform-admins]

Then:

  1. Point the GitHub App's webhook at https://teriac.acme.dev/events/github.
  2. Register https://teriac.acme.dev/auth/oidc/callback as the redirect URI with Okta.
  3. Check the install with helm test teriac -n teriac.

Upgrade with helm upgrade teriac oci://ghcr.io/marcdomain/charts/teriac-server --version <new> -n teriac -f values.yaml. A running apply finishes before the old pod stops.

Routing

Option Values Notes
Gateway API (recommended) httpRoute.* An HTTPRoute attached to your Gateway (Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, GKE, …)
Ingress ingress.* For clusters that still use an Ingress controller
None service.type Port-forward, or your own mesh routing

To expose only the webhooks publicly, narrow the rule, and reach the dashboard through an internal gateway or kubectl port-forward:

httpRoute:
  enabled: true
  parentRefs: [{ name: public-gateway, namespace: gateway-system, sectionName: https }]
  hostnames: [teriac.acme.dev]
  rules:
    - matches: [{ path: { type: PathPrefix, value: /events/ } }]
kubectl -n teriac port-forward svc/teriac-teriac-server 8080:80

With an Ingress

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  hosts:
    - host: teriac.acme.dev
      paths: [{ path: /, pathType: Prefix }]
  tls:
    - { secretName: teriac-tls, hosts: [teriac.acme.dev] }

config.publicUrl defaults to the first Ingress host. Set it yourself with Gateway API.

Dashboard sign-in

Method Values Roles
Okta / any OIDC provider dashboard.oidc.* adminGroups → admin, allowedGroups → viewer, everyone else is refused
Username and password dashboard.users or usersExistingSecret role: admin or viewer per user
API token dashboard.apiToken.existingSecret Admin. For automation and /api

Viewers can see everything. Admins can also release locks and start drift checks.

Make password hashes with node packages/server/src/hash-password.ts. Sessions are signed, HttpOnly cookies. The chart generates the signing key once and keeps it across upgrades.

Cloud credentials

Terraform runs inside the teriac pod, so give the pod the access your Terraform needs.

Workload identity (recommended): annotate the service account.

serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/teriac      # AWS IRSA
    # iam.gke.io/gcp-service-account: teriac@acme.iam.gserviceaccount.com  # GKE
    # azure.workload.identity/client-id: 00000000-0000-0000-0000-000000000000  # Azure
podLabels:
  # azure.workload.identity/use: "true"

Secrets as env vars:

extraEnvFrom:
  - secretRef: { name: teriac-cloud }        # AWS_ACCESS_KEY_ID, ARM_CLIENT_SECRET, …
  - secretRef: { name: teriac-chat-tokens }  # TERIAC_SLACK_TOKEN, TERIAC_TEAMS_WEBHOOK
extraEnv:
  - { name: AWS_REGION, value: eu-central-1 }

Common values

Value Default
github.appId / github.existingSecret Required. App ID, and a Secret with private-key and webhook-secret
github.apiUrl GitHub Enterprise Server API URL, e.g. https://ghe.acme.com/api/v3
config.repoAllowlist [] Repos teriac may act on, e.g. github.com/acme/*
config.requireApplyProject false Refuse a bare teriac apply; one project at a time
config.ackReaction eyes Reaction on a command teriac picked up: eyes, rocket, +1, heart, hooray, laugh, confused, -1 or none
config.progressReply false Off: results are posted once and never edited. On: a spinning "Running…" reply edited into the result (needs a public https URL)
config.commentMode new new: a new comment for each plan and apply. update: one comment at the top, edited in place
config.parallelism 4 Plans and applies running at once, shared fairly between PRs
config.prParallelism parallelism − 1 One PR's share
config.runTimeoutSeconds 3600 Time limit per Terraform command
config.defaultVersion latest Tool version when a project pins none
config.preinstall [] e.g. [terraform@1.9.8, tofu@1.8.5]
config.drift true Run drift schedules
config.publicUrl first Ingress host The URL GitHub and browsers use
config.logLevel info debug, info, warn or error
config.verifySignatures true Check the vendor's signature on downloaded tools; false for mirrors without .sig files.
repos {} Full repos.yaml: rules, approvers, channels, people, drift schedules
extraEnvFrom / extraEnv [] Cloud credentials, TERIAC_SLACK_TOKEN, TERIAC_TEAMS_WEBHOOK, GITHUB_TOKEN (for OpenTofu/Terragrunt downloads), …
serviceAccount.annotations {} Workload identity: IRSA, GKE, Azure
persistence.size 20Gi Checkouts, saved plans, binaries, provider cache, database
persistence.existingClaim Use a PVC you made
backup.enabled false Back up the database on backup.schedule to the backend you fill in: backup.backend.s3.bucket, .gcs.bucket or .azure.container. An empty volume is restored on start. See Backups
resources 250m / 512Mi, limit 2Gi Raise the memory limit for large plans
serviceMonitor.enabled false Prometheus scraping of /metrics
podDisruptionBudget.enabled false Stop node drains from interrupting an apply
networkPolicy.enabled false Allow inbound traffic only from networkPolicy.ingressFrom

Every value is listed under all values below, and with comments in values.yaml.

All values

Every value in the chart, with its default.

Image and names

Value Default
image.repository ghcr.io/marcdomain/teriac-server
image.tag the chart's appVersion Pin a version, or leave empty to match the chart
image.pullPolicy IfNotPresent
imagePullSecrets [] For a private mirror of the image
nameOverride / fullnameOverride Resource names are <release>-teriac-server unless overridden

GitHub

Value Default
github.appId Required. The GitHub App's ID
github.apiUrl GitHub Enterprise Server, e.g. https://ghe.acme.com/api/v3
github.existingSecret A Secret with keys private-key and webhook-secret (recommended)
github.privateKey / github.webhookSecret Or inline, and the chart creates the Secret. Don't commit these to git

Server settings (config)

Value Default Env var
config.repoAllowlist [] TERIAC_REPO_ALLOWLIST
config.requireApplyProject false TERIAC_REQUIRE_APPLY_PROJECT
config.ackReaction eyes TERIAC_ACK_REACTION
config.progressReply false TERIAC_PROGRESS_REPLY
config.commentMode new TERIAC_COMMENT_MODE
config.parallelism 4 TERIAC_PARALLELISM
config.prParallelism parallelism − 1 TERIAC_PR_PARALLELISM
config.runTimeoutSeconds 3600 TERIAC_RUN_TIMEOUT_SECONDS
config.defaultVersion latest TERIAC_DEFAULT_VERSION
config.preinstall [] TERIAC_PREINSTALL, e.g. [terraform@1.9.8, tofu@1.8.5, terragrunt@0.83.2]
config.drift true TERIAC_DRIFT
config.publicUrl first HTTPRoute or Ingress host TERIAC_PUBLIC_URL
config.logLevel info TERIAC_LOG_LEVEL
config.verifySignatures true TERIAC_VERIFY_SIGNATURES.

repos (default {}) is the whole repos.yaml, mounted at /config/repos.yaml.

Dashboard

Value Default
dashboard.oidc.enabled false Okta, Entra ID (Azure AD), Google, Keycloak, Auth0, …
dashboard.oidc.name Okta Sign-in button label
dashboard.oidc.issuer e.g. https://acme.okta.com/oauth2/default
dashboard.oidc.clientId
dashboard.oidc.existingSecret A Secret with key client-secret
dashboard.oidc.clientSecret Or inline, and the chart creates the Secret
dashboard.oidc.scopes [openid, profile, email, groups]
dashboard.oidc.groupsClaim groups
dashboard.oidc.adminGroups [] Groups that become admins
dashboard.oidc.allowedGroups [] If set, only these groups (and admins) may sign in
dashboard.users [] { username, password_hash, role, name }
dashboard.usersExistingSecret Or a Secret with key users.yaml holding that list
dashboard.apiToken.existingSecret A Secret with key api-token
dashboard.sessionHours 12
dashboard.sessionSecret.existingSecret A Secret with key session-secret (32+ characters). Left empty, the chart generates one and keeps it across upgrades

Environment

Value Default
extraEnv [] Extra env vars, e.g. AWS_REGION
extraEnvFrom [] Secrets or ConfigMaps as env vars: cloud credentials, chat tokens

Storage and networking

Value Default
persistence.enabled true Checkouts, saved plans, tool binaries, provider cache, database
persistence.size 20Gi
persistence.storageClass cluster default
persistence.accessModes [ReadWriteOnce]
persistence.existingClaim Use a PVC you made
backup.* Scheduled backups; every value is on the Backups page
service.type ClusterIP
service.port 80
httpRoute.enabled false Gateway API
httpRoute.annotations {}
httpRoute.parentRefs [] e.g. [{ name: public-gateway, namespace: gateway-system, sectionName: https }]
httpRoute.hostnames [] e.g. [teriac.acme.dev]
httpRoute.rules everything (PathPrefix /) Narrow to /events/ to expose only webhooks
ingress.enabled false
ingress.className
ingress.annotations {}
ingress.hosts teriac.example.com, path /
ingress.tls [] With TLS set, publicUrl defaults to https://
networkPolicy.enabled false
networkPolicy.ingressFrom [] Namespaces allowed in: your ingress controller, Prometheus
resources:
  requests: { cpu: 250m, memory: 512Mi }
  limits: { memory: 2Gi }
networkPolicy:
  enabled: true
  ingressFrom:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx

Pod

Value Default
serviceAccount.create true
serviceAccount.name <release>-teriac-server
serviceAccount.annotations {} Workload identity: IRSA, GKE, Azure
resources requests 250m / 512Mi, limit 2Gi memory Raise for large plans
podSecurityContext non-root uid/gid 10001, fsGroup 10001, RuntimeDefault seccomp
securityContext no privilege escalation, read-only root filesystem, all capabilities dropped
terminationGracePeriodSeconds 3600 Time for running plans and applies to finish
podAnnotations / podLabels {}
nodeSelector / tolerations / affinity Standard scheduling
podDisruptionBudget.enabled false Stop node drains from interrupting an apply
serviceMonitor.enabled false Prometheus Operator scraping of /metrics
serviceMonitor.interval 30s
serviceMonitor.labels {} Labels your Prometheus selects on

The security defaults, which suit restricted Pod Security Standards:

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 10001
  runAsGroup: 10001
  fsGroup: 10001
  fsGroupChangePolicy: OnRootMismatch
  seccompProfile:
    type: RuntimeDefault
securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: [ALL]

Rules in values

repos:
  channels:
    infra-alerts: { type: slack, channel: "#infra-drift", token_env: TERIAC_SLACK_TOKEN }
  repos:
    - id: github.com/acme/infra
      projects:
        - match: prod/**
          permissions:
            apply: [team:platform-admins]
            apply_destroy: [admin]       # also covers state rm / state mv / force-unlock
            force_unlock: []             # single commands can be set too; [] = nobody
          approvals: [{ from: team:platform-admins, count: 2 }]
      drift:
        - { name: prod-nightly, projects: ["prod/**"], schedule: "0 6 * * 1-5", timezone: Europe/Berlin, notify: [infra-alerts] }
extraEnvFrom:
  - secretRef: { name: teriac-chat-tokens }   # holds TERIAC_SLACK_TOKEN

Policy checks, the two-person rule, permissions for Terraform commands, and the safety settings risk, freezes, protected, secret_scan, break_glass, summary, keep_approval_on_rebase and stale_base go in the same repos value; see repos.yaml.

How the chart runs it

  • One replica, as a StatefulSet. Locks and history are kept in SQLite on the volume.
  • Locked down. It runs as non-root (uid 10001) with a read-only root filesystem, all Linux capabilities dropped, and the RuntimeDefault seccomp profile.
  • Applies finish before shutdown. terminationGracePeriodSeconds: 3600 lets a running apply finish on upgrades.
  • Config changes roll the pod. Checksums of the config, secrets and dashboard settings are pod annotations.