Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome!

Welcome to the Tazuna documentation.

Tazuna is a CLI tool for managing the bootstrap lifecycle of multi-cluster Kubernetes environments. You declare in tazuna.yaml which manifests to apply to which clusters and in what order, and operate that definition consistently through commands such as apply / destroy / state.

About this documentation

This documentation is organized for everyone from first-time Tazuna users, to those running it continuously as part of CI, to those modifying Tazuna itself, in the following flow.

  • Getting Started — Getting Tazuna up and running on your machine for the first time.
  • Concepts — The problem Tazuna aims to solve, and its design philosophy and architecture. Covers “why it is the way it is.”
  • Guides — Task-oriented, hands-on procedures such as writing tazuna.yaml. Covers “what to do, in what order, with which commands.”
  • Operations — Guidance for ongoing use, such as operating destroy, drift monitoring, and CI pipelines.
  • Reference — Specifications of input files, the CLI, and internal data structures. A spec-style reference for fields, types, defaults, and examples.
  • Contributing — Guidance for those making changes to Tazuna, covering the development environment, testing, documentation, and releases.

Where to start

  • If you have never touched Tazuna before, we recommend working through Getting Started in order.
  • If you want to grasp the design background and terminology first, start from Concepts.
  • If you just want to look up the specification of a particular command or field, consult Reference like a dictionary.

Getting Started

This section walks you through everything needed to get Tazuna running on your local machine for the first time. Once you have completed it, see Guides - Writing Your First tazuna.yaml for how to actually write a tazuna.yaml.

Goals:

  • The tazuna binary is installed on your machine
  • tazuna version runs successfully
  • You understand which clusters and external tools are required

1. Installation

tazuna is a single binary. Install it via one of the following methods.

Use a Release Binary

Download the archive for your OS / arch from GitHub Releases, and place tazuna somewhere on your PATH.

# Example (after extracting the macOS arm64 / Linux amd64 archive)
mv tazuna /usr/local/bin/
tazuna version

This is the recommended method when you want to pin a specific version in CI or guarantee reproducibility.

Use go install

If you have a Go 1.x toolchain, you can build directly from source.

go install github.com/pepabo/tazuna@latest

You can pin a version with @v0.x.y instead of @latest. The binary is placed in $(go env GOBIN) (or $(go env GOPATH)/bin if unset).

Build Directly From Source

If you want to build it yourself from a cloned repository, do the following.

make build      # ./tazuna is generated
make install    # Install into /usr/local/bin (uses sudo)

2. Verify the Installation

After installation, verify that the following command works without any configuration.

tazuna version
tazuna --help

The output of tazuna version looks like the following (actual values vary by environment).

tazuna v0.1.0 (commit abc1234, built 2026-05-25T03:21:00Z, darwin/arm64)

For local builds, version / commit / built will be dev / none / unknown respectively (see tazuna version).

3. Prepare the Prerequisites

For tazuna to operate against a cluster, a few prerequisites are needed. Not all of them are required. You only need to prepare the ones marked “Required” in the table below first; 1Password / git can be set up optionally depending on the features you use.

DependencyRequired?Description
Kubernetes clusterYes (when using apply / destroy / state ...)Setting up the control plane is outside Tazuna’s scope. For local experiments, use KinD / minikube; for remote, EKS / GKE / AKS / kubeadm and so on are all fine.
kubeconfig current-contextYes (same as above)Verify with kubectl config current-context that it points to the cluster you want to apply to. Make sure kubectl get nodes works.
kubectl- (recommended)Tazuna itself does not use it, but you will use it for kubeconfig manipulation and to verify applied results.
kustomize / helmfile / helm binaries- (not needed)Tazuna embeds them as Go libraries, so installing external binaries is not required.
1Password CLI (op)- (only when using type: genesissecret or helmfile.vars’s from: op)Requires the op command to be on PATH and authenticated with a service account.
git- (optional)Used to fill in _metadata.gitCommitHash during tazuna state sync. If run outside a repository or if git is not installed, an empty string is recorded without raising an error.

Cluster selection and kubeconfig handling are also discussed in the guide’s Prerequisites section.

4. Next Steps

At this point tazuna is running on your machine.

Concepts

This section walks through what problems Tazuna is trying to solve, and the design choices it makes to address them.

It focuses on why Tazuna is shaped the way it is rather than how to drive it. For concrete command listings and CLI flags, see the Reference; for step-by-step procedures, see the Guides.

How to read this section

The recommended reading order is as follows.

  1. Design Philosophy and Intended Use Cases — start here to understand what Tazuna exists for and the environments it is designed for.
  2. Overall Architecture — an overview of the components inside the tazuna binary and how they cooperate.
  3. Glossary — definitions of the terms used throughout this section.

For more concrete specifications — the tazuna.yaml schema, each manifest backend, and the behavior of state, multi-cluster, and secret integration — see the Reference. If you encounter an unfamiliar term, consult the glossary first.

Design Philosophy and Intended Use Cases

Tazuna is “a CLI for managing the bootstrap lifecycle of multi-cluster Kubernetes.” This page lays out the design stance behind it: why such a tool is needed, what Tazuna takes on, and what it deliberately leaves to others.

The problem we are trying to solve

A Kubernetes cluster is not yet usable just because its API server is up. To get from there to a state you can call “our cluster,” you need to install:

  • Infrastructure-layer add-ons such as CNI, Ingress, and cert-manager
  • The deployment platform itself, such as ArgoCD or Flux
  • Various operators and CRDs

All of these layers must be installed into the cluster in the correct order, preserving their dependencies.

If you substitute manual work, sprawling runbooks, or simple shell scripts for this, you run into problems like:

  • Losing track of what has been applied (i.e. “there is no state”)
  • The cluster bootstrap procedure going stale

These problems pile up. Tazuna addresses this by bringing in a single declarative configuration file and a unified CLI, explicitly handling just the “bootstrap” phase.

The idea of running Kubernetes clusters immutably

One of the ideas behind Tazuna is to treat the Kubernetes cluster itself as an immutable resource. “Immutable” here means giving up on patching a running cluster in place over time, and instead keeping the cluster in a state where it can always be rebuilt by the same procedure.

When we say “immutable” here, we mean the cluster’s foundation layer. We are not calling it a mutation when an application Pod managed by a Deployment is replaced with a new image, or when HPA changes the replica count. What we have in mind is the infrastructure add-ons such as CNI, Ingress, cert-manager, and ArgoCD, along with the CRDs and operators they depend on — the foundation layer that makes the cluster “a cluster” in the first place. The application layer on top is free to mutate via GitOps.

Over a long operational lifetime, changes like the following slowly accumulate on a cluster:

  • Provisional manifests that someone kubectl applyed during an incident response
  • CRDs or operators swapped out by hand to investigate a version
  • Add-ons installed with a local helm install

As these pile up, the cluster that is actually running and the cluster declared in code drift apart (configuration drift). Once that happens, the cost of rebuilding the cluster shoots up, and you can no longer move when you want to “swap out the whole cluster” — for DR, adding a region, or a Kubernetes version upgrade.

Running clusters immutably means flipping that situation around:

  • Assume the cluster’s contents can all be regenerated from declared configuration
  • The post-bootstrap add-on layer should come out the same even if the cluster is thrown away and rebuilt from scratch
  • Treat anything installed by hand as “fine to disappear at any time,” or fold it back into the declaration

That is the operational stance immutable operation entails. For the application layer, GitOps tools like ArgoCD and Flux already guarantee this. By contrast, the “bootstrap layer” — which includes those GitOps tools themselves — has traditionally relied on runbooks and shell scripts and has been the place where immutability is most easily lost.

Tazuna positions itself as the piece that brings this bootstrap layer onto the same immutable footing.

  • By writing the ordering of CNI / Ingress / cert-manager / ArgoCD and so on into a single declarative configuration file, tazuna.yaml, the cluster’s initial state becomes uniquely regenerable from code.
  • tazuna apply is the operation that closes the gap between that declaration and the live cluster. “Day one of a new cluster” and “catching up an existing cluster” are both handled by the same command, so a freshly rebuilt cluster and a long-lived one both converge to the same declaration.
  • By holding “what is currently installed” in state and pinning down which cluster a declaration belongs to via context_matches, Tazuna structurally prevents “applying to the wrong cluster by mistake” and “not knowing whether something has been applied.”
  • Once bootstrap is done, application operations are handed off to ArgoCD / Flux. The baton of immutable operation passes to the application layer, and both the bootstrap layer and the continuous-delivery layer become reproducible from declarations.

Note that Tazuna does not force you to actually rebuild the cluster every time. The strict immutable style — rebuilding and swapping entire clusters — is within scope, but what we care about more is the property one step before that: keeping the cluster in a state where “even if the Kubernetes cluster breaks at any moment, it can be rebuilt automatically by a guaranteed procedure.”

With just this property in place, you can:

  • Keep using the same cluster for ordinary day-to-day operations
  • Only for DR or major version upgrades, stand up an entirely separate cluster and switch over to it
  • Spin up throwaway clusters for verification whenever you need to

— and choose between these options based on the situation at hand.

By concentrating that “guaranteed rebuild procedure” into the code of tazuna.yaml and the single command tazuna apply, Tazuna provides the foundation for continuously keeping immutable operation a viable choice.

What Tazuna does not take on

There are areas Tazuna deliberately stays out of.

  • Continuous delivery — Tazuna is not a replacement for ArgoCD or Flux. Its job ends at getting those tools into the cluster; from there, it hands off to ArgoCD/Flux.
  • Authoring the manifests themselves — kustomize overlays, helmfile compositions, and Helm chart values are written in each tool’s own idiomatic style. Tazuna only invokes those tools and pushes the result into the cluster.
  • Creating the control plane — the cluster itself, built via kubeadm / kops / a managed service, is assumed to already exist. Tazuna connects to that existing cluster via kubeconfig.
  • The secret management backend itself — Tazuna declares references to where secrets are stored, but does not store the secrets itself.
  • GitOps rollbacks and history managementstate represents “what is currently installed,” and historical version control is left to git.

Intended use cases

Concretely, Tazuna shines in roughly the following situations.

  • Day one of bringing up a new cluster — when you want to install everything from CNI to the deployment platform in the prescribed order with a single tazuna apply.
  • Automatically verifying that a Kubernetes cluster build procedure is sound.
  • Standing up multiple clusters with the same role — when you want to bring up similar clusters such as staging / production / dr from nearly the same tazuna.yaml.

Next, in Overall Architecture, we look at the components that make this happen.

Overall Architecture

This page gives an overview of the main components that make up Tazuna and how they cooperate during tazuna apply.

We only cover responsibility boundaries here. Concrete directory layouts and Go package split policies are intentionally out of scope.

Layer diagram

Roughly speaking, Tazuna has a three-layer structure:

+--------------------------------------------------+
|  CLI                                             |
|   apply / build / check / destroy / state ...    |
+--------------------------------------------------+
                         |
                         v
+--------------------------------------------------+
|  Runner                                          |
|   - load tazuna.yaml / expand includes           |
|   - verify context_matches / filter by tags      |
|   - dispatch each manifest to a Manager          |
+--------------------------------------------------+
                         |
           +-------------+-------------+
           v                           v
+---------------------+     +---------------------+
|  Manager            |     |  Test plugin        |
|                     |     |   wait-until /      |
|  kustomize /        |     |   exist-nonexist    |
|  helmfile /         |     +---------------------+
|  oras /             |
|  genesissecret /    |
|  parallel           |
+---------------------+
           |
           v
+---------------------+
|  Kubernetes cluster |
+---------------------+

Each manifest is reflected into the cluster via a single Manager. Pre/post-apply verification is factored out into the Test plugin, which can run per manifest or for the entire tazuna.yaml.

Main components

Internally, Tazuna works by combining the components below. We only describe each component’s responsibility — “what it takes as input and what it produces as output.”

CLI

The entry point for subcommands such as apply, build, check, destroy, state list, state diff, state sync, secret-to-genesissecret, and version. It only parses flags and wires up the Runner; it holds no logic of its own.

Runner

The orchestrator for Tazuna as a whole. It is responsible for the following:

  • Loading tazuna.yaml
  • Expanding includes
  • Converting paths relative to tazuna.yaml into runtime paths
  • Filtering by --tags
  • Dispatching each manifest to its corresponding Manager
  • Running Test plugins after apply

The Runner does not know about the individual manifest types. It only holds a map of “which Manager to use for which type” and delegates everything else to the Manager.

Manager

A component that implements the “actual way to apply” for each manifest type. Every Manager exposes the following three operations:

  • Apply — reflect that manifest into the cluster
  • Destroy — remove from the cluster what that manifest installed
  • Build — generate only what would be installed if applied, without touching the cluster

The Runner treats all manifest types uniformly through just these three operations. For the responsibility split of each individual backend, see the Reference.

Validator

Schema and path-consistency validation that runs immediately after loading tazuna.yaml. Both apply and check go through this first, rejecting malformed YAML or non-existent paths before touching the cluster.

Context guard

Reads context_matches from tazuna.yaml and verifies that the current kubeconfig context name matches those patterns (a list of regular expressions). If it does not match, apply / destroy stops here and never touches the cluster.

State store

The mechanism Tazuna uses to record “fingerprints of the resources it installed” inside the cluster. The storage is an in-cluster ConfigMap; state list / state diff / state sync all operate against it.

Secret provider

A component that abstracts the “source of secret values” referenced by GenesisSecret and helmfile’s vars.op. Currently, a 1Password implementation is bundled.

Test plugin

A verification mechanism that runs after a manifest is applied (or after the entire tazuna.yaml is applied). The following two kinds are available out of the box:

  • wait-until — wait until the specified resource exists, becomes Ready, or becomes Available
  • exist-nonexist — assert that the specified resource should be present or absent

Hint

An auxiliary component for declaratively validating the type and format of values that helmfile’s vars may take — hostname, URL, email, IP, CIDR, UUID, semver, datetime, and so on.

Prompt

A component that abstracts interactive I/O such as Yes/No confirmations for destructive operations. It exists so that the behavior can be swapped out in non-interactive mode or during tests.

Flow during tazuna apply

