Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Overview

This projects helps you to setup a complete Kubernetes Edge-Cloud Cluster within minutes using the Infrastructure-from-Code tool Pulumi and GitOps via ArgoCD. The cluster is designed with a focus on edge-first workloads, where the control-plane and basic infrastructure run in the cloud (Hetzner), while worker nodes can be added on-premises at the edge via VPN. The project includes a set of preconfigured applications commonly used by developers in SMEs, such as Nextcloud, XWiki, GitLab, Jitsi, and more - all integrated with Authentik for single sign-on.

Within the European-funded project CAPE, this infrastructure serves as a reference implementation and playground for testing various applications, configurations, and edge scenarios and will be continuously developed and improved with the goal to have create a cloud-provider-agnostic bootstrapping setup using the sovereign cloud API SECAPI.

Edge-Cloud infrastructure overview Authentik Dashboard

Project Goals

  • Perform most of the heavy lifting for setting up a Kubernetes cluster on Hetzner with a set of preconfigured applications, which should meet the needs of hard/software developers of SME.
  • Provide a reference implementation with a solid infrastructure software stack: Identify and Access Management (IAM) provider, monitoring, backup, restore, high availability, etc.
  • "Edge-first for workloads": Use cloud servers to setup the basic infrastructure. Edge-worker nodes attach to the cloud infrastructure via VPN and provide cost-effective and privacy-preserving on-site calculations.
  • IfC and GitOps: The infrastructure follows best practices in terms of infrastructure from code (IfC via Pulumi) and GitOps reproducibility via ArgoCD.
  • Modular approach: You can easily add/remove applications and adjust the deployment to your needs, e.g., you can to use multiple control plane nodes to achieve high-availability.
  • Updates via GitOps: By using renovate, all software components of this project can be easily maintained/updated via automated Github pull requests. If something broke, just revert the commit and the cluster will self-heal.
  • Flexible edge-cloud architecture: Edge nodes are considered ephemeral and can be added/removed at any time. The cluster is be able to handle this autonomously and allows applications to move between cloud and edge nodes.
  • Open Source first: All used components are open source and can be replaced by alternatives if needed.

Cluster Bootstrapping process

Cluster bootstrapping steps

  1. Cloud Infrastructure setup: The infrastructure engineer opens the provided devcontainer, creates a new Pulumi stack and seals the secrets (e.g. provider access tokens) using the provided shell scripts. After modifiying the file project_settings.ts, which is the single source of truth for Pulumi and cluster configuration, Pulumi will setup the described Infrastructure as Code in the cloud. This includes networking, storage, DNS, TLS certificates and initial OS-provisioning and Kubernetes setup on cloud servers. Finally ArgoCD will be started.

  2. GitOps Deployment: ArgoCD continuously monitors a configurableGit repository for changes. The minimal implementation contains the basic infrastructure: A self-managing ArgoCD, Prometheus and Grafana for monitoring, renovate update management, followed by Authentik for Identity and Authorization (IaM) management including an application dashboard/portal. headscale and headplane is used for VPN management.

    The initial software stack contains a pre-configured nextcloud for ofice and file storage, a xwiki for documentation and a gitlab for code hosting and CI/CD. Finally the RYAX workflow orchestration engine is deployed using a helm chart.

  3. Mesh node integration: After the VPN server is online, it is used as a VPN service provider to establish a virtual network between cloud and mesh nodes. The mesh nodes ("mesh" = externally-hosted machines adopted over SSH that join over the VPN — not physically at the network edge) connect outbound to the cloud VPN and join the cloud’s Kubernetes cluster. The infrastructure engineer can choose between two ways to integrate mesh nodes into the cluster:

    (a) Fully automated, Pulumi-based, by defining the nodes in project_settings.ts (nodes.mesh) and running make provision-mesh-node

    (b) Manual provisioning using a self-contained script including manual approval via the headplane admin portal. This is useful for integrating and onboarding of transient mesh nodes by non-infrastructure experts.

  4. Run-time usage: DevOps Engineers can monitor the cluster and start deploying their applications and workloads using ArgoCD or RYAX. The Ryax web-based user interface can now be used by non-expert developers or users to deploy preconfigured applications like LLMs or vision processing algorithms (ClickOps), or use a low-code approach.

Deployment Overview

The deployed set of applications is categorized into two groups: Infrastructure Apps and User Apps. The infrastructure apps are required for the cluster to run and are deployed by Pulumi and ArgoCD as part of the bootstrapping process. The user apps are optional and can be deployed by users via ArgoCD.

