Declarative bootstrap for Kubernetes

A single static binary that takes a freshly created cluster from “API server answers” to “workloads can be deployed.” No kubectl, helm, or shell scripts required.

bootstrap.yaml
apiVersion: khook.io/v1
kind: Khook
metadata:
  name: bootstrap
defaults:
  timeout: 5m
steps:
  - name: cilium
    helm:
      chart: cilium
      repo: https://helm.cilium.io/
      version: 1.18.4
      namespace: kube-system
      atomic: true
  - name: all-ready
    needs: [cilium]
    wait:
      for: condition=Ready
      on: pods
      allNamespaces: true
zsh
$ khook apply -f bootstrap.yaml
✓ cilium (helm)  24.108s
✓ all-ready (wait)  9.412s

The problem

Between provisioning and GitOps

Terraform, eksctl, or CAPI creates the cluster; Argo CD or Flux manages it once installed. The steps in between usually live in a bootstrap script of kubectl apply, helm upgrade --install, sleep 30, and retry loops, often run from a null_resource. khook replaces the script with a declarative spec: a DAG of steps that it validates, plans, and converges.

Why khook

Built for day-zero bootstrap

The sequenced, wait-heavy steps between “cluster exists” and “GitOps controller is running.”

📦

One binary, no dependencies

The Kubernetes and Helm SDKs are embedded and khook never shells out. The runner needs only khook.

🕸️

A DAG, not a script

Steps declare needs: and run in parallel levels. Cycles fail validation before the cluster is touched.

🔁

Idempotent

Helm release history decides install vs upgrade, applies converge existing objects, deletes treat “not found” as success. Safe to run on every terraform apply.

🚨

Explicit failures

Per-step timeouts and retries, onError: fail | continue, a summary of what succeeded, failed, or was skipped, and separate exit codes for validation and execution errors.

🔍

Plan and diff

khook plan predicts install, upgrade, or no-op per step against the live cluster; --diff shows object changes via server-side dry-run. Plan never mutates.

🤝

Bootstrap, then hand off

khook installs the CNI, secrets tooling, and GitOps controller, then stops. It is not a GitOps engine.

The DSL

Seven step types

Each step has exactly one action key, which sets its type. Variables (${VAR}, ${VAR:-default}, sprig pipelines) and when: CEL conditions let one spec serve several environments.

VerbWhat it doesInstead of
helm: install or upgrade a chart helm repo add + helm upgrade --install
apply: apply manifests (inline, file, URL, kustomize), optionally waiting on them kubectl apply -f/-k (&& kubectl wait)
delete: remove resources by manifest, selector, or Helm release kubectl delete / helm uninstall
patch: modify a resource in place (strategic/merge/json) kubectl patch
wait: wait for a condition, a jsonpath value, or deletion kubectl wait, sleep loops
rollout: restart or wait for workload rollouts kubectl rollout restart/status
job: run a container to completion in-cluster one-off kubectl run, shell scripts

Variables & conditions

One spec, many environments

  • Env-first variables from --set, --var-file, or KHOOK_VAR_* env vars. Missing variables fail validation, all reported at once.
  • Secret redaction. KHOOK_SECRET_* values, and pipeline outputs derived from them, are masked in logs, plan, and diff.
  • Sprig pipelines on values, as in Helm. Hermetic functions only, so plan and apply see the same spec.
  • CEL conditions. when: is evaluated at load time, before cluster access. Skipped steps still satisfy their dependents’ needs.
variables.yaml
steps:
  - name: app-namespace
    apply:
      manifests:
        - inline: |
            apiVersion: v1
            kind: Namespace
            metadata:
              name: ${APP_NAME | lower | trunc 63}

  - name: argocd
    needs: [app-namespace]
    when: vars.get("ENABLE_ARGOCD", "false") == "true"
    helm:
      chart: argo-cd
      repo: https://argoproj.github.io/argo-helm
      version: ${ARGOCD_CHART_VERSION:-7.7.5}
      namespace: argocd
      createNamespace: true

vs Terraform

Isn’t this just Terraform’s kubernetes/helm providers?

  • Runs after the cluster exists. Providers are configured at plan time, but the cluster endpoint exists only after apply. khook takes the kubeconfig as an input.
  • No live schema needed at plan time. A Terraform plan with CRDs and resources that use them fails because the types don’t exist yet. khook applies in DAG order against the live server.
  • Waiting is a step. Readiness gates are wait: and rollout: steps, not time_sleep and local-exec.
  • No object inventory. The cluster is the source of truth, so handing off to GitOps leaves one owner per object.

Read the full comparison →

Quickstart

Try it on k3d

zsh
$ go install github.com/dvrkn/khook/cmd/khook@latest
$ k3d cluster create dev
$ khook apply -f bootstrap.yaml
✓ cilium (helm)  24.108s
✓ all-ready (wait)  9.412s

Re-running converges without changes.

Get started

Declare the day-zero state of a cluster; khook converges it.