名前はそのまま、取得元だけ変える
一言でいうと
エアギャップ環境のKubernetesにイメージを入れる方法は2つです。ノードごとにアーカイブを直接入れるか、社内レジストリを立てて、ランタイムにミラーを知らせるかです。ミラーは、マニフェストのイメージ名を1つも変えません。その代わり、ランタイムごとに設定ファイルが違い(podmanはregistries.conf、containerdはhosts.toml、k3sはregistries.yaml)、すべてに、プライベートCAを別に教える必要があります。
なぜ必要なのか
ノードが10台のエアギャップ環境のクラスターに、新しいバージョンを載せるたびに、すべてのノードにtarをコピーして入れることはできません。そこで、社内レジストリを置きます。ところが、よくある最初の試みは、マニフェストやチャートのイメージ名を、registry.corp.internal/...に書き換えることです。そうすると、外部のリポジトリのチャートと分かれてしまい、アップストリームが新しいバージョンを出すたびに、名前の書き換えをやり直す必要があり、ダイジェストで固定したマニフェストは、移す過程でダイジェストが変わると壊れます。ミラーは、「registry.k8s.ioは、ここから取得する」ということだけを、ランタイムに知らせて、この問題を避けます。
どう動くのか
社内レジストリ: CNCF distribution(昔のdocker registry)は、設定ファイルのstorage.filesystem.rootdirectoryにイメージを置き、http.addrでアドレスを、http.tlsのcertificate・keyでHTTPSを決めます。proxy.remoteurlを指定すると、プルスルーキャッシュになりますが、そのモードではpushができません。エアギャップ環境では上位がないので、通常のレジストリにして、持ち込み物をpushします。Ubuntu nobleには、これがdocker-registryパッケージ(設定/etc/docker/registry/config.yml、サービスdocker-registry.service)として入っています。
ダイジェストを守って移す: skopeo copyは、レジストリからレジストリへ直接移します。--allは、複数のアーキテクチャの一覧を丸ごと移し、--preserve-digestsは、ダイジェストを守れないときに失敗させます。1つのアーキテクチャだけを移すと、一覧のダイジェストが変わり、image@sha256:...で固定したマニフェストが、ミラーでイメージを見つけられなくなります。skopeo syncは、YAMLの一覧で、複数のイメージを一度に移します。
podman: registries.conf: containers-registries.conf(5)の[[registry]]は、prefixでどの名前に適用するか、locationで元の場所を書き、[[registry.mirror]]のlocationが、先に試す場所です。pull-from-mirrorで、タグ・ダイジェストのどれをミラーから取得するかを選べます。断片ファイルは、/etc/containers/registries.conf.d/に置くと、アルファベット順につなげて読まれます。プライベートCAは、/etc/containers/certs.d/<호스트:포트>/ca.crtです(プレースホルダーはホストとポートです)。
containerd: hosts.toml: containerdのドキュメントによると、/etc/containerd/certs.d/<레지스트리>/hosts.tomlのディレクトリ名が元のレジストリ、serverが元のアドレス、[host."..."]が先に試すミラーで(プレースホルダーはレジストリです)、そこにcapabilities(pull・resolve・push)とcaを書きます。並べたhostを順に試して、すべて失敗したら、serverに退きます。エアギャップ環境では、その退き先がふさがれた外部に向かうので、エラーメッセージが紛らわしくなることがあります。CRIがこのディレクトリを見るようにする設定(config_path)の場所は、containerd 1.xと2.xで違い、ctr images pull --hosts-dirは、同じ形式を、1つのコマンドで試せるようにしてくれます。
k3s: registries.yaml: k3sのドキュメントによると、/etc/rancher/k3s/registries.yamlのmirrorsに、元のレジストリとendpointを、configsに、ミラーのアドレスのtls(ca_fileなど)と認証を書き、変更したあとは、ノードごとにk3sを再起動する必要があります。ドキュメントは、デフォルトのエンドポイントが、常に最後の手段として試されるとも書いています。このラボのk3s(v1.33.3+k3s1)は、containerd 2.0を使います。
現場での姿
このラボのVMで測ってみました。registry.k8s.io/e2e-test-images/busybox:1.36.1-1の一覧には、Linuxの5つのアーキテクチャと、Windowsイメージ2つが入っていて、--allで2つのイメージを移すと、レジストリのストレージが893MBになりました(Linuxのレイヤーは、数MBだけです)。VMのルートディスク(2.4GiB)に置いたときは、最初のイメージでディスクが埋まりました。ミラーのストレージの場所は、持ち込みの容量を見て決める必要があります。一覧ごと移したpauseのダイジェストは、外部と同じsha256:ee6521f2…で、--allなしでもう一度移したコピーは、sha256:7c38f247…と違っていました。k3sは、registries.yamlを読んで、/var/lib/rancher/k3s/agent/etc/containerd/certs.d/registry.k8s.io/hosts.tomlを、# File generated by k3s. DO NOT EDIT.というヘッダーとともに作成しました。手で書いたhosts.tomlと、同じ形式です。レジストリのキーをroot所有にすると、docker-registry.serviceが、status=1/FAILUREで落ちました。
ミラーを使うと、fde-deliveryモジュールで失敗していたimagePullPolicy: AlwaysのPodが起動します。Alwaysは、毎回レジストリにタグのダイジェストを問い合わせますが、今は、その質問を社内ミラーが受け付けてくれるからです。逆に、ミラーが持ったことのないイメージを呼ぶと、ランタイムは、ミラーで見つけられず、元のレジストリに退いて、ふさがれて失敗します。エラーの行に、元のレジストリのアドレスが出力されていても、原因は「持ち込み一覧になかった」ことです。
次のラボですること
k3sのVMで、プライベートCAで社内レジストリをHTTPSで立てて、接続されている間に、2つのイメージを、ダイジェストを守って移します。外部への送信をふさいだあと、podman(registries.conf)、containerd(手で書いたhosts.toml)、k3s(registries.yaml)の3つの道で、元の名前のまま取得します。AlwaysのPodを起動して、持ち込み記録を、レジストリ・ランタイムのダイジェストと照合して残します。
参考ドキュメント: distribution Configuration・Registry as a pull through cache・skopeo-copy(1)・containers-registries.conf(5)・containerd Registry Configuration(hosts.md)・K3s Private Registry Configuration