kubespray-helm-airgap
Helm Chart Management in Air-Gapped Environments
Overview
In air-gapped environments, Helm charts cannot be fetched from public repositories. Two approaches exist for managing Helm charts internally:
- Traditional Helm repo servers (ChartMuseum) -- a dedicated HTTP server that hosts an
index.yamland packaged chart archives. - OCI-compatible registries (modern) -- reuses your existing container registry infrastructure (Harbor, Docker Registry, Zot) to store Helm charts as OCI artifacts.
Core principle: package Helm charts and their referenced container images together, then stage both in internal registries before deployment.
When to Use
- Deploying Helm charts in air-gapped Kubernetes clusters
- Setting up internal Helm chart repositories
- Using an OCI registry for Helm chart storage
- Packaging and transferring Helm charts to isolated networks
- Setting up ChartMuseum in an air-gapped environment
Helm Repo vs OCI Comparison
| Aspect | Helm Repo (Traditional) | OCI Registry (Modern) |
|---|---|---|
| Storage | Dedicated Helm repo server | OCI-compatible container registry |
| Deploy command | helm repo add + helm install |
helm install oci://... |
| Auth | Repo-specific auth | Docker registry auth reuse |
| Security | Separate security policies | Reuse existing registry policies |
| Pros | Familiar, widely compatible | No extra infra, CI/CD friendly, standardized |
| Cons | Extra server to maintain | Requires Helm 3.8+ |
| Example URL | http://192.168.10.10:8080 |
oci://192.168.10.10:35000/helm-charts |
Creating and Packaging Helm Charts
Build a chart from scratch and package it for transfer:
mkdir nginx-chart && cd nginx-chart
mkdir templates
# Chart.yaml
cat > Chart.yaml <<EOF
apiVersion: v2
name: nginx-chart
description: A Helm chart for deploying Nginx
type: application
version: 1.0.0
appVersion: "1.28.0-alpine"
EOF
# values.yaml
cat > values.yaml <<EOF
image:
repository: nginx
tag: 1.28.0-alpine
replicaCount: 1
EOF
# Create templates: deployment.yaml, service.yaml, configmap.yaml, etc.
# Validate the rendered templates
helm template my-release . -f values.yaml
# Package the chart into a tgz archive
helm package .
# Output: nginx-chart-1.0.0.tgz
OCI Registry for Helm Charts
Push charts to an existing OCI-compatible container registry (the same one used for Kubernetes images):
# Push chart to OCI registry
helm push nginx-chart-1.0.0.tgz oci://192.168.10.10:35000/helm-charts
# Pushed: 192.168.10.10:35000/helm-charts/nginx-chart:1.0.0
# Verify the chart is stored
curl -s 192.168.10.10:35000/v2/_catalog | jq | grep helm
curl -s 192.168.10.10:35000/v2/helm-charts/nginx-chart/tags/list | jq
# Install from OCI registry
helm install my-nginx oci://192.168.10.10:35000/helm-charts/nginx-chart --version 1.0.0
# Inspect chart metadata and values
helm show chart oci://192.168.10.10:35000/helm-charts/nginx-chart --version 1.0.0
helm show values oci://192.168.10.10:35000/helm-charts/nginx-chart --version 1.0.0
ChartMuseum Setup
Deploy a ChartMuseum instance as a traditional Helm repository server:
# Create storage directory
mkdir -p /data/chartmuseum/charts
chmod 777 /data/chartmuseum/charts
# Run ChartMuseum container
podman run -d \
--name chartmuseum \
-p 8080:8080 \
-v /data/chartmuseum/charts:/charts \
-e STORAGE=local \
-e STORAGE_LOCAL_ROOTDIR=/charts \
-e DEBUG=true \
ghcr.io/helm/chartmuseum:v0.16.4
# Verify health
curl -s http://192.168.10.10:8080/health | jq
# {"healthy": true}
# Register as a Helm repo
helm repo add internal http://192.168.10.10:8080
helm repo update
# Install the helm-push plugin (required for cm-push)
helm plugin install https://github.com/chartmuseum/helm-push.git
# Push chart to ChartMuseum
helm cm-push nginx-chart-1.0.0.tgz internal
# Done.
# Install from ChartMuseum
helm repo update
helm install my-nginx internal/nginx-chart
Using External Charts Offline
Pull charts from public registries on an internet-connected machine, then transfer them to the air-gapped environment:
# On internet-connected machine:
# Pull chart from public OCI registry
helm pull oci://registry-1.docker.io/bitnamicharts/nginx --version 22.4.7
# Output: nginx-22.4.7.tgz
# Inspect the chart contents
tar -tzf nginx-22.4.7.tgz
zcat nginx-22.4.7.tgz | tar -xOf - nginx/Chart.yaml
zcat nginx-22.4.7.tgz | tar -xOf - nginx/values.yaml
# Transfer the tgz file to the air-gap admin server (USB, SCP over bastion, etc.)
# In air-gap: install directly from tgz
helm install my-nginx ./nginx-22.4.7.tgz --set service.type=NodePort
# Or push to internal OCI registry or ChartMuseum first
helm push nginx-22.4.7.tgz oci://192.168.10.10:35000/helm-charts
Container Image Staging for Charts
Charts reference container images. You must stage those images in the internal registry as well, or pods will fail to pull:
# Check what images a chart needs
zcat nginx-22.4.7.tgz | tar -xOf - nginx/Chart.yaml | grep image:
# Pull, tag, and push images to internal registry
podman pull docker.io/bitnami/nginx:latest
podman tag bitnami/nginx:latest 192.168.10.10:35000/bitnami/nginx:latest
# Configure insecure registry if needed (for HTTP registries)
cat <<EOF >> /etc/containers/registries.conf
[[registry]]
location = "192.168.10.10:35000"
insecure = true
EOF
podman push 192.168.10.10:35000/bitnami/nginx:latest
When installing the chart, override image references to point to the internal registry:
helm install my-nginx ./nginx-22.4.7.tgz \
--set image.repository=192.168.10.10:35000/bitnami/nginx \
--set image.tag=latest
Quick Reference
| Task | Command |
|---|---|
| Package chart | helm package . |
| Push to OCI | helm push chart.tgz oci://REGISTRY/path |
| Install from OCI | helm install NAME oci://REGISTRY/path/chart --version VER |
| Push to ChartMuseum | helm cm-push chart.tgz REPO_NAME |
| Install from ChartMuseum | helm install NAME REPO/chart |
| Pull public chart | helm pull oci://registry-1.docker.io/bitnamicharts/NAME |
| Install from tgz | helm install NAME ./chart.tgz |
| Show chart info | helm show chart oci://REGISTRY/path/chart |
Common Mistakes
- Forgetting to push container images referenced by the chart. The chart deploys successfully but pods fail with
ImagePullBackOffbecause the images are not in the internal registry. - Not configuring insecure registry in
/etc/containers/registries.conf. Podman push to an HTTP registry will fail without this configuration. - Missing the helm-push plugin for ChartMuseum. The
helm cm-pushcommand requires the plugin installed viahelm plugin install https://github.com/chartmuseum/helm-push.git. - Using
helm repocommands with OCI registries. OCI registries do not usehelm repo add. Usehelm pushandhelm install oci://directly. - Chart values referencing public image repositories. Override image references at install time with
--set image.repository=INTERNAL_REGISTRY/imageto point to the internal registry.
More from sigridjineth/kubespray-skills
rke2-operations
Use when managing RKE2 cluster certificates, performing manual or automated version upgrades, rotating TLS certificates, deploying the System Upgrade Controller, or troubleshooting RKE2 certificate and upgrade errors. Use when seeing "x509 certificate has expired" or "CertificateExpirationWarning" events or "Job has reached the specified backoff limit" errors.
3kubeadm-troubleshooting
Use when kubeadm init fails, join fails, nodes show NotReady, pods stuck Pending, certificate errors, or kubelet crashlooping
3kubeadm-init
Use when initializing a Kubernetes control plane with kubeadm, setting up certificates, static pods, or troubleshooting init failures
2kubespray-lab-setup
Use when setting up a local Vagrant/VirtualBox lab environment for Kubespray, provisioning multi-node clusters with Rocky Linux, or configuring admin/load balancer nodes for HA testing.
2kubespray-monitoring
Use when setting up Prometheus, Grafana, and Alertmanager monitoring on Kubespray clusters, configuring etcd metrics collection, deploying NFS storage provisioners, or importing Grafana dashboards.
2kubespray-certificates
Use when Kubernetes certificates are expiring, checking certificate expiration dates, setting up auto-renewal, or manually renewing certificates. Use when seeing x509 certificate errors.
2