Edit

First Deployment

Purpose: For platform engineers and field operators, walks through building an air-gap Zarf package on a connected build host, transferring it, and using it to install a 3-node Kubernetes cluster on a disconnected bastion plus three cluster nodes.

You will end with:

  • A signed Zarf package on the build host (~25 GB compressed).

  • A bastion serving a local container registry, an apt mirror, and a Python wheel index.

  • A 3-node Kubernetes v1.34.3 cluster pulling exclusively from the bastion.

Plan on 60–90 minutes the first time, mostly waiting for downloads.

Prerequisites

Build host (Zone A — connected):

  • Ubuntu 24.04 (other Linux distros work but are not what the build is tested on).

  • Python 3.12 or later (pyproject.toml pins requires-python = ">=3.12").

  • Git 2.30+.

  • 100 GB free disk.

  • Internet access to GitHub, container registries, Ubuntu apt mirrors, and PyPI.

  • Optional: Zarf CLI and Cosign, if you want the build to produce a signed .tar.zst package. Without Zarf the build still produces zarf.yaml and the artifact manifest, but not the compressed package.

Field environment (Zone C — disconnected):

  • 1 bastion host on Ubuntu 24.04, 100 GB free disk, Zarf CLI installed.

  • 3 cluster nodes on Ubuntu 24.04, each ≥ 4 vCPU and ≥ 8 GB RAM.

  • Bastion can SSH to all cluster nodes; cluster nodes can reach the bastion on TCP 5000 (registry), 80 (nginx), and 3000 (Gitea, optional).

Step 1 — Install the CLI on the build host

git clone <repo-url> opencenter-airgap
cd opencenter-airgap
pip install -e .
opencenter-airgap version

pip install -e . installs the opencenter-airgap script defined in pyproject.toml under [project.scripts]. The version subcommand prints the package version plus key dependency versions.

Step 2 — Initialize a project

opencenter-airgap init

This creates config/, build/, dist/, assets/, and a default config/versions.env. It does not create config/components.yaml — the build step will generate that for you on first run.

Step 3 — Pin versions

Open config/versions.env and review the pinned versions. The defaults match the matrix the project ships with:

KUBERNETES_VERSION="v1.34.3"
UBUNTU_VERSION="24.04"
CONTAINERD_VERSION="2.1.5"
CALICO_VERSION="v3.31.3"
FLUXCD_VERSION="v2.7.5"
…

For your first deployment, keep the defaults. The build will fail closed if any value is missing or unparseable.

Step 4 — Build the package

opencenter-airgap build

The build:

  1. Loads config/versions.env.

  2. Generates (or merges into) config/components.yaml from those versions.

  3. Clones the source repos listed in versions.env (Kubespray and openCenter-gitops-base) into build/.

  4. Scans the cloned repos for container images and Helm charts.

  5. Writes config/all-images.txt, config/helm-charts.txt, and config/helm-repos.txt.

  6. Generates zarf.yaml from zarf.yaml.template.

  7. If Zarf CLI is installed, builds dist/zarf-package-opencenter-airgap-amd64-<version>.tar.zst plus …-sbom.json and …sha256 sidecars.

  8. Writes dist/artifact-manifest.json recording every artifact with its checksum.

Build state is checkpointed to build/state.json. If the build dies — usually a flaky download — re-run opencenter-airgap build and it will resume from the last completed step. To start fresh use --clean.

Step 5 — Verify the package

Before transferring the package, run the verification helper:

hack/scripts/verify-package.sh dist/zarf-package-*.tar.zst

This checks the .sha256 sidecar, optionally the Cosign signature if you pass a .pub key, and rejects the package if the SBOM contains latest image tags or HIGH/CRITICAL CVEs. See ../operations/verify-package.md[Verify a Built Package] for the full procedure.

Step 6 — Transfer to the field

The package and its sidecars need to travel together:

ls dist/
# zarf-package-opencenter-airgap-amd64-1.0.0-rc2.tar.zst
# zarf-package-opencenter-airgap-amd64-1.0.0-rc2.tar.zst.sha256
# zarf-package-opencenter-airgap-amd64-1.0.0-rc2-sbom.json
# artifact-manifest.json
cp dist/zarf-package-* /media/usb/
cp dist/artifact-manifest.json /media/usb/

Move the media to the disconnected site by whatever process your environment requires.

Step 7 — Deploy on the bastion

On the bastion:

sudo zarf package deploy zarf-package-*.tar.zst --confirm

This unpacks the package under /opt/opencenter/, starts a container registry on TCP 5000, brings up an nginx file server, and stages the Kubespray playbooks and binaries.

If you have the CLI installed on the bastion as well, opencenter-airgap serve <package> does the deploy and an explicit health check on every service.

Step 8 — Provision the cluster

On the bastion, write the inventory for your three nodes. Replace the IPs with the addresses of your cluster hosts:

# /opt/opencenter/kubespray/inventory/mycluster/inventory.yml
all:
  hosts:
    node1: { ansible_host: 192.168.1.101, ip: 192.168.1.101 }
    node2: { ansible_host: 192.168.1.102, ip: 192.168.1.102 }
    node3: { ansible_host: 192.168.1.103, ip: 192.168.1.103 }
  children:
    kube_control_plane: { hosts: { node1: {} } }
    kube_node: { hosts: { node1: {}, node2: {}, node3: {} } }
    etcd: { hosts: { node1: {} } }
    k8s_cluster:
      children:
        kube_control_plane: {}
        kube_node: {}

Point the cluster nodes at the bastion’s apt and pip mirrors:

cd /opt/opencenter/playbook
ansible-playbook -i ../kubespray/inventory/mycluster/inventory.yml offline-repo.yml

Then run Kubespray:

cd /opt/opencenter/kubespray
ansible-playbook -i inventory/mycluster/inventory.yml cluster.yml

Expect 20–30 minutes.

Step 9 — Check your work

On the bastion:

export KUBECONFIG=/opt/opencenter/kubespray/inventory/mycluster/artifacts/admin.conf

# All nodes Ready
kubectl get nodes
# NAME    STATUS   ROLES           AGE   VERSION
# node1   Ready    control-plane   5m    v1.34.3
# node2   Ready    <none>          4m    v1.34.3
# node3   Ready    <none>          4m    v1.34.3

# Core pods running
kubectl get pods -A | grep -v Running | grep -v Completed
# (only the header should remain)

# Bastion registry is the only image source
kubectl get pods -A -o jsonpath='{.items[*].spec.containers[*].image}' \
  | tr ' ' '\n' | sort -u | head
# Every line should start with <bastion-ip>:5000/

If anything fails the ../reference/troubleshooting.md[Troubleshooting reference] covers the common cases.

Next steps

  • ../operations/add-custom-component.md[Add a Custom Component] — extend the manifest with a tool, image, or chart.

  • ../reference/component-manifest-schema.md[Component Manifest Schema] — full reference for config/components.yaml.

  • ../concepts/architecture-overview.md[Architecture Overview] — why the build is split into three zones.