Multi-provider OpenTofu IaC for a dev/test Kubernetes cluster that mirrors the production MIP environment closely enough to deploy mip-infra following its own upstream docs, changing only environment-specific values (the hardcoded CSCS IPs and hostnames).
Production MIP runs on an RKE2-based KaaS, so the reference stack is self-managed RKE2 — but the repo is structured so the cloud provider and the Kubernetes flavour are swappable options, not baked-in assumptions.
├── modules/
│ └── rke2-bootstrap/ # provider-agnostic: cloud-init + the add-on layer
│ # (cert-manager, MetalLB, Longhorn, ECK, Traefik,
│ # haproxy-edge) that satisfies the mip-infra contract
├── providers/ # one self-contained stack (root module) per option
│ ├── upcloud-rke2/ # self-managed RKE2 on UpCloud VMs — TESTED ✅
│ ├── upcloud-uks/ # UpCloud Managed Kubernetes (UKS) — TESTED
│ ├── exoscale-rke2/ # self-managed RKE2 on Exoscale VMs — UNTESTED ⚠️
│ ├── exoscale-sks/ # Exoscale SKS (managed) — UNTESTED ⚠️
│ ├── hetzner-rke2/ # self-managed RKE2 on Hetzner VMs — UNTESTED ⚠️
│ ├── hetzner-k8s/ # Hetzner has no first-party managed K8s — STUB / N/A
│ ├── lima-rke2/ # local Lima VMs (macOS/Linux), free — TESTED ✅ (macOS)
│ └── openshift/ # OpenShift — STUB
├── scripts/ # shared: prepare-mip-infra-fork.sh (works on any stack)
└── docs/
├── providers.md # the support matrix + what "tested" means
├── architecture.md # provider-agnostic architecture + the mip-infra contract
├── cost-comparison.md # all providers × options, with totals
├── firewall.md # the cloud-firewall model (UpCloud specifics noted)
├── access.md # consoles, dashboards, credentials
└── why-self-managed.md # self-managed RKE2 vs managed Kubernetes, provider-agnostic
Each providers/<name>/ is an independent OpenTofu root module with its own
state, terraform.tfvars.example, and README. Kubermatic and others slot in
as new providers/ entries.
cd providers/upcloud-rke2
cp terraform.tfvars.example terraform.tfvars # operator_cidr, ssh_public_key, acme_email
export UPCLOUD_TOKEN=... # or UPCLOUD_USERNAME/PASSWORD
tofu init && tofu apply
./scripts/fetch-kubeconfig.sh && export KUBECONFIG=$PWD/kubeconfig
kubectl get nodesThen deploy mip-infra per that stack's README
(fork prep → docs/getting-started.md steps). Full status matrix:
docs/providers.md. Cost of every option:
docs/cost-comparison.md.
No cloud account? The same cluster runs free on your laptop (macOS or
Linux, Lima VMs): see
providers/lima-rke2 — make init && make up.
Independent of provider, mip-infra assumes these exist before it deploys —
modules/rke2-bootstrap supplies them for the self-managed stacks, and the
managed stacks supply them via the helm/kubernetes providers:
- cert-manager + a ClusterIssuer named
letsencrypt - a LoadBalancer honouring
metallb.io/address-pool: pool-no-autowith fixed VIPs - RWX StorageClasses named
ceph-corbo-cephfs(default) andceph-corbo-cephfs-retain - the ECK operator + CRDs
- an ingress class
traefik(Argo CD UI) andhaproxy-public(MIP apps) - pod CIDR
10.42.0.0/16(submariner assumes it)
See docs/architecture.md for the full contract and how each stack meets it.
Provider and module versions, the RKE2 version, Helm chart versions, container images and GitHub Actions are all kept current by Renovate (renovate.json) — see docs/maintenance.md.