Here the steps are laid out from the angle of “which component does what.”

  1. CLI parses flags and wires up the Runner.
  2. Validator reads tazuna.yaml and verifies the schema and the existence of paths.
  3. If spec.context_matches is present, the Context guard verifies the kubeconfig.
  4. Runner expands includes and converts manifests[].path into runtime paths.
  5. It walks manifests in order and hands each one that is not filtered out by --tags to the corresponding Manager.
  6. Each Manager internally invokes kustomize, helmfile, oras pull, 1Password retrieval, and so on, and reflects the result into the cluster. The fingerprint is also written to the State store at this point.
  7. If there are per-manifest or whole-file Tests, the Test plugin runs.

tazuna state sync reuses steps 1–4 above, builds a State diff from each Manager’s Build result, and applies only the added or changed entries.

Glossary

This page collects definitions for terms that come up frequently in this section. For more detailed specifications, see the Reference.

Tazuna-specific terms

tazuna.yaml

The single input file given to Tazuna. It carries apiVersion: tazuna.pepabo.com/v1 and kind: Tazuna, and declares the “manifests to be applied” in spec.manifests[].

Manifest

A single entry in manifests[] inside tazuna.yaml. It has name / type / path, and is processed by the Manager corresponding to its type. Note that what this refers to is different from “manifest” in the Kubernetes sense (a YAML file).

Manifest type

A string that specifies how a manifest is processed. There are five types: kustomize, helmfile, genesissecret, parallel, and oras.

Manager

A component that handles processing for a given manifest type. It provides three operations — Apply (reflect into the cluster), Destroy (remove), and Build (generate without touching the cluster) — and the Runner uses just these three to treat all backends uniformly.

Runner

The central orchestrator of Tazuna. It is responsible for loading tazuna.yaml, expanding includes, filtering by --tags, invoking Managers, and launching Test plugins.

Test plugin

A mechanism for expressing verifications you want to run before or after applying a manifest. The built-in plugins are wait-until (wait until Ready/present) and exist-nonexist (assert presence/absence). They are written under spec.manifests[].tests or spec.tests.

State

The record Tazuna uses to track “the resources it installed itself.” It is stored in ConfigMaps under the in-cluster tazuna namespace (tazuna-state-<manifest-name>).

State key

The key string that identifies a single State entry. For namespaced resources it is {manifest}/{group}/{version}/{kind}/{namespace}/{name}; for cluster-scoped resources, {manifest}/{group}/{version}/{kind}/{name}.

ContentHash

The SHA-256 hash value carried by each State entry. It is computed over the resource YAML with metadata.resourceVersion, uid, creationTimestamp, generation, managedFields, selfLink, and status excluded. Whether this hash matches or not is the decision criterion for state diff.

Diff type

The classification tazuna state diff assigns to each resource. There are four types: added, modified, removed, and always-sync.

always-sync

A Diff type that skips diff calculation and syncs every time, such as for Secrets derived from GenesisSecret. Used for targets whose changes cannot be detected via ContentHash, or for which detection should not be done. Even when the value is updated on the 1Password side, the cluster-side hash does not change, so the design is to query the Provider every time without relying on ContentHash. See GenesisSecret Schema - State and always-sync and Drift Monitoring for usage examples and operational treatment.

GenesisSecret

A declaration for generating Kubernetes Secrets from secret values stored in 1Password. It is not a Kubernetes CRD but a YAML schema that Tazuna reads. It is referenced from tazuna.yaml as a manifest with type: genesissecret.

Provider (SecretProvider)

The interface that abstracts the source from which GenesisSecret pulls secret values. Currently, an implementation for 1Password (op) is bundled.

context_matches

spec.context_matches in tazuna.yaml. An array of regular expressions the current kubeconfig context name must match — a guard against applying to the wrong cluster.

context_match_mode

The evaluation mode for context_matches: or (the default) or and.

includes

A manifests[] entry that loads another tazuna.yaml file and expands its manifests[] inline. Nesting is not allowed.

Tag (manifest tag)

A string written under manifests[].tags. Used to narrow down what gets applied, e.g. tazuna apply --tags foo,bar. Multiple tags are evaluated as an OR.

Manifest path

manifests[].path. Written as a path relative to the directory containing tazuna.yaml itself. Tazuna converts it into a path relative to the current working directory at runtime.

tazuna.hint.yaml

A hint file that declares the type and format constraints on values that helmfile’s vars may take. It is read by pkg/hint/.

Kubernetes terms (supplementary)

Below are short definitions of standard Kubernetes terms that come up frequently within Tazuna.

kubeconfig

A YAML file that bundles cluster connection information (cluster / user / context). Tazuna reads current-context from it and operates against the corresponding cluster.

context (kubeconfig context)

A kubeconfig element that combines “which user connects to which cluster” under a single name. The context name is what context_matches checks with its regular expressions.

GVK (Group/Version/Kind)

The three-tuple that uniquely identifies a Kubernetes resource kind. Tazuna’s State key also includes the GVK.

namespaced / cluster-scoped

Whether a resource belongs to a namespace or not. This is reflected in the length of the State key (5 parts or 6 parts).

ConfigMap

A built-in resource for storing arbitrary key-value data in the cluster. Used as the storage location for Tazuna’s State.

External tools

kustomize

An overlay/patch mechanism for Kubernetes manifests. Invoked from the type: kustomize Manager.

helmfile

A tool that bundles multiple Helm releases from a single YAML. Invoked from the type: helmfile Manager.

Helm

The package manager for Helm charts. Used internally by helmfile.

ORAS / OCI artifact

A standard for storing non-container artifacts — such as Kubernetes manifests — in an OCI registry. type: oras pulls them, then delegates processing to the helmfile or kustomize specified in delegate.

1Password

The secret storage that Tazuna references from GenesisSecret and helmfile.vars.op. Retrieval is done via the op command.

Guides

This section collects task-oriented procedures for actually working with Tazuna. While Concepts covers why things are the way they are, this section focuses on what to do, in what order, and with which commands.

Each guide is written to be readable independently, but if you have not yet touched Tazuna, working through them in order is the smoothest path. For detailed command behavior and a flag listing, see the Reference.

Introductory

The group of guides to work through first when introducing Tazuna to a new setup.

  1. Writing Your First tazuna.yaml — walks you all the way from the first tazuna.yaml to a tazuna apply that installs one kustomize-defined add-on into a single Kubernetes cluster.

Subsequent topics (ordering multiple manifests, narrowing with --tags, inspecting State, GenesisSecret, CI integration, and so on) will be added incrementally by gradually extending the tazuna.yaml built here. In the meantime, the specification for each topic can be looked up in the references below.

Writing Your First tazuna.yaml

This guide walks first-time adopters of Tazuna through writing their first tazuna.yaml and installing one add-on into one Kubernetes cluster.

We use a small Deployment written with kustomize as the example, applied to a cluster through Tazuna. In real-world usage you would have ingress-nginx, cert-manager, ArgoCD, and so on lined up here, but a single manifest is enough to understand the first one.

By the end of this guide you will have:

  • A tazuna.yaml and a kustomize directory pair in your own repository
  • Confirmed that tazuna check reports the tazuna.yaml as valid
  • Able to verify “what will be installed” with tazuna build without touching the cluster
  • That content reflected into the cluster via tazuna apply

Prerequisites

Prepare One Kubernetes Cluster

Tazuna does not create the cluster itself. Preparing the control plane is a prerequisite. For experimenting, any lightweight cluster you can run locally is sufficient.

  • If you want to keep things local: stand up a single-node cluster with KinD or minikube
  • If you want to try against a remote first: a managed cluster like EKS / GKE / AKS, or a verification cluster brought up with kubeadm

Any of these works as long as kubectl get nodes returns Ready.

Prepare the tazuna Binary

Either download the binary from the releases page and place it on PATH, or install with go install. See Getting Started for details.

You are ready once tazuna version works.

Check the kubeconfig current-context

Tazuna operates against the cluster currently pointed to by the kubeconfig context. Before doing anything, make absolutely sure your current-context points to the cluster you want to install into.

kubectl config current-context
kubectl get nodes

In environments where you switch between multiple clusters, mistaking this is the most typical entry point for incidents. context_matches is a mechanism to catch such mistakes, but we will not use it in the first walk-through (later guides cover it).

What We Will Build

In this guide, we will create the following directory layout.

my-cluster/
├── tazuna.yaml
└── kustomize/
    └── nginx/
        ├── kustomization.yaml
        └── deployment.yaml

tazuna.yaml declares “what to install,” and kustomize/nginx/ is the actual content. For this guide, knowing just these two layers is enough.

1. Create the kustomize Directory

First, prepare the side that Tazuna calls into — the “thing you want to install” written with kustomize. For practice, we use a minimal configuration with just one nginx Deployment.

my-cluster/kustomize/nginx/kustomization.yaml:

resources:
  - deployment.yaml

my-cluster/kustomize/nginx/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80

At this point you could install the same content into the cluster with kubectl apply -k my-cluster/kustomize/nginx, without using Tazuna at all. Understanding Tazuna becomes easier if you think of it as just an extra declarative-management layer placed on top.

2. Write tazuna.yaml

Next, write tazuna.yaml — “the only input file” for Tazuna.

my-cluster/tazuna.yaml:

apiVersion: tazuna.pepabo.com/v1
kind: Tazuna
spec:
  manifests:
    - name: nginx
      type: kustomize
      path: ./kustomize/nginx

At first, you only need to set these three fields.

  • name — the identifier for this Manifest. State entries and log lines use this name. Use alphanumerics, hyphens, and underscores.
  • type — which backend Tazuna uses for this Manifest. Here we specify kustomize. Other options include helmfile and oras (see Manifest type).
  • path — the directory holding the actual content. Written as a path relative to the directory where tazuna.yaml itself resides.

Note that path is resolved relative to the location of tazuna.yaml itself, not the cwd where the command was run. We chose this so that calling tazuna from anywhere in the repository produces the same result.

3. Verify Validity With check

Once you have finished writing the file, first validate tazuna.yaml without touching the cluster, using tazuna check.

cd my-cluster
tazuna check

If ok is printed, the following checks have all passed.

  • The file can be parsed as YAML
  • name is set, unique, and uses only allowed characters
  • The location referenced by path actually exists

Anything that fails here can all be caught before touching the cluster. It is also a command that fits naturally as the first step in CI.

4. Inspect the Output Without Touching the Cluster Using build

Next, use tazuna build to write “what would be installed by tazuna apply” to stdout, without touching the cluster at all.

tazuna build

Kubernetes manifests equivalent to kustomize build are emitted. In this example, the Deployment/nginx you just wrote should come out as-is.

build does not require a connection to the cluster. Use it when you want to review whether what is about to be applied is really what you intended, or to feed the rendering result into other tools.

5. Reflect Into the Cluster With apply

Once you reach this point, you are just one command away from the real action. Confirm once more that current-context points to the target cluster, then run tazuna apply.

kubectl config current-context   # It must point to the target cluster
tazuna apply

Tazuna performs the following steps in order.

  1. Loads and validates tazuna.yaml
  2. Walks manifests[] in declaration order
  3. Hands each Manifest to its corresponding Manager (here, the kustomize Manager)
  4. The kustomize Manager renders path and reflects the result into the cluster
  5. Records the reflected result inside the cluster as State

State is placed in a ConfigMap (tazuna-state-<manifest-name>) under the tazuna namespace inside the cluster. The tazuna namespace is created automatically if it does not exist, so you do not need to create it beforehand.

Verify That the Apply Took Effect

You can use kubectl as you normally would, and you can also verify from the Tazuna side.

kubectl get deployment -n default nginx
tazuna state list

tazuna state list shows the resources Tazuna currently records as “installed by me.” In this guide, you should see a single Deployment/nginx entry. The resources listed here are also what tazuna destroy can later safely remove.

Common Pitfalls

Here are some common things people stumble over when writing their first one.

  • Misunderstanding the path base — it is relative to the directory containing tazuna.yaml itself. Writing it as a path relative to your cd’d location is a common cause of CI / local behavior diverging.
  • Mistaking which cluster — make it a habit to check kubectl config current-context immediately before apply. context_matches lets you prevent this structurally, but start by making visual confirmation a habit.
  • Mistaking kustomize errors for Tazuna errors — when tazuna build fails, in most cases the underlying error is from kustomize, propagated as-is. Running kustomize build ./path directly to isolate is the fastest way to triage.
  • Mixing in things applied via hand kubectl apply — if you mix Tazuna-driven apply with manual operations against the same cluster, State and reality will drift apart. If you do mix, just remember that tazuna state diff can later show the differences.

Next Steps

The next guide will add more Manifests to the tazuna.yaml you built here, covering installing multiple add-ons into a cluster in order. From there we will gradually move into --tags-based filtering, splitting via includes, and preventing the-wrong-cluster accidents with context_matches.

Operations

This section collects guidance for continuously using Tazuna. For task-level procedures (“installing a new add-on,” “writing tazuna.yaml,” etc.), see Guides; for command and schema specifications, see the Reference.

This section focuses on operations that avoid incidents and operations that can detect drift.

Contents

  • Operating tazuna destroy — the procedure to follow when running destroy on a production cluster, the two-layer guard of TAZUNA_DESTROY_EXECUTABLE and context_matches, and accident-prone scenarios.
  • Drift Monitoring — operating tazuna state diff on a periodic schedule to visualize drift, output formats, and how to wire up notifications.
  • CI Pipeline — the typical setup that runs check / build on PRs and apply on main merges, where to place destroy, and choosing between apply and state sync.

Operating tazuna destroy

This page summarizes the procedure and the guards you should have in place when running tazuna destroy against a near-production cluster. For the command specification itself, see the tazuna destroy reference.

Why Make a Runbook

destroy is the operation that removes Tazuna-managed resources from the cluster, and it is the only systematic write command whose impact can span the entire cluster. Unlike running kubectl delete by hand, anything Tazuna considers its own management target disappears in one shot.

The two-layer guard (prompt + environment variable) is a structural defense, but on the operational side you also need to build in “confirm what will be deleted in advance” and “don’t mistake the context.”

What to Prepare in Advance

If there is any possibility you will run destroy against the cluster, we strongly recommend including spec.context_matches in that tazuna.yaml.