Infrastructure Apps

The list shows REQUIRED infrastructure applications, in ArgoCD sync-wave order. Waves 0–19 are owned by the infra ArgoCD instance (argocd-infra); barriers at waves 1, 3 and 18 gate advancement until everything below them is Synced+Healthy.

RAM columns: req/limit are the summed Kubernetes memory request/limit of the namespace's running containers (— = unset); usage is the live snapshot on cluster (kubectl top pods -A). Storage is summed PVC capacity (kubectl get pvc -A); Longhorn itself uses raw Hetzner block volumes, not PVCs.

App Wave RAM (req/limit) RAM (usage) Storage (PVC) Deployed by Purpose
Cilium 0 — / — (kube-system) — Pulumi, adopted by ArgoCD CNI pod network (k3s runs flannel-backend: none)
ArgoCD (infra) 0 — / — ~1380Mi 2Gi Pulumi GitOps for waves 0–19, self-managed thereafter
Envoy Gateway 0 800Mi / 1024Mi ~155Mi — Pulumi Gateway API controller + shared Gateway, HTTP→HTTPS redirect
External-DNS 0 — / — ~70Mi — ArgoCD (infra) Hetzner DNS records from HTTPRoute/Ingress hosts
kube-vip 0 — / — (kube-system) — ArgoCD (infra) Stable VIP 10.0.0.100 for the k3s API across CP nodes
PriorityClasses 0 — / — — — ArgoCD (infra) Scheduling priorities for graceful degradation (manifests only)
Sealed Secrets 0 — / — (kube-system) — Pulumi Encrypted secrets in git
sealed-secrets-guard 0 — / — — — ArgoCD (infra) CronJob that restarts the controller when a SealedSecret has no Secret
SeaweedFS 0 — / — ~285Mi 60Gi ArgoCD (infra) S3-backed file layer for cloud↔edge apps
ReferenceGrants 1 — / — — — ArgoCD (infra) Cross-namespace Gateway API backendRef grants
Longhorn (Pulumi) + 2 — / — ~6830Mi (block) Pulumi (chart), ArgoCD (config) Replicated block storage across cloud nodes
CloudNativePG 2 — / — ~95Mi — ArgoCD (infra) PostgreSQL operator (authentik, headscale, apps)
Prometheus 2 224Mi / — ~3720Mi 5Gi ArgoCD (infra) Metrics, alerting, Pushgateway
SeaweedFS CSI 2 — / — — — ArgoCD (infra) seaweedfs StorageClass
node-feature-discovery 2 608Mi / 1280Mi ~270Mi — ArgoCD (infra) Hardware-derived node labels (the only labels not from project_settings.ts)
nvidia-gpu 2 — / — — — ArgoCD (infra) nvidia RuntimeClass + device plugin for GPU mesh nodes
nested-runtime 2 — / — — — ArgoCD (infra) runsc (gVisor) RuntimeClass for nested-container nodes
Reloader 2 128Mi / 256Mi ~45Mi — ArgoCD (infra) Restarts pods when a mounted ConfigMap/Secret changes
system-upgrade-controller 2–4 — / — ~65Mi — ArgoCD (infra) k3s node version upgrades (plans at waves 3–4)
Authentik 6 128Mi / 512Mi ~1450Mi 10Gi ArgoCD (infra) Identity and access management (OIDC)
Headscale 8 408Mi / 1600Mi ~770Mi 7Gi ArgoCD (infra) Self-hosted Tailscale control plane (mesh VPN)
Headplane 8 — / — — — ArgoCD (infra) Headscale admin GUI
mesh-gateway 10 — / — — — ArgoCD (infra) Tailscale on every CP host; bridges private plane ↔ mesh plane
node-guard 10 — / — — — ArgoCD (infra) Host nftables guards on every node (mesh anti-loop)
mesh-monitoring 11 — / — — — ArgoCD (infra) iperf3 + ping probes for mesh link health
Descheduler 12 — / — — — ArgoCD (infra) Relocates running pods when node affinity later stops matching
Samba AD 13 1584Mi / 6336Mi ~975Mi 36Gi ArgoCD (infra) On-prem AD domain controllers (SMB identity for TrueNAS)
Loki 14 — / — ~500Mi 20Gi ArgoCD (infra) Cluster log store (not publicly routed; reached via Grafana)
Grafana 14 — / — (prometheus) — ArgoCD (infra) Dashboards over Prometheus + Loki
Alloy 15 1246Mi / 3584Mi ~675Mi — ArgoCD (infra) Log collector DaemonSet shipping to Loki
TrueNAS 16 — / — — — ArgoCD (infra) Lab appliance integration: AD join, Authentik proxy, NFS exports
csi-driver-nfs 17 280Mi / 2800Mi ~165Mi — ArgoCD (infra) Node driver for the static TrueNAS NFS PVs
ArgoCD (apps) 19 — / — ~1830Mi — ArgoCD (infra) GitOps for the user apps instance
Renovate 19 — / — CronJob — ArgoCD (infra) Dependency PRs against GitHub
notify 19 — / — — — ArgoCD (infra) One-shot bootstrap-complete notification
cert-manager (Pulumi) — / — ~180Mi — Pulumi TLS certificates (wildcard + per-host)
WireGuard (Pulumi) 80Mi / 160Mi ~20Mi — Pulumi Dedicated admin VPN (the only way in once hardened)
Total 5486Mi / 17808Mi ~19Gi ~140Gi

