Apron deploys Hangar onto a Kubernetes cluster: it onboards a cluster you already have, or creates a local one first. When this was written it did that by copying a template repo and rewriting it with one script. kubara, an open-source GitOps platform framework from STACKIT, does the same job with a CLI, versioned catalogs and generated Helm values. This review compares the two and records the decision taken on October 16: Apron keeps its services in one kubara catalog, uses kubara as its only renderer, and lets DIY users install kubara's plain output without kubara.
Since then. v0.5 is built: the apron CLI (init, render, up, fleet, identity, verify, resume, down) replaced the script on October 16, 2026, with templates harvested from the live clusters, profiles independent of runtimes and Traefik on every cluster, and both lab clusters were rebuilt with it the next day. Tower now deploys as an Airframe app. The kubara port (v1) has not started. This page is the review and decision as written.
sed, so it can't upgrade a fleet.apron check, and points the way for kubara. kubara validates each service's settings on its own, but can't say "an upper cluster can't run the CI/CD control plane". apron check enforces those rules now; proposing catalog-level rules to kubara is how it becomes unnecessary.apron up dev --name dev or apron up prod --name prod, onboards Hangar onto an existing cluster, or creates a local one on kiac or kind first, then generates, bootstraps and verifies it. kubara is a pinned tool the script drives, like kind or Helm; the apron catalog ships inside the repo, and nothing else in Hangar has to exist first.kubara is a single Go binary, Apache-2.0, that started inside STACKIT and was open-sourced in 2026. You describe clusters and the services each one runs in config.yaml. kubara generate merges that with one or more catalogs and writes reviewable files to Git: reusable charts under platform-components/, per-cluster values under platform-configs/<cluster>/. kubara bootstrap installs Argo CD and a few CRDs once. From then on Argo CD reconciles, and kubara isn't running.
A catalog is to a platform what a Helm chart is to an app. It holds a Catalog.yaml with a plain semver version, one ServiceDefinition per service, the charts, and Go templates (.tplt, with Sprig) for each cluster's values. Catalogs are published as OCI artifacts and listed per cluster, in order. kubara ships two: bootstrap (Argo CD, CRDs) and general (17 services, from cert-manager to Velero).
On each cluster, the bootstrap catalog's Argo CD chart creates one ApplicationSet per service. Each selects clusters labelled <service>: enabled and layers values in a fixed order: chart defaults, then values.generated.yaml, then additional-values.yaml, then any values-*.yaml you add. Generated files are never hand-edited; your changes live in the last two.
The two tools started from the same pain: hand-maintained cluster repos that drift apart. kubara solved generation and upgrades. Apron solved fleet safety rules and Hangar's specific stack.
| Concern | Apron today | kubara | Better |
|---|---|---|---|
| Input | cluster.yaml, deleted after one run | config.yaml, committed, all clusters in one file | kubara |
| Rendering | Literal sed over a copy of a live cluster; macOS only | Go templates with Sprig, reading the cluster, its services and the catalog | kubara |
| Re-runnable | No; run once on a fresh clone | Yes; generated files are rewritten, yours are left alone | kubara |
| Versioning | None; each cluster repo is a fork | OCI catalogs, immutable semver, pinned per cluster | kubara |
| Per-service validation | Field presence in the script | configSchema per service; kubara schema for editors | kubara |
| Cross-cluster rules | Zone and role invariants, refused before writing | Only hub vs spoke per service | Apron |
| Topology | Every cluster runs and pulls with its own Argo CD | Default: hub holds spoke kubeconfigs. A hub with no spokes works too | compatible |
| Secrets | Infisical via ESO; fresh passwords per cluster | ESO with a backend you choose; OpenBao locally, Bitwarden in examples | compatible |
| Stack opinions | Traefik, Thanos, Tempo, Rollouts, Sloth, Crossplane, Tekton | Traefik, oauth2-proxy, Homer, Velero, Longhorn, MetalLB | overlap on Traefik |
| Provisioning | None (kind config only) | Optional Terraform for STACKIT and T Cloud; --local kind for evaluation | neither for Hangar |
| Maturity | Built for two live clusters | Young: config schema v1alpha5, releases every two weeks, migrations in the CLI | pin it |
These are the reasons to move, and which ones kubara fixes on its own.
The script deletes cluster.yaml and isn't safe to re-run. Each cluster keeps its own copy of 38 Applications and about 62,000 lines of vendored installs. Bumping cert-manager across the fleet is N pull requests on N diverging copies, the drift Apron was built to stop. It only stops it on day 0.
substitute() calls sed -i '' "s|…|…|g", which is BSD syntax. GNU sed reads '' as the script, so the run fails on Linux and in CI. Templating replaces sed entirely.
The 00- to 60- prefixes read as an order, but every Application starts at once; one manifest in the repo has a sync wave. kubara has the same property: one ApplicationSet per service, all in parallel, with a retry limit of 5 and a 5-minute maximum backoff. Crossplane packages before the XRs that need them is the case to test.
Substitution targets are real names from the old dev cluster and the prod cluster, which is why the script must skip hack/ and order rules longest-first. Templates with parameters remove this class of bug.
Two .DS_Store files are committed. AI triage has no components.aiTriage toggle yet, as the README notes.
Each of today's components.* toggles becomes a ServiceDefinition. Where kubara's general catalog already ships the same service, Hangar uses kubara's and supplies its own values in values-hangar.yaml. Where it doesn't, the apron catalog ships the chart. Name collisions fail by default in kubara, so Apron never redefines a general service by accident.
apron check refuses them on any cluster without the control-plane role.apron/ Catalog.yaml # spec.version: 1.0.0 services/ hangar.yaml # zone + roles live here infisical.yaml argocd-apps.yaml # second Argo CD platform-cicd.yaml autopilot.yaml … platform-components/helm/ argocd-apps/ platform-cicd/ … # today's Applications, # as charts platform-configs/helm/ argocd-apps/values.generated.yaml.tplt … hack/ apron-check # was customize-cluster.sh cluster-identity.sh # unchanged
version: v1alpha5
clusters:
- name: kind-staging
stage: upper
type: hub # its own Argo CD
dnsName: staging.hangar.example
ingressClassName: traefik
argocd:
selfManaged: enabled
repo: { … }
catalogs:
- oci://ghcr.io/kubara-io/catalogs/general:2026.42.1
- oci://ghcr.io/jfillman/catalogs/apron:1.0.0
services:
hangar:
status: enabled
config: { zone: upper, roles: [workloads] }
traefik: { status: enabled } # Hangar's gateway too
platform-cicd: { status: disabled }Where zone and roles live. kubara's cluster record has a free-form stage and a type of hub or spoke, nothing richer. An always-on hangar service carries zone and roles in its configSchema, so kubara types and checks them, and every template can read them through .cluster.services.hangar.config.
# services/platform-cicd.yaml apiVersion: kubara.io/v1alpha1 kind: ServiceDefinition metadata: name: platform-cicd annotations: { kubara.io/category: delivery } spec: chartPath: platform-cicd status: disabled # opt-in, control-plane only clusterTypes: [hub] configSchema: type: object properties: cicdReady: { type: boolean, default: false }
What apron check keeps doing. Everything kubara can't express. It reads config.yaml, refuses the same combinations the script refuses today (an upper zone with platform-cicd, provider-github, autopilot or a full service catalog; a control-plane cluster outside the lower zone; Autopilot without the full catalog), generates the Infisical and Rekor passwords, renders cluster identity, and prints the clusters.yaml record. It runs before kubara generate locally and as a required CI check. What it drops: copying, directory deletion, and the whole sed table.
How it goes upstream. Rules that span services are a gap any multi-cluster kubara user hits. Propose a small addition to kubara: rules in Catalog.yaml, checked during generate and schema, for example "service platform-cicd requires hangar.config.zone == lower". Open it as an ADR in kubara's own format, with apron check as the working reference. Once it lands, Apron's rules move into its catalog and the check script retires.
Catalog.yaml plus one ServiceDefinition per service. An open format already in use, not a Hangar invention.apron.lock and installed by the apron script like kind or Helm.apron up --name is the kubectl context, the fleet registry entry, the Infisical project stem, the issuer path and the hostname stem. It is recorded once, in the cluster's config; no tool, chart, template or script contains a cluster name.Naming. kubara calls its packaged unit a catalog, and a cluster's entry in config.yaml its cluster profile. Apron uses catalog for its service set, to match, and profile for a named cluster shape: dev, prod or baseline.
Technology Apron should build on, so Hangar doesn't end up imitating it:
HTTPRoutes, not Ingress.apron.lock (kubara, catalog versions, chart pins) as reviewed PRs. kubara and its catalogs already use it.Every Hangar component installs by itself. For Apron that means a person with a laptop and this one repo gets a working cluster without reading kubara's docs or cloning another Hangar repo. kubara changes what the script calls inside, not what the user runs. The tagline holds: one config file, one script, a known order.
Apron's main job is onboarding: point it at a Kubernetes cluster and it deploys Hangar there. Local runtimes are a convenience that creates the cluster first and then runs exactly the same onboarding. Two choices describe a cluster. The profile says what it runs: dev is the lower-zone control plane that also hosts Infisical; prod is the upper-zone workloads cluster. The runtime says where it runs: kiac (one Apple container VM per node, with Cilium and a Gateway built in), kind (Docker or Podman, with Calico), or an existing cluster you already run, such as EKS or AKS. Any profile runs on any runtime; runtime-specific pieces such as the CNI or the gateway's ports follow the runtime, not the profile. You also give each cluster its name. The lab's rebuild happens to put dev on kiac and prod on kind, because that's the best use of its two machines.
./apron init prod --name my-cluster --runtime existing --context my-cluster --out ../gitops-my-cluster ./apron up --out ../gitops-my-cluster # onboard Hangar onto a cluster you already have # the lab: create local clusters first, then the same onboarding ./apron init dev --name dev --runtime kiac --out ../gitops-dev ./apron up --out ../gitops-dev # lower zone, control plane, hosts Infisical ./apron init prod --name prod --runtime kind --out ../gitops-prod ./apron up --out ../gitops-prod # upper zone, workloads ./apron up prod --runtime kind --baseline-only # Apron alone: no Airframe, Glidepath or Autopilot ./apron resume --out ../gitops-dev # runtime's own resume (e.g. kiac resume) + refresh hosts ./apron verify --out ../gitops-dev # runtime health check, then every Argo CD app healthy ./apron down dev
apron up does. Blue is the only runtime-specific step. kubara appears in steps 4 and 6 as a tool the script runs.| Profile | Replaces in the lab | Zone · roles | Runtime | Turns on |
|---|---|---|---|---|
| dev | the dev cluster | lower · control-plane, workloads | any | Infisical host, platform CI/CD, full service catalog, Autopilot, observability |
| prod | the prod cluster | upper · workloads | any | Attached-tier catalog, Rollouts, Tower's cluster access (Tower itself deploys as an app); Infisical as a remote consumer |
| --baseline-only | none | either | any | Argo CD, cert-manager, ESO, Kyverno, Prometheus, Loki, Traefik. No other Hangar repo referenced |
Self-contained, concretely. The apron catalog is a directory in the repo, passed to kubara as --catalog ./catalog, so no registry is needed; publishing it as OCI is for other users, not a requirement. The kubara binary and the general catalog version are pinned in apron.lock. Runtime flags live in the cluster's own config, recorded at init, which also fixes the lab's open gap: the old prod cluster's recreate steps were never written down.
What still needs Git. Argo CD reconciles from a repository, so up shows the generated diff and pushes only when told to. That is the one external dependency, the same as today.
kubara's documented model is a hub that runs Argo CD and registers spokes, reading each spoke's kubeconfig from the secret backend. Glidepath ADR-0005 rejected exactly that: a hub holding every cluster's credentials is a path from dev to prod. kubara still fits, because nothing requires spokes. Declare every Hangar cluster as type: hub with argocd.selfManaged: enabled and it bootstraps and reconciles only itself, as Apron's clusters do now. kubara's own --local mode runs exactly this way.
The zone line maps onto repos. Keep one kubara repo per zone, or per cluster as today. Upper clusters pin argocd.repo.components.targetRevision to a tag, so a catalog bump reaches them only through a reviewed merge.
kubara's examples include an Argo CD Agent lab in which spokes pull manifests over outbound mTLS, with no hub kubeconfigs. That gives Tower one place to see every cluster while still honouring ADR-0005's reason. It's a candidate for revisiting that ADR, not a v1 dependency.
Hangar will install on EKS and AKS through a Terraform module. Terraform builds the cloud: network, cluster, DNS, workload identity and the cluster's Infisical identity. Apron takes over inside the cluster, exactly as it does on a local runtime. The home-lab rebuild doesn't use any of this; it's the path for clusters that live in a cloud account.
Three rules carry over from the lab. You name the cluster once, as cluster_name, and every resource takes its name from it. Terraform stops at Argo CD, so it never fights Argo CD over drift. Infisical is the only secrets backend: there is no AWS Secrets Manager or Azure Key Vault option.
terraform-hangar/
modules/
aws-cluster/ # VPC, EKS, node groups, EKS Pod Identity agent, Route 53 zone
azure-cluster/ # VNet, AKS with OIDC issuer + workload identity, Azure DNS zone
infisical-cluster/ # Infisical project, machine identity and its auth, project access
apron-bootstrap/ # cloud-agnostic: Argo CD, repo creds, Infisical creds, root app
hangar-aws/ # root: aws-cluster + infisical-cluster + apron-bootstrap
hangar-azure/ # root: azure-cluster + infisical-cluster + apron-bootstrap
examples/
aws-prod/ azure-prod/
| Module | Creates | Key inputs | Outputs |
|---|---|---|---|
| aws-cluster | VPC, EKS, managed node groups, EKS Pod Identity agent, Route 53 zone | cluster_name, region, Kubernetes version, node sizes | endpoint, CA, OIDC issuer URL, zone id |
| azure-cluster | VNet, AKS with OIDC issuer and workload identity on, Azure DNS zone | cluster_name, location, Kubernetes version, node sizes | endpoint, CA, OIDC issuer URL, zone id |
| infisical-cluster | infisical_project, infisical_identity, its auth method, infisical_project_identity | cluster_name, Infisical URL and org | identity credentials for the cluster |
| apron-bootstrap | argocd namespace, Argo CD, repo creds, Infisical creds Secret, root Application | kubeconfig parts, cluster repo URL, profile | the cluster's fleet record |
apron-bootstrap is apron up for a cluster that already exists. It runs the same steps from Argo CD onward, and the cluster repo it points at is rendered by Apron from the same cluster config, with runtime: existing. The issuer comes from the cloud: EKS and AKS publish their own OIDC issuers, so cloud clusters skip the lab's publish-to-hangarplatform.dev step, and the fleet record carries the provider's issuer URL.
Infisical, concretely. The infisical-cluster module uses Infisical's own Terraform provider. Version 1 uses Universal Auth, the method Hangar's provider-infisical already uses, so its client secret passes from Terraform to a Kubernetes Secret. That puts it in Terraform state, so state lives in an encrypted remote backend (S3 with KMS, or an Azure Storage account). Infisical also offers AWS Auth and Azure Auth, and the provider has resources for both. They would remove the stored secret, which matches Hangar's keyless direction, once External Secrets and provider-infisical are confirmed to support them.
A cloud cluster must reach Infisical. The lab's Infisical runs on the dev cluster behind a home network, so AWS and Azure can't reach it. Pick one: Infisical Cloud, or a self-hosted Infisical with a public URL (for example a cloud cluster built with the dev profile, which hosts Infisical). The module takes the URL as an input either way.
Moving it forward upstream. kubara ships Terraform only for STACKIT and T Cloud, and its provider list is fixed in the CLI. The aws-cluster and azure-cluster modules are written as ordinary reusable modules first, then offered to kubara as AWS and Azure providers.
Hangar keeps argocd-platform for the cluster's own baseline and argocd-apps for tenant apps (gitops-strategy §2). Apron installs both on every profile. kubara's bootstrap installs one Argo CD, so after the kubara port the second becomes an argocd-apps service in the apron catalog, together with tenant onboarding and the AppProject charts.
Apron installs and owns Traefik, the Gateway API CRDs and the Gateway on every cluster, the way the prod cluster already does, and every UI is an HTTPRoute on it. The runtime only decides how traffic reaches it: port mappings on kind, a NodePort on kiac (kiac's own --gateway Traefik is no longer used), and a LoadBalancer Service, so an NLB or Azure load balancer, on EKS and AKS. kubara's general catalog also ships Traefik, so this choice carries straight into the port. Because routes are Gateway API, a cloud cluster can later swap to its native controller without touching apps.
kubara's bootstrap Argo CD chart creates its own ExternalSecrets (the image pull secret and spoke credentials) and assumes a store named <cluster>-<stage> with keys at paths like <cluster>/<stage>/cluster_secrets/docker_config. Hangar uses one ClusterSecretStore, platform-secret-store, backed by Infisical projects named platform-cicd-<cluster> with flat keys such as registry-credentials-dockerconfigjson. Left alone, kubara's ExternalSecrets would look for a store and keys that don't exist and never sync. kubara lets later values-*.yaml files override its generated values, so Hangar points those references at its own store and keys; the lasting fix is a small upstream PR making the store name and key prefix configurable. Apron's own templates already use Hangar's names, so the rebuild isn't affected.
Config is at v1alpha5 and the CLI carries migrations between versions; releases come every two weeks. Pin the kubara binary and both catalog versions in CI, and upgrade on purpose.
Apron deploys Hangar onto a Kubernetes cluster, under the name you choose, and keeps it there. It onboards a cluster you already run, or creates a local one on kiac or kind first. Anything another platform could use goes upstream.
apron CLI's init, up, verify, checkresume and downapron catalog: Hangar-only services, charts and value templatesapron check in CIclusters.yamlcustomize-cluster.sh with the apron CLI: init records the cluster's name, profile, runtime and repos; up runs a runtime phase (create on kiac or kind, or check an existing cluster) and then one onboarding phase for every runtime; check keeps today's rules; fleet, identity and down cover the rest. No sed, no cluster name or GitHub owner in any template.apron init --create-repos or by hand; the old repos are left untouched.Done whendev and prod are rebuilt with apron up and every Skyport walkthrough replays green.
kubara bootstrap --local with only the general catalog to learn its conventions on kind. Join the kubara Slack channel and introduce Hangar's use case.kubara catalog create apron), keeping the apron CLI as the entry point; port Infisical, Rollouts, Sloth and the second Argo CD first, written to kubara's own chart and changelog conventions so they can be upstreamed unchanged.values-*.yaml overrides.apron up prod --baseline-only on kind (the runtime CI can run), then install the same rendered output a second time by the DIY path to prove it needs no kubara. File kubara gaps as kubara issues, not Apron workarounds.Done whenthe kubara-rendered output matches what v0.5 renders for both profiles, from a fresh clone of Apron alone, with no hand edits under platform-components/.
kubara-io/catalogs for Argo Rollouts, Sloth, Thanos, Tempo and an Infisical secret store. Remove each from Apron as it ships in a general release.apron check as the reference implementation.ghcr.io/jfillman/catalogs/apron as OCI.apron down then apron up.Done whena cert-manager bump is one change to a pinned catalog version, regenerated for every cluster and reviewed as one diff, and at least one Hangar service ships in kubara's general catalog.
vcluster runtime; per-PR clusters inside dev become apron up pr-N --runtime vcluster.Done whena new apron version reaches the dev cluster first and waits as a PR on every upper cluster.
Catalog.yaml; apron check becomes a thin call to kubara. The rest of the script stays, because the lab lifecycle is Apron's job.hangar-aws and hangar-azure Terraform modules on top of apron-bootstrap, with Infisical as the only secrets backend.--runtime.Done whenthe apron catalog contains only Hangar's own services, every rule runs inside kubara, and apron up still works from Apron alone.
cluster.yaml.example, hack/customize-cluster.shdocs/cluster-provisioning.md, docs/gitops-strategy.md, docs/local-clusters.md