A pipeline-based CLI tool for processing Kubernetes manifests and configuration files.
Many Templates recursively discovers .many.yaml pipeline definitions in a directory tree, copies the source to an
output directory, and executes each pipeline in-place. Pipelines compose steps: Go templating (with Sprig),
Kustomize builds, Helm renders, file generation from inline templates, and YAML stream splitting.
- Installation
- Quick Start
- Examples
- CLI Flags
.many.yamlSchema- Pipeline Steps
- Context
- Execution Model
- Environment Variables
go install github.com/systemstart/many-templates/cmd/many@latestOr build from source:
go build -o many ./cmd/manyGiven a directory tree with .many.yaml pipeline definitions:
infrastructure/
├── .many.yaml
├── ingress.yaml
└── values.yaml
Where .many.yaml contains:
context:
domain: "example.com"
pipeline:
- name: render
type: template
template:
files:
include: [ "**/*.yaml" ]Run:
many \
-input-directory ./infrastructure \
-output-directory ./output \
-overwrite-output-directoryThe source tree is copied to ./output, templates are rendered in-place using the context variables, and .many.yaml
files are removed from the output.
WARNING: this should only demonstrate the usage for a non-trivial project. Whether the resulting manifests provide a working setup is not scope of this example.
The examples/many-sites/ directory demonstrates instances mode with a real-world deployment
configuration. A shared set of service templates is rendered for two domains --- fediverse.example and
development.example --- each selecting a different subset of services:
- fediverse.example --- Mastodon, Matrix/Element, Mobilizon, Pixelfed
- development.example --- Forgejo, Woodpecker CI, Harbor
Both instances share infrastructure services (Dex, LLDAP, ESO), a global context file for common
configuration, and per-instance context for the domain name. External S3 and SMTP are configured globally.
The instances.yaml file defines the two instances with their include filters and context overrides.
many \
-input-directory examples/many-sites/services \
-output-directory output \
-instances examples/many-sites/instances.yaml \
-context-file examples/many-sites/context.yaml| Flag | Description | Default |
|---|---|---|
-input-directory |
Source directory to process | required |
-output-directory |
Destination for rendered output | required |
-overwrite-output-directory |
Delete and recreate output directory | false |
-context-file |
Global context YAML file (removed from output if inside input) | none |
-max-depth |
Max directory recursion depth (-1 = unlimited, 0 = root only) |
-1 |
-processing |
Single .many.yaml to run (skips directory discovery) |
none |
-instances |
Instances YAML file for matrix mode | none |
-log-level |
debug, info, warn, error |
info |
-logging-type |
json, text, tint |
tint |
When no -processing flag is given, many walks the input directory tree collecting all .many.yaml files. Pipelines
are sorted by directory depth (parents before children) and executed independently.
many \
-input-directory ./infrastructure \
-output-directory ./output \
-max-depth 2When -processing points to a specific .many.yaml (must be within the input directory), only that pipeline runs:
many \
-processing ./infrastructure/cert-manager/.many.yaml \
-input-directory ./infrastructure \
-output-directory ./outputWhen -instances points to an instances YAML file, many runs the same input tree (or a filtered subset) multiple
times with different contexts, producing separate output directories. This is useful when you have a shared set of
templates that need to be rendered for multiple environments, domains, or tenants.
-instances is incompatible with -processing.
many \
-input-directory ./applications \
-output-directory ./output \
-instances instances.yaml \
-context-file global.yamlThe instances file defines a list of instances, each with a name, output directory, optional input subdirectory, optional include filter, and optional context:
instances:
- name: prod-east
output: prod-east/
include:
- api
- frontend
context:
region: us-east-1
replicas: 3
- name: staging
input: staging-apps/
output: staging/
context:
region: us-west-2
replicas: 1| Field | Description | Default |
|---|---|---|
name |
Unique identifier for the instance | required |
output |
Output subdirectory (relative to -output-directory) |
required |
input |
Input subdirectory (relative to -input-directory) |
"" (root) |
include |
List of immediate subdirectory names to include (empty = include all) | [] |
context |
Additional context merged on top of global context for this instance | {} |
Context merge order: -context-file global context -> instance context -> per-directory .many.yaml context.
Instance context acts as an additional global layer for that run.
Include filtering: When include is specified, only the listed immediate subdirectories of the input directory are
copied to the output. Root-level files are always copied. When include is empty or absent, the entire input tree is
copied.
For each instance, many:
- Copies the input tree (filtered by
include) to the instance output directory - Merges global context with instance context
- Discovers and executes pipelines within the instance output
- Removes
.many.yamlfiles from the instance output
If an instance fails, remaining instances still run. A summary of failed instances is reported at the end.
Each .many.yaml defines a pipeline scoped to its directory.
# Context variables available to template steps.
context:
domain: "example.com"
certManager:
installCRDs: true
# Ordered list of steps. Executed sequentially.
pipeline:
- name: template-configs
type: template
template:
files:
include: [ "kustomization.yaml", "values.yaml", "patches/**/*.yaml" ]
- name: build
type: kustomize
kustomize:
dir: "."
- name: split-manifests
type: split
split:
input: build
by: kind
outputDir: manifests/
- name: template-output
type: template
template:
files:
include: [ "manifests/**/*.yaml" ]Each step has a name (unique within the pipeline), a type, and type-specific configuration. Steps execute
sequentially. Steps that produce output (kustomize, helm) store their result keyed by name, which subsequent steps can
reference.
Renders files in-place using Go's text/template
with Sprig functions. Context variables from the pipeline's context block (
merged with any global context) are passed as template data.
- name: render
type: template
template:
files:
include: [ "**/*.yaml" ]
exclude: [ "kustomization.yaml" ]| Field | Description | Default |
|---|---|---|
files.include |
Glob patterns for files to template | ["**/*"] |
files.exclude |
Glob patterns for files to skip | [] |
Globs are matched relative to the pipeline directory. Patterns support ** for recursive matching
via doublestar.
Runs kustomize build and captures the multi-document YAML output. Requires kustomize on PATH.
- name: build
type: kustomize
kustomize:
dir: "."
enableHelm: true| Field | Description | Default |
|---|---|---|
dir |
Directory containing kustomization.yaml |
"." |
enableHelm |
Pass --enable-helm to kustomize |
false |
Runs helm template to render a chart. Requires helm on PATH.
- name: render-chart
type: helm
helm:
chart: ./charts/my-app
releaseName: my-app
namespace: default
valuesFiles: [ "values.yaml" ]
set:
image.tag: "v1.2.3"| Field | Description | Default |
|---|---|---|
chart |
Path to chart directory or chart reference | required |
releaseName |
Helm release name | required |
namespace |
Target namespace | "default" |
valuesFiles |
List of values files | [] |
set |
Map of --set overrides |
{} |
Creates a file from an inline Go template rendered with Sprig functions against
the pipeline context. Unlike template (which renders existing files in-place), generate synthesizes new files purely
from context data, removing the need for placeholder files in the source tree.
- name: gen-config
type: generate
generate:
output: manifests/config.yaml
template: |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .app_name }}
data:
domain: {{ .domain }}| Field | Description | Default |
|---|---|---|
output |
Output file path relative to the pipeline directory | required |
template |
Inline Go template string | required |
Parent directories for the output path are created automatically.
Takes a multi-document YAML stream from a previous kustomize or helm step and splits it into individual files.
- name: split-manifests
type: split
split:
input: build
by: kind
outputDir: manifests/| Field | Description | Default |
|---|---|---|
input |
Name of a previous step whose output to split | required |
by |
Splitting strategy | "kind" |
outputDir |
Directory to write split files into | "." |
fileNameTemplate |
Go template for file paths (only with custom strategy) |
--- |
kind --- One file per Kind. Multiple resources of the same Kind share a file as multi-document YAML.
manifests/
├── deployment.yaml
├── service.yaml
└── ingress.yaml
resource --- One file per resource: <kind>-<name>.yaml.
manifests/
├── deployment-api.yaml
├── deployment-worker.yaml
├── service-api.yaml
└── configmap-app-config.yaml
group --- Directories per API group, files per resource.
manifests/
├── apps/
│ └── deployment-api.yaml
├── core/
│ └── service-api.yaml
└── networking.k8s.io/
└── ingress-main.yaml
kind-dir --- Directories per Kind (pluralized), files per resource name.
manifests/
├── deployments/
│ ├── api.yaml
│ └── worker.yaml
├── services/
│ └── api.yaml
└── ingresses/
└── main.yaml
custom --- File paths determined by a Go template. The template receives the full manifest as a map.
split:
input: build
by: custom
outputDir: manifests/
fileNameTemplate: "{{ .metadata.namespace | default \"cluster\" }}/{{ .kind | lower }}-{{ .metadata.name }}.yaml"Each .many.yaml can define a context block. These values are available to all template steps in that pipeline:
context:
domain: "example.com"
replicas: 3Templates reference values with {{ .domain }}, {{ .replicas }}, etc.
A global context file can be provided via -context-file. It applies to all pipelines. Pipeline-local context overrides
global values (shallow merge at top-level keys):
many \
-input-directory ./infra \
-output-directory ./output \
-context-file global.yaml# global.yaml
domain: "default.example.com"
environment: production# infra/app/.many.yaml — domain is overridden, environment is inherited
context:
domain: "app.example.com"
pipeline:
- name: render
type: template
template:
files:
include: [ "**/*.yaml" ]After merging global and pipeline-local context, all string values in the context map are rendered as Go templates against the full context. This lets context values reference other context values:
# global.yaml
domain: "example.com"
forgejo_url: "https://forgejo.{{ .domain }}"
app_url: "https://app.{{ .domain }}"After interpolation, forgejo_url becomes https://forgejo.example.com and app_url becomes
https://app.example.com. The same Sprig functions available in template steps
can be used in context values (e.g. {{ .name | upper }}).
Interpolation is a single pass --- values can reference plain context keys but not other interpolated values. Strings inside nested maps and slices are also interpolated. Non-string values (ints, bools) are left unchanged. If a context value fails to parse or execute as a template, the pipeline aborts with an error.
- The entire source tree is copied to the output directory.
.many.yamlfiles are discovered in the output tree, sorted by directory depth.- Each pipeline executes in-place within the output tree.
- Template steps modify files in-place. Kustomize/Helm steps produce YAML streams in memory. Split steps write streams to files.
.many.yamlfiles and the context file are removed from the output after all pipelines complete.
A failing step aborts its pipeline. Other pipelines continue. A summary of failed pipelines is printed at the end, and the exit code is non-zero if any pipeline failed.
If a .env file exists in the working directory, it is loaded automatically
via godotenv.