(*) Longhorn RAM is spread across many pods, not one process, and scales with node count and attached volume count/size. The bulk is a per-node instance-manager pod, which runs the userspace engine + replica process for every volume attached to that node (each pre-allocates buffers/page cache). The rest: one longhorn-manager and one csi-plugin per node, plus HA CSI sidecars (provisioner/attacher/resizer/snapshotter ×3) and a share-manager per RWX volume. Moving/consolidating volumes shifts where the instance-manager RAM lands but does not remove it — it is inherent to Longhorn running storage engines in userspace.

User Apps

User configurable OPTIONAL applications, deployed by the apps ArgoCD instance (argocd-apps, deployment/argocd-apps/app-of-apps/). None of them is required for the cluster to run.

RAM columns: req/limit are the summed Kubernetes memory request/limit of the namespace's running containers (— = unset); usage is the live snapshot on cluster (kubectl top pods -A). Storage is summed PVC capacity (kubectl get pvc -A); NFS-backed apps additionally use TrueNAS datasets that are not PVCs.

Collaboration and office

App RAM (req/limit) RAM (usage) Storage (PVC) Purpose
Nextcloud 128Mi / 512Mi ~1050Mi 20Gi File sync and share
Nextcloud Collabora (in nextcloud ns) Online document editing backend for Nextcloud
XWiki — / — ~2580Mi 13Gi Wiki / documentation, OIDC + Postgres
Zulip 960Mi / 3904Mi ~5100Mi 32Gi Team messaging
Rocket.Chat 1024Mi / 4096Mi ~1330Mi 16Gi Team messaging
Jitsi 128Mi / 512Mi ~740Mi 32Gi Video conferencing
Rallly 640Mi / 2012Mi ~580Mi 4Gi Group availability polling
Zammad 1728Mi / 3392Mi ~3460Mi 20Gi Ticketing / helpdesk
Subtotal 4608Mi / 14428Mi ~14.5Gi 137Gi

Code hosting and CI

App RAM (req/limit) RAM (usage) Storage (PVC) Purpose
GitLab 6199Mi / — ~6890Mi 125Gi Self-hosted code hosting, registry, CI
GitLab Runner 640Mi / 2560Mi ~160Mi 503Gi Default untagged cloud CI runner
GitLab Runner (mesh) (in gitlab-runner ns) Runner pinned to mesh nodes
GitLab Runner (thor) (in gitlab-runner ns) arm64 GPU-node runner
GitLab Runner (eda) (in gitlab-runner ns) Privileged buildah runner for EDA image builds
GitLab Runner (eda-run) (in gitlab-runner ns) gVisor runner executing EDA toolchains via module load
gitlab-mirror — / — — 200Gi Hourly bare-repo mirror of GitLab onto NFS
gitlab-s3-proxy 64Mi / 256Mi ~10Mi — Lab-pinned TCP proxy to the appliance S3 endpoint
ci-build-image — / — — — Builds the CI image (buildah + skopeo); no workload
osxcar-sdv-switch — / — — — Mirrors an external repo and runs its EDA deploy pipeline
Subtotal 6903Mi / 2816Mi ~6.9Gi 828Gi

EDA / engineering workstations

