- Go 100%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .gitignore | ||
| go.mod | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
deploy
A Go CLI tool for deploying Helm charts and raw Kubernetes manifests via kapp and vals.
Supports multiple environments, namespace-scoped deployments, optional CRD deployment and a --dry-run mode that shows what would change without applying anything.
Requirements
| Tool | Purpose |
|---|---|
helm |
Render Helm charts |
vals |
Resolve secret references in YAML |
kapp |
Apply / delete Kubernetes resources |
kubectl |
Create namespaces |
All four tools must be available in $PATH.
Build
go build -o deploy deploy.go
No external Go dependencies – only the standard library is used.
Usage
./deploy [flags]
| Flag | Default | Description |
|---|---|---|
--env |
staging |
Environment name (folder under --deploy, e.g. dev, staging, production) |
--namespace |
(all) | Only process _deployment_<namespace>.json |
--deploy |
deploy |
Root folder containing the per-environment deployment folders (<deploy>/<env>) |
--apps |
apps |
Directory holding shared/per-app values files. Use e.g. --apps base for the legacy layout — there is no automatic fallback |
--cluster |
cluster |
Directory holding cluster-wide values (cluster.yaml/.yml + <env>/) |
--dry-run |
false |
Show what would change without applying anything |
Every values file the tool looks up accepts either a .yaml or a .yml extension (both are checked; if both exist, both are applied, .yaml first).
Examples
# Deploy staging (all deployment files) — looks under ./deploy/staging
./deploy
# Deploy a specific environment — looks under ./deploy/production
./deploy --env production
# Deploy only the monitoring namespace in staging
./deploy --namespace monitoring
# Dry-run for production
./deploy --env production --dry-run
# Dry-run for a single namespace
./deploy --env production --namespace monitoring --dry-run
# Use a custom apps/cluster/deploy folder layout
./deploy --env production --apps charts --cluster cluster-config --deploy environments
Directory structure
.
├── deploy.go
├── cluster/ # overridable via --cluster
│ ├── cluster.yaml|.yml # Global cluster values (applies to every env)
│ ├── dev/
│ │ └── *.yaml|*.yml # Every matching file here is applied for --env dev
│ ├── staging/
│ │ └── *.yaml|*.yml
│ └── production/
│ └── *.yaml|*.yml
├── apps/ # overridable via --apps (no automatic fallback)
│ ├── default.yaml|.yml # Applied to every application, in every namespace/env
│ ├── prometheus.yaml|.yml # Applied only to the app literally named "prometheus"
│ └── prometheus-common.yaml|.yml # Shared file, used via the `base` field (see below)
└── deploy/ # overridable via --deploy
├── dev/
│ ├── _deployment.json
│ └── <namespace>/
│ ├── <name>.yaml|.yml # Per-chart values override
│ └── <manifest>.yaml # One or more raw manifests (see "manifest" field)
├── staging/
│ ├── _deployment.json # Deployment config (all namespaces)
│ ├── _deployment_monitoring.json # Deployment config (monitoring only)
│ ├── _deployment_storage.json # Deployment config (storage only)
│ └── <namespace>/
│ ├── <name>.yaml|.yml
│ └── <manifest>.yaml
└── production/
├── _deployment.json
└── ...
--env staging resolves the environment folder as <deploy>/<env>, i.e. ./deploy/staging by default. This is where _deployment*.json, manifests, and per-namespace value overrides are looked up. The cluster/<env> and kapp app-naming (<env>-<namespace>-<name>) still use just the environment name, independent of --deploy.
Every values file lookup below accepts either extension: if both <name>.yaml and <name>.yml exist, both are applied (.yaml first).
Important asymmetry, by design: cluster/<env>/ is a folder listing — every file inside it is applied to every app in that environment, because it is meant to hold genuinely environment-wide settings. apps/<name> is the opposite: an exact single-file match directly under apps/ (apps/<name>.yaml and/or .yml), never a folder listing. Do not create an apps/<some-name>/ folder to group several apps' files together — it will simply be ignored (the tool only ever looks for apps/<name>.yaml/.yml as a file). This distinction exists because a folder that happened to share one app's exact name once caused a real incident: another app's file got silently merged in, and the wrong container image got deployed.
Values files are loaded in this order (later files take precedence):
<cluster>/cluster.yaml|.yml— global cluster values, applied to every environment<cluster>/<env>/*.yamland*.yml— every matching file in the environment's cluster folder, applied alphabetically<apps>/default.yaml|.yml— defaults applied to every application<apps>/<name>.yaml|.yml— this app's own file, exact match (or<apps>/<base-field>.yaml|.ymlif the app'sbasefield is set)<deploy>/<env>/<namespace>/<name>.yaml|.yml— per-app, per-namespace override
Migrating from the old layout
- Cluster values now live under a dedicated
cluster/folder (overridable with--cluster): move./base/cluster.ymlto./cluster/cluster.yaml(or keep the.ymlextension, both work), and split any environment-specific overrides that used to live in./<env>/cluster.ymlinto one or more.yaml/.ymlfiles under./cluster/<env>/. - The
base/folder is now calledapps/by default (overridable with--apps). Unlike before, there is no automatic fallback: if you still use the oldbase/folder, pass--apps baseexplicitly, or rename the folder toapps/. - Per-chart values stay a single, flat file per app directly under
apps/:<apps>/<name>.yamlor.yml. If you previously grouped several apps' files into one folder (e.g.apps/<namespace>/<name>.yaml), flatten them out toapps/<name>.yaml— app names are unique across the whole deployment, so no namespace prefix is needed, and it avoids the exact incident described above. - Environment folders (
dev,staging,production, …) have moved from the repo root into adeploy/folder (overridable with--deploy): move./stagingto./deploy/staging,./productionto./deploy/production, and so on. - Optionally add
./apps/default.yaml(or.yml) for values that should apply to every application (e.g. common labels or annotations), on top of any per-chart file. manifestaccepts a single string or an array:"manifest": "cm.yaml"and"manifest": ["cm.yaml"]are both valid and equivalent. (A brief earlier revision required an array only — that restriction has been lifted.)
Deployment files
When --namespace is not set, the tool processes all deployment files in <deploy>/<env>/ in this order:
_deployment.json(if present)_deployment_<namespace>.json(all matches, alphabetically)
When --namespace is set, only <deploy>/<env>/_deployment_<namespace>.json is processed.
_deployment.json / _deployment_<namespace>.json
{
"global_manifest": ["imagePullSecret.yaml"],
"app_k8s_deploy_helm_charts": [
{
"namespace": "monitoring",
"namespaced_manifest": ["monitoring-network-policy.yaml"],
"applications": [
{
"name": "kube-prometheus-stack",
"chart_ref": "prometheus-community/kube-prometheus-stack",
"state": "present",
"version": "58.0.0",
"deploy_crds": true
},
{
"name": "alertmanager",
"chart_ref": "prometheus-community/alertmanager",
"base": "prometheus-common",
"state": "present"
},
{
"name": "gateway-api",
"chart_ref": "oci://reg.example.com/charts/gateway-api",
"deploy_crds": true,
"manifest": "kapp-config.yaml"
},
{
"name": "gateway-api-crds",
"state": "present",
"manifest": [
"my-configmap.yaml",
"https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml"
]
},
{
"name": "old-app",
"chart_ref": "my-repo/old-app",
"state": "absent"
}
]
}
]
}
Application fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Release name and kapp app identifier |
state |
string | ✅ | present to deploy, absent to delete |
chart_ref |
string | ❌ | Helm chart reference. If omitted, Helm is skipped entirely |
version |
string | ❌ | Helm chart version. Omit to use latest |
manifest |
string or array of strings | ❌ | One or more raw manifests, each either a local path (relative to <deploy>/<env>/<namespace>/) or an http(s):// URL (downloaded). Applied in the order listed |
deploy_crds |
bool | ❌ | Pass --include-crds to helm template (default: false) |
base |
string | ❌ | Override per-chart values file. Loads <apps>/<base>.yaml/.yml instead of <apps>/<name>.yaml/.yml |
manifest field
manifest accepts either a single string or a list — a single file doesn't need to be wrapped in [...]:
{ "name": "gateway-api", "manifest": "kapp-config.yaml" }
{ "name": "gateway-api", "manifest": ["kapp-config.yaml", "extra-rbac.yaml"] }
Both forms end up applied the same way; a single string is just shorthand for a one-element list.
Each entry is either:
- a local path, resolved as
<deploy>/<env>/<namespace>/<entry>— must exist, or the deploy fails withmanifest file not found: ..., or - an
http://orhttps://URL, which is downloaded directly (a real network request, even during--dry-run— see "Dry-run mode" below) — if the download fails or returns a non-2xx status, the deploy fails with that error.
All listed manifests (local and remote, in any mix) are piped through vals eval and merged into the same kapp deploy, in the order given. Each rendered chunk (the Helm chart's output, and every individual manifest) is automatically separated with a YAML document separator (---) when concatenated into the file handed to kapp — without it, a file combining multiple resources without proper multi-document separation is invalid/ambiguous YAML that kubectl and kapp can silently misparse. You don't need to add --- yourself between manifest files or at the top of a manifest — the tool takes care of it.
base field
By default the tool looks for a file named after the application, <apps>/<name>.yaml or .yml (e.g. ./apps/alertmanager.yaml), directly under apps/ — not a subfolder.
Set base to use a different file from the apps directory instead:
{ "name": "alertmanager", "base": "prometheus-common" }
This loads ./apps/prometheus-common.yaml (or .yml) instead of ./apps/alertmanager.yaml.
Useful when multiple charts share the same values.
apps/default.yaml / apps/default.yml
Any values in <apps>/default.yaml or <apps>/default.yml are applied to every application, in every namespace and environment, before the per-chart file (<apps>/<name>.yaml/.yml or base-overridden) and before the per-namespace override. Use it for values every chart should share, such as common labels, tolerations, or image pull secrets.
global_manifest / namespaced_manifest
These apply raw manifests per namespace, independent of any single application — typically things every namespace needs regardless of which apps live there, such as an image-pull secret, a default NetworkPolicy, or a ResourceQuota.
| Field | Scope | Type | Resolved relative to |
|---|---|---|---|
global_manifest |
top-level, applies to every namespace in the file | string or array of strings | <cluster>/<entry> |
namespaced_manifest |
per namespace group, applies only to that namespace | string or array of strings | <deploy>/<env>/<namespace>/<entry> |
Both accept the same single-string-or-list shorthand and the same http(s):// URL support as the per-app manifest field (see above). For a given namespace, global_manifest and namespaced_manifest entries are combined and deployed together as one additional kapp app, named <env>-<namespace>-namespace-manifests, separate from any application's own kapp app. It's created once per namespace, before that namespace's applications are processed. If a namespace has neither global_manifest nor namespaced_manifest entries, no extra kapp app is created for it.
Image-pull secret example — put this once under <cluster>/imagePullSecret.yaml, resolving the actual credentials via vals (e.g. from Vault), and list it in global_manifest:
apiVersion: v1
kind: Secret
metadata:
name: regcred
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: ref+vault://secret/registry#/dockerconfigjson
{ "global_manifest": ["imagePullSecret.yaml"], "app_k8s_deploy_helm_charts": [ /* ... */ ] }
That's the whole setup — the tool then creates this Secret in every namespace listed in the file automatically. Reference it from a chart's imagePullSecrets values (or apps/default.yaml if every chart needs it) as - name: regcred.
Behaviour matrix
chart_ref |
manifest |
Result |
|---|---|---|
| ✅ | (empty/omitted) | Helm chart rendered and deployed via kapp |
| ✅ | ✅ (one or more) | Helm chart + all listed manifests merged into one kapp deploy |
| ❌ | ✅ (one or more) | Only the raw manifests are processed via vals and deployed via kapp |
| ❌ | (empty/omitted) | Entry is skipped with a warning |
kapp app naming
Each application is registered in kapp under the name:
<env>-<namespace>-<name>
Example: staging-monitoring-kube-prometheus-stack
The kapp app itself is always stored in the gitops namespace.
Dry-run mode
--dry-run treats rendering and cluster-mutating steps differently:
- Rendering (
helm template,vals eval,cat/downloading manifests) – runs for real, even in--dry-run. This is local/read-only and non-destructive, and the result is exactly what the diff below is based on; without it, the diff would be computed against an empty file. This includes downloadinghttp(s)://manifest URLs — a genuine network request happens even during a dry-run. - Namespace creation (
kubectl create namespace ... | kubectl apply -f -) – only printed, never executed, sincekubectl applyhere really would create the namespace on the cluster. - kapp – instead of
kapp -y deploy, the tool actually runs:
kapp deploy --diff-run --diff-changes
This connects to the real cluster and shows exactly which resources would be added, changed or deleted – it's read-only against the cluster, so it's safe to run for real, and it's what makes --dry-run actually useful: you see the genuine kapp diff in your terminal, not just a printed command.
kapp delete (for state: absent) and the final kapp -y deploy remain only printed in dry-run, since those steps do mutate the cluster.
vals secret resolution
All YAML passed to kapp runs through vals eval -f - first.
This resolves secret references such as:
password: ref+vault://secret/myapp#/password
token: ref+awssecrets://myapp/token
See the vals documentation for supported backends.
CRD deployment
Setting deploy_crds: true passes --include-crds to helm template.
Note:
helm template --include-crdsdoes not render CRDs that live in thecrds/directory of a chart. For reliable CRD deployment it is recommended tohelm pull --untarthe chart and apply thecrds/directory separately before the main deploy.