Skip to main content

KubeAid CLI - Architecture

A GitOps-native, multi-cloud Kubernetes provisioning tool. Go • ClusterAPI • ArgoCD • AWS / Azure / Hetzner / KubeOne

License: Apache-2.0  •  Runtime: single kubeaid-cli binary (+ Docker for K3D)  •  Source: Obmondo/kubeaid-cli


Who should read this

This document is the single entry point for understanding how KubeAid CLI is put together. It is written for three audiences:

  • Operators who want to know what the tool does before trusting it with a production cluster.
  • Contributors who need to find the right file to edit for a given feature.
  • Integrators who are adding a new cloud provider, template, or addon.

If you only want to use the CLI, start at the README. This doc assumes you already understand Kubernetes, ClusterAPI, and GitOps at a conceptual level.

Table of contents

  1. Introduction
  2. Architecture at a glance
  3. Two-binary model
  4. Bootstrap flow
  5. Cloud providers
  6. Configuration system
  7. Template engine
  8. GitOps with ArgoCD
  9. Secrets & identity flow
  10. Cluster lifecycle
  11. Codebase map
  12. Development guide

1. Introduction

KubeAid CLI provisions, upgrades, tests, and recovers production Kubernetes clusters across multiple clouds, from a single YAML config. Everything it does eventually lands as commits in a Git repository called KubeAid Config, and ArgoCD reconciles that repository onto the target cluster.

What makes it different:

  • GitOps-native. Day-2 operations are Git commits, not kubectl apply.
  • One config, many clouds. The same general.yaml shape provisions AWS, Azure, Hetzner, or bare-metal clusters.
  • Opinionated addon stack. Cilium, Cert-Manager, Sealed Secrets, KubePrometheus, Rook-Ceph, Velero - pre-wired.
  • Self-managing result. After clusterctl move, the target cluster manages itself; the bootstrap environment can be thrown away.

Supported providers:

ProviderProvisioningManagement cluster
AWSClusterAPI (CAPA)K3D (local)
AzureClusterAPI (CAPZ) + CrossPlaneK3D (local)
Hetzner HCloudClusterAPI (CAPH)K3D (local)
Hetzner bare metalClusterAPI (CAPH) + Robot APIK3D (local)
Hetzner hybridClusterAPI (CAPH)K3D (local)
Generic bare metalKubeOneNone (direct)

2. Architecture at a glance

Five diagrams. Each zooms in on one concern; together they cover the whole system.

2.1 System overview

2.2 Bootstrap sequence

2.3 Config → templates → GitOps data flow


3. Binary model

kubeaid-cli is a single self-contained binary. cmd/kubeaid-cli embeds the cmd/kubeaid-core command tree and runs it in-process, so Cluster API, ArgoCD, templating, and the cloud SDKs are all compiled in — there is no separate engine process or container to pull. A second small binary, kubeaid-storagectl, runs on bare-metal hosts to apply storage plans.

BinaryRoleWhere it runs
kubeaid-cliThe whole tool — parses config, renders manifests, drives Cluster API / KubeOne and ArgoCD. The kubeaid-core engine is compiled in.Operator workstation
kubeaid-storagectlApplies storage plans (disk partition, ZFS pool, Ceph prep).On each bare-metal host

Operators install only kubeaid-cli. Go, the CAPI providers, kubectl, clusterctl, and helm are vendored into the binary; Docker is required only to run a local K3D cluster (Kubernetes-in-Docker) — and only for the Cluster API clouds, since the bare-metal (KubeOne) path installs directly onto the hosts.


4. Bootstrap flow

kubeaid-core cluster bootstrap is the most important code path. It runs four phases sequentially. Entry point: pkg/core/bootstrap_cluster.go.

Why only Hetzner has a "prerequisite infra" phase

AWS and Azure are provisioned declaratively by ClusterAPI + CrossPlane. Hetzner bare-metal has no such provider - networks, VSwitches, and OS installs must be done imperatively via the Robot API before CAPH can reconcile machines.

The pivot

After Phase 3, the target cluster hosts its own CAPI controllers and ArgoCD. clusterctl move transfers all CAPI custom resources from the K3D management cluster to the target, and the K3D cluster can be disposed.


5. Cloud providers

Every provider implements the CloudProvider interface. The interface is deliberately small - it only covers operations that differ meaningfully between clouds; provisioning is delegated to ClusterAPI/KubeOne, and addons to ArgoCD.