App RAM (req/limit) RAM (usage) Storage (PVC) Purpose
remote-desktop 2336Mi / 28800Mi ~1000Mi 611Gi Ubuntu/Xfce EDA desktop, runtime module load
remote-desktop-bender 2336Mi / 28800Mi ~630Mi 606Gi Second EDA desktop, pinned to the bender node
desktop-rollout — / — — — CronJob syncing remote-desktop only while it has no sessions
eda-pcb-agent 3104Mi / 24704Mi ~355Mi 500Gi Shared KiCad PCB workstation over RDP
eda-fileserver — / — — 2Gi Provisions the TrueNAS datasets and NFS exports for EDA
eda-xilinx-2024-1 — / — — — Builds the Vivado 2024.1 module image
eda-xilinx-2026-1 — / — — — Builds the Xilinx 2026.1 module image
eda-petalinux-2024-1 — / — — — Builds the PetaLinux 2024.1 module image
eda-hyperlynx-2604 — / — — — Builds the Siemens HyperLynx 2604 module image
image-registry 192Mi / 8448Mi ~420Mi 500Gi Lab-local OCI registry for the EDA module images
windows 4096Mi / 6144Mi ~3570Mi 45Gi Windows 11 VM (QEMU/KVM), reached over RDP
Subtotal 12064Mi / 96896Mi ~5.8Gi 2264Gi

AI

App RAM (req/limit) RAM (usage) Storage (PVC) Purpose
Ollama 4096Mi / 32768Mi ~10150Mi 40Gi GGUF model serving on the Jetson Thor GPU node
ollama-turing 2048Mi / 12288Mi ~160Mi — Ollama on the discrete-GPU mesh node (2× RTX 2070)
LiteLLM 1152Mi / 2560Mi ~985Mi 4Gi LLM gateway, per-user virtual keys
Open WebUI 640Mi / 2560Mi ~830Mi 12Gi Web chat UI, routed through LiteLLM
Hermes 2048Mi / 12288Mi ~345Mi 520Gi Coding agent with an OpenAI-compatible endpoint for IDE extensions
SearXNG 192Mi / 512Mi ~120Mi — In-cluster metasearch backend for Hermes
Subtotal 10176Mi / 62976Mi ~12.3Gi 576Gi

Workflow

App RAM (req/limit) RAM (usage) Storage (PVC) Purpose
Ryax 7542Mi / 9024Mi ~4930Mi 176Gi Low-code workflow engine (ClickOps deployment of workloads)
Subtotal 7542Mi / 9024Mi ~4.8Gi 176Gi

Total (user apps): 40.3Gi requested / 181.8Gi limit, ~44.3Gi RAM in use, ~3.9Ti PVC.

The limit total is deliberately far above physical RAM: the EDA desktops and the model servers carry high ceilings so a single session can burst, and they are never all busy at once.

vllm (app-of-apps/vllm.yaml.disable) is present but disabled — it is exclusive with Ollama on Thor's GPU. module-runtime is a documented pod-spec contract checked at pre-commit, not an Application.

Setup Instructions

Enough said! Let's start creating your edge-cloud cluster. See setup instructions

Architecture and Implementation Details

Architecture overview

Cluster lifecycle management

Storage architecture

High-availability strategy

AD identity chain — how a person becomes a uid, and the two independent sources that can disagree. Read before touching anything uid-related.

Development

GPU metrics — per-card utilization for the two GPU mesh nodes, and why the Jetson deliberately reports no GPU memory.

Frequently Asked Questions

What are the resource requirements for the control plane?

See table in README.md bottom. TODO linkme

Minimum system works on a single server with

  • 12 GByte of DDR memory
  • 1.5 of 16 CPUs (10%) are busy in idle state
  • 1 TB object/S3 bucket storage
What does it cost?

--> TODO update

  • 26€ monthly costs (no redundance, single control plane server):

    • 16€ Server: CX53 (16CPU/32GB/320GB)
    • 5€ 100 GByte SSD Block storage
    • 5€ Object storage / S3 bucket
  • 47€ monthly costs (with high availability (HA), 3 Kubernetes control plane servers): - 32€ (2x 16€) server CX53 (16CPU/32GB/320GB) - 5€ server CX33 (4CPU/8GB/80GB) - 5€ 100 GByte SSD Block storage - 5€ Object storage / S3 bucket

