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 / plan / status / state.
Main commands:
tazuna apply— appliestazuna.yamlto the cluster and writes State back.--syncapplies only the diff,--sync --prunedeletes obsolete resources, and--sync --atomichandles rolling State back on a mid-run error.tazuna plan— shows, as a unified diff, which fields would change if you apply.tazuna status— lists the readiness of the managed resources recorded in State.tazuna state diff/tazuna state drift— detect declared drift and live drift separately.tazuna destroy— removes Tazuna-managed resources from the cluster.
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 State’s _metadata.gitCommitHash during tazuna apply. 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. - DAG Execution via
dependsOn— how the parallel execution model usingdependsOnis derived, and the background behind retiringtype: parallel. - 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 / plan / |
| status / state list/diff/drift / ... |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Runner |
| - load tazuna.yaml / expand includes |
| - verify context_matches / filter by tags |
| - resolve dependsOn into parallel layers |
| - dispatch each manifest to a Manager |
+--------------------------------------------------+
|
+-------------+-------------+
v v
+---------------------+ +---------------------+
| Manager | | Test plugin |
| | | wait-until / |
| kustomize / | | exist-nonexist |
| helmfile / | +---------------------+
| oras / |
| genesissecret |
+---------------------+
|
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, plan, status, state list, state diff, state drift, 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 - Analyzing
dependsOnand splitting into layers that can run in parallel - Dispatching each manifest to its corresponding Manager (manifests within a layer run in parallel via goroutines)
- Running Test plugins after apply
- Writing back to the State ConfigMap after a successful 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. For details on dependsOn, see DAG Execution via dependsOn.
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, written on a successful apply; state list / state diff / state drift / status all operate from here.
Secret provider
A component that abstracts “where secrets are fetched from,” referenced by GenesisSecret and helmfile’s vars.op. Two implementations are built in: 1Password (onepassword) and envfile (envfile). You can declare multiple providers in tazuna.yaml’s spec.providers[] and select between them by name via spec.provider on the GenesisSecret side. See Secret provider for details.
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. - Analyze
dependsOnand split into layers that can run in parallel (one manifest at a time in declaration order if unused). - Within each layer, hand the
manifestsnot excluded by--tagsto their corresponding Manager in parallel via goroutines. - Each Manager internally invokes kustomize / helmfile /
oras pull/ 1Password retrieval and so on, and reflects the result into the cluster. - If there are per-manifest or whole-file Tests, the Test plugin runs.
- Write the fingerprints of manifests whose apply and tests succeeded back to the State store.
tazuna apply --sync uses steps 1-5 above, builds a State diff from each Manager’s Build result, and applies only the added or changed entries. With --prune, it also deletes resources that are in State but absent from the Build.
DAG Execution via dependsOn
Tazuna has two execution models for tazuna.yaml’s manifests[]: a mode that runs them sequentially in declaration order, and a lightweight DAG mode using dependsOn. This page summarizes how the two are switched and what ordering guarantees and parallelism the DAG mode provides.
Switching rules
When no Manifest in tazuna.yaml declares dependsOn, manifests are applied one at a time in declaration order as before. This is the default behavior, kept for backward compatibility.
spec:
manifests:
- name: cni
type: kustomize
path: ./cni
- name: ingress
type: helmfile
path: ./ingress
- name: app
type: kustomize
path: ./app
In this case Tazuna applies them one at a time in the order cni -> ingress -> app.
If even one Manifest has a dependsOn, the Runner switches to DAG mode.
spec:
manifests:
- name: cni
type: kustomize
path: ./cni
- name: cert-manager
type: helmfile
path: ./cert-manager
dependsOn: [cni]
- name: ingress
type: helmfile
path: ./ingress
dependsOn: [cni]
- name: app
type: kustomize
path: ./app
dependsOn: [cert-manager, ingress]
In this case, the Runner builds the following layers.
| Layer | Manifest | Execution |
|---|---|---|
| Layer 1 | cni | Runs only one |
| Layer 2 | cert-manager / ingress | Parallel execution |
| Layer 3 | app | Runs only one |
Topological sort
In DAG mode, topological sorting is done with Kahn’s algorithm. Each iteration takes the set of nodes with in-degree 0 as “one layer”; after that layer finishes, it decrements the in-degree of the nodes that depended on them, and this repeats.
Manifests within a layer are stably sorted so they are emitted in their original declaration order, so the output order does not waver no matter how many times you run the same tazuna.yaml (the parallel execution itself is unordered).
Parallelism within a layer
Manifests that fall into the same layer are applied in parallel via goroutines. Because there is a barrier at each layer boundary, ordering guarantees such as “install app only after waiting for cert-manager” are not broken.
If apply fails within a layer, errors are aggregated after waiting for the other goroutines in that layer to finish. Mid-run cancellation is not currently performed.
Validation
During tazuna check and tazuna apply, dependsOn is subject to the following checks.
- Each listed Manifest name must exist in the full set of Manifests after
includesexpansion. - A Manifest must not include itself in
dependsOn. - The dependency graph must contain no cycle.
If any of these is violated, Tazuna exits with an error without touching the cluster at all.
Why dependsOn rather than parallel
Earlier versions of Tazuna had a Manifest type called type: parallel. It was “a box that runs child Manifests in parallel,” but it had the following problems.
- Dependencies could only be expressed through nested
parallelstructures, making complex cases hard to write. - Manifests under
parallelwere outside the scope of state management (State / drift detection). - “Ordering dependency,” “parallelism,” and “grouping” were all mixed into a single field.
dependsOn rewrites this in the vocabulary of a graph: nodes (Manifests) and edges (dependencies). Parallelism is derived automatically from the dependency graph, so users only need to declare “order” and can leave parallelization to the runtime. type: parallel has been removed in this refactor.
Best practices
- Declare a dependency only on a “Manifest that needs its prerequisite resources to be Ready.” If you merely want to group manifests,
tagsis more appropriate. - Before writing a long chain, consider once whether “this is really just waiting on a CRD, so the Test plugin’s
WaitUntilcould serve instead.” - To avoid circular dependencies, the trick is to use
dependsOnonly in one direction, from lower to higher layers (CNI -> cert-manager -> ingress -> app).
Related
- Overall architecture: Overall Architecture
- Schema:
tazuna.yaml- Manifest - The apply itself:
tazuna apply
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 four types: kustomize / helmfile / genesissecret / oras. The former parallel has been replaced by dependsOn-based DAG execution and removed.
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)
An interface that abstracts the source from which GenesisSecret retrieves secrets. Two implementations are built in - 1Password (onepassword) and envfile (envfile) - and you can declare several in tazuna.yaml’s spec.providers[] and select between them by name via spec.provider on the GenesisSecret side. See Secret provider for details.
default-op
The name reserved for the built-in 1Password provider. It is used as a backward-compatibility fallback when a GenesisSecret’s spec.provider is an empty string. This name cannot be re-declared in spec.providers[].
dependsOn
On a manifests[] entry, the list of Manifest names that must complete before that Manifest is applied. If even one dependsOn is written in tazuna.yaml, the Runner switches to DAG mode and runs Manifests at the same dependency depth in parallel. See DAG Execution via dependsOn for details.
Layer (DAG layer)
The set of Manifests that can run in parallel, derived as the result of resolving dependsOn. Manifests in the same layer do not depend on one another, and the Runner applies them in parallel via goroutines. Because a barrier is inserted at each layer boundary, ordering guarantees across layers are not broken.
live-drifted / live-missing
The drift categories emitted by tazuna state drift. live-drifted is a resource whose State hash and live-cluster hash do not match; live-missing is a resource recorded in State but NotFound on the live cluster.
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.yamlgenerated bytazuna initpaired with a kustomize directory - 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. Generate and write tazuna.yaml
Next, prepare tazuna.yaml, the single input file for Tazuna. You can hand-write it from scratch, but it is easier to start by generating the skeleton with tazuna init.
cd my-cluster
tazuna init
my-cluster/tazuna.yaml is created with the following content (comment lines omitted; the value of minimumSupportedTazunaVersion becomes the version of the tazuna used to generate it).
apiVersion: tazuna.pepabo.com/v1
kind: Tazuna
spec:
minimumSupportedTazunaVersion: "1.4.0"
manifests: []
The benefit of using tazuna init is that, in addition to apiVersion / kind, it automatically fills in minimumSupportedTazunaVersion. This is “the minimum version of tazuna required to process this tazuna.yaml”; an older tazuna below it that tries to process the file stops with an error. This makes accidents less likely even when tazuna versions are mixed across teams or repositories (see tazuna init and minimumSupportedTazunaVersion for details).
After that, replace the generated empty manifests: [] with the nginx you want to install this time.
my-cluster/tazuna.yaml:
apiVersion: tazuna.pepabo.com/v1
kind: Tazuna
spec:
minimumSupportedTazunaVersion: "1.4.0"
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 diffandtazuna state drifton a periodic schedule to visualize the two kinds of drift, output formats, and how to wire up notifications. - CI Pipeline — the typical setup that runs
check/build/planon PRs andapplyonmainmerges, where to placedestroy, and choosing state-sync options. - Observability — configuring OpenTelemetry trace export via
--otlp-endpoint, the 3-layer trace tree, and the main span attributes.
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 apply --sync --prune— if you remove a Manifest fromtazuna.yamland then runapply --sync --prune, it is deleted under theremovedclassification (seetazuna apply). 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 summarizes how to set up operations that periodically run tazuna state diff and tazuna state drift to visualize drift. For the command specs, see tazuna state diff and tazuna state drift; for the spec of State’s contents, see Internal Structure of State.
Two kinds of drift
There are two kinds of drift Tazuna can see, differing in direction.
| Name | What it diffs | Command used for detection |
|---|---|---|
| Declared drift | Resources that should be generated from tazuna.yaml vs State | tazuna state diff |
| Live drift | State vs the actual objects on the live cluster | tazuna state drift |
Declared drift captures “you updated tazuna.yaml but haven’t applied it” and “you removed a Manifest but it still remains on the cluster.” Live drift captures “you didn’t change tazuna.yaml, but someone ran kubectl apply directly” and “it was deleted by hand on the cluster side.”
What we call drift (declared 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(add--syncto apply only the diff). - 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 apply --sync --prune.
- GenesisSecret’s
always-sync: this is not drift, so it is fine to exclude it from notifications.
Detecting live drift
Whereas tazuna state diff looks only at “Build result vs State,” tazuna state drift compares “State vs the live cluster.” Even when you have not changed tazuna.yaml, it can detect resources rewritten by hand with kubectl apply or removed with kubectl delete.
# Example: check live drift every 30 minutes and post to Slack
if ! tazuna state drift -f tazuna.yaml | tee drift.txt | grep -q "No drift detected."; then
curl -X POST "$SLACK_WEBHOOK_URL" --data "$(jq -Rs '{text: .}' < drift.txt)"
fi
There are two output categories: live-drifted (hash mismatch) and live-missing (gone from the cluster).
Monitoring both live drift and declared drift is the recommended setup. The former surfaces operational mistakes by cluster operators, while the latter surfaces reach problems in the GitOps pipeline - each separately.
Related
- Command spec:
tazuna state diff/tazuna state drift/tazuna apply - 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, tazuna plan | plan only (read-only) |
| Apply | Reflect the contents of main into the cluster | tazuna apply (with --sync / --prune as needed) | 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).
If you can grant CI read access to the cluster, running tazuna plan on PRs lets you paste “which fields actually change” into a PR comment as a unified diff.
- name: tazuna plan
if: github.event_name == 'pull_request'
run: tazuna plan -f tazuna.yaml > plan.txt
- name: post plan to PR
if: github.event_name == 'pull_request'
run: |
gh pr comment "$PR_NUMBER" --body-file <(printf '```\n%s\n```\n' "$(cat plan.txt)")
Because tazuna plan is read-only, its advantage is that it fits easily within a PR’s permission boundary.
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 operating modes
tazuna apply has three operating modes (see tazuna apply for details).
| Mode | When what runs | Drift detection |
|---|---|---|
tazuna apply | Runs every Manifest in tazuna.yaml through its Manager and saves state | No (always overwrites) |
tazuna apply --sync | Compares the Build result with State, and only reflects the differences (added / modified / always-sync) | Yes |
tazuna apply --sync --prune | In addition to the above, deletes resources present in State but absent from the Build result | Yes |
Rough guidance:
- Bootstrap phase / small number of Manifests:
tazuna applyis simple and predictable. - Manifest count grows and a single
applybecomes heavy: narrow to the diff only with--sync. - You want automatic
removeddeletion:--sync --prune. Weigh it against the risk of accidental deletion.
Whether to Use --atomic
With tazuna apply --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 the binary “either everything went in or nothing did,” but State-level consistency is preserved. See tazuna apply 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 plan - Incident prevention: Operating
tazuna destroy - Drift detection: Drift Monitoring
- tracing: Observability
Observability
Tazuna has built-in support for OpenTelemetry-based tracing. Every CLI command can export traces via OTLP/gRPC, which you can use for timing measurements and error tracking in CI and cluster operations.
Tracing is opt-in. If you pass no flags, a no-op tracer is used and there are zero external dependencies.
Enabling
The following global flags have been added to the root command.
| Flag | Type | Default | Description |
|---|---|---|---|
--otlp-endpoint | string | "" | The endpoint of the OTLP/gRPC collector (e.g. localhost:4317). No-op when empty. |
--otlp-insecure | bool | true | Use plaintext gRPC for the OTLP exporter (no TLS). |
Only commands passed --otlp-endpoint fire traces. To keep a short-lived CLI from hanging on the collector, shutdown has a 5-second timeout.
# Point it at something that can receive OTLP/gRPC, such as Jaeger / Tempo / OTel Collector
tazuna apply -f tazuna.yaml --otlp-endpoint=localhost:4317
tazuna plan -f tazuna.yaml --otlp-endpoint=localhost:4317
Trace structure
Tazuna emits a 3-layer trace tree.
tazuna.Apply / tazuna.Plan / tazuna.Status / tazuna.StateDrift ← Runner top-level span
└── tazuna.ApplyToCluster ← Runner internal span
└── Kustomize.Apply / Helmfile.Apply / GenesisSecret.Apply / ORAS.Apply
← Manager span
- Runner span (tracer name
tazuna/runner) - measures the overall execution time of a top-level command. It is the first span opened from the CLI. - Manager span (tracer name
tazuna/manager) - measures each Manifest-type-specific operation (equivalent tokubectl apply/helmfile sync/ oras pull / etc.) as one span apiece.
Because the Runner span and Manager span names are kept separate, it is easy to analyze by service / operation in Datadog / Jaeger and the like.
Main span attributes
| Attribute | When it is attached | Example value |
|---|---|---|
tazuna.yaml.path | Runner span | ./tazuna.yaml |
manifests.count | Runner span | 12 |
apply.sync | tazuna.Apply span | true / false |
apply.prune | tazuna.Apply span | true / false |
apply.atomic | tazuna.Apply span | true / false |
manifest.name | Manager span | ingress-nginx |
manifest.type | Manager span | kustomize / helmfile / oras / genesissecret |
manifest.path | Manager span | ./kustomize/ingress |
genesissecret.provider | GenesisSecret.Apply span | primary-op / default-op |
On error, the span is marked with an error status and the message is attached via span.RecordError.
Using it in CI
Using a SaaS collector such as Datadog / Honeycomb / Grafana Cloud lets you track apply duration and failure rate over time. An example of passing it from CI:
# GitHub Actions
- name: tazuna apply (with tracing)
env:
OTEL_EXPORTER_OTLP_HEADERS: api-key=${{ secrets.OTEL_API_KEY }}
run: tazuna apply -f tazuna.yaml --otlp-endpoint=otel.example.com:4317
Since it is a short-lived CLI, filtering by service.name=tazuna on the collector side should let you see the span tree for each CI run directly.
Related
- Flag spec: CLI - Global flags
- Drift monitoring: Drift Monitoring
- CI integration: CI Pipeline
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. - Secret provider — how to declare the Secret providers referenced by GenesisSecret: choosing between the built-in
onepassword/envfile, declaringspec.providers[], and thedefault-opreserved name. - Per Manifest type — one page each for the four types
kustomize/helmfile/oras/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 here into manifest-type-specific fields (kustomize / helmfile / genesissecret / oras) or Test plugin fields. 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 |
|---|---|---|---|---|
minimumSupportedTazunaVersion | string | - | "" | The minimum version (semver) of the tazuna binary required to process this tazuna.yaml. See minimumSupportedTazunaVersion for details. |
manifests | [Manifest] | Yes | - | The array of Manifests Tazuna processes. An empty array is not allowed. If dependsOn is used, they run in the layer order derived from the dependency graph; otherwise in declaration order. |
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. |
providers | [ProviderConfig] | - | [] | The list of Secret provider declarations referenced by GenesisSecret. Write it when you use something other than the built-in default-op. |
minimumSupportedTazunaVersion
- Declares, in semver form, the minimum version of the tazuna binary that can safely process this
tazuna.yaml(e.g.1.4.0). A leadingvis accepted (v1.4.0also works). - On any operation that loads
tazuna.yaml(apply/destroy/plan/build/check/status/tags/state *, etc.), if the running tazuna’s version is below this value it exits with an error. This prevents accidentally processing atazuna.yamlthat requires newer syntax with an old binary. - When unset (empty string), there is no constraint.
- If the value is not a valid semver, it is a configuration error.
- When the running tazuna is a local build (a non-semver version such as
dev), the comparison is skipped, so that local development is not blocked by this gate.
spec:
minimumSupportedTazunaVersion: "1.4.0"
manifests: []
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 / 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. |
dependsOn | [string] | - | [] | The list of Manifest names that must complete before this Manifest is applied. See dependsOn for details. |
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. |
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) |
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).
dependsOn
- An array of Manifest names that must have completed before this Manifest is applied.
- Must be a name contained in the full set of Manifests after
includesexpansion. - It cannot include itself (self-dependency is rejected as a special case of a cycle).
- The overall dependency graph must not contain a cycle.
- If even one
dependsOnis used intazuna.yaml, the Runner switches to DAG mode and runs Manifests at the same dependency depth in parallel. If none is used, it runs one at a time in declaration order as before.
See DAG Execution via dependsOn for details and motivation.
Example:
spec:
manifests:
- name: cni
type: kustomize
path: ./cni
- name: cert-manager
type: helmfile
path: ./cert-manager
dependsOn: [cni]
- name: ingress
type: helmfile
path: ./ingress
dependsOn: [cni]
- name: app
type: kustomize
path: ./app
dependsOn: [cert-manager, ingress]
Providers
spec.providers[] is the list of Secret provider declarations referenced by GenesisSecret. Write it when you use something other than the built-in default-op (1Password), or when you want to line up multiple providers and select between them.
spec:
providers:
- name: primary-op
type: onepassword
onepassword: {}
- name: ops-envfile
type: envfile
envfile:
path: ./secrets/ops.env
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | - | The name referenced from a GenesisSecret’s spec.provider. default-op is a reserved name and cannot be used. |
type | string | Yes | - | The provider type. onepassword or envfile. |
onepassword | object | △ | null | Additional config used when type: onepassword (currently an empty object). |
envfile | object | △ | null | Additional config used when type: envfile. Has path. |
See Secret provider for details.
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 / 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. |
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. - If
spec.minimumSupportedTazunaVersionis set, it must be a valid semver and the running tazuna’s version must be at least that value (local builds skip the comparison). Note that this check runs not only oncheckbut on every operation that loadstazuna.yaml. - For each element of
spec.manifests[]:- When
includesis absent:pathandtypemust be set. typemust be a known value (kustomize/helmfile/genesissecret/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.manifests[].dependsOnmust reference only existing Manifest names and contain no self-reference or circular dependency.spec.context_matchesmust be compilable as regular expressions.spec.context_match_modemust be one ofor/and/ unset.- For each element of
spec.providers[]:namemust be unique and non-empty, must not bedefault-op,typemust be one ofonepassword/envfile, and it must have config consistent withtype. - For
type: helmfile: each value inhelmfile.varsmust satisfy one ofenv/static/op(see the helmfile reference page). - 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 | - | "" | The name of the source Provider. Specify one of the names declared in tazuna.yaml’s spec.providers[], or the built-in default-op. When empty, it falls back to default-op for backward compatibility. See Secret provider for details. |
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 | Conditional (*) | null | Specify this to write the retrieved values to standard output in dotenv format (KEY=VALUE, one pair per line, sorted). The field can currently be an empty object {}. |
(Note) Each element of outputs[] must specify exactly one of kubernetesSecret or stdout. Specifying both at once, or leaving both null, is a validation error.
stdout
An output with stdout: {} writes the values retrieved from the Provider to standard output in dotenv format. It is useful for migrating values managed in 1Password to an envfile, or for cases where you want to load them as environment variables via shell eval.
outputs:
- stdout: {}
Output format:
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
The line order is stable, in ascending order of key name. Because it does not touch the cluster, even an operator without kubectl permissions can run it.
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
Secret provider
A Secret provider abstracts where a type: genesissecret Manifest fetches secrets from. You declare providers in tazuna.yaml’s spec.providers[] and bind them by specifying that name in each GenesisSecret YAML’s spec.provider.
This page summarizes how the provider registry works and how to declare the two built-in providers, onepassword and envfile.
Registry basics
At startup the Runner assembles a provider registry. The registry contains two kinds of entries.
- Each provider declared in
tazuna.yaml’sspec.providers[] - The built-in
default-op(for 1Password)
default-op is always registered in the registry and does not need to be declared. When a GenesisSecret’s spec.provider is an empty string, this default-op is chosen as the backward-compatibility fallback.
When you write a name in a GenesisSecret’s spec.provider, the provider is looked up from the provider registry by that name and used.
# GenesisSecret YAML
apiVersion: tazuna.pepabo.com/v1
kind: GenesisSecret
spec:
provider: ops-envfile # <- match this with spec.providers[].name in tazuna.yaml
secrets:
- uri: env://ignored
items:
DATABASE_URL:
mapTo: DATABASE_URL
outputs:
- kubernetesSecret:
namespace: default
name: app-config
Declaring spec.providers[]
Write it in tazuna.yaml’s spec.providers[].
# tazuna.yaml
spec:
providers:
- name: primary-op
type: onepassword
onepassword: {}
- name: ops-envfile
type: envfile
envfile:
path: ./secrets/ops.env
manifests:
- name: app-config
type: genesissecret
path: ./genesissecrets/app-config.yaml
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | - | The name referenced from a GenesisSecret’s spec.provider. default-op is reserved and cannot be used. |
type | string | Yes | - | The provider type. Currently onepassword or envfile. |
onepassword | object | △ | null | Additional config used when type: onepassword. |
envfile | object | △ | null | Additional config used when type: envfile. |
Config inconsistent with type (e.g. envfile: written even though type: onepassword) is rejected by validation.
type: onepassword
A provider that retrieves values from 1Password items. The built-in implementation calls the op CLI to pull values out. The onepassword additional config can currently be an empty object (the field is split out only to leave room for future extension).
spec:
providers:
- name: primary-op
type: onepassword
onepassword: {}
On the GenesisSecret side, write spec.secrets[].uri in the form op://<host>/<vault>/<item>. See GenesisSecret schema - uri format for details.
type: envfile
A provider that reads values from a local file in dotenv format (KEY=VALUE, one pair per line). It is useful for unit tests that don’t depend on 1Password, cases where you want to feed local secrets in CI, and situations right before bootstrap where 1Password authentication is not yet available.
spec:
providers:
- name: ops-envfile
type: envfile
envfile:
path: ./secrets/ops.env
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | - | The path to the dotenv file. Resolved as a path relative to the directory where tazuna.yaml itself resides. |
The contents of ./secrets/ops.env are one pair per line, as follows.
DATABASE_URL=postgres://localhost/app
API_TOKEN=ghp_xxx
On the GenesisSecret side, spec.secrets[].uri is not used (because envfile returns the key-values of a single file as-is). Make the keys of items match the key names in the envfile.
spec.provider resolution flow
The handling of a GenesisSecret’s spec.provider value is as follows.
Value of spec.provider | Resolved provider |
|---|---|
"" (empty / unset) | The built-in default-op (1Password) |
"default-op" | The built-in default-op |
| Any other name | Looks up the entry with the same name from tazuna.yaml’s spec.providers[] |
If the referenced name does not exist in spec.providers[], apply fails with an error. At the tazuna check stage too, references to undefined names will be rejected by validation in the future.
Validation
During tazuna check and at the startup of each command, spec.providers[] is subject to the following checks.
- Each
namemust be unique. namemust not be an empty string.namemust not bedefault-op(a reserved name).typemust be one ofonepassword/envfile.- There must be no config inconsistent with
type(such asenvfile:attached totype: onepassword). envfile.pathis required whentype: envfile.
Related
- The whole of GenesisSecret: GenesisSecret Schema
- The schema-side entry:
tazuna.yaml- Providers - Terminology: Provider (SecretProvider)
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 a ConfigMap inside the cluster and is the starting point for tazuna state list / tazuna state diff / tazuna state drift / tazuna status / tazuna plan. Writes happen automatically on a successful tazuna apply (regardless of --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 apply --sync classify the comparison between the Build result and existing State by Diff type.
| Diff type | Meaning | apply --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 with --prune |
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 drift/tazuna status/tazuna plan/tazuna apply --sync - Terminology: State / State key / ContentHash / Diff type / always-sync
Per-Manifest-Type Reference
tazuna.yaml’s manifests[].type can take four 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 / dependsOn / includes / tests), see tazuna.yaml schema - Manifest.
The former
type: parallelhas been removed in this refactor, replaced by thedependsOn-based DAG execution model. To run Manifests in parallel, line them up as ordinary Manifests and leavedependsOnempty; they fall into the same layer and run in parallel. See DAG Execution viadependsOnfor details.
Contents
kustomize— apply resources rendered by kustomizehelmfile— apply the result of helmfile templateoras— pull an artifact from an OCI registry and delegate to helmfile / kustomizegenesissecret— 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 |
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. |
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: 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 process each entry of outputs[]. kubernetesSecret is CreateOrUpdated; stdout: {} is written to standard output in dotenv format (KEY=VALUE, sorted). |
Destroy | Read the GenesisSecret YAML (Provider retrieval also runs) and delete the Secret matching the namespace / name of each outputs[].kubernetesSecret. A stdout output does nothing. |
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.
Selecting a Provider
In a GenesisSecret YAML’s spec.provider you can specify which Secret provider to retrieve values from. When empty / unset, the built-in default-op (1Password) is used.
If you declare multiple providers in tazuna.yaml’s spec.providers[], you can mix onepassword and envfile. See Secret provider 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 init— generate an includes-basedtazuna.yamlskeletontazuna apply— applytazuna.yamlto the cluster (writes state back)tazuna build— emit the rendering result without touching the clustertazuna check— validatetazuna.yamltazuna destroy— delete Tazuna-managed resources from the clustertazuna plan— diff the Build result against the live cluster field by fieldtazuna status— show the readiness of managed resources recorded in Statetazuna state list— list the resources recorded in Statetazuna state diff— show the difference between the Build result and Statetazuna state drift— detect drift between State and the live clustertazuna 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. |
--otlp-endpoint | - | string | "" | The OpenTelemetry OTLP/gRPC endpoint (e.g. localhost:4317). When empty, a no-op tracer is used and it runs with zero external dependencies. |
--otlp-insecure | - | bool | true | Use plaintext gRPC for the OTLP exporter (no TLS). |
--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/plan/status/state list/state diff/state drift/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.
In addition, on every command that loads tazuna.yaml (including plan / status / state list / state diff / state drift on top of the above), spec.minimumSupportedTazunaVersion is compared against the running tazuna’s version at load time. If the running version is below it, the command exits with an error immediately. See tazuna.yaml schema - minimumSupportedTazunaVersion for details.
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. |
KUBECONFIG | Path | All cluster-touching commands | Follows the same kubeconfig resolution rules as ordinary kubectl. |
The old
TAZUNA_STATE_SYNC_DELETEenvironment variable has been removed along withtazuna state sync. To delete resources in theremovedcategory, usetazuna apply --sync --prune.
tazuna init
Generates a minimal includes-based tazuna.yaml skeleton. It suits the first step of bringing a new repository or component under Tazuna management.
tazuna init [-f tazuna.yaml] [--force]
Behavior
- If the output target (
-f/--file-path, defaulttazuna.yaml) already exists and--forceis not given, it exits with an error (so an existing file is not accidentally clobbered). - Writes
tazuna.yamlwith the following content and printscreated: <path>to stdout.- Sets
apiVersion/kindto the canonical values. - Pins
spec.minimumSupportedTazunaVersionto the version of the tazuna that generated it. As a result, an older tazuna binary that tries to process this file stops with an error. spec.manifestsis empty ([]). A commented example for loading each component’stazuna.yamlvia includes is attached.
- Sets
The generated skeleton (the version depends on the tazuna that generated it):
apiVersion: tazuna.pepabo.com/v1
kind: Tazuna
spec:
# The minimum version (semver) of tazuna required to process this tazuna.yaml.
# A tazuna binary below it exits with an error to prevent misapplication.
minimumSupportedTazunaVersion: "1.4.0"
# Load each component's tazuna.yaml via includes.
# Add includes entries to manifests as in the example below.
#
# manifests:
# - name: infra
# includes:
# - path: ./infra/tazuna.yaml
# - path: ./addons/tazuna.yaml
manifests: []
The skeleton as generated passes tazuna check as-is.
About version pinning
- If the running tazuna has a semver version (a release build), that value is normalized and embedded into
minimumSupportedTazunaVersion(a leadingvis stripped). - When run with a local build (a non-semver version such as
dev), the placeholder0.0.0is embedded. Regenerate with a release build, or manually rewrite it to an appropriate version.
For the comparison rule of minimumSupportedTazunaVersion itself, see tazuna.yaml schema - minimumSupportedTazunaVersion.
Flag
In addition to global flags, the following are accepted.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--force | - | bool | false | Overwrites the output target even if it already exists. |
No arguments are accepted.
Examples
tazuna init
tazuna init -f infra/tazuna.yaml
tazuna init --force
Related
- Meaning of the generated fields:
tazuna.yamlschema - How to use includes:
tazuna.yamlschema - Using includes
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]
[--sync [--prune] [--atomic]]
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. - Split
manifests[]into layers derived fromdependsOn. If nodependsOnis used, the number of layers equals the number of manifests, giving behavior identical to the traditional sequential execution in declaration order. - Hand the manifests within each layer to their corresponding Manager in parallel and apply them to the cluster.
- After each Manifest applies successfully, run its
tests. - For Manifests whose apply and tests succeeded, write the content hash back to the state ConfigMap (the starting point for subsequent
state diff/state drift/plan). - After all Manifests are applied, execute
spec.tests(overall Tests).
For dependsOn behavior, see DAG Execution via dependsOn.
State integration (–sync / –prune / –atomic)
tazuna apply is designed to write the state ConfigMap even by default. The role of tazuna state sync, formerly a separate command, has been merged into apply with the --sync flag (see “Migration: replacing the old state sync” below).
| Flag | Behavior |
|---|---|
| None | Apply every Manifest unconditionally via the Manager’s Apply(), then save state. |
--sync | Switches to diff mode. Compares each Manager’s Build() result against the saved state and CreateOrUpdates only resources classified as added / modified / always-sync. |
--sync --prune | In addition to the above, Deletes resources that are in state but absent from the Build result (removed). Requires --sync. |
--sync --atomic | Defers each Manifest’s state save to the end. If even one error occurs partway, it exits without updating state at all. Requires --sync. |
Specifying --prune / --atomic without --sync is a validation error.
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. |
--sync | - | bool | false | Enables diff mode. Applies only the diff between state and the Build result. |
--prune | - | bool | false | Deletes resources present in state but absent from the Build result. Requires --sync. |
--atomic | - | bool | false | Saves state in a batch after all Manifests succeed. State is not changed on a mid-run error. Requires --sync. |
--no-cache and --offline cannot be specified together.
Examples
# Normal apply (state is saved automatically)
tazuna apply -f tazuna.yaml
# Filter by tags
tazuna apply -f tazuna.yaml --tags web,batch
# Diff mode: apply only resources that differ from state
tazuna apply -f tazuna.yaml --sync
# Diff mode + deletion of obsolete resources
tazuna apply -f tazuna.yaml --sync --prune
# Diff mode + atomic (roll state back on a mid-run error)
tazuna apply -f tazuna.yaml --sync --atomic
# Stream traces to an OTLP/gRPC endpoint
tazuna apply -f tazuna.yaml --otlp-endpoint=localhost:4317
Migration: replacing the old tazuna state sync
Earlier versions of Tazuna had a separate command, tazuna state sync, responsible for applying the diff between state and the Build result. In this refactor that command has been removed and merged into tazuna apply --sync. The correspondence is as follows.
| Old command | New command |
|---|---|
tazuna state sync | tazuna apply --sync |
TAZUNA_STATE_SYNC_DELETE=true tazuna state sync | tazuna apply --sync --prune |
tazuna state sync --atomic | tazuna apply --sync --atomic |
As a behavioral difference, the new apply saves state even without --sync (the old apply wrote no state at all). As a result, a single apply makes state list / state diff / state drift / plan / status always return meaningful results.
Related
- Evaluated
context_matches - Filter spec:
manifests[].tags - Verify rendering before apply:
tazuna build - For a field-level dry-run diff, see
tazuna plan - For readiness checks after applying, see
tazuna status - For State diffs, see
tazuna state diff/tazuna state drift - Remove existing resources:
tazuna destroy - DAG execution model: DAG Execution via
dependsOn
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 plan
Compares the result of Building the Manifests declared in tazuna.yaml against the state of the live cluster, and outputs, field by field, how things would change if you apply. It performs only read access to the cluster and changes nothing.
tazuna plan [-f tazuna.yaml] [--tags ...]
Behavior
- Load
tazuna.yaml. - Filter by
--tags. - Render each Manifest with its corresponding Manager’s
Build(). - Convert the rendered result into a set of
client.Objects. - Fetch each object from the cluster by GVK / namespace / name.
- Objects that could not be fetched (NotFound) are output as
+ to be created. - For objects that were fetched, output a unified diff of desired vs live, marked with
~.
Does not evaluate context_matches.
Why a client-side diff
Tazuna’s plan is implemented under the slogan of “server-side dry-run,” but the implementation is a client-side diff. This is the result of the following trade-off.
| Approach | Pros | Cons |
|---|---|---|
| server-side dry-run | Reflects admission webhooks / defaulting | The controller-runtime fake client does not fully support dry-run apply |
| client-side diff (adopted) | Reproducible in integration tests | Webhook / defaulting results are not visible |
Understand it as choosing “a plan that can be tested” even at some cost to accuracy. Fields rewritten by an admission webhook (mutation) and server-side defaulting are not reflected in the plan output.
Output Format
Manifest: nginx
+ Deployment/default/nginx-new (to be created)
~ ConfigMap/default/nginx-conf
spec:
replicas: 1
+ replicas: 3
Manifest: cert-manager
+ Issuer/cert-manager/letsencrypt-prod (to be created)
+ <Kind/ns/name> (to be created)— a resource that does not yet exist on the live cluster~ <Kind/ns/name>— a resource that exists but has field differences. The indented unified diff fromk8s.io/apimachinery/pkg/util/diff.Difffollows directly below.
If there are no differences, only the single line No changes detected. is emitted.
Fields excluded when computing the diff
To avoid noise, the following fields are stripped before comparing live and desired.
metadata.resourceVersion/uid/generationmetadata.managedFieldsmetadata.creationTimestamp/selfLinkstatus
Skipped Manifests
- Manifests with an empty
name type: genesissecret(being always-sync, it does not fit the concept of a field diff in plan)
Flag
In addition to global flags, the following are accepted.
| Flag | Alias | Type | Default | Description |
|---|---|---|---|---|
--tags | -t | []string | [] | Limits the plan target to Manifests carrying at least one of the specified tags (OR evaluation). |
Examples
tazuna plan
tazuna plan -f tazuna.yaml
tazuna plan -f tazuna.yaml --tags web,batch
tazuna plan -f tazuna.yaml --otlp-endpoint=localhost:4317
Related
- To actually apply, see
tazuna apply - For State-based diffs, see
tazuna state diff - For detecting manual changes, see
tazuna state drift
tazuna status
Fetches the managed resources recorded in the State ConfigMap one by one and lists the readiness of each. It performs only read access to the cluster and changes nothing.
tazuna status [-f tazuna.yaml]
Behavior
- Load
tazuna.yaml. - For each Manifest, read the corresponding State ConfigMap.
- Fetch each resource recorded in State from the cluster by GVK / namespace / name.
- Determine readiness from the fetched results and output in three columns.
Does not evaluate context_matches.
Criteria
| Status | Criteria |
|---|---|
Ready | The object was fetched and passed the Kind-specific Ready check |
NotReady | The object was fetched but did not pass the Ready check |
Missing | Recorded in State but NotFound on the live cluster |
Error | An error other than NotFound occurred while fetching (insufficient permissions, API server error, etc.) |
Ready check per Kind
The Ready check branches by Kind. Kinds other than Deployment / StatefulSet / DaemonSet / Pod are treated as Ready as soon as they can be fetched (so that resources with no concept of readiness, such as ConfigMap / Secret / Service / Ingress / CRD, are handled uniformly).
| Kind | Ready condition |
|---|---|
Deployment | Immediately Ready if spec.replicas == 0. Otherwise status.readyReplicas == status.replicas == status.availableReplicas and replicas > 0 |
StatefulSet | Immediately Ready if spec.replicas == 0. Otherwise status.readyReplicas == status.replicas and replicas > 0 |
DaemonSet | status.numberReady == status.desiredNumberScheduled and desiredNumberScheduled > 0 |
Pod | status.phase == "Running" AND Ready condition is True |
| Others | Ready as soon as it can be fetched |
This design avoids painting Service / ConfigMap and the like - which inherently have no concept of readiness - as NotReady. If you want to check readiness specific to Ingress / CRD / Custom Resource and so on, the proper approach is to write a WaitUntil in CEL with the separate Test plugin.
Output Format
It is printed in three columns (STATUS / KIND / NAMESPACE/NAME). For cluster-scoped resources, the NAMESPACE/NAME column has no namespace and shows only NAME.
Manifest: ingress-nginx
STATUS KIND NAMESPACE/NAME
Ready Deployment ingress-nginx/controller
Ready Service ingress-nginx/controller
Ready ConfigMap ingress-nginx/controller
NotReady Deployment ingress-nginx/admission-webhook
Missing Secret ingress-nginx/missing-tls
Manifest: aws-credentials
(no state)
A Manifest whose State has not been created yet (never applied / synced) is shown as (no state).
Flag
No specific flags besides the global flags.
Examples
tazuna status
tazuna status -f tazuna.yaml
tazuna status -f tazuna.yaml --otlp-endpoint=localhost:4317
Related
- For the declared-vs-State diff, see
tazuna state diff - For detecting manual changes, see
tazuna state drift - To apply, see
tazuna apply - Terminology: State
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
- For the declared-vs-State diff, see
tazuna state diff - For drift against the live cluster, see
tazuna state drift - To apply the diff, see
tazuna apply --sync - For the readiness of managed resources, see
tazuna status - 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.
Difference from state drift
Both state diff and state drift show a “diff,” but they compare different things.
| Aspect | state diff | state drift |
|---|---|---|
| Compared against | Build result vs State | State vs live cluster |
| Changes detected | Changes on the tazuna.yaml side | Manual kubectl apply / manual deletion |
| Output categories | added / modified / removed / always-sync | live-drifted / live-missing |
By switching the command you use depending on whether the drift is “an update on the tazuna.yaml side not yet applied” or “the cluster side touched directly,” you can build low-noise monitoring.
Flag
No specific flags besides the global flags.
Examples
tazuna state diff
tazuna state diff -f tazuna.yaml
Related
- To apply, see
tazuna apply --sync - For detecting diffs against the live cluster, see
tazuna state drift - For the full rendering result, see
tazuna build - Terminology: Diff type / ContentHash
tazuna state drift
Reconciles the resources recorded in the State ConfigMap against the actual objects on the live cluster, detecting resources changed by hand (live-drifted) and resources still recorded in State but removed from the live cluster (live-missing). It performs only read access to the cluster and changes nothing.
tazuna state drift [-f tazuna.yaml]
Behavior
- Load
tazuna.yaml. - For each Manifest, read the corresponding State ConfigMap (
tazuna-state-<manifest-name>in thetazunanamespace). - Fetch each resource recorded in State from the cluster by GVK / namespace / name.
- For fetched resources, recompute the ContentHash and reconcile it against the hash saved in State.
- Output non-matching ones as
live-driftedand those whose fetch itself failed (NotFound) aslive-missing.
It does not evaluate context_matches. Unlike tazuna state diff, it does not use the Build result. It only compares the hash recorded in State against the live object.
Difference from state diff
| Aspect | state diff | state drift |
|---|---|---|
| Compared against | Build result vs State | State vs live cluster |
| Cluster read | State ConfigMap only | State + each actual resource |
| What can be detected | Changes on the tazuna.yaml side | Manual kubectl apply / manual deletion |
| GenesisSecret | Always always-sync | Skips |
It helps to remember the division of labor as: state diff detects “you updated tazuna.yaml but haven’t applied it yet,” while state drift detects “you didn’t change tazuna.yaml, but someone touched the cluster.”
Output Format
When drift is found, it is output on a per-Manifest basis as follows.
Manifest: ingress-nginx
STATUS RESOURCE HASH
live-drifted ingress-nginx/apps/v1/Deployment/ingress-nginx/controller def456... (stored: abc123...)
live-missing ingress-nginx//v1/ConfigMap/ingress-nginx/extra (stored: zzz999...)
If there is no drift, only the following single line is emitted.
No drift detected.
state drift itself does not change its exit code based on whether drift exists. If you want CI to treat “drift present” as a failure, filter on whether the output does not contain No drift detected.
Skipped Manifests
The following Manifests are skipped as out of scope for drift detection.
- Manifests with an empty
name(because a State key cannot be created) type: genesissecret(by always-sync design, it re-fetches from the Provider every time, so it has no concept of drift to begin with)- Manifests whose State ConfigMap is empty or absent (never applied / synced)
Flag
No specific flags besides the global flags.
Examples
tazuna state drift
tazuna state drift -f tazuna.yaml
tazuna state drift -f tazuna.yaml --otlp-endpoint=localhost:4317
Reproduction steps with KinD
The easiest way to reproduce live-drifted locally is the following procedure.
# 1. Apply normally to create state
tazuna apply -f tazuna.yaml
# 2. Change a resource on the cluster side by hand
kubectl -n default patch deployment nginx --type=merge \
-p '{"spec":{"replicas":3}}'
# 3. drift を検知する
tazuna state drift -f tazuna.yaml
# => live-drifted エントリが 1 件出る
live-missing can be reproduced similarly by running kubectl delete by hand.
Related
- For the declared-vs-State diff, see
tazuna state diff - For how to set up drift monitoring, see Drift Monitoring
- Terminology: State / ContentHash
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 / plan / status / state ... / secret-to-genesissecret / tags / version). |
cmd/internal/ | Internal utilities shared between subcommands (kubeconfig loading, logger, OTLP tracer setup, and so on). |
api/v1/ | Go struct definitions corresponding to the YAML schemas (tazuna.yaml / tazuna.hint.yaml / GenesisSecret / TestPluginSpec / ORAS / Secret Provider). |
pkg/runner/ | Orchestration of the whole tazuna apply, DAG resolution of dependsOn, and the implementation of plan / status / state diff / state drift. |
pkg/manager/ | Per-manifest-type Manager implementations (kustomize / helmfile / genesis_secret, 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
├── dependson-minimal/ # DAG execution via dependsOn
├── 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, apply --sync
├── state-modified/ # "modified" judgment in state diff
├── state-drift/ # state drift's live-drifted / live-missing
├── 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.