ProviderPackageDistinctive work
AWSpkg/cloud/awsCloudFormation IAM stack; CAPA provider
Azurepkg/cloud/azureCrossPlane provisions resource group, VNet, OIDC blob; Workload Identity
Hetznerpkg/cloud/hetznerNetwork, VSwitch, NAT GW, failover IP; Robot API for bare-metal OS install and storage plans
BareMetalembedded in core (KubeOne)No CAPI; kubeone apply runs directly against target hosts, for both bootstrap and Kubernetes version upgrades (see upgrade-bare-metal.md)
Localnot applicableThe K3D management cluster is the main cluster

6. Configuration system

Two YAML files drive everything: general.yaml (shape of the cluster) and secrets.yaml (API tokens, SSH keys). They are parsed, defaults are filled in, the result is validated, and the parsed structs become globals for the rest of the run.

Key files:

Top-level GeneralConfig fields:

FieldPurpose
GitKubeAid Config repo URL, branch, credentials reference
ClusterName, Kubernetes version, pod/service CIDRs, feature flags
CloudDiscriminated union: exactly one of aws/azure/hetzner/bareMetal/local
ForksOptional forks of KubeAid/KubeAid-Config repos
MonitoringSetupKubePrometheus, Grafana, alerting config
DisasterRecoveryVelero backup target (S3/Azure Blob)

Why a two-file split?

general.yaml is expected to land in Git (the KubeAid Config repo itself). secrets.yaml stays on the operator's machine and is referenced via Sealed Secrets inside the cluster.


7. Template engine