spec:
  context_matches:
    - ^prod-tokyo$
  context_match_mode: or
  manifests:
    # ...

With this in place, destroy will fail without touching the cluster at all if current-context is not prod-tokyo. It is the cheapest possible insurance against local context misconfiguration. See tazuna.yaml - context_matches for details.

Standard Flow

Steps 1 to 3 below are operational pre-checks, not internal processing of tazuna destroy. Tazuna itself does not call state list, but running it by hand before destroy is strongly recommended.

# 1. Check current-context (always do this immediately before destroy)
kubectl config current-context
kubectl get nodes

# 2. Understand what will disappear (recommended)
tazuna state list

# 3. Narrow the scope if needed (use exactly the same --tags as at execution time)
tazuna state list --tags experimental   # For confirmation

# 4. Real execution. Deletion only proceeds when both the prompt and the env var are satisfied
TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy --tags experimental

tazuna destroy itself runs in the following order.

  1. Load and validate tazuna.yaml.
  2. Evaluate context_matches. Abort immediately on mismatch.
  3. If --force is not set, prompt for Y/N.
  4. If TAZUNA_DESTROY_EXECUTABLE=true is not set, only log and exit.
  5. If all guards are satisfied, invoke each Manager’s Destroy in order.

Resources are not deleted unless both “Yes at the prompt” and “the env var TAZUNA_DESTROY_EXECUTABLE=true” are set.

Narrowing the Scope

If you want to remove only a specific group rather than the entire cluster, narrow it down with --tags.

TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy --tags experimental

--tags is evaluated as OR (see tazuna.yaml - tags). A useful pattern is to attach a dedicated tag like lifecycle:deprecated to things scheduled for removal, and then delete by that tag.

tazuna destroy --tags <empty> targets all Manifests. When you are doing an unnarrowed destroy, look at the full inventory in advance with tazuna state list to make sure no unexpected resources are present.

Accident-Prone Scenarios

Common patterns and their countermeasures.

ScenarioWhere it stops / why it becomes an incidentRecommended countermeasure
Current-context was production when you thought it was stagingIf context_matches is written, it stops at step 2. Without it, execution proceeds.Always include context_matches in production-tier tazuna.yaml. Avoid --force so that the prompt remains as a stop.
Wiring destroy into CI and triggering it accidentally on mainWithout the env var, it stops at step 4. If TAZUNA_DESTROY_EXECUTABLE=true is permanently set across CI, execution proceeds.Do not wire destroy into CI. If you absolutely need it, create a dedicated manual workflow and pass the env var only in that job temporarily.
Forgetting --tags and wiping everythingExecution proceeds when both the prompt and env var are satisfied.Treat “unnarrowed destroy” as something you do not do on near-production clusters. When you do, always run state list beforehand.
Things applied by hand with kubectl apply are not removed by destroyResources not in State are treated as outside Tazuna’s management and are not targets of destroy.If you decide state has diverged, visualize the divergence with tazuna state diff first, then decide on the response.

Looser Alternatives to destroy

When you do not need to “delete it completely right now,” remember that the following options exist.

  • tazuna state sync + TAZUNA_STATE_SYNC_DELETE=true — remove a Manifest from tazuna.yaml and then run state sync; it will be deleted under the removed classification (see tazuna state sync). This cannot be used when you want to reset without changing the source of truth in tazuna.yaml.
  • A destroy narrowed with --tags — see above.

Drift Monitoring

This page covers how to set up operations that periodically run tazuna state diff to visualize drift. For the command spec, see tazuna state diff; for the spec of State’s contents, see Internal Structure of State.

What We Call Drift

Drift here is the difference between the resource set that should be generated from tazuna.yaml (the Build result) and the resource set recorded in the in-cluster State. This corresponds exactly to the output of tazuna state diff.

Diff typeCases DetectedTypical example of drift
addedPresent in the Build result, absent from StateUpdated tazuna.yaml to add a Manifest, but it has not yet been applied
modifiedPresent in both, but with different contentHelm values change, kustomize overlay change, image tag update not yet reflected
removedPresent in State, absent from the Build resultRemoved a Manifest from tazuna.yaml, but the resource is still in the cluster
always-syncAlways treated as synchronizedSecrets originating from GenesisSecret. Not drift but “places to check every time.”

tazuna state diff does not look at the cluster’s actual state. Results of hand-running kubectl apply against the cluster (resources not in State) are not detected here. They are ignored as outside Tazuna’s management.

Output Format

tazuna state diff emits output like the following on a per-Manifest basis.

Manifest: ingress-nginx
  STATUS         RESOURCE                                                     HASH
  modified       ingress-nginx/apps/v1/Deployment/ingress-nginx/controller    abc123... -> def456...

Manifest: aws-credentials
  STATUS         RESOURCE                                                     HASH
  always-sync    aws-credentials//v1/Secret/default/aws-credentials           xyz789...

If there are no differences, only the following single line is emitted.

No changes detected.

The most straightforward way to judge “no drift” today is by this one line (filter on whether the output contains No changes detected.). tazuna state diff itself does not change its exit code based on whether differences exist. Note that having differences is not an error.

Shapes of Monitoring

In practice, “drift monitoring” is one of (or a combination of) the following.

a. Run a CI Job Periodically

Run tazuna state diff a few times a day with GitHub Actions’ schedule and save the output.

  • Pro: Reuses existing CI credentials. Easy to post to Slack or similar when differences appear.
  • Con: Cluster connection info must be brought into CI. Not suitable for short intervals.

Points to note:

  • The job only needs read access to the cluster (tazuna state diff does not modify the cluster).
  • Dump the output to a file with tazuna state diff -f path/to/tazuna.yaml > diff.txt and only send a notification when it does not contain No changes detected., which eliminates noise during quiet periods.

b. Run as an In-cluster Job

Build a container image including the tazuna binary and run it periodically as a CronJob.

  • Pro: Authentication is confined to a ServiceAccount. Easy to use short intervals.
  • Con: You need to build and distribute the image. The job side also needs access to the same tazuna.yaml repository as CI.

If you distribute the full tazuna.yaml set as an OCI artifact via type: oras, the job side does not need to clone the repository. Combined with tazuna apply --offline, the registry also becomes unnecessary.

Wiring Up Notifications

The notification side wants the following three pieces of information.

  • Which Manifest has differences
  • Which Diff type it is (removed deserves special attention)
  • Which resource it is (in State key form)

The State key format is fixed as manifest/group/version/kind/namespace/name (cluster-scoped resources omit namespace), so grep-based post-processing is sufficient. See Internal Structure of State - State key for details.

Minimal notification prototype:

if ! tazuna state diff -f tazuna.yaml | tee diff.txt | grep -q "No changes detected."; then
  curl -X POST "$SLACK_WEBHOOK_URL" --data "$(jq -Rs '{text: .}' < diff.txt)"
fi

jq -Rs '{text: .}' is the standard idiom for wrapping the contents of diff.txt as a raw string into the {"text": "..."} JSON format expected by Slack’s Incoming Webhook (-R reads raw input, -s slurps all lines into a single string).

Responding to Detection

When drift appears, your options are one of the following.

  • The change was intentional: catch State up to the cluster with tazuna apply (or tazuna state sync).
  • The change was unintentional:
    • modified: track who changed it when via git log / cluster audit log, then decide whether to roll back or absorb the change into tazuna.yaml.
    • added: most often this is a Manifest added to tazuna.yaml but not yet applied. Either apply, or revert tazuna.yaml, depending on intent.
    • removed: a Manifest was removed from Tazuna but the resource still exists in the cluster. Clean it up with tazuna destroy narrowed by --tags, or with tazuna state sync + TAZUNA_STATE_SYNC_DELETE=true.
  • GenesisSecret’s always-sync: this is not drift, so it is fine to exclude it from notifications.

CI Pipeline

This page covers the typical layout for incorporating Tazuna into a CI / CD pipeline in a repository that holds tazuna.yaml. Tazuna can be used both to apply from your local machine and to apply from CI. This page primarily focuses on the latter.

Typical Setup

By role, the pipeline divides into the following three stages.

StagePurposeCommands to runCluster access
VerifyGuarantee that tazuna.yaml is not brokentazuna check, tazuna buildNone
ApplyReflect the contents of main into the clustertazuna apply (or tazuna state sync)Required
RemoveDelete Tazuna-managed resourcestazuna destroyRequired

“Verify” is fine to run on every PR. “Apply” is usually triggered by pushes to main. We recommend not making “Remove” a permanent fixture in CI (see Operating tazuna destroy).

Verification Stage

If you put tazuna.yaml on CI, the minimum bar to clear is to run tazuna check. It runs without touching the cluster, so the PR cost is nearly zero.

# GitHub Actions example (essentials only)
- name: tazuna check
  run: tazuna check -f tazuna.yaml

- name: tazuna build (preview)
  run: tazuna build -f tazuna.yaml > rendered.yaml

Keeping build output as an artifact on PRs lets reviewers verify “what will ultimately be applied” from the rendered result. If you use type: oras, you can also consider running with --offline to leverage a pre-warmed cache (see tazuna build).

Apply Stage

The basic setup runs tazuna apply triggered by pushes to main.

on:
  push:
    branches: ["main"]

jobs:
  apply:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write   # if you connect to the cluster via OIDC
    steps:
      - uses: actions/checkout@v6
      - name: install tazuna
        run: |
          curl -L https://github.com/pepabo/tazuna/releases/download/v0.1.0/tazuna_Linux_x86_64.tar.gz \
            | tar xz -C /usr/local/bin tazuna
      - name: configure kubeconfig
        run: |
          # Set current-context using aws-iam-authenticator / gke-gcloud-auth-plugin
          # / kubeconfig secret etc., depending on cluster-side specifics
          aws eks update-kubeconfig --name prod-tokyo --region ap-northeast-1
      - name: tazuna apply
        run: tazuna apply -f tazuna.yaml

Points to note:

  • The current-context for tazuna apply is exactly the kubeconfig current-context. In CI, always explicitly include a step that sets the current-context.
  • Including spec.context_matches in tazuna.yaml makes the system fail fast if it would apply against a kubeconfig pointing to the wrong cluster. It is a useful safety net in CI as well.
  • Tazuna exits non-zero on failure, so no special error handling is needed on the CI side (see CLI - Exit Codes).

apply or state sync

There are two commands you can run in CI.

CommandWhen what runsDrift detection
tazuna applyRuns every Manifest declared in tazuna.yaml through its ManagerNo (always overwrites)
tazuna state syncCompares the Build result with State, and only reflects the differences (added / modified / always-sync)Yes

Rough guidance:

  • Bootstrap phase / small number of Manifests: tazuna apply is simple and predictable.
  • Manifest count grows and a single apply becomes heavy: switch to tazuna state sync to narrow down to differences only.
  • You want automatic removed deletion: state sync + TAZUNA_STATE_SYNC_DELETE=true. Balance against the risk of accidental deletion.

Whether to Use --atomic

When you pass tazuna state sync --atomic, it exits without updating State if any resource errors out. The apply itself still progresses partway, so CI cannot treat the run as binary “either everything went in or nothing did,” but State-level consistency is preserved. See tazuna state sync for details.

Removal Stage

We do not recommend running tazuna destroy from CI.

  • Setting the environment variable TAZUNA_DESTROY_EXECUTABLE=true permanently in CI leaves only the prompt as a guard against accidental firing (and since CI cannot respond to prompts, combined with --force it would delete unconditionally).
  • If you absolutely need to delete from CI, create a dedicated manually-triggered workflow and pass the env var only for that job.

See Operating tazuna destroy for details.

Phased Apply With Tags

tazuna apply --tags lets you narrow down which Manifests CI applies. For example, this fits patterns like “run the infrastructure layer and the application layer in separate CI passes” or “run experimental add-ons in a separate job.”

tazuna apply --tags infra        # Install the foundation first
tazuna apply --tags application  # Then the application side

Tag design happens on the tazuna.yaml side via manifests[].tags.

Wiring Up Monitoring and Notifications

The recommended setup is to run periodic tazuna state diff on a separate path from CI. See Drift Monitoring for details. If you only watch the success/failure of CI apply, you will miss cases where drift occurs without any tazuna.yaml update.

Reference

This section collects the specifications of the input files, CLI, and internal data structures that Tazuna accepts, in a form intended to be referenced as a contract.

For why it is shaped this way, see Concepts; for how to drive it, see Guides. The reference sticks to enumerating facts, focusing on fields, types, defaults, and examples.

Contents

Currently the reference includes the following pages. From here we will progressively expand into per-Manifest-type details, CLI, Test plugin, and the internal structure of State.

  • tazuna.yaml schema — the top-level structure of tazuna.yaml, the only input file to Tazuna, and the common-field specifications of spec.manifests[] / spec.context_matches / includes, and so on.
  • tazuna.hint.yaml schema — the schema of the hint file that declares constraints over the vars of a helmfile Manifest. Type, required, conditional-required, format-validation rules, and top-level rules such as oneof_required.
  • GenesisSecret schema — the YAML schema for generating Kubernetes Secrets from an external secret store (1Password). It is referenced from tazuna.yaml as a Manifest with type: genesissecret.
  • Test plugin — the common fields of TestPluginSpec (written under manifests[].tests and spec.tests) and the spec for the built-in WaitUntil / ExistNonExist plugins.
  • Internal Structure of State — State’s storage location (ConfigMaps in the tazuna namespace), the string format of State keys, ContentHash computation rules, and Diff type classification spec.
  • Per Manifest type — one page each for the 5 types kustomize / helmfile / oras / parallel / genesissecret, covering the meaning of path, type-specific fields, and apply / destroy / build behavior.
  • CLI — the spec of subcommands, global flags, and environment variables of the tazuna binary. Each subcommand has its own page summarizing flags and behavior.

Reading Conventions

  • Field names are shown in their YAML form (camelCase or snake_case).
  • All fields without a Required annotation are optional.
  • “Default” is the value Tazuna uses when the field is omitted. Zero values (empty string / empty slice / false / 0) are taken as-is unless otherwise noted.
  • Example YAML is written in its minimal form. Additional fields needed in real operation are described individually in each section.

tazuna.yaml Schema

This page describes the spec of tazuna.yaml, Tazuna’s only input file. We do not go deep into manifest-type-specific fields (kustomize / helmfile / genesissecret / parallel / oras) or Test plugin fields here. Those are covered on their own dedicated reference pages.

Root (Tazuna)

The root object of tazuna.yaml. Like a Kubernetes manifest, it has three fields: apiVersion / kind / spec.

FieldTypeRequiredDefaultDescription
apiVersionstring--When set, must be exactly tazuna.pepabo.com/v1. May be omitted.
kindstring--When set, must be exactly Tazuna. May be omitted.
specTazunaSpecYes-The body that defines Tazuna’s behavior.

Minimal example:

apiVersion: tazuna.pepabo.com/v1
kind: Tazuna
spec:
  manifests:
    - name: nginx
      type: kustomize
      path: ./kustomize/nginx

TazunaSpec

FieldTypeRequiredDefaultDescription
manifests[Manifest]Yes-The array of Manifests Tazuna processes in order. An empty array is not allowed.
context_matches[string]-[]An array of regular expressions that the current kubeconfig context name must match. If non-empty, it is evaluated before apply / destroy.
context_match_modestring-orEvaluation mode for context_matches. Either or (match any) or and (match all).
tests[TestPluginSpec]-[]An array of Test plugins to run after all Manifests have been applied.

context_matches

  • Each element must be a regular expression compilable with Go’s regexp package. Compilation failure is caught at the tazuna check stage.
  • If empty or unset, the context check is skipped.
  • When set, tazuna apply / tazuna destroy verify current-context before touching the cluster. Mismatch aborts processing.

context_match_mode

  • or (default): matching any one of context_matches is enough.
  • and: must match all of context_matches.
  • Any other value is a validation error.

Example:

spec:
  context_matches:
    - ^staging-
    - -tokyo$
  context_match_mode: and
  manifests: []

Manifest

Each element of spec.manifests[]. One Manifest corresponds to “one unit to install into the cluster” handled by one backend (kustomize / helmfile / others).

FieldTypeRequiredDefaultDescription
namestringYes-Manifest identifier. Must match ^[a-zA-Z0-9_-]+$ and be unique across all Manifests after includes expansion. _metadata is reserved and cannot be used.
descriptionstring-""Human-facing description. Has no effect on behavior.
typestringConditional (*)-One of kustomize / helmfile / genesissecret / parallel / oras.
pathstringConditional (*)-A path relative to the directory in which tazuna.yaml itself resides.
tags[string]-[]Tags used for filtering by tazuna apply --tags ... and so on. OR evaluation.
includes[IncludeFile]-[]An entry that loads another tazuna.yaml. When set, the other Manifest-specific fields are ignored. See Using includes for details.
kustomizeManifestKustomize-nullOptions referenced when type: kustomize.
helmfileManifestHelmfile-nullOptions referenced when type: helmfile.
genesisSecretobject-nullOptions referenced when type: genesissecret. Currently an empty object.
parallelManifestParallel-nullOptions referenced when type: parallel. Write nested Manifests under children[].
orasManifestORAS-nullOptions referenced when type: oras.
tests[TestPluginSpec]-[]An array of Test plugins to run after this Manifest is applied.

(*) When specifying includes, type / path are not required. Otherwise, type and path are required.

name

  • Required.
  • Allowed characters are ^[a-zA-Z0-9_-]+$.
  • _metadata is reserved for internal use and cannot be used as a Manifest name.
  • Must be unique across all Manifests after includes expansion. Duplicates fail at tazuna check.

tazuna check treats name validation as an error, but tazuna apply / build / destroy only emit a warning log during the transition period. When adopting Tazuna for the first time, it is safer to run tazuna check first.

path

  • Required when not using includes.
  • Interpreted as a path relative to the directory in which tazuna.yaml itself resides, not the cwd from which the command was run.
  • Existence is checked at tazuna check time.
  • What path should point to differs by type.
typeWhat path points to
kustomizeA directory containing kustomization.yaml
helmfileA directory containing helmfile.yaml
genesissecretThe GenesisSecret definition YAML file (not a directory)
parallelNot used in practice. The path from children[] is used. It cannot be empty due to validation.
orasNot used in practice. Cannot be empty due to validation, so write any directory.

See each Manifest-type page for details.

type

  • Required when not using includes.
  • See Manifest type for the value list.
  • Unsupported values raise a validation error.

tags

  • An array of strings. Tazuna itself does not interpret the contents.
  • When filtering with the --tags flag, only Manifests with at least one of the specified tags are targeted (OR evaluation).

IncludeFile

Each element of manifests[].includes[]. Loads another tazuna.yaml and expands its manifests[].

FieldTypeRequiredDefaultDescription
pathstringYes-Path to the tazuna.yaml to load. Written relative to the calling tazuna.yaml.

Using includes

spec:
  manifests:
    - name: infra
      includes:
        - path: ./infra/tazuna.yaml
        - path: ./addons/tazuna.yaml
  • Manifests that have includes have their own “Manifest-body” fields (type / path / tags, etc.) ignored.
  • includes is non-nestable. Even if the included tazuna.yaml has its own includes, those are not expanded.
  • names of Manifests defined in the include target must also be unique across all final Manifests.

Manifest-Type-Specific Fields

The fields corresponding to type (kustomize / helmfile / genesisSecret / parallel / oras) are each broken out to their own dedicated reference page.

Here we only indicate their existence and minimum role.

FieldRole
kustomizeOptions for type: kustomize. Has defaultNamespace.
helmfileOptions for type: helmfile. Has vars / includeCRDs / wait / kubeVersion and so on.
genesisSecretExtension point for type: genesissecret. An empty object in the current version.
parallelOptions for type: parallel. Has children[].
orasOptions for type: oras. Has reference / delegate.

tests Field

For the detailed spec of TestPluginSpec (the element type of spec.tests and manifests[].tests), see Test plugin. Here we only describe placement and timing.

  • Overall tests (spec.tests): executed after all Manifests have been applied.
  • Per-Manifest tests (manifests[].tests): executed immediately after that Manifest is applied.

Validation Summary

Below is the list of validations tazuna check performs against tazuna.yaml. No cluster access is involved, and anything that fails here is caught in advance.

  • If apiVersion / kind are set, they must equal the canonical values exactly.
  • For each element of spec.manifests[]:
    • When includes is absent: path and type must be set.
    • type must be a known value (kustomize / helmfile / genesissecret / parallel / oras).
    • The location pointed to by path must exist.
  • spec.manifests[].name must be present, use allowed characters, be unique, and not be a reserved word.
  • spec.context_matches must be compilable as regular expressions.
  • spec.context_match_mode must be one of or / and / unset.
  • For type: helmfile: each value in helmfile.vars must satisfy one of env / static / op (see the helmfile reference page).
  • For type: parallel: parallel.children[] must be non-empty and each child must be a valid Manifest.
  • For type: oras: oras.reference is required, and oras.delegate.type must be either helmfile or kustomize.
  • When specifying includes: each include.path is required and the file must exist.

tazuna.hint.yaml Schema

tazuna.hint.yaml is a hint file that declaratively constrains “what values can be taken” and “which are required” for the vars of a type: helmfile Manifest. Place it in the same directory as the helmfile Manifest; Tazuna consults it during vars resolution.

For the schema of tazuna.yaml itself, see tazuna.yaml schema.

Placement and Loading

Tazuna looks for tazuna.hint.yaml directly under the helmfile Manifest’s path directory.

  • The file name is fixed as tazuna.hint.yaml.
  • If absent, it is silently ignored (no error, for backward compatibility).
  • At most one per helmfile Manifest.

Not read for non-helmfile Manifest types.

Root (TazunaHint)

FieldTypeRequiredDefaultDescription
apiVersionstring--Indicates the schema version. The value is currently not validated.
kindstring--Indicates the resource kind. The value is currently not validated.
varsmap<string, HintVar>Yes-A collection of declarations keyed by varName.
rules[HintRule]-[]Top-level validation rules that span vars.

apiVersion / kind are not validated for their values, but by convention writing apiVersion: tazuna.pepabo.com/v1 / kind: TazunaHint makes it easier to align when validation is added later.

Minimal example:

apiVersion: tazuna.pepabo.com/v1
kind: TazunaHint
vars:
  clusterName:
    type: string
    required: true
  replicas:
    type: string
    default: "3"

HintVar

Each entry of vars (one var declaration).

FieldTypeRequiredDefaultDescription
typestringYes-The var’s type. One of string / slice / map.
requiredbool-falseWhether the user must provide a value.
defaultany-nullValue injected when not provided. Cannot be combined with required: true.
descriptionstring-""Human-facing description. Has no effect on behavior.
formatstring-""Value format-validation rule. Only available when type: string. See format for details.
required_with[string]-[]Conditional-required meaning “if any of the vars listed here is provided, then this var is also required.” Cannot be combined with required: true. References must exist in vars.
required_without[string]-[]Conditional-required meaning “if all vars listed here are unprovided, then this var is required.” Cannot be combined with required: true. References must exist in vars.

Behavior When a Value Is Not Provided

After helmfile-Manifest-side vars resolution, each var is processed in this order.

  1. If a value is already provided, use it as-is.
  2. If not provided and required: true, error.
  3. If a default is set, inject that value.
  4. Otherwise, inject the type-specific zero value (string"", slice[], map{}).

Conditional-required (required_with / required_without) evaluation is done against the set of values explicitly provided by the user, not the result after zero-value injection. This is to prevent vars with injected zero values from being mistakenly treated as “provided.”

format

format is string-format validation for type: string vars. If the value is an empty string (including zero-value injection), validation is skipped. Validation runs only when a non-empty string is present.

ValueValidation
hostnameRFC 952 / 1123 compliant hostname pattern (alphanumerics / - / .; each label starts and ends with alphanumerics)
urlParseable with net/url.ParseRequestURI, and the scheme must be non-empty
emailSimple user@domain.tld form (a simple regex)
ipAn IPv4 / IPv6 address parseable with net.ParseIP
cidrA CIDR notation parseable with net.ParseCIDR
uuidRFC 4122 form (hyphen-separated)
semverSemantic version. The v prefix is optional. Pre-release / build metadata are supported
datetimeA datetime string in time.RFC3339 format

Specifying any value not listed is a validation error.

HintRule

rules are top-level rules for declaring cross-var validations. They are evaluated after per-var processing is complete.

FieldTypeRequiredDefaultDescription
typestringYes-Rule kind. Currently only oneof_required.
vars[string]Yes-Array of var names targeted by the rule. At least 2 entries required. References must exist in vars.
messagestring-""Custom message to display on validation error.

oneof_required

A rule that errors unless at least one of the vars listed in vars is provided by the user.

rules:
  - type: oneof_required
    vars:
      - certManagerIssuerName
      - certManagerClusterIssuerName
    message: "either certManagerIssuerName or certManagerClusterIssuerName must be set"

The judgment criterion, like conditional-required, is the set of user-provided values before zero-value injection. Vars whose values came from default are not treated as “provided.”

Validation

When tazuna.hint.yaml is loaded, schema-level validation runs first.

  • vars[*].type must be one of string / slice / map.
  • vars[*].required: true and vars[*].default must not be combined.
  • When vars[*].format is set, type: string must also be set.
  • vars[*].format must be a known value (see format).
  • References of vars[*].required_with / vars[*].required_without must exist in vars.
  • vars[*].required: true and required_with / required_without must not be combined.
  • rules[*].type must be a known value (currently only oneof_required).
  • rules[*].vars length must be at least 2.
  • References of rules[*].vars must exist in vars.

After those pass, the following validations run against the result of the helmfile Manifest’s vars resolution (see Behavior When a Value Is Not Provided and format).

  • The type declared in the hint matches the type of the value passed in on the helmfile side (e.g. an error if a var declared type: slice receives a staticMap).
  • Required vars (required: true) have a value.
  • required_with / required_without conditions are satisfied.
  • For string vars with a format, the value (if non-empty) satisfies the format pattern.
  • rules are satisfied (e.g., at least one of the vars in oneof_required is provided).

GenesisSecret Schema

GenesisSecret is a declaration for retrieving secret values from an external secret store (currently 1Password) and generating them as Kubernetes Secrets.

GenesisSecret is not a Kubernetes CRD but a YAML schema that Tazuna reads. No GenesisSecret resource appears in the cluster; the applied result is a Secret.

From tazuna.yaml, it is referenced as a Manifest with type: genesissecret.

# tazuna.yaml
spec:
  manifests:
    - name: aws-credentials
      type: genesissecret
      path: ./genesissecrets/aws.yaml

The path for type: genesissecret points directly to a single YAML file (unlike other Manifest types that point to a directory).

Root (GenesisSecret)

FieldTypeRequiredDefaultDescription
apiVersionstring--Indicates the schema version. The value is currently not validated.
kindstring--Indicates the resource kind. The value is currently not validated.
specGenesisSecretSpecYes-The GenesisSecret body.

There is no struct field corresponding to apiVersion / kind; writing them is ignored without being read. By convention, writing apiVersion: tazuna.pepabo.com/v1 / kind: GenesisSecret makes it easier to align if validation is added later.

GenesisSecretSpec

FieldTypeRequiredDefaultDescription
providerstring-""Specifies the Provider to retrieve from. The current Manager does not reference the value. The Provider for the entire Tazuna run (the 1Password implementation) is determined at tazuna apply startup.
secrets[GenesisSecretGenerate]Yes-Retrieval targets. Multiple may be written.
outputs[GenesisSecretOutput]Yes-Output destinations. Multiple may be written.

GenesisSecretGenerate

Each element of secrets[]. Represents one “Provider-side item.”

FieldTypeRequiredDefaultDescription
uristringYes-URI pointing to the Provider item. See uri format for details.
itemsmap<string, GenesisSecretGenerateItem>Yes-Mapping from keys returned by the Provider to keys in the output Secret.
preferLabelbool-falseWhether to key the fields returned by the Provider by label name. When false, they are keyed by ID (which may be a random string). Set to true when you want to write human-assigned field names from 1Password as items keys.

uri Format

In the 1Password Provider, the url.Parse result is interpreted with the first path segment as the vault name and the second as the item name. The scheme and host are not used in the current version.

tazuna secret-to-genesissecret writes them out in this form when auto-generating:

op://<op-host>/<vault>/<item>

Example:

uri: op://example.1password.com/Platform/aws-credentials

The scheme and host pass parsing but are not referenced. Think of them as space reserved for distinguishing between Providers in the future, and you are safe.

GenesisSecretGenerateItem

The structure corresponding to the values of the items map (keys are the Provider-returned field’s ID or label).

FieldTypeRequiredDefaultDescription
mapTostringYes-The data key name in the output Kubernetes Secret. The value retrieved from the Provider is stored under this key in the Secret.

Example:

items:
  accessKeyID:
    mapTo: AWS_ACCESS_KEY_ID
  secretAccessKey:
    mapTo: AWS_SECRET_ACCESS_KEY

The items key accessKeyID corresponds to the Provider-side field name (the label name when preferLabel: true), and mapTo becomes the key name in the Kubernetes Secret as-is. If the items key does not exist on the Provider side, an error is raised.

GenesisSecretOutput

Each element of outputs[]. Represents one “output destination.”

FieldTypeRequiredDefaultDescription
kubernetesSecretGenesisSecretOutputKubernetesSecretConditional (*)nullSpecify when the output destination is a Kubernetes Secret.
stdoutobject-nullDefined in the schema but not supported in the current version. If kubernetesSecret is null, a runtime error is raised.

(*) In the current version, each element of outputs[] requires kubernetesSecret. While stdout exists structurally, if kubernetesSecret == nil, it fails with the error .spec.output currently supports only KubernetesSecret.

GenesisSecretOutputKubernetesSecret

FieldTypeRequiredDefaultDescription
namespacestringYes-Namespace of the output Secret.
namestringYes-Name of the output Secret.
labelsmap<string, string>-nullLabels added to the output Secret.
annotationsmap<string, string>-nullAnnotations added to the output Secret.
typestring-OpaqueThe corev1 SecretType. An empty string is treated as Opaque (Kubernetes’s default Opaque, not strictly kubernetes.io/opaque). You can specify kubernetes.io/tls and so on.
contextstring-""Exists structurally but not referenced by the current Manager implementation. The output cluster is Tazuna’s overall current-context.

Resolution Flow

During tazuna apply, a type: genesissecret Manifest is processed as follows.

  1. Read the YAML file pointed to by manifests[].path (relative to the directory of tazuna.yaml itself).
  2. Pass each element of spec.secrets[] to the Provider and retrieve the field set.
  3. Merge the results of all secrets[] into one map[string]string, renaming keys using items’s mapTo (if a key collides, the later one wins).
  4. For each kubernetesSecret of spec.outputs[], CreateOrUpdate a Kubernetes Secret with the specified namespace / name.
    • The merged map goes into StringData as-is.
    • labels / annotations / type are set as declared.

On tazuna destroy, the same Provider retrieval runs, and the Secrets identified by outputs[].kubernetesSecret’s namespace / name are deleted.

On tazuna build, only one Secret YAML (corresponding to outputs[0].kubernetesSecret) is written to stdout (even if multiple outputs are declared, only the first is targeted by build).

State and always-sync

Secrets generated from GenesisSecret are always classified as always-sync in tazuna state diff. They are not targets of ContentHash-based diffing; the Provider side is the source of truth and they are synchronized every time. See Diff type / always-sync for details.

Examples

Minimal example:

apiVersion: tazuna.pepabo.com/v1
kind: GenesisSecret
spec:
  secrets:
    - uri: op://example.1password.com/Platform/aws-credentials
      preferLabel: true
      items:
        accessKeyID:
          mapTo: AWS_ACCESS_KEY_ID
        secretAccessKey:
          mapTo: AWS_SECRET_ACCESS_KEY
  outputs:
    - kubernetesSecret:
        namespace: default
        name: aws-credentials

Example outputting type: kubernetes.io/tls:

apiVersion: tazuna.pepabo.com/v1
kind: GenesisSecret
spec:
  secrets:
    - uri: op://example.1password.com/Platform/tls-wildcard
      preferLabel: true
      items:
        certificate:
          mapTo: tls.crt
        privateKey:
          mapTo: tls.key
  outputs:
    - kubernetesSecret:
        namespace: ingress-nginx
        name: wildcard-tls
        type: kubernetes.io/tls
        labels:
          managed-by: tazuna

Test plugin

A Test plugin is a mechanism for verifying cluster state before or after a Manifest is applied. It is written in tazuna.yaml and integrated into the tazuna apply flow.

This page summarizes the YAML schema of Test plugins and the spec for the two built-ins (WaitUntil / ExistNonExist).

Placement and Timing

Test plugins can be written in two places in tazuna.yaml.

LocationExecution timing
manifests[].testsExecuted immediately after that Manifest is applyed
spec.testsExecuted after all Manifest applies have completed

Test plugins are not executed by commands that do not mutate the cluster, such as tazuna build / tazuna check / tazuna state diff. They are also not executed during tazuna destroy.

TestPluginSpec (Common Fields)

The elements of manifests[].tests and spec.tests share the same TestPluginSpec structure.

FieldTypeRequiredDefaultDescription
typestringYes-Plugin kind. WaitUntil or ExistNonExist (case-sensitive).
waitUntilWaitUntilArgsConditional (*)nullRequired when type: WaitUntil.
existNonExistExistNonExistArgsConditional (*)nullRequired when type: ExistNonExist.
minConsecutiveSuccessCountint-1If the test function succeeds this many times consecutively, the entire test plugin is considered successful. 0 is internally corrected to 1.
minConsecutiveFailureCountint-0If the test function fails this many times consecutively, the entire test plugin is aborted as failure. When 0, this check is not performed, and only timeoutSeconds is the stop condition.
timeoutSecondsint-Effectively infiniteOverall timeout in seconds. Failure if exceeded. With 0 (unset), effectively unlimited (internally set to about 280 days).
intervalSecondsint-0Wait seconds between test function re-runs. With 0, re-runs happen immediately.

(*) The type-to-waitUntil / existNonExist correspondence is verified at runtime. Specifying waitUntil while type: ExistNonExist, or any other mismatch, results in a runtime error.

Evaluation Loop Behavior

All Test plugins run in the following loop.

  1. Run the test function (plugin-specific logic) once.
  2. Append the result (success / failure) to the history.
  3. If the last minConsecutiveSuccessCount results are all successes, exit as success.
  4. If minConsecutiveFailureCount is non-zero and the last minConsecutiveFailureCount results are all failures, exit as failure.
  5. If timeoutSeconds has elapsed, exit as failure.
  6. Sleep for intervalSeconds seconds and go back to 1 (immediate if 0).

If you want to express “one success is enough,” “retry interval is 1 second,” “abort at 60 seconds max,” you get the following.

tests:
  - type: WaitUntil
    timeoutSeconds: 60
    intervalSeconds: 1
    waitUntil:
      # ...

WaitUntilArgs

A plugin that loops waiting until the specified resource enters “the desired state.” The condition is expressed as a CEL expression.

FieldTypeRequiredDescription
resource.apiVersionstringYesTarget resource apiVersion. Examples: apps/v1, cert-manager.io/v1.
resource.kindstringYesTarget resource kind. Examples: Deployment, Certificate.
namespacestringYesTarget resource namespace.
namestringYesTarget resource name.
conditionstringYesA CEL expression returning a boolean. Within the expression, the retrieved resource is referenced as object, an unstructured map. The expression’s result must be bool (type-checked at compile time).

Each iteration runs as a “Get the resource → evaluate the CEL expression” pair. Failures to retrieve the resource (including 404) are also treated as a “failure” for that loop iteration.

Common condition examples:

# Deployment is Ready as requested
condition: "object.status.readyReplicas == object.spec.replicas"

# Available conditions has become True (list-evaluation of conditions)
condition: >-
  object.status.conditions.exists(c,
    c.type == "Available" && c.status == "True")

For the CEL language spec itself, see the official CEL documentation. Tazuna only adds the object variable and the constraint “result must be bool.”

Example (WaitUntil)

tests:
  - type: WaitUntil
    timeoutSeconds: 120
    intervalSeconds: 2
    waitUntil:
      resource:
        apiVersion: apps/v1
        kind: Deployment
      namespace: ingress-nginx
      name: ingress-nginx-controller
      condition: "object.status.readyReplicas == object.spec.replicas"

ExistNonExistArgs

A plugin that asserts that the specified resource “should exist” or “should not exist.”

FieldTypeRequiredDescription
resource.apiVersionstringYesTarget resource apiVersion.
resource.kindstringYesTarget resource kind.
namespacestringYesTarget resource namespace.
namestringYesTarget resource name.
shouldExistboolYesWhen true, success if the resource exists. When false, success if it does not exist.

The decision is made on a single Get result. Existence is judged by whether Get returns NotFound, then matched against shouldExist. Errors other than NotFound (such as insufficient permissions) are treated as a “failure” for that iteration.

Example (ExistNonExist)

tests:
  # Assert that the expected resource was installed
  - type: ExistNonExist
    existNonExist:
      resource:
        apiVersion: apps/v1
        kind: Deployment
      namespace: tazuna-managed
      name: nginx
      shouldExist: true

  # Assert that a deprecated resource does not remain
  - type: ExistNonExist
    timeoutSeconds: 10
    intervalSeconds: 1
    existNonExist:
      resource:
        apiVersion: v1
        kind: Secret
      namespace: tazuna-managed
      name: legacy-token
      shouldExist: false

Internal Structure of State

State is the record Tazuna uses to track “resources I installed.” It is stored in ConfigMaps inside the cluster and is the starting point for tazuna state list / tazuna state diff / tazuna state sync.

This page covers the storage format of State and the spec of State key / ContentHash / Diff type that support it.

Storage Location

State is stored inside the cluster. Tazuna does not use local files or remote storage.

ElementValue
Namespacetazuna (auto-created if absent)
ConfigMap nametazuna-state-<manifest-name>
FormatOne entry per resource as a key/value pair in ConfigMap.data

One Manifest corresponds to one ConfigMap. Since each Manifest’s name after includes expansion is unique (see tazuna.yaml’s name), ConfigMap names do not collide.

State key

A State key is the identifier of a single entry inside a ConfigMap. As a struct it carries manifestName / group / version / kind / namespace / name; its string format comes in two variants.

Resource scopeFormatNumber of parts
namespaced{manifest}/{group}/{version}/{kind}/{namespace}/{name}6
cluster-scoped{manifest}/{group}/{version}/{kind}/{name}5

group is an empty segment for the core group (""). For example, a core/v1 ConfigMap (namespaced) is written like my-manifest//v1/ConfigMap/default/my-cm, with the gap between the second and third slashes empty.

Encoding to ConfigMap data Key

Kubernetes ConfigMap.data keys only allow [-._a-zA-Z0-9]+ and cannot contain /. To work around this, Tazuna replaces / in the State key string with __ when writing to the ConfigMap, and reverses the substitution on read.

state key:    nginx/apps/v1/Deployment/default/nginx
data key:     nginx__apps__v1__Deployment__default__nginx

Since the Kubernetes DNS-1123 names (manifest name / group / namespace / name) do not contain _, __ works safely as a separator.

State Entry (StateEntry)

Each entry is written into one value in the ConfigMap as the following JSON form.

{"contentHash":"<hex sha256>"}
FieldTypeDescription
contentHashstringThe SHA-256 hash of the resource’s contents. See ContentHash for details.

_metadata Key

ConfigMaps also contain one more reserved key, _metadata. This is not a State entry but metadata for the State as a whole.

{"gitCommitHash":"<sha>","lastSyncedAt":"<rfc3339>"}
FieldTypeDescription
gitCommitHashstringThe git commit hash recorded at sync time.
lastSyncedAtstringSync timestamp.

_metadata cannot be used for a Manifest name (it is treated as a reserved word in tazuna.yaml’s name). This is a guard against the collision.

ContentHash

ContentHash is a SHA-256 hex string computed from each resource’s YAML representation. Tazuna computes it after stripping server-side-assigned fields and status.

Excluded fields:

FieldReason for exclusion
metadata.resourceVersionUsed for cluster generation tracking; changes on every apply
metadata.uidAssigned by the cluster; changes on every apply
metadata.creationTimestampAssigned by the cluster; changes on every apply
metadata.generationAssigned by the cluster; changes on every apply
metadata.managedFieldsServer-side apply tracking information
metadata.selfLinkAssigned by the cluster
statusDynamic field written by controllers

Computation procedure:

  1. Deep-copy the object via JSON marshal / unmarshal.
  2. Remove the excluded fields above.
  3. JSON-marshal the rest.
  4. Take SHA-256 and convert to a hex string.

Including status would change the hash on every Pod restart, so we narrow it down to the granularity “is this the same state as expressed by tazuna.yaml.”

Diff type

tazuna state diff and tazuna state sync classify the comparison between the Build result and existing State by Diff type.

Diff typeMeaningstate sync behavior
addedPresent in the Build result, absent from StateApply
modifiedPresent in both, but with different ContentHashApply
removedPresent in State, absent from the Build resultSkipped by default. Deleted only when TAZUNA_STATE_SYNC_DELETE=true
always-syncSkip diff computation; always treat as a sync targetApply

state diff output is stably sorted in the order addedmodifiedremovedalways-sync, with State keys ascending within each Diff type.

always-sync Targets

In the current version, Secrets derived from type: genesissecret are classified as always-sync. These have the Provider side as the source of truth and are synchronized every time; they are not targets of ContentHash-based diffing. See GenesisSecret schema - State and always-sync for details.

Per-Manifest-Type Reference

tazuna.yaml’s manifests[].type can take 5 values. This section breaks each type into its own page, covering what path points to, type-specific fields, and apply / destroy / build behavior.

For the spec of common Manifest fields (name / path / type / tags / includes / tests), see tazuna.yaml schema - Manifest.

Contents

  • kustomize — apply resources rendered by kustomize
  • helmfile — apply the result of helmfile template
  • oras — pull an artifact from an OCI registry and delegate to helmfile / kustomize
  • parallel — process child Manifests in parallel
  • genesissecret — generate Kubernetes Secrets from a GenesisSecret YAML

Type-to-Specific-Field Correspondence

Each type has a corresponding options object inside manifests[]. Only the field corresponding to type is read; the others are ignored.

typeSpecific field nameField type
kustomizekustomizeManifestKustomize
helmfilehelmfileManifestHelmfile
orasorasManifestORAS
parallelparallelManifestParallel
genesissecretgenesisSecretEmpty object (no fields in the current version)

type: genesissecret is all lowercase, but the corresponding field name is genesisSecret (camelCase). YAML keys are uniformly camelCase, and only the value of type is a plain identifier (all lowercase).

Type-to-path Correspondence

path is interpreted as a path relative to the directory in which tazuna.yaml itself resides. What it should point to differs by type.

typeWhat path points to
kustomizeA directory containing kustomization.yaml
helmfileA directory containing helmfile.yaml
orasNot used in practice. Cannot be empty due to validation, so write any directory.
parallelNot used in practice. The path from children[] is used. It cannot be empty due to validation.
genesissecretGenesisSecret YAML file (unlike other types, a single file rather than a directory)

type: kustomize

A kustomize Manifest applies the Kubernetes manifests rendered by kustomize into the cluster. Internally, Tazuna calls kustomize (sigs.k8s.io/kustomize) and uses the result equivalent to kustomize build <path>.

path

Points to the directory containing kustomization.yaml. Write the path relative to the directory of tazuna.yaml itself. If the directory does not have a valid kustomization.yaml, tazuna build / apply fails.

Specific Fields

Written inside the manifests[].kustomize object.

FieldTypeRequiredDefaultDescription
defaultNamespacestring-""The namespace assigned to rendered resources whose metadata.namespace is unset. When empty, the namespace written on the resource (or the Kubernetes default default if absent) is used.

Behavior

OperationInternal processing
BuildRender the equivalent of kustomize build <path> and write the YAML result to stdout.
ApplyConvert the rendering result to a set of unstructured objects, supplement defaultNamespace, and CreateOrUpdate them one by one.
DestroyConvert the rendering result to a set of unstructured objects, supplement defaultNamespace, and delete them one by one.

kustomize build itself does not require a connection to the cluster. It is self-contained with local files only.

Examples

manifests:
  - name: ingress-nginx
    type: kustomize
    path: ./kustomize/ingress-nginx
    tags:
      - infra

  - name: app-overlay
    type: kustomize
    path: ./kustomize/app/overlays/staging
    kustomize:
      defaultNamespace: staging

type: helmfile

A helmfile Manifest is a Manifest type that renders the equivalent of helmfile template and then applies the result to the cluster for multiple Helm releases described by helmfile.

Internally, Tazuna calls app.Template of the helmfile/helmfile package, converts the output YAML to unstructured objects, and CreateOrUpdates them. Helm release history is not stored on the cluster side (helm rollback becomes unavailable). The stance is that for bootstrapping, declarative regeneration is preferred over rollback.

path

Points to the directory containing helmfile.yaml (or other files helmfile recognizes such as helmfile.yaml.gotmpl). Write the path relative to the directory of tazuna.yaml itself.

Specific Fields

Written inside the manifests[].helmfile object.

FieldTypeRequiredDefaultDescription
varsmap<string, HelmFileVar>-{}Variables passed to helmfile. See vars for details.
includeCRDsbool-falsePasses the equivalent of --include-crds to helmfile template.
defaultNamespacestring-""The namespace assigned to rendered resources whose metadata.namespace is unset.
extraValueFiles[string]-[]Additional --values files passed to helmfile template.
waitbool-falseWhen true, wait after Apply until the target resources become Ready. See wait behavior for details.
timeoutSecondsint-0Maximum wait seconds for wait. With 0, 300 seconds (5 minutes) is used internally.
kubeVersionstring-""Value passed as --kube-version to helmfile template.

vars

vars keys are helmfile-side variable names; values are HelmFileVar.

At tazuna.yaml load time, vars are resolved in the following order.

  1. Retrieve the value according to each var’s from (env / static / op).
  2. If a tazuna.hint.yaml is in the same directory, run its validation and default injection.

Sometimes a value is injected via tazuna.hint.yaml’s default even when vars does not specify it. Conversely, violating a tazuna.hint.yaml constraint causes an error here.

HelmFileVar

FieldTypeRequiredDescription
fromstringYesWhere the value is retrieved from. One of env / static / op.
envstringConditional (*)Required when from: env. The name of the environment variable to reference.
staticstringConditional (*)Used when from: static. A scalar value.
staticSlice[string]Conditional (*)Used when from: static. A slice value.
staticMapmap<string, string>Conditional (*)Used when from: static. A map value.
opOnePasswordVaultSelectorConditional (*)Required when from: op.

(*) Depending on the value of from, one of env / static / op is required. For from: static, exactly one of static / staticSlice / staticMap must be set.

OnePasswordVaultSelector

FieldTypeRequiredDescription
keystringYesWhether to reference the field by id or label. Either id or label.
vaultstringYes1Password vault name.
itemstringYes1Password item name.
fieldstringYesField to retrieve. The field ID when key is id, or the label when key is label.

wait Behavior

When wait: true, after Apply finishes, it waits for all target resources to become Ready. Polling happens at 2-second intervals; exceeding timeoutSeconds (300 by default) is an error.

Ready judgment per Kind:

KindReady condition
DeploymentImmediately Ready if spec.replicas == 0. Otherwise: status.readyReplicas == status.replicas AND status.availableReplicas == status.replicas AND status.replicas > 0
StatefulSetImmediately Ready if spec.replicas == 0. Otherwise: status.readyReplicas == status.replicas AND status.replicas > 0
DaemonSetstatus.numberReady == status.desiredNumberScheduled AND status.desiredNumberScheduled > 0
Podstatus.phase == "Running" AND Ready condition is True
OthersTreated as Ready as soon as retrievable (ConfigMap / Secret / Service, etc.)

When you want to express resource-specific conditions that wait cannot handle (such as a CRD’s status), using Test plugin’s WaitUntil (CEL expression) is more flexible.

Behavior

OperationInternal processing
BuildWrite the helmfile template output YAML to stdout.
ApplyConvert the helmfile template result to unstructured form, supplement defaultNamespace, and CreateOrUpdate in order. If wait is true, wait for Ready.
DestroyConvert the helmfile template result to unstructured form, supplement defaultNamespace, and delete in order. wait is not applied.

All of Apply / Destroy / Build resolve vars at the helmfile template stage. If resolution fails (environment variable unset, field missing in 1Password item, etc.), it fails without touching the cluster.

Examples

manifests:
  - name: cert-manager
    type: helmfile
    path: ./helmfile/cert-manager
    helmfile:
      includeCRDs: true
      wait: true
      timeoutSeconds: 120
      vars:
        clusterIssuerEmail:
          from: env
          env: CLUSTER_ISSUER_EMAIL
        dnsProviderApiToken:
          from: op
          op:
            key: label
            vault: Platform
            item: cert-manager
            field: cloudflare-api-token
        extraLabels:
          from: static
          staticMap:
            managed-by: tazuna
            tier: platform

type: oras

An oras Manifest is a Manifest type that pulls an artifact placed in an OCI registry and delegates its content to helmfile or kustomize to apply to the cluster.

The ORAS Manager handles the pull → extract → invoke delegate Manager chain. The artifact’s content itself is written in the same idiomatic style as helmfile / kustomize.

path

An ORAS Manifest’s manifests[].path is not used in practice. Since validation does not allow it to be empty, write some directory.

The path passed to the delegate Manager is the cache directory locally extracted after the pull (descended to a sub-path if target is specified).

Specific Fields

Written inside the manifests[].oras object.

FieldTypeRequiredDefaultDescription
referencestringYes-OCI artifact reference. Accepts both the tag form (ghcr.io/example/foo:v1.0.0) and the digest form (ghcr.io/example/foo@sha256:...).
targetstring-""Relative sub-path from the artifact root after extraction. Omitting it means the root. Values that escape the artifact root via .. and so on are rejected.
plainHTTPbool-falseWhen true, connect to the registry over HTTP (non-TLS).
insecureSkipVerifybool-falseWhen true, skip TLS certificate verification when connecting to the registry.
authORASAuth-nullOverrides the registry credentials. Defaults to using docker config.json. See Credential resolution for details.
delegateORASDelegateYes-Configuration of the delegate Manager invoked after the pull.

ORASAuth

FieldTypeRequiredDescription
usernamestring-Registry username.
passwordstring-Registry password.

If both fields are empty, this is not treated as an override (see Credential resolution).

ORASDelegate

FieldTypeRequiredDefaultDescription
typestringYes-Manifest type to delegate to. helmfile or kustomize.
helmfileManifestHelmfile-nullOptions passed to the delegate as-is when type: helmfile.
kustomizeManifestKustomize-nullOptions passed to the delegate as-is when type: kustomize.

Behavior

OperationInternal processing
BuildPull artifact → call the delegate’s Build.
ApplyPull artifact → call the delegate’s Apply.
DestroyPull artifact → call the delegate’s Destroy.

The delegate is invoked with a new Manifest assembled like this:

  • name / description / tags / tests are carried over from the original ORAS Manifest as-is.
  • type is delegate.type (helmfile / kustomize).
  • path is the local path after the pull (with sub-path appended if target is set).
  • Specific fields use delegate.helmfile / delegate.kustomize as-is.

Test plugin is also evaluated in the same Manifest context.

Pull and Cache

ORAS pulls are cached locally per digest. Subsequent pulls of an artifact with the same digest do not access the registry.

  • Cache directory:
    • If $XDG_CACHE_HOME is set: $XDG_CACHE_HOME/tazuna/oras
    • Otherwise: $HOME/.cache/tazuna/oras
  • Cache structure:
    • Artifact is extracted under blobs/<sanitized digest>/
    • Tag → digest mapping is recorded in refs/<sanitized reference>
  • Specifying --no-cache to apply / build / destroy bypasses the cache and always refetches from the registry.
  • Specifying --offline forbids registry access. If the cache misses, it is an error.
  • --no-cache and --offline cannot be specified together.
  • See the apply / build / destroy pages for CLI flags.

Constraints at Extraction Time

Tar extraction inside the artifact applies the upper bounds defined in ADR004.

LimitValue
Total size after extraction1 GiB
Number of tar entries10000

The following invalid entries are rejected.

  • Entries containing absolute paths
  • Entries that escape the extraction directory via .. (zip slip)
  • Symlinks / hardlinks that escape the extraction directory
  • Unsupported types (character device / block device / FIFO, etc.)

The artifact’s OCI manifest is required to have only one layer. Multi-layer artifacts are not accepted.

Credential Resolution

Credentials for the registry are resolved in the following priority.

  1. oras.auth override (at least one of username / password is non-empty)
  2. docker’s credential store ($DOCKER_CONFIG or ~/.docker/config.json)
  3. anonymous (no authentication)

Even if you write oras.auth, if both fields are empty it is not treated as an override and falls back to the docker side (to avoid unintended anonymization).

Within the same process, token caching is shared, so multiple pulls against the same registry do not pay the cost of re-acquiring tokens.

Examples

Tag-based + helmfile delegation:

manifests:
  - name: cert-manager
    type: oras
    path: ./oras/cert-manager   # not actually used but required
    oras:
      reference: ghcr.io/example/cert-manager-helmfile:v1.14.0
      delegate:
        type: helmfile
        helmfile:
          includeCRDs: true
          wait: true

Digest-based + kustomize delegation + sub-path:

manifests:
  - name: ingress-nginx
    type: oras
    path: ./oras/ingress-nginx
    oras:
      reference: registry.example.com/platform/ingress-bundle@sha256:abc123...
      target: kustomize/ingress-nginx
      auth:
        username: ci-bot
        password: ${REGISTRY_TOKEN}
      delegate:
        type: kustomize
        kustomize:
          defaultNamespace: ingress-nginx

type: parallel

A parallel Manifest is a container that processes multiple child Manifests in parallel. The child type is one of kustomize / helmfile / genesissecret / oras (nesting parallel is not intended).

path

A parallel Manifest’s own manifests[].path is not used in practice. Since validation does not allow it to be empty, write any directory.

The actual processed path is specified per children[].path.

Specific Fields

Written inside the manifests[].parallel object.

FieldTypeRequiredDefaultDescription
children[Manifest]Yes-Array of child Manifests to process in parallel. At least one entry required.

Each element of children[] has the same structure as Manifest. The names of Manifests inside children[] must also be unique within the same space as all Manifest names after include expansion (verified at tazuna check).

Behavior

OperationInternal processing
ApplyCall the corresponding Manager’s Apply for each element of children[] in goroutines in parallel. Errors are aggregated and returned.
DestroyCall the corresponding Manager’s Destroy for each element of children[] in parallel.
BuildCall the corresponding Manager’s Build for each element of children[] in parallel, and return a string joined by \n---\n while preserving declaration order. Empty outputs are skipped.

The processing order of children[] is not guaranteed. Use it only for groups safe to run in parallel. For ordering dependencies (waiting for A’s CRD before installing B, etc.), instead of parallel, line them up in declaration order under normal manifests[], or use the child Manifest’s Test plugin to express Ready waits.

Examples

Example of bundling two parallel-safe kustomize Manifests into one parallel:

manifests:
  - name: observability
    type: parallel
    path: ./parallel/observability   # not actually used but required
    parallel:
      children:
        - name: prometheus
          type: kustomize
          path: ./kustomize/prometheus
          tags:
            - observability
        - name: grafana
          type: kustomize
          path: ./kustomize/grafana
          tags:
            - observability

type: genesissecret

A genesissecret Manifest is a Manifest type that reads a separately-written GenesisSecret YAML and generates Kubernetes Secrets using values retrieved from an external secret store (currently 1Password).

All that this Manifest type carries on the tazuna.yaml side is “which GenesisSecret YAML to read”. For the spec of spec.secrets / spec.outputs and so on inside it, see GenesisSecret schema.

path

Unlike other Manifest types, path points directly to a single YAML file, not a directory. Write it relative to the directory of tazuna.yaml itself.

manifests:
  - name: aws-credentials
    type: genesissecret
    path: ./genesissecrets/aws.yaml   # ← points directly at the file

Specific Fields

Written inside the manifests[].genesisSecret object.

In the current version this is an empty object with no fields. The field name is reserved for future extension.

manifests:
  - name: aws-credentials
    type: genesissecret
    path: ./genesissecrets/aws.yaml
    # genesisSecret: {}  # empty for now, no need to write it

Behavior

OperationInternal processing
BuildRead the GenesisSecret YAML, retrieve values from the Provider, and write a single Secret YAML (corresponding to outputs[0].kubernetesSecret) to stdout.
ApplyRead the GenesisSecret YAML, retrieve values from the Provider, and CreateOrUpdate a Kubernetes Secret for each entry of outputs[].kubernetesSecret.
DestroyRead the GenesisSecret YAML (Provider retrieval also runs), and delete the Secret matching namespace / name of each entry in outputs[].kubernetesSecret.

Build differs from Apply in that it outputs only the first entry of outputs (even when multiple outputs are written, tazuna build’s output is for one entry). See GenesisSecret - Resolution flow for details.

Relationship to State

Secrets generated by type: genesissecret are always handled as always-sync in tazuna state diff. They are not targets of ContentHash-based diffing; the Provider side is the source of truth and they are synchronized every time. See Internal Structure of State - Diff type and GenesisSecret - State and always-sync for details.

CLI

This section covers the spec of every subcommand provided by the tazuna binary, one command per page.

The pages are designed to be read as a contract. For command choice and operational usage, see Guides; for what each command is solving in the first place, see Concepts.

Subcommand List

Global Flags

Persistent flags inherited by every subcommand.

FlagAliasTypeDefaultDescription
--file-path-fstringtazuna.yamlPath to tazuna.yaml.
--log-level-lstringinfoLog level. One of debug / info / warn / error.
--version---A flag set only on the root command. Prints version info and exits. Equivalent to tazuna version.

Common Behavior

kubeconfig

Subcommands that access the cluster load kubeconfig at startup and operate against the cluster pointed to by current-context. Tazuna does not provide its own KUBECONFIG environment variable or --kubeconfig equivalent flag; it follows the same resolution rules as kubectl.

Evaluating context_matches

When spec.context_matches is set in tazuna.yaml, the current-context name is matched against it immediately before touching the cluster.

  • Commands where evaluation runs: apply / destroy
  • Commands where evaluation does not run: build / check / state list / state diff / state sync / tags / version / secret-to-genesissecret

The evaluation mode follows spec.context_match_mode (or / and, default or). See tazuna.yaml schema - context_matches for details.

Validating tazuna.yaml

apply / build / destroy / check / tags all load and validate tazuna.yaml at the very start of execution. On validation failure, no cluster access happens. For the list of check items, see tazuna.yaml schema - Validation summary.

Exit Codes

Exit CodesMeaning
0Success
Non-zeroFailure. An error in the form error: ... is printed to stderr.

Non-zero exit can be treated as failure as-is by CI. There is currently no distinction in exit code per command.

Environment Variables

In addition to CLI flags, here is the list of environment variables Tazuna consults.

Environment VariablesValueAffected commandsEffect
TAZUNA_DESTROY_EXECUTABLEtruedestroyUnless this is set to true, destroy does not actually delete anything. Even if you say Yes at the prompt, nothing happens without this environment variable.
TAZUNA_STATE_SYNC_DELETEtruestate syncOnly when this is true, resources that remain in State but no longer exist in the cluster (removed) are deleted. The default is to skip deletion.
KUBECONFIGPathAll cluster-touching commandsFollows the same kubeconfig resolution rules as ordinary kubectl.

tazuna apply

Reflects the Manifests declared in tazuna.yaml into the cluster. The central command of Tazuna.

tazuna apply [-f tazuna.yaml] [--tags ...] [--no-cache | --offline]

Behavior

The execution order is as follows. Cluster access happens from step 5 onward.

  1. Load and validate tazuna.yaml.
  2. If spec.context_matches is set, match against the current-context. Abort immediately on mismatch.
  3. Filter by --tags.
  4. Walk manifests[] in declaration order.
  5. Hand each Manifest to its corresponding Manager and apply it to the cluster.
  6. Execute each Manifest’s tests.
  7. After all Manifests are applied, execute spec.tests (overall Tests).

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--tags-t[]string[]Limits the processing target to Manifests with at least one of the specified tags (OR evaluation).
--no-cache-boolfalseFor type: oras Manifests, always refetch from the registry without using the cache.
--offline-boolfalseFor type: oras Manifests, forbid access to the registry. If the cache misses, it is an error.

--no-cache and --offline cannot be specified together.

Examples

tazuna apply -f tazuna.yaml
tazuna apply -f tazuna.yaml --tags web,batch
tazuna apply -f tazuna.yaml --log-level debug

tazuna build

Renders the Manifests declared in tazuna.yaml and writes the result to stdout. Does not modify the cluster. Useful for previewing before apply or as pipe input to other tools.

tazuna build [-f tazuna.yaml] [--tags ...] [--no-cache | --offline]

Behavior

  1. Load and validate tazuna.yaml.
  2. Filter by --tags.
  3. Passes each Manifest to its Manager’s Build, concatenates the results, and writes them to stdout.

Does not evaluate context_matches. Whether the cluster is reached depends on the Manager’s Build implementation, but the built-in Managers basically do not require kubeconfig (ORAS’s registry pull does perform network access separately).

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--tags-t[]string[]Limits the processing target to Manifests with at least one of the specified tags (OR evaluation).
--no-cache-boolfalseFor type: oras Manifests, always refetch from the registry without using the cache.
--offline-boolfalseFor type: oras Manifests, forbid access to the registry. If the cache misses, it is an error.

--no-cache and --offline cannot be specified together.

Examples

tazuna build -f tazuna.yaml
tazuna build -f tazuna.yaml --tags web
tazuna build -f tazuna.yaml | kubectl diff -f -

tazuna check

Verifies the validity of tazuna.yaml without touching the cluster. Suitable as the first thing to run in CI.

tazuna check [-f tazuna.yaml] [--fix]

Behavior

  1. Load tazuna.yaml.
  2. Run validation against the file and all expanded manifests[].
  3. If no problems, write ok to stdout and exit with status 0.
  4. With --fix, auto-number Manifests whose name is unset, write back tazuna.yaml, and write fixed: <path> to stdout.

See tazuna.yaml schema - Validation summary for the list of check items. No cluster access is performed.

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--fix-boolfalseAuto-numbers Manifests whose name is unset and writes back tazuna.yaml.

--fix overwrites the file. We recommend running it under version control.

Examples

tazuna check
tazuna check -f path/to/tazuna.yaml
tazuna check --fix

tazuna destroy

Deletes Tazuna-managed resources from the cluster. To prevent accidents, a two-stage guard is in place.

TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy [-f tazuna.yaml] \
  [--tags ...] [--force] [--no-cache | --offline]

Behavior

  1. Load and validate tazuna.yaml.

  2. If spec.context_matches is set, match against the current-context. Abort immediately on mismatch.

  3. If --force is not set, display the following prompt and require Y/N confirmation.

    !!! All resources managed by Tazuna will be deleted !!!
    Are you sure you want to delete them?
    
  4. Unless the environment variable TAZUNA_DESTROY_EXECUTABLE is true, only log output happens and the command exits without touching the cluster.

  5. Only when both guards pass: applies the --tags filter, then invokes each Manager’s Destroy in order to delete the corresponding resources from the cluster.

In other words, resources are not deleted unless both “Yes at the prompt” and “TAZUNA_DESTROY_EXECUTABLE=true” are satisfied.

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--force-boolfalseSkips the pre-deletion confirmation prompt. It does not skip the environment-variable guard.
--tags-t[]string[]Targets only Manifests with at least one of the specified tags for deletion (OR evaluation).
--no-cache-boolfalseFor type: oras Manifests, always refetch from the registry without using the cache.
--offline-boolfalseFor type: oras Manifests, forbid access to the registry.

--no-cache and --offline cannot be specified together.

Environment Variables

Environment VariablesValueDescription
TAZUNA_DESTROY_EXECUTABLEtrueUnless this is set, destroy does not delete anything. It is a kill switch to prevent destroy from accidentally running in CI.

Examples

TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy
TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy --tags experimental
TAZUNA_DESTROY_EXECUTABLE=true tazuna destroy --force

tazuna state list

Reads the Tazuna State stored in the cluster and lists the resources under Tazuna’s management along with their content hashes.

tazuna state list [-f tazuna.yaml]

Behavior

  1. Load tazuna.yaml.
  2. Reads the State ConfigMap corresponding to each Manifest’s name (tazuna-state-<manifest-name> in the tazuna namespace).
  3. Formats each resource’s GVK / namespace / name / content hash recorded in State to stdout.

Does not evaluate context_matches. Only read access to the cluster is performed; no resources, including State, are modified.

Flag

No specific flags besides the global flags.

Examples

tazuna state list
tazuna state list -f tazuna.yaml

tazuna state diff

Compares the Build result of each Manager with the State stored in the cluster and outputs per-resource differences. Does not modify the cluster.

tazuna state diff [-f tazuna.yaml]

Behavior

  1. Load tazuna.yaml.
  2. For each Manifest, call the Manager’s Build to construct “the resources that should currently be generated from tazuna.yaml.”
  3. Reconcile with the in-cluster State and classify each resource into one of the following.
Diff typeMeaning
addedPresent in the Build result, absent from State
modifiedPresent in both, but with different content hashes
removedPresent in State, absent from the Build result
always-syncClassification that skips diff computation and is always synchronized. Secrets derived from type: genesissecret go here

Does not evaluate context_matches. Only read access to the cluster is performed; nothing is modified.

Flag

No specific flags besides the global flags.

Examples

tazuna state diff
tazuna state diff -f tazuna.yaml

tazuna state sync

Compares the Build result of each Manager with State, and reflects only the added or modified resources into the cluster. The State of successfully synchronized resources is written back to the ConfigMap.

tazuna state sync [-f tazuna.yaml] [--atomic]

Behavior

  1. Load tazuna.yaml.
  2. For each Manifest, call Build and compute the difference against State.
  3. Reflect resources classified as added / modified / always-sync into the cluster.
  4. Resources classified as removed are skipped by default. They are deleted only when TAZUNA_STATE_SYNC_DELETE=true is set.
  5. Write back the State of successfully synchronized resources.

When --atomic is specified, if any resource errors out, the command exits without updating State at all (the in-progress apply itself is not rolled back).

Does not evaluate context_matches.

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--atomic-boolfalseIf an error occurs, exit without updating State.

Environment Variables

Environment VariablesValueDescription
TAZUNA_STATE_SYNC_DELETEtrueDelete resources classified as removed. When not set, deletion does not happen.

Examples

tazuna state sync
tazuna state sync -f tazuna.yaml
tazuna state sync --atomic
TAZUNA_STATE_SYNC_DELETE=true tazuna state sync

tazuna secret-to-genesissecret

Writes an existing Secret in the cluster out to 1Password and generates a GenesisSecret YAML that references it. This is a one-way migration / inventory command, not something to run repeatedly in routine operation.

tazuna secret-to-genesissecret \
  --op-host <host> \
  [--namespace <ns>] \
  [--label-selector <sel>] [--name-regex <re>] \
  [--vault <vault>] [--note <note>] \
  [--dump-dir <dir>] [--dry-run]

Behavior

  1. Narrow down the Secrets in --namespace (default default) by --label-selector / --name-regex.
  2. Write the data of each Secret out to the 1Password --vault as an Item.
  3. Emit a GenesisSecret YAML referencing that Item to --dump-dir (default .).
  4. With --dry-run, neither write to 1Password nor generate YAML; only output the selection result of target Secrets.

It does not read tazuna.yaml, so -f / --file-path is ignored. Among the global flags, only -l / --log-level actually takes effect. Since both reads against the cluster and writes against 1Password run, the 1Password CLI (op) must be authenticated.

Flag

In addition to global flags, the following are accepted.

FlagTypeDefaultRequiredDescription
--op-hoststring-YesHost part of the 1Password service-account URL (e.g. example.1password.com).
--namespacestringdefault-The Kubernetes namespace where the target Secrets exist. Shell completion enumerates the actual cluster namespaces.
--label-selectorstring""-A label selector to narrow down target Secrets. Example: app=foo,tier=db.
--name-regexstring""-A regular expression on the name of target Secrets.
--vaultstring""-The 1Password vault name. Shell completion enumerates the actual vaults.
--notestring""-Note attached to the generated 1Password Item.
--dump-dirstring.-Output directory for the generated GenesisSecret YAML.
--dry-runboolfalse-Output only the selection result without writes.

Examples

tazuna secret-to-genesissecret \
  --op-host example.1password.com \
  --namespace production \
  --label-selector tazuna.pepabo.com/migrate=true \
  --vault platform \
  --dump-dir ./genesissecrets

tazuna secret-to-genesissecret \
  --op-host example.1password.com \
  --name-regex '^db-.*' \
  --dry-run

tazuna tags

Lists the tags declared in tazuna.yaml. For each tag, displays the names of the Manifests carrying that tag.

tazuna tags [-f tazuna.yaml] [--tags ...]

Behavior

  1. Load and validate tazuna.yaml.
  2. Walk every Manifest after includes expansion and aggregate tags into a tag name → list of Manifest names map.
  3. Sort by tag name and output to stdout. The format is:
<tag>:
- <manifest-name>
- <manifest-name>
  1. If --tags is given, the output is narrowed to those tag names.

No cluster access.

Flag

In addition to global flags, the following are accepted.

FlagAliasTypeDefaultDescription
--tags-t[]string[]Narrows the output to the specified tag names.

Examples

tazuna tags
tazuna tags -f tazuna.yaml
tazuna tags --tags frontend,backend

tazuna version

Outputs the version information embedded in the binary.

tazuna version
tazuna --version

Both forms are equivalent.

Behavior

Outputs a single line in the following form and exits.

tazuna <version> (commit <commit>, built <date>, <os>/<arch>)
  • <version> — the release tag. dev for local builds.
  • <commit> — the commit hash at release time. none when not injected.
  • <date> — the build timestamp at release time. unknown when not injected.
  • <os>/<arch>runtime.GOOS / runtime.GOARCH.

<version> / <commit> / <date> are injected at release time by goreleaser. For local builds via go install / go run, they are not injected, so the default values appear.

