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
tazunabinary is installed on your machine tazuna versionruns 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.
| Dependency | Required? | Description |
|---|---|---|
| Kubernetes cluster | Yes (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-context | Yes (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.
- Proceed to Guides - Writing Your First tazuna.yaml, where you will write an actual
tazuna.yamland runapply. - When you just want to look up the specification, see the Reference.
- If you want to understand “why it is shaped this way,” see Concepts.
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.
- Design Philosophy and Intended Use Cases — start here to understand what Tazuna exists for and the environments it is designed for.
- Overall Architecture — an overview of the components inside the
tazunabinary and how they cooperate. - 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 applyis 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
stateand pinning down which cluster a declaration belongs to viacontext_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 viakubeconfig. - The secret management backend itself — Tazuna declares references to where secrets are stored, but does not store the secrets itself.
- GitOps rollbacks and history management —
staterepresents “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.yamlinto 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 Availableexist-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.”
- CLI parses flags and wires up the Runner.
- Validator reads
tazuna.yamland verifies the schema and the existence ofpaths. - If
spec.context_matchesis present, the Context guard verifies the kubeconfig. - Runner expands
includesand convertsmanifests[].pathinto runtime paths. - It walks
manifestsin order and hands each one that is not filtered out by--tagsto the corresponding Manager. - 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. - 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.
- Writing Your First tazuna.yaml — walks you all the way from the first
tazuna.yamlto atazuna applythat 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.
--tagsevaluation:tazuna.yaml- tags- Inspecting State: Internal Structure of State /
tazuna state list - GenesisSecret: GenesisSecret Schema
- CI integration: CI Pipeline
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.yamland a kustomize directory pair in your own repository - Confirmed that
tazuna checkreports thetazuna.yamlas valid - Able to verify “what will be installed” with
tazuna buildwithout 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 specifykustomize. Other options includehelmfileandoras(see Manifest type).path— the directory holding the actual content. Written as a path relative to the directory wheretazuna.yamlitself 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
nameis set, unique, and uses only allowed characters- The location referenced by
pathactually 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.
- Loads and validates
tazuna.yaml - Walks
manifests[]in declaration order - Hands each Manifest to its corresponding Manager (here, the kustomize Manager)
- The kustomize Manager renders
pathand reflects the result into the cluster - 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
pathbase — it is relative to the directory containingtazuna.yamlitself. Writing it as a path relative to yourcd’d location is a common cause of CI / local behavior diverging. - Mistaking which cluster — make it a habit to check
kubectl config current-contextimmediately beforeapply.context_matcheslets you prevent this structurally, but start by making visual confirmation a habit. - Mistaking kustomize errors for Tazuna errors — when
tazuna buildfails, in most cases the underlying error is from kustomize, propagated as-is. Runningkustomize build ./pathdirectly 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 thattazuna state diffcan 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 ofTAZUNA_DESTROY_EXECUTABLEandcontext_matches, and accident-prone scenarios. - Drift Monitoring — operating
tazuna state diffon a periodic schedule to visualize drift, output formats, and how to wire up notifications. - CI Pipeline — the typical setup that runs
check/buildon PRs andapplyonmainmerges, where to placedestroy, 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.
- Load and validate
tazuna.yaml. - Evaluate
context_matches. Abort immediately on mismatch. - If
--forceis not set, prompt for Y/N. - If
TAZUNA_DESTROY_EXECUTABLE=trueis not set, only log and exit. - 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.
| Scenario | Where it stops / why it becomes an incident | Recommended countermeasure |
|---|---|---|
| Current-context was production when you thought it was staging | If 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 main | Without 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 everything | Execution 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 destroy | Resources 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 fromtazuna.yamland then runstate sync; it will be deleted under theremovedclassification (seetazuna state sync). This cannot be used when you want to reset without changing the source of truth intazuna.yaml.- A destroy narrowed with
--tags— see above.
Related
- Command spec:
tazuna destroy - Evaluated
context_matches:tazuna.yaml - Pre-deletion check:
tazuna state list
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 type | Cases Detected | Typical example of drift |
|---|---|---|
added | Present in the Build result, absent from State | Updated tazuna.yaml to add a Manifest, but it has not yet been applied |
modified | Present in both, but with different content | Helm values change, kustomize overlay change, image tag update not yet reflected |
removed | Present in State, absent from the Build result | Removed a Manifest from tazuna.yaml, but the resource is still in the cluster |
always-sync | Always treated as synchronized | Secrets 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 diffdoes not modify the cluster). - Dump the output to a file with
tazuna state diff -f path/to/tazuna.yaml > diff.txtand only send a notification when it does not containNo 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.yamlrepository 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 (
removeddeserves 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(ortazuna 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 intotazuna.yaml.added: most often this is a Manifest added totazuna.yamlbut not yet applied. Either apply, or reverttazuna.yaml, depending on intent.removed: a Manifest was removed from Tazuna but the resource still exists in the cluster. Clean it up withtazuna destroynarrowed by--tags, or withtazuna state sync+TAZUNA_STATE_SYNC_DELETE=true.
- GenesisSecret’s
always-sync: this is not drift, so it is fine to exclude it from notifications.
Related
- Command spec:
tazuna state diff/tazuna state sync - Internal structure of State: Internal Structure of State
- Terminology: Diff type / always-sync
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.
| Stage | Purpose | Commands to run | Cluster access |
|---|---|---|---|
| Verify | Guarantee that tazuna.yaml is not broken | tazuna check, tazuna build | None |
| Apply | Reflect the contents of main into the cluster | tazuna apply (or tazuna state sync) | Required |
| Remove | Delete Tazuna-managed resources | tazuna destroy | Required |
“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 applyis exactly the kubeconfig current-context. In CI, always explicitly include a step that sets the current-context. - Including
spec.context_matchesintazuna.yamlmakes the system fail fast if it wouldapplyagainst 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.
| Command | When what runs | Drift detection |
|---|---|---|
tazuna apply | Runs every Manifest declared in tazuna.yaml through its Manager | No (always overwrites) |
tazuna state sync | Compares the Build result with State, and only reflects the differences (added / modified / always-sync) | Yes |
Rough guidance:
- Bootstrap phase / small number of Manifests:
tazuna applyis simple and predictable. - Manifest count grows and a single
applybecomes heavy: switch totazuna state syncto narrow down to differences only. - You want automatic
removeddeletion: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=truepermanently in CI leaves only the prompt as a guard against accidental firing (and since CI cannot respond to prompts, combined with--forceit 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.
Related
- Spec:
tazuna apply/tazuna build/tazuna check/tazuna state sync - Incident prevention: Operating
tazuna destroy - Drift detection: Drift Monitoring
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.yamlschema — the top-level structure oftazuna.yaml, the only input file to Tazuna, and the common-field specifications ofspec.manifests[]/spec.context_matches/includes, and so on.tazuna.hint.yamlschema — the schema of the hint file that declares constraints over thevarsof a helmfile Manifest. Type, required, conditional-required, format-validation rules, and top-level rules such asoneof_required.- GenesisSecret schema — the YAML schema for generating Kubernetes Secrets from an external secret store (1Password). It is referenced from
tazuna.yamlas a Manifest withtype: genesissecret. - Test plugin — the common fields of
TestPluginSpec(written undermanifests[].testsandspec.tests) and the spec for the built-inWaitUntil/ExistNonExistplugins. - Internal Structure of State — State’s storage location (ConfigMaps in the
tazunanamespace), 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 ofpath, type-specific fields, and apply / destroy / build behavior. - CLI — the spec of subcommands, global flags, and environment variables of the
tazunabinary. 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
apiVersion | string | - | - | When set, must be exactly tazuna.pepabo.com/v1. May be omitted. |
kind | string | - | - | When set, must be exactly Tazuna. May be omitted. |
spec | TazunaSpec | Yes | - | 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
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_mode | string | - | or | Evaluation 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
regexppackage. Compilation failure is caught at thetazuna checkstage. - If empty or unset, the context check is skipped.
- When set,
tazuna apply/tazuna destroyverify current-context before touching the cluster. Mismatch aborts processing.
context_match_mode
or(default): matching any one ofcontext_matchesis enough.and: must match all ofcontext_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).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | - | Manifest identifier. Must match ^[a-zA-Z0-9_-]+$ and be unique across all Manifests after includes expansion. _metadata is reserved and cannot be used. |
description | string | - | "" | Human-facing description. Has no effect on behavior. |
type | string | Conditional (*) | - | One of kustomize / helmfile / genesissecret / parallel / oras. |
path | string | Conditional (*) | - | 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. |
kustomize | ManifestKustomize | - | null | Options referenced when type: kustomize. |
helmfile | ManifestHelmfile | - | null | Options referenced when type: helmfile. |
genesisSecret | object | - | null | Options referenced when type: genesissecret. Currently an empty object. |
parallel | ManifestParallel | - | null | Options referenced when type: parallel. Write nested Manifests under children[]. |
oras | ManifestORAS | - | null | Options 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_-]+$. _metadatais reserved for internal use and cannot be used as a Manifest name.- Must be unique across all Manifests after
includesexpansion. Duplicates fail attazuna 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.yamlitself resides, not the cwd from which the command was run. - Existence is checked at
tazuna checktime. - What
pathshould point to differs by type.
type | What path points to |
|---|---|
kustomize | A directory containing kustomization.yaml |
helmfile | A directory containing helmfile.yaml |
genesissecret | The GenesisSecret definition YAML file (not a directory) |
parallel | Not used in practice. The path from children[] is used. It cannot be empty due to validation. |
oras | Not 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
--tagsflag, 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[].
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | - | 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
includeshave their own “Manifest-body” fields (type/path/tags, etc.) ignored. includesis non-nestable. Even if the includedtazuna.yamlhas its ownincludes, 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.
| Field | Role |
|---|---|
kustomize | Options for type: kustomize. Has defaultNamespace. |
helmfile | Options for type: helmfile. Has vars / includeCRDs / wait / kubeVersion and so on. |
genesisSecret | Extension point for type: genesissecret. An empty object in the current version. |
parallel | Options for type: parallel. Has children[]. |
oras | Options 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/kindare set, they must equal the canonical values exactly. - For each element of
spec.manifests[]:- When
includesis absent:pathandtypemust be set. typemust be a known value (kustomize/helmfile/genesissecret/parallel/oras).- The location pointed to by
pathmust exist.
- When
spec.manifests[].namemust be present, use allowed characters, be unique, and not be a reserved word.spec.context_matchesmust be compilable as regular expressions.spec.context_match_modemust be one ofor/and/ unset.- For
type: helmfile: each value inhelmfile.varsmust satisfy one ofenv/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.referenceis required, andoras.delegate.typemust be eitherhelmfileorkustomize. - When specifying
includes: eachinclude.pathis 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)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
apiVersion | string | - | - | Indicates the schema version. The value is currently not validated. |
kind | string | - | - | Indicates the resource kind. The value is currently not validated. |
vars | map<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).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | The var’s type. One of string / slice / map. |
required | bool | - | false | Whether the user must provide a value. |
default | any | - | null | Value injected when not provided. Cannot be combined with required: true. |
description | string | - | "" | Human-facing description. Has no effect on behavior. |
format | string | - | "" | 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.
- If a value is already provided, use it as-is.
- If not provided and
required: true, error. - If a
defaultis set, inject that value. - 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.
| Value | Validation |
|---|---|
hostname | RFC 952 / 1123 compliant hostname pattern (alphanumerics / - / .; each label starts and ends with alphanumerics) |
url | Parseable with net/url.ParseRequestURI, and the scheme must be non-empty |
email | Simple user@domain.tld form (a simple regex) |
ip | An IPv4 / IPv6 address parseable with net.ParseIP |
cidr | A CIDR notation parseable with net.ParseCIDR |
uuid | RFC 4122 form (hyphen-separated) |
semver | Semantic version. The v prefix is optional. Pre-release / build metadata are supported |
datetime | A 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | 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. |
message | string | - | "" | 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[*].typemust be one ofstring/slice/map.vars[*].required: trueandvars[*].defaultmust not be combined.- When
vars[*].formatis set,type: stringmust also be set. vars[*].formatmust be a known value (see format).- References of
vars[*].required_with/vars[*].required_withoutmust exist invars. vars[*].required: trueandrequired_with/required_withoutmust not be combined.rules[*].typemust be a known value (currently onlyoneof_required).rules[*].varslength must be at least 2.- References of
rules[*].varsmust exist invars.
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: slicereceives astaticMap). - Required vars (
required: true) have a value. required_with/required_withoutconditions are satisfied.- For string vars with a
format, the value (if non-empty) satisfies the format pattern. rulesare satisfied (e.g., at least one of the vars inoneof_requiredis provided).
Related
- Term:
tazuna.hint.yaml - Schema of helmfile.vars:
tazuna.yamlmanifest-type-specific fields
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)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
apiVersion | string | - | - | Indicates the schema version. The value is currently not validated. |
kind | string | - | - | Indicates the resource kind. The value is currently not validated. |
spec | GenesisSecretSpec | Yes | - | 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
provider | string | - | "" | 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.”
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
uri | string | Yes | - | URI pointing to the Provider item. See uri format for details. |
items | map<string, GenesisSecretGenerateItem> | Yes | - | Mapping from keys returned by the Provider to keys in the output Secret. |
preferLabel | bool | - | false | Whether 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).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mapTo | string | Yes | - | 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.”
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kubernetesSecret | GenesisSecretOutputKubernetesSecret | Conditional (*) | null | Specify when the output destination is a Kubernetes Secret. |
stdout | object | - | null | Defined 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
namespace | string | Yes | - | Namespace of the output Secret. |
name | string | Yes | - | Name of the output Secret. |
labels | map<string, string> | - | null | Labels added to the output Secret. |
annotations | map<string, string> | - | null | Annotations added to the output Secret. |
type | string | - | Opaque | The 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. |
context | string | - | "" | 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.
- Read the YAML file pointed to by
manifests[].path(relative to the directory oftazuna.yamlitself). - Pass each element of
spec.secrets[]to the Provider and retrieve the field set. - Merge the results of all
secrets[]into onemap[string]string, renaming keys usingitems’smapTo(if a key collides, the later one wins). - For each
kubernetesSecretofspec.outputs[],CreateOrUpdatea KubernetesSecretwith the specifiednamespace/name.- The merged map goes into
StringDataas-is. labels/annotations/typeare set as declared.
- The merged map goes into
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
Related
- Reference from
tazuna.yaml:tazuna.yamlmanifest-type-specific fields - Provider terminology: Provider (SecretProvider)
- Write an existing Secret out to 1Password and GenesisSecret:
tazuna secret-to-genesissecret - Term: GenesisSecret
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.
| Location | Execution timing |
|---|---|
manifests[].tests | Executed immediately after that Manifest is applyed |
spec.tests | Executed 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | Plugin kind. WaitUntil or ExistNonExist (case-sensitive). |
waitUntil | WaitUntilArgs | Conditional (*) | null | Required when type: WaitUntil. |
existNonExist | ExistNonExistArgs | Conditional (*) | null | Required when type: ExistNonExist. |
minConsecutiveSuccessCount | int | - | 1 | If the test function succeeds this many times consecutively, the entire test plugin is considered successful. 0 is internally corrected to 1. |
minConsecutiveFailureCount | int | - | 0 | If 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. |
timeoutSeconds | int | - | Effectively infinite | Overall timeout in seconds. Failure if exceeded. With 0 (unset), effectively unlimited (internally set to about 280 days). |
intervalSeconds | int | - | 0 | Wait 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.
- Run the test function (plugin-specific logic) once.
- Append the result (success / failure) to the history.
- If the last
minConsecutiveSuccessCountresults are all successes, exit as success. - If
minConsecutiveFailureCountis non-zero and the lastminConsecutiveFailureCountresults are all failures, exit as failure. - If
timeoutSecondshas elapsed, exit as failure. - Sleep for
intervalSecondsseconds and go back to 1 (immediate if0).
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.
| Field | Type | Required | Description |
|---|---|---|---|
resource.apiVersion | string | Yes | Target resource apiVersion. Examples: apps/v1, cert-manager.io/v1. |
resource.kind | string | Yes | Target resource kind. Examples: Deployment, Certificate. |
namespace | string | Yes | Target resource namespace. |
name | string | Yes | Target resource name. |
condition | string | Yes | A 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.”
| Field | Type | Required | Description |
|---|---|---|---|
resource.apiVersion | string | Yes | Target resource apiVersion. |
resource.kind | string | Yes | Target resource kind. |
namespace | string | Yes | Target resource namespace. |
name | string | Yes | Target resource name. |
shouldExist | bool | Yes | When 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
Related
- Field for placement:
testsfield intazuna.yaml - Term: Test plugin
- Position in overall architecture: Overall Architecture - Test plugin
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.
| Element | Value |
|---|---|
| Namespace | tazuna (auto-created if absent) |
| ConfigMap name | tazuna-state-<manifest-name> |
| Format | One 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 scope | Format | Number 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>"}
| Field | Type | Description |
|---|---|---|
contentHash | string | The 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>"}
| Field | Type | Description |
|---|---|---|
gitCommitHash | string | The git commit hash recorded at sync time. |
lastSyncedAt | string | Sync 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:
| Field | Reason for exclusion |
|---|---|
metadata.resourceVersion | Used for cluster generation tracking; changes on every apply |
metadata.uid | Assigned by the cluster; changes on every apply |
metadata.creationTimestamp | Assigned by the cluster; changes on every apply |
metadata.generation | Assigned by the cluster; changes on every apply |
metadata.managedFields | Server-side apply tracking information |
metadata.selfLink | Assigned by the cluster |
status | Dynamic field written by controllers |
Computation procedure:
- Deep-copy the object via JSON marshal / unmarshal.
- Remove the excluded fields above.
- JSON-marshal the rest.
- 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 type | Meaning | state sync behavior |
|---|---|---|
added | Present in the Build result, absent from State | Apply |
modified | Present in both, but with different ContentHash | Apply |
removed | Present in State, absent from the Build result | Skipped by default. Deleted only when TAZUNA_STATE_SYNC_DELETE=true |
always-sync | Skip diff computation; always treat as a sync target | Apply |
state diff output is stably sorted in the order added → modified → removed → always-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.
Related
- CLIs that operate on it:
tazuna state list/tazuna state diff/tazuna state sync - Terminology: State / State key / ContentHash / Diff type / always-sync
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 kustomizehelmfile— apply the result of helmfile templateoras— pull an artifact from an OCI registry and delegate to helmfile / kustomizeparallel— process child Manifests in parallelgenesissecret— 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.
type | Specific field name | Field type |
|---|---|---|
kustomize | kustomize | ManifestKustomize |
helmfile | helmfile | ManifestHelmfile |
oras | oras | ManifestORAS |
parallel | parallel | ManifestParallel |
genesissecret | genesisSecret | Empty 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.
type | What path points to |
|---|---|
kustomize | A directory containing kustomization.yaml |
helmfile | A directory containing helmfile.yaml |
oras | Not used in practice. Cannot be empty due to validation, so write any directory. |
parallel | Not used in practice. The path from children[] is used. It cannot be empty due to validation. |
genesissecret | GenesisSecret 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
defaultNamespace | string | - | "" | 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
| Operation | Internal processing |
|---|---|
Build | Render the equivalent of kustomize build <path> and write the YAML result to stdout. |
Apply | Convert the rendering result to a set of unstructured objects, supplement defaultNamespace, and CreateOrUpdate them one by one. |
Destroy | Convert 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
Related
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
vars | map<string, HelmFileVar> | - | {} | Variables passed to helmfile. See vars for details. |
includeCRDs | bool | - | false | Passes the equivalent of --include-crds to helmfile template. |
defaultNamespace | string | - | "" | The namespace assigned to rendered resources whose metadata.namespace is unset. |
extraValueFiles | [string] | - | [] | Additional --values files passed to helmfile template. |
wait | bool | - | false | When true, wait after Apply until the target resources become Ready. See wait behavior for details. |
timeoutSeconds | int | - | 0 | Maximum wait seconds for wait. With 0, 300 seconds (5 minutes) is used internally. |
kubeVersion | string | - | "" | 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.
- Retrieve the value according to each var’s
from(env/static/op). - If a
tazuna.hint.yamlis 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
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Where the value is retrieved from. One of env / static / op. |
env | string | Conditional (*) | Required when from: env. The name of the environment variable to reference. |
static | string | Conditional (*) | Used when from: static. A scalar value. |
staticSlice | [string] | Conditional (*) | Used when from: static. A slice value. |
staticMap | map<string, string> | Conditional (*) | Used when from: static. A map value. |
op | OnePasswordVaultSelector | Conditional (*) | 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
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Whether to reference the field by id or label. Either id or label. |
vault | string | Yes | 1Password vault name. |
item | string | Yes | 1Password item name. |
field | string | Yes | Field 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:
| Kind | Ready condition |
|---|---|
Deployment | Immediately Ready if spec.replicas == 0. Otherwise: status.readyReplicas == status.replicas AND status.availableReplicas == status.replicas AND status.replicas > 0 |
StatefulSet | Immediately Ready if spec.replicas == 0. Otherwise: status.readyReplicas == status.replicas AND status.replicas > 0 |
DaemonSet | status.numberReady == status.desiredNumberScheduled AND status.desiredNumberScheduled > 0 |
Pod | status.phase == "Running" AND Ready condition is True |
| Others | Treated 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
| Operation | Internal processing |
|---|---|
Build | Write the helmfile template output YAML to stdout. |
Apply | Convert the helmfile template result to unstructured form, supplement defaultNamespace, and CreateOrUpdate in order. If wait is true, wait for Ready. |
Destroy | Convert 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
Related
- helmfile.vars constraints:
tazuna.hint.yamlschema - Terminology: helmfile / Helm / 1Password
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
reference | string | Yes | - | OCI artifact reference. Accepts both the tag form (ghcr.io/example/foo:v1.0.0) and the digest form (ghcr.io/example/foo@sha256:...). |
target | string | - | "" | 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. |
plainHTTP | bool | - | false | When true, connect to the registry over HTTP (non-TLS). |
insecureSkipVerify | bool | - | false | When true, skip TLS certificate verification when connecting to the registry. |
auth | ORASAuth | - | null | Overrides the registry credentials. Defaults to using docker config.json. See Credential resolution for details. |
delegate | ORASDelegate | Yes | - | Configuration of the delegate Manager invoked after the pull. |
ORASAuth
| Field | Type | Required | Description |
|---|---|---|---|
username | string | - | Registry username. |
password | string | - | Registry password. |
If both fields are empty, this is not treated as an override (see Credential resolution).
ORASDelegate
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | Manifest type to delegate to. helmfile or kustomize. |
helmfile | ManifestHelmfile | - | null | Options passed to the delegate as-is when type: helmfile. |
kustomize | ManifestKustomize | - | null | Options passed to the delegate as-is when type: kustomize. |
Behavior
| Operation | Internal processing |
|---|---|
Build | Pull artifact → call the delegate’s Build. |
Apply | Pull artifact → call the delegate’s Apply. |
Destroy | Pull artifact → call the delegate’s Destroy. |
The delegate is invoked with a new Manifest assembled like this:
name/description/tags/testsare carried over from the original ORAS Manifest as-is.typeisdelegate.type(helmfile/kustomize).pathis the local path after the pull (with sub-path appended iftargetis set).- Specific fields use
delegate.helmfile/delegate.kustomizeas-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_HOMEis set:$XDG_CACHE_HOME/tazuna/oras - Otherwise:
$HOME/.cache/tazuna/oras
- If
- Cache structure:
- Artifact is extracted under
blobs/<sanitized digest>/ - Tag → digest mapping is recorded in
refs/<sanitized reference>
- Artifact is extracted under
- Specifying
--no-cachetoapply/build/destroybypasses the cache and always refetches from the registry. - Specifying
--offlineforbids registry access. If the cache misses, it is an error. --no-cacheand--offlinecannot be specified together.- See the
apply/build/destroypages for CLI flags.
Constraints at Extraction Time
Tar extraction inside the artifact applies the upper bounds defined in ADR004.
| Limit | Value |
|---|---|
| Total size after extraction | 1 GiB |
| Number of tar entries | 10000 |
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.
oras.authoverride (at least one ofusername/passwordis non-empty)- docker’s credential store (
$DOCKER_CONFIGor~/.docker/config.json) - 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
Related
- Delegate targets:
type: helmfile/type: kustomize - CLI:
tazuna apply/tazuna build/tazuna destroy - Term: ORAS / OCI artifact
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
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
| Operation | Internal processing |
|---|---|
Apply | Call the corresponding Manager’s Apply for each element of children[] in goroutines in parallel. Errors are aggregated and returned. |
Destroy | Call the corresponding Manager’s Destroy for each element of children[] in parallel. |
Build | Call 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
Related
- Types usable as child Manifests:
kustomize/helmfile/genesissecret/oras - Common Manifest fields:
tazuna.yaml- Manifest
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
| Operation | Internal processing |
|---|---|
Build | Read the GenesisSecret YAML, retrieve values from the Provider, and write a single Secret YAML (corresponding to outputs[0].kubernetesSecret) to stdout. |
Apply | Read the GenesisSecret YAML, retrieve values from the Provider, and CreateOrUpdate a Kubernetes Secret for each entry of outputs[].kubernetesSecret. |
Destroy | Read 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.
Related
- GenesisSecret YAML schema: GenesisSecret schema
- Write GenesisSecret from an existing Secret:
tazuna secret-to-genesissecret - Terminology: GenesisSecret / Provider (SecretProvider) / always-sync
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
tazuna apply— applytazuna.yamlto the clustertazuna build— emit the rendering result without touching the clustertazuna check— validatetazuna.yamltazuna destroy— delete Tazuna-managed resources from the clustertazuna state list— list the resources recorded in Statetazuna state diff— show the difference between the Build result and Statetazuna state sync— reflect the differences State-firsttazuna secret-to-genesissecret— write existing Secrets to 1Password and GenesisSecrettazuna tags— list the tags written intazuna.yamltazuna version— output version information
Global Flags
Persistent flags inherited by every subcommand.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--file-path | -f | string | tazuna.yaml | Path to tazuna.yaml. |
--log-level | -l | string | info | Log 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 Codes | Meaning |
|---|---|
0 | Success |
| Non-zero | Failure. 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 Variables | Value | Affected commands | Effect |
|---|---|---|---|
TAZUNA_DESTROY_EXECUTABLE | true | destroy | Unless 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_DELETE | true | state sync | Only 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. |
KUBECONFIG | Path | All cluster-touching commands | Follows 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.
- Load and validate
tazuna.yaml. - If
spec.context_matchesis set, match against the current-context. Abort immediately on mismatch. - Filter by
--tags. - Walk
manifests[]in declaration order. - Hand each Manifest to its corresponding Manager and apply it to the cluster.
- Execute each Manifest’s
tests. - After all Manifests are applied, execute
spec.tests(overall Tests).
Flag
In addition to global flags, the following are accepted.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--tags | -t | []string | [] | Limits the processing target to Manifests with at least one of the specified tags (OR evaluation). |
--no-cache | - | bool | false | For type: oras Manifests, always refetch from the registry without using the cache. |
--offline | - | bool | false | For 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
Related
- Evaluated
context_matches - Filter spec:
manifests[].tags - Verify rendering before apply:
tazuna build - Remove existing resources:
tazuna destroy
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
- Load and validate
tazuna.yaml. - Filter by
--tags. - 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.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--tags | -t | []string | [] | Limits the processing target to Manifests with at least one of the specified tags (OR evaluation). |
--no-cache | - | bool | false | For type: oras Manifests, always refetch from the registry without using the cache. |
--offline | - | bool | false | For 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 -
Related
- To apply:
tazuna apply - Differences:
tazuna state diff
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
- Load
tazuna.yaml. - Run validation against the file and all expanded
manifests[]. - If no problems, write
okto stdout and exit with status 0. - With
--fix, auto-number Manifests whosenameis unset, write backtazuna.yaml, and writefixed: <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.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--fix | - | bool | false | Auto-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
Related
- Detailed validation rules:
tazuna.yamlschema
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
-
Load and validate
tazuna.yaml. -
If
spec.context_matchesis set, match against the current-context. Abort immediately on mismatch. -
If
--forceis 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? -
Unless the environment variable
TAZUNA_DESTROY_EXECUTABLEistrue, only log output happens and the command exits without touching the cluster. -
Only when both guards pass: applies the
--tagsfilter, 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.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--force | - | bool | false | Skips 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 | - | bool | false | For type: oras Manifests, always refetch from the registry without using the cache. |
--offline | - | bool | false | For type: oras Manifests, forbid access to the registry. |
--no-cache and --offline cannot be specified together.
Environment Variables
| Environment Variables | Value | Description |
|---|---|---|
TAZUNA_DESTROY_EXECUTABLE | true | Unless 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
Related
- Evaluated
context_matches - The applying side:
tazuna apply
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
- Load
tazuna.yaml. - Reads the State ConfigMap corresponding to each Manifest’s
name(tazuna-state-<manifest-name>in thetazunanamespace). - 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
Related
- View differences:
tazuna state diff - Apply differences:
tazuna state sync - Terminology of State: Glossary - State
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
- Load
tazuna.yaml. - For each Manifest, call the Manager’s Build to construct “the resources that should currently be generated from
tazuna.yaml.” - Reconcile with the in-cluster State and classify each resource into one of the following.
| Diff type | Meaning |
|---|---|
added | Present in the Build result, absent from State |
modified | Present in both, but with different content hashes |
removed | Present in State, absent from the Build result |
always-sync | Classification 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
Related
- Apply:
tazuna state sync - For the full rendering result, see
tazuna build - Terminology: Diff type / ContentHash
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
- Load
tazuna.yaml. - For each Manifest, call Build and compute the difference against State.
- Reflect resources classified as
added/modified/always-syncinto the cluster. - Resources classified as
removedare skipped by default. They are deleted only whenTAZUNA_STATE_SYNC_DELETE=trueis set. - 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.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--atomic | - | bool | false | If an error occurs, exit without updating State. |
Environment Variables
| Environment Variables | Value | Description |
|---|---|---|
TAZUNA_STATE_SYNC_DELETE | true | Delete 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
Related
- View differences:
tazuna state diff - Terminology: Diff type / always-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
- Narrow down the Secrets in
--namespace(defaultdefault) by--label-selector/--name-regex. - Write the data of each Secret out to the 1Password
--vaultas an Item. - Emit a GenesisSecret YAML referencing that Item to
--dump-dir(default.). - 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.
| Flag | Type | Default | Required | Description |
|---|---|---|---|---|
--op-host | string | - | Yes | Host part of the 1Password service-account URL (e.g. example.1password.com). |
--namespace | string | default | - | The Kubernetes namespace where the target Secrets exist. Shell completion enumerates the actual cluster namespaces. |
--label-selector | string | "" | - | A label selector to narrow down target Secrets. Example: app=foo,tier=db. |
--name-regex | string | "" | - | A regular expression on the name of target Secrets. |
--vault | string | "" | - | The 1Password vault name. Shell completion enumerates the actual vaults. |
--note | string | "" | - | Note attached to the generated 1Password Item. |
--dump-dir | string | . | - | Output directory for the generated GenesisSecret YAML. |
--dry-run | bool | false | - | 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
Related
- Reference the generated YAML from
tazuna.yamlas atype: genesissecretManifest. - Terminology: GenesisSecret / Provider (SecretProvider)
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
- Load and validate
tazuna.yaml. - Walk every Manifest after
includesexpansion and aggregatetagsinto atag name → list of Manifest namesmap. - Sort by tag name and output to stdout. The format is:
<tag>:
- <manifest-name>
- <manifest-name>
- If
--tagsis given, the output is narrowed to those tag names.
No cluster access.
Flag
In addition to global flags, the following are accepted.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--tags | -t | []string | [] | Narrows the output to the specified tag names. |
Examples
tazuna tags
tazuna tags -f tazuna.yaml
tazuna tags --tags frontend,backend
Related
- Filter spec for
--tags:manifests[].tags - To apply with filtering:
tazuna apply
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.devfor local builds.<commit>— the commit hash at release time.nonewhen not injected.<date>— the build timestamp at release time.unknownwhen 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 withmake build, and repository layout. - Testing — the 3 layers of unit / integration / e2e and their
maketarget correspondence, KinD cluster preparation. - Documentation — the structure of
docs/, previewing withmdbook, updating the English translation viapo/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.
- Branch off
mainfor your working branch. - Keep each change small and focused on one topic.
- Before pushing, run
make testandmake lintlocally and ensure they pass. - 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.
| Target | Contents |
|---|---|
make build | Generates ./tazuna via go build . |
make install | After make build, runs sudo mv tazuna /usr/local/bin |
make format | go fmt ./... |
make lint | golangci-lint run |
make test | go test ./... (unit tests) |
make test-integration | go test -tags=integration ./... |
make test-e2e | After make build && make devenv-create, runs go test -tags=e2e -count=1 ./test/e2e/... |
make test-all | unit + integration + e2e |
make cover | Runs tests with -race -covermode=atomic -coverprofile=coverage.out, then outputs a summary |
make all | Runs format → test → build → lint in order |
make devenv-create | Stands up a kind cluster named tazuna (or switches context if one already exists) |
make devenv-destroy | kind 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.
| Path | Role |
|---|---|
main.go | Entry 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
| Layer | Command | Targets | Prerequisites |
|---|---|---|---|
| unit | make test | go test ./... | None. Runs on every push / PR via the CI workflow in GitHub Actions. |
| integration | make test-integration | go test -tags=integration ./... | None. Additional tests tagged with the integration build tag are targets. |
| e2e | make test-e2e | go test -tags=e2e -count=1 ./test/e2e/... | KinD cluster. Internally runs make build && make devenv-create. |
| All | make test-all | unit → integration → e2e | Same as e2e |
| Coverage | make cover | Runs unit tests with -race -covermode=atomic -coverprofile=coverage.out, then outputs a summary | None |
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.
make build(build./tazuna)make devenv-create(stand up the KinD clustertazuna, or switch context if one already exists)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
| Item | Value |
|---|---|
| Cluster name | tazuna |
| kubeconfig context | kind-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.yamlfield 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
- Workflow:
.github/workflows/release.yaml - Trigger:
push: tags: ["*"](any tag push) - Publish target: GitHub Releases
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.
| Axis | Value |
|---|---|
| GOOS | linux / darwin |
| GOARCH | amd64 / arm64 |
| CGO | CGO_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
- Verify
mainis stable (CI green). - Cut and push a tag in
vX.Y.Zform. - Confirm the
Releaseworkflow runs and the release is published. - 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.