Go templates (pkg/templates) embed every YAML manifest the CLI will ever emit. At runtime, templates are rendered with ParsedGeneralConfig as their data context and written into outputs/configs/*.yaml, which is the working copy of the KubeAid Config repo.

Template categories:

CategoryExample output pathPurpose
CAPI clusterargocd-apps/capi-cluster/values.yamlCluster CR, control-plane, node groups
ArgoCD appsargocd-apps/<addon>/Chart.yaml, values.yamlOne directory per addon
Root appargocd-apps/root/templates/*.yamlApp-of-apps manifest
Sealed Secretssealed-secrets/*.yamlEncrypted secret material
KubeOne (bare metal)kubeone.yamlDirect install manifest

Rendering is deterministic: re-running bootstrap on the same config regenerates the same files byte-for-byte, which makes the PR workflow reviewable.


8. GitOps with ArgoCD

KubeAid uses the app-of-apps pattern. A single "root" ArgoCD Application manages many child Applications, each representing one addon.

Why app-of-apps? Operators only hand ArgoCD one manifest (root); every other addon is reconciled transitively. Enabling or disabling an addon is a one-line flip in general.yaml.

Related code:


9. Secrets & identity flow

One of the most important architectural properties of KubeAid CLI is that no long-lived secret is ever committed to Git. There are two parallel flows: user-provided secrets (API tokens, SSH keys, registry creds) go through Sealed Secrets; cloud access for in-cluster controllers goes through provider-native identity (Workload Identity on Azure, kube2iam-brokered IAM roles on AWS, Basic Auth on Hetzner Robot). Both flows converge at the target cluster without leaving plaintext material in the KubeAid Config repo.

Two-plane model:

PlaneSourceTransitAt rest in cluster
User secretssecrets.yaml on operator hostEncrypted by kubeseal → Git → ArgoCDSecret decrypted by controller
Cloud identity (AWS)CLI flags / envUsed to bootstrap the CloudFormation IAM stack; then CAPA instance profiles + kube2iam take overIAM role assumed via kube2iam pod annotation
Cloud identity (Azure)CLI flags / envUsed to create CrossPlane + UAMI; then Workload IdentityFederated token, no secret stored
Cloud identity (Hetzner)ROBOT_USER/ROBOT_PASSWORD + HCLOUD_TOKENMounted into kubeaid-core; wired to hetzner-robot addon via SealedSecretController reads SealedSecret

Why this matters:

  • Git repo is safe to share. The KubeAid Config repo contains only manifests and ciphertext; operators can open PRs, grant team-wide read access, or mirror it publicly without leaking credentials.
  • Rotation is surgical. Rotating a cloud credential means editing secrets.yaml and re-running bootstrap (or a dedicated rotation command). The sealed-secrets controller's keypair is the only long-lived secret, and it is backed up by the backup-sealed-secrets CronJob.
  • Disaster recovery preserves identity. Velero backs up the sealed-secrets controller's private key; a recovered cluster can decrypt the same SealedSecret manifests from Git without operator intervention.

10. Cluster lifecycle

Every lifecycle command has its own entry point under pkg/core. The commands share configuration parsing, template rendering, and ArgoCD sync; they differ in which subset of phases they run.

CommandEntry pointWhat it does
cluster bootstrapbootstrap_cluster.goFour-phase provision (see §4)
cluster upgradeupgrade_cluster.goBump K8s version: update values file, recreate MachineTemplates, rolling replace
cluster testtest_cluster.goSmoke-test a provisioned cluster (Cilium, DNS, storage)
cluster deletedelete_cluster.goDelete Cluster CR, wait for CAPI cleanup, tear down infra
cluster recoverrecover_cluster.goRestore from Velero backup onto a fresh cluster

The shared primitives - create dev env, setup cluster, setup KubeAid Config - live alongside them (create_dev_env.go, setup_cluster.go, setup_kubeaid_config.go).


11. Codebase map

kubeaid-cli/
├── cmd/
│ ├── kubeaid-cli/ # Binary entry point (embeds kubeaid-core root)
│ ├── kubeaid-core/ # Engine: Cobra command tree, compiled into kubeaid-cli
│ └── kubeaid-storagectl/ # Bare-metal storage plan executor
├── pkg/
│ ├── core/ # Lifecycle orchestration (bootstrap, upgrade, delete…)
│ ├── cloud/
│ │ ├── aws/ # IAM, CAPA wiring
│ │ ├── azure/ # CrossPlane, OIDC, Workload Identity
│ │ ├── hetzner/ # Network, VSwitch, Robot API, storage plans
│ │ └── cloud_provider.go
│ ├── kubernetes/
│ │ ├── argocd/ # ArgoCD client, install, sync
│ │ ├── capi/ # ClusterAPI helpers, clusterctl move
│ │ └── ...
│ ├── config/ # GeneralConfig, SecretsConfig, parser, validator
│ ├── templates/ # embed.FS of Go templates
│ ├── constants/ # Shared names, env vars, flag names, timeouts
│ ├── globals/ # Process-wide state (parsed configs, CP instance)
│ └── utils/
│ ├── assert/ # Fail-fast helpers (os.Exit on error)
│ ├── git/ # Clone, commit, PR
│ ├── commandexecutor/ # Run external CLIs (kubeone, clusterctl, helm)
│ ├── kubernetes/ # Client factories, apply, wait
│ ├── logger/ # slog setup, context-attached attrs
│ └── templates/ # Template rendering primitives
├── docs/ # This file + per-feature guides
├── tools/generators/ # Code generation for config schema
└── Makefile # Build, lint, image, run targets

Global state lives in pkg/globals/globals.go - intentionally small: the cloud provider instance, parsed configs, the ArgoCD client, and a handful of cloud-specific handles. New state should have a strong reason before going here.

Error handling is fail-fast. pkg/utils/assert wraps assert.AssertErrNil / assert.Assert and exits the process with a structured log line on any unexpected failure. This keeps call sites free of repetitive error-plumbing while still producing readable incident logs.


12. Development guide

12.1 Build

All build targets live in the Makefile. Version, commit, and build date are injected into the binary via -ldflags -X.

TargetOutput
make build./build/kubeaid-cli (CGO-off, ready to ship)
make build-storagectl./build/kubeaid-storagectl
make lintgolangci-lint run ./...
make formatgolangci-lint fmt - imports, golines, etc.
make testUnit tests; writes coverage.out
make check-coverageEnforce testcoverage.yaml thresholds
make addlicenseAdds Apache-2.0 headers to any Go file that lacks one
make run-generatorsRegenerates config artifacts from the struct definitions
make fetch-k8s-eolRefreshes embedded Kubernetes end-of-life data
make management-cluster-deleteDeletes the local K3D management cluster

12.2 Local dev loop

# 1. Build the CLI.
make build

# 2. Generate a config, then bootstrap against it. The CLI pulls the matching
# kubeaid-core image and runs the engine in a container.
./build/kubeaid-cli config generate --configs-directory ./outputs/configs/<cluster>/
./build/kubeaid-cli cluster bootstrap --configs-directory ./outputs/configs/<cluster>/

# 3. Tear down the local management cluster when done.
make management-cluster-delete

12.3 Coding standards

  • Follow Google's Go style decisions and best practices.
  • Run make lint and make format before pushing; CI is strict.
  • Fail fast via pkg/utils/assert instead of bubbling errors up through every caller.
  • Use log/slog everywhere; attach per-entity attrs with logger.AppendSlogAttributesToCtx so parallel flows remain filterable.

12.4 Contributing

  1. Open an issue describing the problem or feature.
  2. Fork the repo and create a topic branch.
  3. Run make lint and any relevant tests (go test ./...).
  4. Open a PR that references the issue; describe the why in the body, not just the what.
  5. For multi-step features, include a short architecture note in docs/

12.5 External references