Lets encrypt staging vs. production certificates
  • If you re-create the cluster multiple times within a couple of days, you might hit the rate limits of Let's Encrypt production certificates.
  • I use subdomains for testing like myAwesomeCluster, which are set in project_settings.ts (subdomain), then run ./scripts/environment/updateConfigFromProjectSettings.sh to apply updated domain settings in ArgoCD.
  • in Pulumi you can select between staging or production certificates, see project_settings.ts ("certIssuerType") and running ./scripts/environment/updateConfigFromProjectSettings.sh to apply the new domain settings in ArgoCD. - Hint: To open a website with an untrusted (or staging) certificate in chrome just type thisisunsafe in Vivaldi (probably other chrome-based browsers too)
  • A single wildcard cert covers almost every host (per-host certs are the exception). See doc/tls-certificates.md for the wildcard-by-default architecture, backup/recovery flow, and CA consumers.
Why ArgoCD?

It is a well known GitOps tool, which allows us to deploy applications in a declarative way. It also provides a nice UI to monitor the deployment status and logs.

Why don't you use a separate git repository for ArgoCD?

It is a well known GitOps tool, which allows us to deploy applications in a declarative way. It also provides a nice UI to monitor the deployment status and logs. It is good practice to keep cluster infrastructure and application deployment separate. Here we want to keep everything in one repository for simplicity. In a production environment with different infra/deployment teams, you might want to separate them.

Why Hetzner as cloud provider? I am hosting several private stuff on Hetzner for years and I am very happy. They offer a good balance between price and performance and up-time. They also have good API support. So the answer is: I am familiar with Hetzner. But the code base is meant to be easily adaptable to other cloud providers, especially via SECAPI. --> Roadmap
Why didn't you use openDesk? I started with [opendesk](https://www.opendesk.eu/) TODO fix link, because it sounded like a good fit for our use case and is well maintained, production-ready and used by the german government. But after deploying it the footprint was too high. It required more than 24 Gbyte RAM to deploy most of the apps. The IaM components Nubus/keycloak were very resource hungry and the encapsulated approach of opendesk made it difficult to configure these components to fit my needs. So I decided to build this cluster infrastructure from scratch using separate tools.
Secrets in git - are you crazy? In general: Bad idea! But here, the secrets are well encrypted using kubeseal. See this [article](https://aws.amazon.com/de/blogs/opensource/managing-secrets-deployment-in-kubernetes-using-sealed-secrets/) for a detailed explanation.
  • Pulumi: Secrets are encrypted/decrypted with the Pulumi passphrase.
  • ArgoCD: Secrets are encrypted with a mechanism called SealedSecrets. You create the key for this in Pulumi and encrypt secrets for ArgoCD within the folder /deployment. Pulumi passes the key to ArgoCD, which can then decrypt thos secrets at deploy-time.
Why testing with subdomains? I recreated the cluster around 100 times. To avoid hitting the rate limits of Let's Encrypt production certificates, I use a subdomains like "\*.testNN." (incrementing number) for my tests and increment regularly (adjust project_settings.ts and run `./scripts/environment/updateConfigFromProjectSettings.sh` to apply the new domain settings in ArgoCD).

Also in my office there is a DNS proxy, which caches DNS entries with a long TTL. So after re-creating the cluster the IP addresses changed, but the DNS were still cached with the old IPs. By using different subdomains, I can avoid this issue.

Acknowledgements

CAPE - European Open Compute Architecture for Powerful Edge This work is part of the CAPE project, which has received funding from the European Union's Horizon Europe research and innovation programme under grant agreement No 101135. The content of this project reflects only the authors' view and the European Commission is not responsible for any use that may be made of the information it contains.

Disclaimer

  • This project was not audited for security, so it should not be used in production environments without further adjustments and hardening.
  • It is not meant to be an "all-in-one", "ready-to-deploy-and-use" solution, but rather a starting point and reference implementation for further development and adjustments to your needs.

Open Source Strategy

All used software parts in this project are open source and can be replaced by alternatives if needed.

Publications

The basic infrastructure for this project was published at the Computer Frontiers 2026 conference, as part of a multi-partner project presentation. The paper can be downloaded here CAPE's Composable Server Infrastructure for the Edge-Cloud Continuum.pdf

Roadmap

  • see ToDo.md for detailed tasks and next steps
  • Improve documentation, add more screenshots
  • Test, test, test
  • Harden the cluster for production use
  • Generalize setup and make it work for other cloud provider --> SECAPI
  • Move repository to github.com/cape-project-eu

About

Starting point for setting up a Kubernetes-based edge-cloud cluster on the cloud provider Hetzner with ArgoCD GitOps using Pulumi Infrastructure from Code (IfC). Comes preconfigured with a complete management and update Infrastructure, Authentik, VPN (Headscale, Headplane), Prometheus/Grafana, Gitlab, Nextcloud and Ryax workflow orchestrator

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages