Kubernetes Upgrade On An Air-gapped BCM Cluster
Section 4.22 covers Kubernetes upgrades in the case of a BCM Kubernetes cluster connected directly to the internet. An air-gapped cluster environment is one where the cluster is not directly connected to the internet.
A BCM cluster that air-gapped and already runs Kubernetes can be upgraded with the procedure outlined in this current section (section C). It is based on the upgrade procedure in section 4.22. The main differences are that the BCM version requirements differ, and that a local registry must be built.
Appendix B describes how an air-gapped cluster can be created if it does not already exist.
C.1 Air-gapped Kubernetes Upgrade Prerequisites
Before beginning the upgrade, the following general prerequisites and version checks must be carried out:
-
The cluster version must be at a sufficiently high point release number (section 9.1.2 of the Administrator Manual):
-
for BCM version 10 the point release version number must be 10.31.0 or later
-
for BCM version 11 the point release version number must be 11.32.0 or later
-
-
The upgrade path should take into account the Kubernetes version skew policy, as defined at https://kubernetes.io/releases/version-skew-policy/. The principles to take into account are:
-
Only two adjacent minor releases may exist in the cluster at any time. So, v1.34 and v1.35 are acceptable; but v1.33, v1.34, and v1.35 are not acceptable.
-
The target version must not violate the skew values. So, if the cluster nodes are currently on two different adjacent minor releases, then the only valid upgrade path is to the newest of the two minor releases. For example if some nodes are running v1.34 and some are running v1.35, then the only valid upgrade path is to upgrade the v1.34 nodes to v1.35. This ensures that the cluster remains functional and compliant with the Kubernetes version skew policies during the rolling upgrade process.
-
-
The upgrade path should also take into account the NVIDIA BCM Release Notes and Compatibility Matrix corresponding to the BCM version that is running. This must be consulted, to see if there is support for the desired target Kubernetes version (for example v1.35.x).
-
An upgrade path preference, for minor or patch, must be decided on for the case where all the nodes are on the same minor version:
-
Minor release upgrade: the latest release of the next minor release (for example: v1.34 to v1.35)
-
Patch release upgrade: the latest patch release of the current minor release (for example: v1.35.3 to v1.35.4)
-
-
The current Kubernetes cluster must already be operational, with healthy and ready nodes.
-
A local registry must already be deployed, and its FQDN must be retrievable (for example: master.cm.cluster:5000)
-
The upstream instructions on preparing for the upgrade (https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/#before-you-begin) should be read.
-
A node that is to be upgraded must be drained.
C.2 Initial Host Preparations For BCM Kubernetes Air-gapped Upgrade
The initial steps must be performed on an initial host computer that has internet access. The initial host requires:
-
Internet access
-
An OS that matches that of the air-gapped cluster: Ubuntu 22.04, Ubuntu 24.04, Ubuntu 26.04, Rocky Linux 9, or RHEL 9.
-
An architecture that matches that of the air-gapped cluster
-
Currently, the tarball supports one architecture at a time. So if the cluster has a mixed architecture, for example with the head node being x86-64 (using
x86_64RPM packages oramd64APT packages), but the Kubernetes control plane nodes being ARMv8 (usingarm64packages), then that requires a more customized approach.For architecture configurations, the
--helpoutput of the download Bash scripts (format:download_<descriptive label>.sh) indicates how the--archoption can be used. These scripts are located in theairgap-scriptsdirectory, described shortly.
-
-
The BCM package repositories must be configured on the initial host. The relevant BCM repository list files can be copied over from a working cluster with the same OS.
-
For Ubuntu, these files are of the form
cm-*.listandubuntu.sources, and are located within the/etc/apt/sources.list.d/directory.There are some known architecture-related bugs in the file specifications for the Ubuntu repositories. These should be fixed for the BCM 11.34 point release. The issues and associated workarounds for the 11.33 release are in the following files:
-
ubuntu.sources: The default Ubuntu sources file needs an architecture specification to be added. The default file can be replaced with the following deb822-format (man sources.list.5) text:Example
-
cm-cuda-ubuntu2404-*.list: The file for the default CUDA repositories list is modified as needed. Here, the “*” is eitherx86_64orarm64. The change is to allow CUDA packages to be picked up from whichever architecture is needed:Example
-
cm.list. The BCM repository list file is modified so that the extra architectures that are needed are specified:Example
In the preceding session:
-
The architecture key/value entries are specified for the repositories in the square brackets, using the traditional one-line format, but using a deb822 format is also possible.
-
The URLs specified in the preceding session take the <user>
:<password> format indicated. The values that are used for <user>:<password> should already be configured in the originalcm.listrepository file, and can simply be taken over. -
The server and path of the URL on the actual cluster may differ from the preceding example.
-
-
-
For RHEL variants these files should be
cm.repoandepel.repo, located within/etc/yum.repos.d/
-
-
The BCM
airgap-scriptsdirectory from the matching BCM version must be on the initial host. How the installation of that directory is carried out depends on ifcm-setupis on the initial host, as explained next.
C.2.0.1 Initial Host Has cm-setup Installed
If the cm-setup package is already installed on the initial host, then for BCM version 11, the air-gap scripts are by default in the path defined by the environment variable K8S_AG_SCRIPTS.
Example
For BCM version 10, the Python directory path is python3.9 instead.
This environment variable path should be added to the existing PATH:
C.2.0.2 Initial Host Does Not Have cm-setup Installed
If the cm-setup package is not already installed on the initial host, then the administrator should define the air-gap scripts path, $K8S_AG_SCRIPTS, and then add the air-gap scripts to the PATH environment.
C.2.0.3 Using The Air-gap Scripts
-
helm 3.x(not 4.x at the time of writing, April 2026) must be added to/usr/local/bin/helmas per the instructions at https://helm.sh/docs/intro/install/.Alternatively, the following helper script, that BCM ships with
cm-setup, can be used:Example
Optionally, a Helm check can be carried out with:
Example
-
Skopeo, an image manager, is installed. This installation is carried out on the internet-connected host only, and not on the air-gapped head node. Skopeo is needed here in order to pull the container images that make up the tarball:
Example
The air-gapped head node obtains its own copy of Skopeo from the tarball, and no attempt should be made to install Skopeo there from a distribution repository (section C.3).
-
To work around a potential Docker Hub pull rate limit issue (https://docs.docker.com/docker-hub/usage/pulls/) the administrator can now sign up for an account at https://app.docker.com/signup, and authenticate with
skopeo login docker.io. This is to help with running the image installation scripts from theairgappeddirectory in the next bullet point.Any credentials are fine—the credentials will not end up in the air-gapped tarball or environment. They are only needed for authentication to conveniently pull images from Docker Hub. This is because unauthenticated pulls are limited to a rate of 100 per 6 hours. Authenticated pulls, on the other hand, are allowed at a rate of 200 per 6 hours, which should be sufficient for preparing the
airgappedtarball.
-
-
A working directory, for example
/root/airgapped, can then be created, and installation scripts within theairgappeddirectory can then be run:Example
The preceding scripted downloads are for Kubernetes version 1.34. To use a different version, such as v1.33 or v1.35, the
--kube-versionflag should be adjusted accordingly. If--kube-versionis not set ondownload_ubuntu_packages.sh,download_r9_packages.sh, ordownload_container_images.sh, each script falls back to its own default Kubernetes version, which is not necessarily the version that is to be deployed.download_helm_charts.shanddownload_crds.shtake no version-related option, and are unaffected by this.download_ubuntu_packages.sh,download_r9_packages.sh, anddownload_container_images.shalso accept a-c/--configoption, giving the path to acm-kubernetes-setup.confconfiguration file to read the Kubernetes version from, instead of--kube-version. Ondownload_ubuntu_packages.shanddownload_r9_packages.sh,--kube-versionand-care mutually exclusive: only one may be given. Ondownload_container_images.shboth may be given together, in which case--kube-versiontakes precedence over the value in the configuration file.Using
-cthis way requires acm-kubernetes-setup.conffile to already exist, generated by a previous run of thecm-kubernetes-setupwizard.The charts fetched by
download_helm_charts.shdetermine which NVIDIA GPU Operator versions can be deployed later on. Thecm-kubernetes-setupwizard presents a list of GPU Operator versions, but in an air-gapped cluster only a version whose chart is among the downloaded ones can be installed. There is no route to fetch any of the others once the cluster is air-gapped.Certification and availability are separate properties: an asterisk next to a version in the wizard marks it as certified for NVIDIA AI Enterprise (section 6.7.1). A version that was downloaded is not necessarily a certified one, and a certified version is not necessarily among those that were downloaded.
The downloaded charts can therefore be listed before
cm-kubernetes-setupis run, so that the version picked in the wizard is known to be one that is available:The same
helm-chartsdirectory is present inside the extractedairgappeddirectory on the head node, so the charts can also be checked there.The charts are fetched from chart repositories on the internet, and the set of repositories that is used changes between BCM releases. BCM 11.34 fetches from four repositories that BCM 11.33 did not:
-
oci://cr.agentgateway.dev/charts/agentgateway, for the AgentGateway operator -
oci://cr.agentgateway.dev/charts/agentgateway-crds, for the AgentGateway CRDs operator -
https://argoproj.github.io/argo-helm, for the Argo CD operator
-
https://flannel-io.github.io/flannel, for the Flannel network plug-in
A site that restricts outbound connections from the internet-connected host must permit these destinations before
download_helm_charts.shis run. A site that is moving from BCM 11.33 must add them to any existing list of permitted destinations.The repository that serves the Calico CRDs chart, https://docs.tigera.io/calico/charts, is not a new one. It already served the Tigera Operator chart in BCM 11.33.
To ensure the commands all succeed, it is best to execute them in sequence and let them complete. The download scripts all write their output to the current working directory.
The downloads take a while, and are best run inside a
screenortmuxsession so that a dropped connection does not interrupt them. The container image download accounts for most of the elapsed time—on a test system with fast networking it took around 15 times as long as the package, chart, and CRD downloads put together. Absolute times depend on the available bandwidth, so a slow download is not in itself a sign that something has gone wrong.While the packages are downloading, APT emits warnings similar to:
One or more of these appears for each of the
packages,packages-head,packages-non-dgx, andpackages-cudadirectories. These warnings are expected and can be ignored. The_aptuser (with the underscore) cannot read the working directory when it is under/root, so APT drops its download sandbox and continues unsandboxed. The downloads themselves succeed, and the scripts exit normally. This is the same permission restriction that later requires the tarball to be extracted somewhere that APT can write to (section C.3).Without the Run:ai air-gapped package, the completed download is about 8.6 GB, of which the container images account for about 8.1 GB. The Run:ai package is a further 13–14 GB on top of that, so a working directory for a deployment that includes Run:ai needs upwards of 20 GB. Note that the
totalreported bylsin the listing that follows counts only the files in the directory itself, which is why it reflects the size of the Run:ai tarball rather than the size of the whole download. -
-
Run:ai air-gapped setup
If Run:ai is required in the air-gapped installation, then the Run:ai air-gapped package must also be downloaded to the working directory. The package is published as an NGC resource. The available versions are listed on the resource page at https://catalog.ngc.nvidia.com/orgs/nvidia/teams/runai/resources/runai-airgapp-package/version, and an NGC API key is needed to download it (https://docs.nvidia.com/ngc/latest/ngc-catalog-user-guide.html#ngc-api-keys):
Example
The <
runai version> part is replaced with the required patch version. For example:2.25.11, which gives a tarball named:runai-airgapped-package-2.25.11.tar.gzAlternatively, NVIDIA Run:ai Support can be contacted to request the appropriate tarball.
A minimum version of 2.23.60 is required, since that is when multi-arch support was added. The integrity of the downloaded file should be verified with
sha256sumagainst the checksum provided by NVIDIA Run:ai Support.The
cm-setup-runai-bcm11package must also be downloaded to the working directory. This package implements thecm-runai-setupwizard, and is not part of the Run:ai air-gapped tarball. Listing the contents of the tarball does not show it. The package is published separately, in the NVIDIA Mission Control package repository, which is publicly reachable and needs no credentials. The available versions are listed at:The package is published only as a
.deb, so an air-gapped Run:ai deployment on RHEL is not supported in this release. The package is architecture-specific, so the build that matches the head node architecture must be downloaded:Example
In the repository each package has a corresponding
.sha256file. The content of the.sha256file is the checksum. For thesha256sumutility to be able to check the package integrity, the.sha256file must have the file name of its corresponding package appended on the same line to the.sha256file checksum content:Example
The package must not be installed if the check reports anything other than
OK.The directory contents should then appear similar to:
-
Confirming the downloaded content is complete
Verifying nothing is missing before tarballing:
The download scripts run in the preceding steps make this repository complete for the release being installed. No additional package, image, or chart list needs to be assembled by hand. Each directory in the preceding listing corresponds to one content category:
-
packages,packages-head,packages-non-dgx, andpackages-cudahold the OS packages fetched bydownload_ubuntu_packages.shordownload_r9_packages.sh -
k8s-imagesholds the container images fetched bydownload_container_images.sh -
helm-chartsholds the Helm charts fetched bydownload_helm_charts.sh, from every chart repository this release depends on (see the repository list earlier in this section) -
crdsholds the CRD manifests fetched bydownload_crds.sh
The exact package names, image tags, and chart versions in each directory change between BCM and Kubernetes releases. Checking their contents against a fixed list is not useful. Instead, each category should be confirmed to have downloaded files before proceeding. A plain listing of the top-level directories is not enough:
packages,packages-head,packages-non-dgx, andpackages-cudaorganize their packages into subdirectories such asallandamd64, and those subdirectories can exist without containing any packages. Searching recursively for files avoids that gap:Example
A content category that produces no output has no downloaded files, even if its top-level directory (or a subdirectory within it) exists. Its download script did not complete, and should be re-run before the
airgappeddirectory is tarballed. -
-
The
airgappeddirectory can now be tarballed.In the tarball, the packages that are to be used by the software images must include all the architectures that are to be used by the Kubernetes cluster.
Example
The
airgapped.tgzfile should be saved to some transfer media (for example: a USB stick), and should be copied over to the active head node of the air-gapped cluster.
C.3 Head Node Preparation Steps
Before starting this section, the cluster’s control-plane, worker, and etcd nodes must already be assigned, as covered under Node assignment (section 4.2.8, here). This assignment is done using the same cm-kubernetes-setup wizard screens used here, is not air-gap-specific, and cannot be arranged once the cluster is air-gapped.
C.3.0.1 Selecting The Run:ai Operator Dependencies
When the cm-kubernetes-setup wizard is run, with the intent of running Run:ai as well, the operator selection screen must include the dependencies that Run:ai requires. For Run:ai 2.25, these are:
-
NVIDIA GPU Operator
-
Grafana Operator
-
Kgateway
-
Knative Operator
-
Kubeflow Training Operator
-
Kubernetes Metrics Server
-
Kubernetes MPI Operator
-
Kubernetes State Metrics
-
LeaderWorkerSet Operator
-
MetalLB
-
Network Operator (technically optional, but recommended)
-
NIM Operator (optional)
-
Prometheus Adapter
-
Prometheus Operator Stack
The Run:ai operator and its prerequisites are described in more detail in section 6.11.
C.3.1 Unpacking The Files
The airgapped.tgz file that was copied over to the head node can then be extracted. For Ubuntu, extracting to a file system that APT can write to, such as /tmp, is recommended. This is because APT runs as the _apt user (with the underscore) during a part of the installation process, and _apt does not have sufficient permissions under /root to process the extracted files as needed. For Rocky Linux and RHEL the archive can be extracted in place, and the move to /tmp or similar is not needed.
Example
The system packages can now be installed onto the head node and the required software images, using the untarballed airgap system files together with the configuration file saved from earlier on. The command must be run from within the extracted airgapped directory, because the installer looks for the packages subdirectories relative to the current working directory:
Example
The packages that this step installs include Skopeo and its dependencies, which are carried in the packages-head directory of the tarball. Skopeo is required later on, when the container images are pushed into the local registry. It should not be installed on the head node with apt or dnf: an air-gapped head node has no distribution repository to install it from, and the step shown here has already provided it.
If the air-gapped cluster has a secondary head node (section 15.1 of the Administrator Manual), then the system package installation does not have to be repeated on it. The --airgap-install-system-packages run on the active head node also updates the secondary head node. The tarball and the configuration file therefore do not have to be copied across for this step, and cm-kubernetes-setup should not be run on a passive head node.
This applies to the system package installation. Other components can still have their own secondary head node steps.
Earlier BCM releases did instruct that the tarball and the configuration file be copied to the secondary head node, and that the wizard be run there a second time with the --skip-headnode-check flag. That run reaches a stage which installs packages into the software image, and it is refused on a passive head node with the message This node is not the active head node (PASSIVE). The documented answer was Skip, which leaves the software image untouched. The message also suggests --force, which must not be used here. Software images are local to each head node and are synchronized from the active head node. This means that a write made on a passive head node is reverted at the next synchronization, and that until then the passive head node serves a different image to any node provisioning from it compared to the active head node.
Package installation for multiarch software images in the Kubernetes cluster is automatic. This is assuming that the software images have the packages for the required architectures in the tgz file.
C.3.2 Setting Up The Container Images And Helm Charts For An Air-gapped Cluster During Upgrade
From the airgapped directory, all Docker images and Helm charts can be pushed to the registry with:
C.3.3 Extracting The CRD Files For An Air-gapped Cluster During Upgrade
The downloaded CRD files can then be extracted using:
root@basecm11:~# $K8S_AG_SCRIPTS/extract_crds.sh
C.4 Upgrading The Kubernetes Cluster
The upgrade steps start with the control plane nodes.
C.4.1 Updating The First Control Plane Node
The node that is to be upgraded should first be updated to the latest binaries. This can be done with an imageupdate run from cmsh on the head node (section 5.6.2 of the Administrator Manual).
For example for a node with the arbitrary name of controlplane01:
Example
If the -w|--write flag is not used, then a dry-run is carried out.
After the image update is done, the kubeadm upgrade plan command is run from the node itself:
Example
Explicitly providing the kubeconfig for the correct Kubernetes cluster is a good practice. This is important if the BCM cluster has multiple Kubernetes clusters.
The following session shows output from an upgrade plan from 1.32.10 to 1.33.11. The plan shows that there is indeed a newer 1.32.11 available already:
Example
The upgrade plan suggests that an upgrade to v1.32.11 must be done first.
However, the skew versions policies (here) allow jumping to v1.33.6.
A check of the changelogs through the appropriate versions should be done. The changelogs are located under: https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/ and are well-organized. After studying these, if the cluster administrator decides it makes sense to just jump ahead to v1.33.6, then that can be carried out as follows:
Instead using v1.32.11 as suggested in the preceding session plan, the value v1.33.6 is used. The --kubeconfig flag is also set as a matter of best practice:
Example
The following should then show in the output:
Example
C.4.1.1 Air-gapped Control Plane Validation
-
checking that the first control plane node is in the
Readystate, and running the target Kubernetes versionExample
-
checking that all critical pods are running and in the
Readystate, especially those inkube-systemnamespace:Example
An extra check is to see if the appropriate kube-apiserver version image, v1.33.6, is being used by the upgraded controlplane01 node, by executing:
Example
C.4.1.2 Updating Subsequent Control Plane Nodes
The following commands can be repeated on the other control plane nodes.
From the head node:
Example
From the node itself:
Example
In the preceding, an upgrade apply with version does not need to be provided.
The validation steps from before (here) should be repeated.
If all control-plane nodes are running without issues, then the worker nodes can also be updated.
C.4.1.3 Updating The Worker Nodes In The Air-gapped Update
Updating worker nodes is done in a similar way to updating control plane nodes, but usually with some additional care, since worker nodes can be running user workloads.
Section 4.22.6, about updating the worker nodes in a non-air-gapped cluster, has more on being careful during worker node updates. In particular, it explains how to drain nodes beforehand, so that end users do not get upset by lost jobs.
The upgrade step itself is carried out with:
Example
The --kubeconfig flag should be provided in the preceding command.
To summarize, a node that needs to be drained requires the following commands:
C.4.1.4 Draining On The Head Node
Example
C.4.1.5 Draining On The Non-head Node
Example
Validation can be done, as before (here), by:
-
checking that all critical pods are running and in the
Readystate, especially those inkube-systemnamespace, by running:kubectl get nodes -
checking that all critical pods are running and in the Ready state (especially those in
kube-systemnamespace, but not only those), by running:kubectl get pod -A