- Published on
k3s 閉域網インストール完全ガイド — イメージ tarball の持ち込みからプライベートレジストリ、エージェント参加まで
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — 審査を通った 3 つのファイルでクラスターを立てる
- 持ち込み対象の算出 — 接続網の区間で何をダウンロードするのか
- サーバーノードのインストール — イメージの配置と SKIP_DOWNLOAD
- エージェントの参加と埋め込み etcd による HA 構成
- プライベートレジストリの経路 — registries.yaml と自己署名 CA
- ここで詰まります — 閉域網 k3s の失敗モード
- バージョンドリフトとインストール直後の検証
- おわりに — 持ち込みリストがそのまま設計文書です
- 参考資料
はじめに — 審査を通った 3 つのファイルでクラスターを立てる
閉域網のインストールが難しいのは、Kubernetes が難しいからではありません。インストールツールの大半が「必要になったらそのとき落としてくる」を前提に設計されているのに、閉域網にはその「そのとき」が存在しないからです。get.k3s.io のスクリプトをそのまま実行すると、1 行目で curl がタイムアウトして終わります。
だから閉域網のインストールは順序が逆です。まず何が持ち込まれる必要があるのかリストを確定させます。そのうえで接続網の区間でそのリストを正確に埋め、チェックサムを付けて審査に上げ、媒体が内側に入ってきてからようやくインストールを始めます。リストから一つでも漏れると内側では取り返す方法がなく、次の持ち込み審査まで数日から数週間待つことになります。
この記事は k3s を対象に、そのリストと手順をコマンド単位で書いた文書です。検証の基準バージョンは以下のとおりです。
| 項目 | 値 | 確認時点 | 確認した出典 |
|---|---|---|---|
| k3s 最新安定版 | v1.36.2+k3s1(2026-06-24 リリース) | 2026-07-31 | k3s リリースページ |
| 公式ドキュメントの例 | v1.33.3+k3s1 | 2026-07-31 | k3s Air-Gap Install |
| 条件付きインポート | v1.33.1+k3s1 以上 | 2026-07-31 | k3s Air-Gap Install |
| 証明書の自動更新 | 期限まで 120 日以内(再起動時) | 2026-07-31 | k3s Certificate |
以下のコマンドはすべて v1.36.2+k3s1 を基準に書いています。別のバージョンを使う場合はバージョン文字列だけ変えれば済みますが、アーカイブとバイナリのバージョンは必ず同じでなければなりません。理由は後ろで扱います。
持ち込み対象の算出 — 接続網の区間で何をダウンロードするのか
k3s の閉域網インストールに必要なファイルは最低 3 つです。ここにワークロードイメージとデプロイ用マニフェストが加わります。
#!/usr/bin/env bash
# collect-k3s.sh — インターネットが使えるステージング機で実行
set -euo pipefail
K3S_VERSION="v1.36.2+k3s1"
ARCH="amd64"
# URL パスでは + を %2B にエンコードする必要があります
URLVER="${K3S_VERSION/+/%2B}"
BASE="https://github.com/k3s-io/k3s/releases/download/${URLVER}"
OUT="./k3s-airgap-${K3S_VERSION}"
mkdir -p "${OUT}"
cd "${OUT}"
# 1) システムイメージアーカイブ(zstd 推奨、媒体の容量を節約)
curl -fL -o "k3s-airgap-images-${ARCH}.tar.zst" \
"${BASE}/k3s-airgap-images-${ARCH}.tar.zst"
# 2) k3s バイナリ
curl -fL -o k3s "${BASE}/k3s"
# 3) インストールスクリプト(実行時にネットワークを使わないよう SKIP_DOWNLOAD と併用)
curl -fL -o install.sh https://get.k3s.io
# 4) チェックサムファイル(リリースに掲載されている場合)
curl -fL -o "sha256sum-${ARCH}.txt" "${BASE}/sha256sum-${ARCH}.txt" || \
echo "WARN: リリースに sha256sum-${ARCH}.txt がありません。自前で算出した値で代替してください。"
ls -la
sha256sum-amd64.txt というアセットについては、今回の確認時点でリリースページのアセット一覧に存在するかどうかを断定できませんでした。あればそれを使い、なければ以下のように持ち込み担当者が自分で算出したマニフェストを正本とするほうが安全です。どのみち閉域網の審査では、「誰がいつどこで受け取ったファイルなのか」を証明する自前のマニフェストが必要になります。
# 自前のチェックサムマニフェスト生成(接続網の区間)
cd "./k3s-airgap-v1.36.2+k3s1"
sha256sum k3s k3s-airgap-images-amd64.tar.zst install.sh > MANIFEST.sha256
cat MANIFEST.sha256
SELinux が有効な RHEL 系ノードなら、k3s-selinux RPM も一緒に持ち込む必要があります。公式ドキュメントは、SELinux が有効なノードでは k3s のインストール前にこの RPM を手動でインストールするよう明記しています。このファイルを落とすとインストール自体は通りますが、コンテナがボリュームを読めないという形で後から遅れて壊れます。
持ち込み物チェックリスト
| ファイル | 必須かどうか | 落とすと起きること |
|---|---|---|
| k3s(バイナリ) | 必須 | インストールスクリプトがダウンロードを試みてタイムアウト |
| k3s-airgap-images-amd64.tar.zst | 必須 | システム Pod がすべて ErrImagePull |
| install.sh | 必須 | systemd ユニットとシンボリックリンクを手で作る羽目に |
| MANIFEST.sha256 | 事実上必須 | 媒体が壊れているかどうかを内側で判別できない |
| k3s-selinux RPM | 条件付き | SELinux 有効ノードでボリュームアクセス失敗 |
| 自前のワークロードイメージアーカイブ | 必須 | アプリケーションだけ ErrImagePull — 最も多い事故 |
| Helm チャート / マニフェスト | 必須 | デプロイ段階でまた持ち込み審査を待つことになる |
最後の 2 行がこの表の核心です。k3s のアーカイブには k3s 自身を起動するために必要なイメージしか入っていません。CoreDNS、Traefik、local-path-provisioner、metrics-server、pause 程度です。社内アプリケーションのイメージは当然なく、Prometheus も差し替え用の Ingress コントローラーもありません。これは後ろの失敗モードの節でもう一度取り上げます。
媒体の持ち込みと完全性の検証
媒体が内側に入ってきたら、インストール前に必ず検証から始めます。圧縮アーカイブが静かに壊れたまま入ってくると、k3s はインポート失敗をログ 1 行で流してそのまま起動してしまい、数分後に Pod の状態を見てはじめて気づくことになります。
# 閉域網ノードで
cd /opt/staging/k3s-airgap-v1.36.2+k3s1
# 1) チェックサム検証 — ここで失敗したらこれ以上進みません
sha256sum -c MANIFEST.sha256
# 2) アーカイブ自体が開くか確認(zstd が必要)
zstd -t k3s-airgap-images-amd64.tar.zst && echo "archive OK"
# 3) アーカイブに入っているイメージ一覧を確認
zstd -dc k3s-airgap-images-amd64.tar.zst | tar -tf - | grep -E 'manifest|repositories' | head
zstd バイナリがノードにない場合があります。閉域網の Linux イメージは最小インストールであることが多く、zstd がデフォルトパッケージではないディストリビューションもあります。この場合の選択肢は 2 つです。OS パッケージミラーから zstd を先にインストールするか、そもそも接続網で .tar.gz または無圧縮の .tar アセットを受け取ってくるかです。リリースには k3s-airgap-images-amd64.tar、.tar.gz、.tar.zst の 3 形態がすべて掲載されています。容量がもったいなくても、最初のインストールでは .tar.gz のほうが失敗ポイントが一つ少なくて済みます。
サーバーノードのインストール — イメージの配置と SKIP_DOWNLOAD
ここからが実際のインストールです。順序が重要です。先にイメージを配置し、そのあとでインストールスクリプトを実行します。逆にすると、k3s が起動しながらイメージを見つけられずリトライループに入ります。
#!/usr/bin/env bash
# install-k3s-server.sh — 閉域網サーバーノード
set -euo pipefail
STAGE=/opt/staging/k3s-airgap-v1.36.2+k3s1
# 1) システムイメージアーカイブの配置
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp "${STAGE}/k3s-airgap-images-amd64.tar.zst" /var/lib/rancher/k3s/agent/images/
# 2) 条件付きインポートキャッシュの有効化(v1.33.1+k3s1 以上)
# このファイルがあると、アーカイブが変わらない限り再起動時の再インポートを飛ばします
sudo touch /var/lib/rancher/k3s/agent/images/.cache.json
# 3) バイナリの配置
sudo cp "${STAGE}/k3s" /usr/local/bin/k3s
sudo chmod +x /usr/local/bin/k3s
# 4) インストールスクリプト実行 — ダウンロードを飛ばすよう指示
sudo chmod +x "${STAGE}/install.sh"
sudo INSTALL_K3S_SKIP_DOWNLOAD=true "${STAGE}/install.sh"
INSTALL_K3S_SKIP_DOWNLOAD=true がこの手順のすべてだと言っても過言ではありません。この値がないと、スクリプトはリリースサーバーへ接続を試みます。サーバーオプションも一緒に渡すなら INSTALL_K3S_EXEC を使います。
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
INSTALL_K3S_EXEC="server --cluster-init --tls-san 10.10.20.10 --tls-san k8s-api.internal.example --write-kubeconfig-mode 0644 --disable traefik" \
/opt/staging/k3s-airgap-v1.36.2+k3s1/install.sh
オプションをコマンドラインに並べるより、設定ファイルで管理するほうがましです。閉域網では再インストールとノード増設が頻繁で、そのたびに誰かがオプションを一つ落とします。
# /etc/rancher/k3s/config.yaml
cluster-init: true
tls-san:
- 10.10.20.10
- k8s-api.internal.example
write-kubeconfig-mode: '0644'
disable:
- traefik
node-label:
- topology.kubernetes.io/zone=dc-a
起動確認は以下のように行います。
sudo systemctl status k3s --no-pager
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl -n kube-system get pods
# イメージのインポートが実際に行われたか containerd ストアで直接確認
sudo k3s ctr images ls | awk '{print $1}' | sort -u | head -20
k3s ctr images ls が空ならインポートが失敗しています。このときは journalctl -u k3s -n 200 ではなく containerd のログを見るべきです。パスは /var/lib/rancher/k3s/agent/containerd/containerd.log です。
エージェントの参加と埋め込み etcd による HA 構成
エージェントノードも同じ順序です。イメージアーカイブとバイナリを配置したあと、参加情報を環境変数で渡してインストールスクリプトを実行します。
# サーバーノードでトークンを確認
sudo cat /var/lib/rancher/k3s/server/node-token
#!/usr/bin/env bash
# install-k3s-agent.sh — 閉域網エージェントノード
set -euo pipefail
STAGE=/opt/staging/k3s-airgap-v1.36.2+k3s1
SERVER_URL="https://10.10.20.10:6443"
JOIN_TOKEN="K10xxxxxxxx::server:xxxxxxxx"
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp "${STAGE}/k3s-airgap-images-amd64.tar.zst" /var/lib/rancher/k3s/agent/images/
sudo touch /var/lib/rancher/k3s/agent/images/.cache.json
sudo cp "${STAGE}/k3s" /usr/local/bin/k3s
sudo chmod +x /usr/local/bin/k3s
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_URL="${SERVER_URL}" \
K3S_TOKEN="${JOIN_TOKEN}" \
"${STAGE}/install.sh"
HA が必要なら、データストアの選択を最初に済ませておく必要があります。k3s のデフォルトデータストアは SQLite で、SQLite ではサーバーノードを増やせません。埋め込み etcd に行くなら、最初のサーバーを --cluster-init で立ち上げ、残りのサーバーを --server で接続します。
# サーバー 1 — クラスター初期化(config.yaml に cluster-init: true を入れても同じ)
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_TOKEN="共有シークレット" \
INSTALL_K3S_EXEC="server --cluster-init --tls-san 10.10.20.9" \
./install.sh
# サーバー 2、3 — 既存サーバーに参加
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_TOKEN="共有シークレット" \
INSTALL_K3S_EXEC="server --server https://10.10.20.10:6443 --tls-san 10.10.20.9" \
./install.sh
公式ドキュメントはこう明記しています。埋め込み etcd クラスターが定足数を維持するには、サーバーノードが奇数でなければなりません。サーバー n 台の定足数は (n/2)+1 です。2 台構成は 1 台構成と障害許容能力が同じでありながら運用の複雑さだけが上がるので、意味がありません。そして --cluster-dns、--cluster-domain、--cluster-cidr、--service-cidr といったネットワーク系フラグは、すべてのサーバーノードで同一でなければなりません。閉域網でノードを 1 台ずつ増設していると、この値はずれやすくなります。
すでに SQLite で単一サーバーを立ててしまったあとでも方法はあります。公式ドキュメントによれば、既存サーバーを --cluster-init フラグ付きで再起動すると etcd に切り替わります。ただしこれは、本番でバックアップも取らずに試す類の作業ではありません。
プライベートレジストリの経路 — registries.yaml と自己署名 CA
閉域網でイメージを配布する正攻法は、社内レジストリを置いてノードがそこだけを見るようにすることです。k3s では /etc/rancher/k3s/registries.yaml でこれを設定します。
# /etc/rancher/k3s/registries.yaml
mirrors:
docker.io:
endpoint:
- 'https://registry.internal.example:5000'
registry.k8s.io:
endpoint:
- 'https://registry.internal.example:5000'
ghcr.io:
endpoint:
- 'https://registry.internal.example:5000'
rewrite:
'^(.*)': 'mirror/ghcr/$1'
configs:
'registry.internal.example:5000':
auth:
username: k3s-puller
password: '持ち込み時に差し替え'
tls:
ca_file: /etc/rancher/k3s/certs/internal-ca.crt
レジストリ全部を一つのエンドポイントに寄せたいなら、ワイルドカード項目を使います。公式ドキュメントは、mirrors と configs の両方でアスタリスク項目をデフォルト設定として使えると明記しており、アスタリスクは引用符で囲む必要があるとしています。
# すべてのレジストリを社内レジストリへ(ワイルドカード)
mirrors:
'*':
endpoint:
- 'https://registry.internal.example:5000'
configs:
'registry.internal.example:5000':
tls:
ca_file: /etc/rancher/k3s/certs/internal-ca.crt
rewrite はパスの先頭部分を書き換えます。Harbor のようにプロジェクト単位でネームスペースが強制されるレジストリでは、これがないと元のパスをそのまま使えません。上の例は ghcr.io/foo/bar を registry.internal.example:5000/mirror/ghcr/foo/bar へ送ります。
自己署名 CA を使うなら、信頼設定が 2 か所 必要です。ここで半分くらいがつまずきます。
# 1) containerd 用 — registries.yaml の ca_file が指す位置
sudo mkdir -p /etc/rancher/k3s/certs
sudo cp /opt/staging/internal-ca.crt /etc/rancher/k3s/certs/internal-ca.crt
sudo chmod 644 /etc/rancher/k3s/certs/internal-ca.crt
# 2) OS トラストストア — helm、skopeo、crictl、curl など他のクライアント用
# RHEL 系
sudo cp /opt/staging/internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
# Debian 系
sudo cp /opt/staging/internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
# 3) registries.yaml の変更は再起動しないと反映されません — すべてのノードで
sudo systemctl restart k3s # サーバーノード
sudo systemctl restart k3s-agent # エージェントノード
3 番目の段階は公式ドキュメントが明示的に強調しています。「設定変更を反映するには各ノードで k3s を再起動する必要がある」です。そしてこのファイルは、イメージを pull するすべてのノードに配布しなければなりません。サーバーにだけ入れてエージェントに入れ忘れるミスがよくあります。
検証はこうします。
# レジストリ設定が containerd に反映されたか確認
sudo k3s ctr --namespace k8s.io images pull \
registry.internal.example:5000/library/busybox:1.36
# 失敗したら containerd ログを見ます(kubelet のメッセージではなく)
sudo tail -n 100 /var/lib/rancher/k3s/agent/containerd/containerd.log
ノード間のイメージ再配布 — 埋め込みレジストリミラー
k3s には Spegel ベースの埋め込み分散レジストリミラーがあります。あるノードにすでにあるイメージを、外部レジストリなしで別のノードが取得できるようにしてくれます。閉域網で社内レジストリを立てる前の段階や、エッジノードが社内レジストリにも届かない場合に有用です。
# /etc/rancher/k3s/config.yaml(すべてのサーバーノード)
embedded-registry: true
# /etc/rancher/k3s/registries.yaml(すべてのノード)
mirrors:
'*':
公式ドキュメントによれば、ノードは内部 IP で互いに TCP 5001(利用可能なイメージ一覧を共有する P2P ネットワーク)と 6443(各ノードがホストするローカル OCI レジストリ)に到達できる必要があります。ファイアウォールの細かい閉域網では 5001 が塞がれていることが多いので、あらかじめ開けておきます。
一つ誤解を解いておきます。この機能が行うのは、すでにどこかのノードに存在するイメージの再配布だけです。ここにイメージを押し込む push 機能はありません。最初の投入は依然として airgap アーカイブか k3s ctr images import で行う必要があります。
ここで詰まります — 閉域網 k3s の失敗モード
ハッピーパスだけを書いた文書は閉域網では役に立ちません。実際に足止めされる地点を整理します。
| 症状 | 本当の原因 | 確認方法 |
|---|---|---|
| システム Pod は上がるのに自分のアプリだけ ErrImagePull | airgap アーカイブに自分のイメージがない | k3s ctr images ls に該当イメージがないか確認 |
| ErrImagePull のメッセージが docker.io を指す | デフォルトレジストリエンドポイントのフォールバックの結果だけが露出 | containerd.log で実際の最初の試行先を確認 |
| registries.yaml を直したのに変わらない | 再起動していない、またはエージェントに配布していない | systemctl restart 後に再試行、全ノードにファイルがあるか確認 |
| helm は通るのに containerd だけ TLS 失敗 | ca_file だけ設定して OS トラストストアが漏れている(またはその逆) | curl でのレジストリ接続と ctr images pull を別々に試す |
| 外部ドメインの照会が 5 秒ずつ止まる | CoreDNS の上流が到達不可 | CoreDNS ConfigMap の forward 先とノードの resolv.conf を確認 |
| サーバーを増やそうとしたが参加できない | データストアが SQLite | k3s kubectl get nodes とサーバー起動フラグを確認 |
| 再起動後にシステム Pod が pull を試みる | アーカイブのバージョンとバイナリのバージョンが不一致 | k3s -v とアーカイブのファイル名のバージョンを比較 |
| コンテナがボリュームを読めない(RHEL 系) | k3s-selinux RPM が未インストール | getenforce と rpm -q k3s-selinux |
失敗モード 1 — アーカイブにないイメージ
最も多く、最も脱力する失敗です。k3s の airgap アーカイブは k3s 自身が必要とするイメージだけを含んでいます。社内アプリケーション、差し替えた Ingress コントローラー、監視スタックは、すべて別途持ち込む必要があります。
社内レジストリがまだないなら、一時的にノードへ直接インポートできます。
# 接続網の区間で: 必要なイメージを一つのアーカイブにまとめる
docker pull myapp/api:1.4.2
docker pull myapp/worker:1.4.2
docker save -o myapp-images.tar myapp/api:1.4.2 myapp/worker:1.4.2
sha256sum myapp-images.tar >> MANIFEST.sha256
# 閉域網ノードで: 方法 A — イメージディレクトリに置いて再起動
sudo cp myapp-images.tar /var/lib/rancher/k3s/agent/images/
sudo systemctl restart k3s
# 方法 B — 再起動なしで即インポート
sudo k3s ctr --namespace k8s.io images import myapp-images.tar
sudo k3s ctr --namespace k8s.io images ls | grep myapp
方法 B で入れたイメージを使うときは、Pod スペックの imagePullPolicy を確認してください。タグが latest だとデフォルトポリシーが Always になり、ノードにイメージがあっても pull を試みて失敗します。閉域網ではタグを常に明示的なバージョンで書き、必要なら imagePullPolicy: IfNotPresent を打ち込んでおきます。
失敗モード 2 — デフォルトエンドポイントのフォールバックが作る偽の手がかり
containerd には、registries.yaml のミラー設定とは無関係に、最後の試行として本来のレジストリへ接続する動作があります。閉域網ではこの最後の試行が必ず失敗し、kubelet がユーザーに見せるエラーはこの最後の試行の結果です。そのため、ミラーの設定が間違っているのか、ミラーにイメージがないのか、そもそもミラーを経由していないのかを区別できません。
対応は 2 つあります。
# 対応 A — 本当の原因は containerd ログにあります
sudo grep -iE 'failed|error' /var/lib/rancher/k3s/agent/containerd/containerd.log | tail -40
# 対応 B — /etc/rancher/k3s/config.yaml
# ミラーが設定されたレジストリについて、デフォルトエンドポイントのフォールバックを切ります
disable-default-registry-endpoint: true
公式ドキュメントはこのオプションを「当該レジストリにミラーが設定されている場合、containerd のデフォルトレジストリエンドポイントへのフォールバックを無効化する」と説明しています。2024 年 1 月のリリースで実験的機能として導入され、そして、registries.yaml にミラー項目があるレジストリにのみ適用されます。ミラーを設定していないレジストリは依然としてフォールバックします。このオプションを入れるとエラーメッセージが実際の失敗地点を指すようになり、デバッグ時間が大きく減ります。
失敗モード 3 — CoreDNS が何も見つけられない
クラスター内部の名前(kubernetes.default.svc.cluster.local)はきちんと解決されるのに、社内ドメインや外部ドメインの照会が 5 秒ずつ止まってから失敗する、という症状です。CoreDNS のデフォルト Corefile はクラスタードメイン外の問い合わせをノードのリゾルバに転送(forward)しますが、そのリゾルバが閉域網から到達できないパブリック DNS(たとえば 8.8.8.8)を指していると、問い合わせのたびにタイムアウトを待つことになります。
まず実際の設定を確認します。推測せず、クラスターから直接読んでください。
# CoreDNS が何を forward 先にしているか確認
sudo k3s kubectl -n kube-system get configmap coredns -o yaml
# ノードのリゾルバ確認
cat /etc/resolv.conf
sudo resolvectl status 2>/dev/null | head -30
# 社内 DNS が実際に応答するかノードから直接確認
dig @10.10.10.53 registry.internal.example +short
対応は 2 通りです。一つ目は、ノードの /etc/resolv.conf を社内 DNS だけ指すように整理することです。systemd-resolved が管理するノードならシンボリックリンクのせいでファイルを直接直しても戻るので、resolved の設定を変える必要があります。二つ目は、ノードのファイルを触りにくい場合に、k3s へ kubelet 用のリゾルバファイルを別途指定することです。
# 閉域網専用のリゾルバファイルを別に置きます
sudo tee /etc/rancher/k3s/resolv.conf > /dev/null <<'EOF'
nameserver 10.10.10.53
nameserver 10.10.10.54
search internal.example
options timeout:1 attempts:2
EOF
# /etc/rancher/k3s/config.yaml
resolv-conf: /etc/rancher/k3s/resolv.conf
--resolv-conf フラグは公式 CLI ドキュメントに「Kubelet resolv.conf file」と記載されており、環境変数 K3S_RESOLV_CONF でも指定できます。社内 DNS 自体がまったくない環境なら、CoreDNS の Corefile で forward 先を社内リゾルバに固定するか、必要な名前だけ hosts プラグインで打ち込むほうがましです。options timeout:1 は、それでも漏れる問い合わせの遅延を 1 秒に抑えてくれます。
バージョンドリフトとインストール直後の検証
アーカイブとバイナリがずれたとき
閉域網で数か月にわたり複数人が持ち込みをしていると、必ず起きる問題です。先月持ち込んだ v1.35.6 のアーカイブがイメージディレクトリに残っているのに、今回持ち込んだバイナリは v1.36.2 という状況です。
このとき起きることはこうです。k3s v1.36.2 は自分に合った CoreDNS、pause、local-path-provisioner のタグを要求しますが、ノードにインポートされたイメージは v1.35.6 用のタグです。containerd ストアにタグがないので pull を試み、閉域網なので失敗します。ログには「イメージが見つからない」としか残らず、バージョン不一致という手がかりはどこにもありません。
診断と整理はこうします。
# 1) バイナリのバージョン
k3s -v
# 2) イメージディレクトリに何が溜まっているか
ls -la /var/lib/rancher/k3s/agent/images/
# 3) containerd ストアのシステムイメージタグ
sudo k3s ctr --namespace k8s.io images ls | grep -E 'coredns|pause|local-path|metrics-server'
# 整理 — 古いアーカイブを消し、新しいアーカイブだけ残して再起動
sudo systemctl stop k3s
sudo rm -f /var/lib/rancher/k3s/agent/images/k3s-airgap-images-amd64-v1.35.6.tar.zst
sudo rm -f /var/lib/rancher/k3s/agent/images/.cache.json
sudo cp /opt/staging/k3s-airgap-images-amd64.tar.zst /var/lib/rancher/k3s/agent/images/
sudo systemctl start k3s
.cache.json も一緒に消す理由は、v1.33.1+k3s1 以上ではこのキャッシュファイルが「このアーカイブはすでにインポート済み」という判断に使われるからです。アーカイブファイルを差し替えたのにキャッシュが残っていると、インポートを飛ばすことがあります。公式ドキュメントのアップグレード手順もこう明記しています。新しいアーカイブを入れ、既存のアーカイブを削除してください。
ドリフトをそもそも防ぐ方法は、持ち込みの単位をバージョンで束ねることです。
# 持ち込みディレクトリ名とファイル名の両方にバージョンを刻みます
/opt/staging/k3s-v1.36.2+k3s1/
├── MANIFEST.sha256
├── VERSION # v1.36.2+k3s1 の 1 行
├── install.sh
├── k3s
└── k3s-airgap-images-amd64.tar.zst
# インストールスクリプトにバージョンゲートを入れます
EXPECTED="$(cat "${STAGE}/VERSION")"
ACTUAL="$(/usr/local/bin/k3s -v | awk '/^k3s version/{print $3}')"
if [ "${ACTUAL}" != "${EXPECTED}" ]; then
echo "FATAL: バイナリ ${ACTUAL} と持ち込みバンドル ${EXPECTED} が違います" >&2
exit 1
fi
インストール直後の検証スクリプト
インストールが終わったら終わり、ではありません。閉域網では「とりあえず立っているように見える」状態と「実際に使える」状態の隔たりが大きいです。以下のスクリプトをインストール直後に回しておくと、数日後の事故が大きく減ります。
#!/usr/bin/env bash
# verify-airgap-k3s.sh
set -uo pipefail
ERR=0
K="sudo k3s kubectl"
echo "== 1. ノード状態"
${K} get nodes -o wide
NR=$(${K} get nodes --no-headers | grep -cv " Ready ") || true
[ "${NR}" -gt 0 ] && { echo "FAIL: NotReady ノード ${NR} 台"; ERR=$((ERR+1)); }
echo "== 2. システム Pod"
BAD=$(${K} -n kube-system get pods --no-headers | grep -cvE "Running|Completed") || true
[ "${BAD}" -gt 0 ] && { ${K} -n kube-system get pods | grep -vE "Running|Completed"; ERR=$((ERR+1)); }
echo "== 3. 外部レジストリへ出ようとする試みがあるか"
if sudo grep -qiE 'docker\.io|registry\.k8s\.io|ghcr\.io' \
/var/lib/rancher/k3s/agent/containerd/containerd.log 2>/dev/null; then
echo "WARN: containerd ログに外部レジストリ接続の痕跡があります"
echo " registries.yaml のミラー設定と disable-default-registry-endpoint を確認してください"
fi
echo "== 4. DNS"
${K} run dnscheck --rm -i --restart=Never --image=registry.internal.example:5000/library/busybox:1.36 -- \
nslookup kubernetes.default.svc.cluster.local || { echo "FAIL: クラスター DNS"; ERR=$((ERR+1)); }
echo "== 5. 証明書の期限"
sudo k3s certificate check --output table
echo "== 6. データストア"
if [ -d /var/lib/rancher/k3s/server/db/etcd ]; then
echo "datastore: embedded etcd"
ls -la /var/lib/rancher/k3s/server/db/snapshots/ 2>/dev/null | tail -5
else
echo "datastore: sqlite(サーバーノードの増設不可)"
fi
echo "== 結果: エラー ${ERR} 件"
exit "${ERR}"
5 番の k3s certificate check --output table は、閉域網の運用で特に重要です。k3s のクライアント・サーバー証明書は発行日から 365 日有効で、サービス再起動時に期限まで 120 日以内なら自動更新されます。ただし再起動がなければ更新もありません。数か月間だれも触らない閉域網クラスターが証明書の期限切れで死ぬ経路が、まさにここです。詳しい対応は Day 2 運用編で扱います。
おわりに — 持ち込みリストがそのまま設計文書です
閉域網の k3s インストールで本当の作業はコマンドではなくリストです。アーカイブ、バイナリ、インストールスクリプト、チェックサム、CA、ワークロードイメージ、チャート、SELinux RPM まで、内側で必要になるすべてのバイトを外側であらかじめ数えておく仕事です。コマンド自体は 10 行にもなりません。
そしてそのリストは一度作って終わりではありません。バージョンが上がればアーカイブもバイナリも一緒に上がる必要があり、どちらか一方だけが上がった瞬間、クラスターは静かにおかしくなります。持ち込みの単位にバージョンを刻み、インストールスクリプトにゲートを入れる 5 行が、次の持ち込み審査を待つ 3 週間を防いでくれます。