If you’ve spent enough time managing Kubernetes deployments, you’ve likely hit the configuration wall. You start with standard YAML, realize you need different values for dev and prod, and reach for Kustomize or Helm.
Fast forward six months, and you are trapped in one of two nightmares: either you are maintaining a sprawling directory tree of overlays/ just to patch a single image tag in Kustomize, or you are deeply nested in Helm template spaghetti, debugging a malformed manifest because a Go-template {{ if }} block missed a single whitespace indentation.
Kubernetes manifests are structured data. Yet, our standard industry tools either treat them as raw text strings to be chopped up (Helm) or as rigid objects requiring layers of external patch files (Kustomize).
Hull is an open-source Kubernetes package manager that takes a completely different approach: it treats YAML as a native AST (Abstract Syntax Tree), utilizes clean inline expression templating, and centralizes environment configuration to eliminate both Kustomize overlays and Helm text templating.
Here is a hands-on look at how to build a multi-environment deployment without the traditional boilerplate.
1. Getting Started
Hull is a single Go binary. You can install it directly via:
go install github.com/ebogdum/hull/cmd/hull@latest
Let's scaffold a new package to see how the architecture compares:
hull create my-api && cd my-api
This generates three core components:
-
hull.yaml: The orchestrator (defines environments, inheritance, and dependencies). -
values.yaml: Your default configuration schema. -
templates/: The actual Kubernetes manifests.
2. Centralizing Environments (Killing Kustomize Overlays)
In Kustomize, setting up dev and prod requires building out a directory tree with a base/ directory, an overlays/dev/ directory, and an overlays/prod/ directory—each containing its own kustomization.yaml and distinct patch files.
In Hull, this environment topology lives entirely within a single orchestrator file: hull.yaml.
apiVersion: hull/v1
name: my-api
version: 1.0.0
environments:
dev:
namespace: api-dev
values:
replicas: 1
image:
tag: "latest"
prod:
# Inheriting dev pulls in its configuration as a baseline.
inherits: dev
namespace: api-prod
values:
replicas: 3
image:
tag: "v1.4.2"
Because prod explicitly inherits from dev, you only define the keys that actually change. When you target an environment, Hull resolves these blocks deterministically as data before rendering anything.
3. Inline Variables (Killing Helm Template Spaghetti)
Helm forces you to wrap text strings in Go-template syntax, which easily breaks indentation or fails silently if an object isn't completely defined. Hull evaluates expressions directly against the parsed YAML tree using ${...} syntax.
Open templates/deployment.yaml to see how clean the context injection looks:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${name}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: ${name}
template:
metadata:
labels:
app: ${name}
spec:
containers:
- name: app
image: ${image.repository}:${image.tag}
ports:
- containerPort: 8080
# Control flow is handled natively as actual YAML keys
$if: ${features.cacheEnabled}
env:
- name: REDIS_HOST
value: redis-cache
Because $if is a native YAML key and the ${...} expressions are evaluated safely against the parsed tree, your output is structurally guaranteed to be valid YAML. You completely avoid the risk of emitting broken whitespace or malformed text blocks.
4. Debugging the Configuration Spaghetti
One of the hardest parts of layered configuration in both Helm and Kustomize is figuring out where a final value actually originated. Did replicas get set by the base chart defaults, an inherited value file, a specific environment override, or a runtime CLI flag?
Hull includes a trace command built specifically to map out this resolution path:
hull values . --env prod --trace
The output points directly to the exact file or structural block that won the evaluation:
replicas: 3 # source: environments.prod.values (hull.yaml)
image:
repository: myorg/webapp # source: values.yaml (package default)
tag: v1.4.2 # source: environments.prod.values (hull.yaml)
5. Native State Tracking and Drift Detection
Kustomize relies entirely on piping raw text output to kubectl apply, meaning it has no concept of what is actually happening inside your cluster over time. Helm tracks state using internal Secrets, but doesn't natively reconcile changes made out-of-band.
Hull acts as a full deployment engine that actively tracks cluster state. Install your package directly with:
hull install my-api . --env prod --create-namespace
Hull stamps resources with strict ownership metadata (managedBy=hull) and stores the exact rendered manifest. If a developer later bypasses GitOps or CI/CD and manually modifies the cluster—such as running kubectl scale deploy my-api --replicas=10—you can catch it instantly:
hull drift my-api
Hull evaluates the live cluster state against the stored release and reports the precise structural diff. To bring the cluster back to your declared state, a single command handles it:
hull reconcile my-api
The Takeaway
If you are tired of maintaining complex, deep directory structures in Kustomize or fighting whitespace bugs in Helm, Hull offers a clean, data-driven architecture. By replacing text-replacement and patch-files with native YAML AST handling and deterministic environment inheritance, it drastically simplifies how multi-environment Kubernetes setups are managed.
The project is fully open-source under the MIT license. Explore the documentation, OCI distribution features, and GitOps workflows over on GitHub: ebogdum/hull.
Top comments (0)