Flag

No specific flags. No arguments are accepted.

Examples

tazuna version
tazuna --version

Contributing

This section is guidance for those who want to change Tazuna’s codebase, documentation, or releases. The repository root’s CONTRIBUTING.md is the primary source; this section supplements it with one page per topic.

Contents

  • Development Environment — toolchain setup with mise, building a local binary with make build, and repository layout.
  • Testing — the 3 layers of unit / integration / e2e and their make target correspondence, KinD cluster preparation.
  • Documentation — the structure of docs/, previewing with mdbook, updating the English translation via po/en.po, and publishing to GitHub Pages.
  • Release — releases via goreleaser triggered by tag push, version embedding, SBOM / signing / provenance.

Bug Reports / Feature Proposals

Use the Issue templates on GitHub. Free-form issues without a template are also accepted.

For security-related problems, follow the procedure in SECURITY.md. Do not create a public issue.

Pull Request Flow

Same as the description in CONTRIBUTING.md. Restated here.

  1. Branch off main for your working branch.
  2. Keep each change small and focused on one topic.
  3. Before pushing, run make test and make lint locally and ensure they pass.
  4. Open a PR against main. It does not enter review until CI is green.

Use the .github/PULL_REQUEST_TEMPLATE.md PR template in the repository.

Development Environment

This page is for people who want to modify Tazuna itself, covering setting up your local environment and building / running / verifying code changes. For documentation and release flows, see separate pages (Documentation / Release).

Set Up the Toolchain With mise

The repository includes mise.toml, which pins the required toolchain.

[tools]
go = "1.26.0"
golangci-lint = "latest"
helm = "latest"

If you have mise installed, running mise install at the repository root will set up everything. If you manage Go or golangci-lint separately on your system, align them with mise.toml’s versions yourself.

Note: if the Go version required by go.mod (go 1.26.x) is newer than the version pinned in mise.toml, Go’s toolchain download mechanism will absorb the difference at build time. If you want to deliberately avoid toolchain downloads, align mise.toml with the go line in go.mod.

Tazuna itself does not use the helm binary (Helmfile backend is embedded as a Go library). helm is listed in mise.toml to leave room for a future development flow that treats helm as a dependent tool.

Main make Targets

Only the targets defined in Makefile are listed.

TargetContents
make buildGenerates ./tazuna via go build .
make installAfter make build, runs sudo mv tazuna /usr/local/bin
make formatgo fmt ./...
make lintgolangci-lint run
make testgo test ./... (unit tests)
make test-integrationgo test -tags=integration ./...
make test-e2eAfter make build && make devenv-create, runs go test -tags=e2e -count=1 ./test/e2e/...
make test-allunit + integration + e2e
make coverRuns tests with -race -covermode=atomic -coverprofile=coverage.out, then outputs a summary
make allRuns format → test → build → lint in order
make devenv-createStands up a kind cluster named tazuna (or switches context if one already exists)
make devenv-destroykind delete cluster --name tazuna

The KinD cluster name is fixed as tazuna, and the kubeconfig context name is kind-tazuna. Because e2e assumes a KinD cluster, the first run of make test-e2e internally triggers make devenv-create (see also Testing).

Repository Layout

The responsibilities of the main directories are roughly as follows.

PathRole
main.goEntry point. Just calls cmd.Execute().
cmd/Cobra subcommand definitions (apply / build / check / destroy / state ... / secret-to-genesissecret / tags / version).
cmd/internal/Internal utilities shared between subcommands.
api/v1/Go struct definitions corresponding to the YAML schemas (tazuna.yaml / tazuna.hint.yaml / GenesisSecret / TestPluginSpec / ORAS).
pkg/runner/Orchestration for the whole of tazuna apply.
pkg/manager/Per-manifest-type Manager implementations (kustomize / helmfile / genesis_secret / parallel, and the oras/ subpackage).
pkg/state/State representation and ConfigMap persistence.
pkg/testplugin/WaitUntil / ExistNonExist implementations.
pkg/genesissecret/Provider interface and the 1Password-targeted implementation.
pkg/hint/Loading and validation of tazuna.hint.yaml.
pkg/op/Invocation of op (the 1Password CLI).
pkg/validator/Validation of tazuna.yaml.
pkg/context/Evaluation of context_matches.
pkg/prompt/Abstraction of interactive input (Yes/No during destroy, etc.).
pkg/resource/Helpers for Kubernetes resource operations commonly used at apply time.
test/e2e/E2E test bodies and fixtures (testdata/).
docs/This documentation site.

The responsibility splits often referenced in the reference (Manager / Runner / Validator and so on) are easier to cross-check by reading Overall Architecture.

Try Behavior With a Local Binary

By calling ./tazuna generated by make build directly, you can try behavior using your in-development binary instead of the release version.

make build
./tazuna check -f path/to/tazuna.yaml
./tazuna build -f path/to/tazuna.yaml --tags infra

When you want it on PATH, use make install (it requires sudo). If you want to do live verification with KinD, bring up a cluster with make devenv-create, switch current-context with kubectl config use-context kind-tazuna, and then run.

Testing

Tazuna’s tests are split into 3 layers: unit / integration / e2e. Each layer corresponds 1:1 with a make target; unit always runs on PRs (CI), while e2e is a manual layer that assumes KinD.

Layers and How to Run

LayerCommandTargetsPrerequisites
unitmake testgo test ./...None. Runs on every push / PR via the CI workflow in GitHub Actions.
integrationmake test-integrationgo test -tags=integration ./...None. Additional tests tagged with the integration build tag are targets.
e2emake test-e2ego test -tags=e2e -count=1 ./test/e2e/...KinD cluster. Internally runs make build && make devenv-create.
Allmake test-allunit → integration → e2eSame as e2e
Coveragemake coverRuns unit tests with -race -covermode=atomic -coverprofile=coverage.out, then outputs a summaryNone

In CI (.github/workflows/ci.yaml), go test -race -covermode=atomic -coverprofile=coverage.out ./... is run, so the contents largely match make cover.

Unit Tests

Written as *_test.go in every package. Standard Go tests. They have no dependency on KinD or external CLIs, and go test ./... is self-contained.

PR review requires this layer to be green as a prerequisite.

Integration Tests

Additional tests tagged with the integration build tag. Run with make test-integration. The place to isolate scenarios that have no external dependencies but are too heavy for unit tests.

Running go test -tags=integration ./... directly is equivalent.

E2E Tests

Real-cluster tests placed in test/e2e/. Isolated by the e2e build tag, with only ./test/e2e/... targeted.

Running make test-e2e internally runs the following in order.

  1. make build (build ./tazuna)
  2. make devenv-create (stand up the KinD cluster tazuna, or switch context if one already exists)
  3. go test -tags=e2e -count=1 ./test/e2e/...

-count=1 is set so that e2e tests are not cached and actually run every time. The KinD cluster is cleaned up by make devenv-destroy (kind delete cluster --name tazuna).

KinD Cluster Specifications

ItemValue
Cluster nametazuna
kubeconfig contextkind-tazuna
Config file.github/kind-config.yaml

Because CI and developer local environments share the same KinD config, e2e that passes locally generally also passes in CI.

Test Data

E2E fixtures are placed per-case under test/e2e/testdata/. Each case directory holds a tazuna.yaml alongside the actual content (kustomize/ / helmfile/ etc.) corresponding to its type.

test/e2e/testdata/
├── kustomize-minimal/         # minimal case for type: kustomize
├── helmfile-minimal/          # minimal case for type: helmfile
├── parallel-minimal/          # type: parallel
├── testplugin-minimal/        # basic Test plugin (WaitUntil/ExistNonExist)
├── testplugin-cel/            # case with complex CEL expressions in WaitUntil
├── tags-filter-minimal/       # --tags filter
├── state-minimal/             # state list/diff/sync
├── state-modified/            # "modified" judgment in state diff
├── destroy-minimal/           # tazuna destroy
└── check-invalid/             # tazuna check error cases

When adding a new feature, the common flow is to add a corresponding fixture directory with a single minimal tazuna.yaml, and add a *_test.go under test/e2e/.

Lint

make lint

Simply calls golangci-lint run. The golangci-lint version is managed via mise.toml, so running mise install is sufficient — no additional steps required.

CI also runs the same golangci-lint. Running it locally before opening a PR reduces back-and-forth.

Documentation

This page is for people who want to make changes to the Tazuna documentation site (the site you are reading now). The site is self-contained under docs/, built with mdBook + mdbook-i18n-helpers (gettext / PO files).

The sources are written in Japanese, and the English edition is derived via translation in po/en.po.

Directory Layout

docs/
├── book.toml            # mdBook の設定
├── src/                 # ドキュメント本体(日本語ソース)
│   ├── SUMMARY.md
│   ├── introduction.md
│   ├── concepts/
│   ├── getting-started/
│   ├── guides/
│   ├── operations/
│   ├── reference/
│   └── contributing/
├── po/
│   └── en.po            # 英語訳
├── theme/
│   └── fonts.css        # Google Fonts (M PLUS U) の上書き
├── static/
│   └── index.html       # /en/ と /ja/ をリンクする landing
└── THIRDPARTY.md        # フォント等のサードパーティ資産

docs/src/SUMMARY.md is the table of contents itself. When adding a new page, you also need to add one line here (mdBook builds even if the entry is missing, but the page will not appear on the site).

Required Tools

cargo install mdbook --locked
cargo install mdbook-i18n-helpers --locked

msgmerge (bundled with gettext) is used when updating the PO. On macOS, brew install gettext.

Preview Locally

Japanese (source language):

cd docs
mdbook serve --open

English:

cd docs
MDBOOK_BOOK__LANGUAGE=en mdbook serve --open

It rebuilds and reloads the browser on every file save.

Build Locally

cd docs
mdbook build -d book/ja
MDBOOK_BOOK__LANGUAGE=en mdbook build -d book/en
cp static/index.html book/index.html

Open book/index.html in your browser to switch between /ja/ and /en/ via links.

Updating the English Translation

After editing text under docs/src/, regenerate the PO template and merge into en.po.

cd docs
MDBOOK_OUTPUT__XGETTEXT__POT_FILE=messages.pot \
  mdbook build -d po --no-create-missing
msgmerge --update po/en.po po/messages.pot

Then open po/en.po and fill in msgstr for the added msgids. If the correspondence between the source (Japanese) and English translation breaks, blanks or missing items appear on the site, so the basic flow is to update en.po in the same PR as the source change.

Deployment

.github/workflows/docs.yaml handles publishing.

  • Push to main: build the site and publish to GitHub Pages (actions/upload-pages-artifact + actions/deploy-pages).
  • PR: build only. The output can be downloaded as a workflow-run artifact named github-pages. The recommended review process is to extract it locally and look at it.

Since GitHub Pages allows only one deployment per site, per-PR live preview URLs are intentionally not provided.

Third-party Assets

The site loads M PLUS U from Google Fonts at runtime. When adding new external assets such as fonts / icons / images, add license and attribution to docs/THIRDPARTY.md.

Documentation-side Conventions

The conventions that existing pages follow are noted here for reference.

  • Tonal split: concepts are mostly prose (concepts/), guides are “purpose → prerequisites → procedure,” reference is field tables and code fragments.
  • Anchor links: links into glossary or other references include the #anchor. Example: [Manifest type](../concepts/glossary.md#manifest-type).
  • Code and identifiers: tazuna.yaml field names, CLI flags, and Go type names are wrapped in backticks.
  • Language policy: sources are written in Japanese. Comments in code can be Japanese, but logs / error messages / CLI output are consistently in English (a project-wide policy).

Release

Tazuna releases use the setup of tag push triggering goreleaser via GitHub Actions. You normally do not invoke goreleaser directly from your machine when cutting a release.

This page summarizes “what happens when a release is cut,” “where version strings come from,” and “how to verify the artifacts.”

Trigger and Output

The tag name is embedded as the version string as-is. We recommend following semantic versioning (vX.Y.Z) (see also Changelog auto-generation).

Outputs

Build targets in .goreleaser.yaml are as follows.

AxisValue
GOOSlinux / darwin
GOARCHamd64 / arm64
CGOCGO_ENABLED=0
ldflags-s -w -trimpath + version embedding

The archive name is tazuna_<Os>_<Arch>.tar.gz (with amd64 normalized to x86_64), and the release includes:

  • Per-OS/arch .tar.gz (binary)
  • checksums.txt (SHA256)
  • Per-archive + source SBOMs (*.sbom.json, SPDX)
  • checksums.txt.sigstore.json (cosign keyless signing bundle)

GitHub Actions’ actions/attest-build-provenance separately generates SLSA build provenance, and links it in a form verifiable with gh attestation verify.

Version String Embedding

main.go declares the following vars, which are injected with -X at release-build time.

var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)

In .goreleaser.yaml’s ldflags:

-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}

The injected values flow through cmd.SetVersionInfo and appear in tazuna version.

For local builds via go install / make build and similar, nothing is injected, so version / commit / date remain dev / none / unknown.

Changelog

The changelog section of .goreleaser.yaml assembles the GitHub Releases description.

  • Order: ascending commit (sort: asc)
  • Exclusions: commits with ^docs: / ^test: prefixes
  • Format: header ## Tazuna {{.Version}}, with a compare link to the previous release in the footer

PR title / commit message conventions follow CONTRIBUTING.md / the PR template. docs: / test: prefixes do not appear in release notes, so attaching them to PRs that do not change product behavior makes inventory easier.

Artifact Verification

Users of the release have roughly 3 verification options.

# 1. Verify checksum
sha256sum -c checksums.txt

# 2. Verify checksums.txt signature (cosign keyless)
cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  checksums.txt

# 3. SLSA provenance の検証
gh attestation verify <file> --repo pepabo/tazuna

The third is to confirm SLSA build provenance via the GitHub CLI. Usable right after release and from internal CI as well.

Practical Steps When Cutting

  1. Verify main is stable (CI green).
  2. Cut and push a tag in vX.Y.Z form.
  3. Confirm the Release workflow runs and the release is published.
  4. If needed, hand-curate the auto-generated changelog.

The documentation site is published by a separate workflow (docs.yaml), triggered by pushes to main. It is independent of release cutting.