コンテンツへスキップ
← ブログ

containerd 1.x に猶予はもうない

containerd 1.7 の延長サポートは 2026年9月に終わる。しかもその延長は、すでにサポートが切れた Kubernetes のバージョンしか対象にしていない。version 3 への設定書き直し、クラスタを落とすレジストリ変換、そして移行先として意味のある唯一のブランチをまとめる。

·26分で読めます
  • containerd
  • Kubernetes
  • コンテナ
  • Linux

この移行には 2 通りの読み方がある。溜まった作業のひとつとして読むか、締め切りとして読むか。そしてどちらに属するかは、脚注ひとつで決まる。containerd のリリース表は 1.7 ブランチを「LTS、2026年9月まで」と示していて、余裕があるように見える。その下の脚注は、2026年3月以降このサポートは名前の挙がった 2 名のメンテナが提供しており、Google Kubernetes Engine 上の Kubernetes 1.32、1.31、1.30 での利用に的を絞っていること、その用途に必要でない変更は拒否されうることを明記している。Kubernetes 1.32 は 2026年2月に EOL を迎えた。GKE 上でサポート切れの Kubernetes を動かしているのでなければ、その浮き輪はそもそもあなたに向けて投げられていない。

3 枚のパネルからなるカバー画像。左のパネルは「containerd 1.x」と題され、io.containerd.grpc.v1.cri という単一の設定ブロックの中に runtimes、snapshotter、sandbox_image、レジストリの mirrors テーブルが詰め込まれ、その上に「1.7 — 延長サポートは 2026年9月に終了」と書かれた赤い EOL のスタンプが押されている。中央の列は「どこへ動くのか」と題され、5 つの変換を並べている。version 2 から version 3 へ、io.containerd.grpc.v1.cri が cri.v1.runtime と cri.v1.images に分割、sandbox_image が pinned_images.sandbox に、registry.mirrors が config_path 付きの certs.d ディレクトリに、cri-containerd の同梱パッケージが 3 つの別々のインストールに。右のパネルは「containerd 2.3 LTS」と題され、ランタイム用とイメージ用の 2 つの設定ブロックが並び、その下に 2028年4月まで伸びるサポート期間のバーが描かれている。
実際に動くもの:設定のバージョン番号がひとつ、2 つに割れたプラグインがひとつ、名前が変わった設定がひとつ、ディレクトリツリーに置き換わったレジストリ設定ブロックがひとつ、そして 3 つの別々のダウンロードに置き換わった同梱パッケージがひとつ。

つまりこれは実際の期限がある移行であり、手順書のバージョン番号を 1 つ上げるだけでは足りない。以下で扱うのは全体像だ。LTS という語に惑わされずに containerd のリリースポリシーを読む方法、2.1 が着地点として最悪である理由、プラグイン ID の移動を含む version 2 から version 3 への設定書き直し、固有のバグと固有の障害パターンを持つレジストリ変換、2.1 で静かに変わったイメージ取得経路、2.0 で削除されたものすべて、ノード単位の手順書、切り戻しで戻るものと戻らないものの率直な話、そして問題があれば非ゼロで終了する検証スクリプト。

症状と、そのどれもが containerd と名乗らない理由

この種の障害はどれも「ランタイムのバージョンの問題です」とは名乗らない。だから診断が遅れる。アップグレード後にノードが戻ってきて、CRI プラグインがそもそも存在しない。デーモンは動いていて systemctl status は緑、それでもノード上のすべての Pod が ContainerCreating のまま止まる。4 年間ずっと社内ミラーから取得していたイメージが、いきなり Docker Hub から取得され始め、最初に気づくのは外向き通信の請求書だ。あるチームが 2 年前に手で追加した RuntimeClass が解決しなくなり、それを使うワークロードだけが落ちる。どの場合も、ランタイムは動いていて、そのランタイムが間違っている。[ctrrel]

見えている現象たいていの意味どこで扱うか
あるノードの全 Pod が ContainerCreating で止まり、デーモンは健全CRI プラグインが読み込まれていない。containerd はそれでも起動し、失敗は自分のログにしか出さないレジストリ
イメージが急に社内ミラーではなく公開レジストリから取得されるregistry.mirrors ブロックが config_path への書き直しを生き延びなかったレジストリ
外部接続のないノードで pause コンテナが registry.k8s.io を取りにいくsandbox_imagepinned_images.sandbox に引き継がれていないプラグイン分割
gVisor か Kata のワークロードだけが落ち、他はすべて正常RuntimeClass が名指しするランタイムハンドラが新しい設定に無いKubernetes
先週まで取得できていた古いイメージが manifest エラーで失敗するDocker schema 1 の取得:containerd 2.0 でデフォルト無効になり、2.1 で完全に削除された削除
イメージ取得と結び付けて考えられていない変更のあとで取得の挙動が変わるTransfer Service が満たせない設定が、ノードをローカル取得に戻したイメージ取得
デーモンが起動のたびに「Configuration migrated from version 2」とログに書くファイルが一度も書き直されていない。互換レイヤが背負っており、レジストリのバグが住んでいるのもそこだ設定ファイル

共通しているのは、containerd 2.x が version 2 の設定ファイルに対して意図的に寛容であることだ。読み込み、メモリ上で変換し、そのまま起動する。アップグレードの瞬間には親切で、その後は負債になる。移行が半分だけ終わった状態でいつまでも留まることができ、それを完了させる圧力がどこにも働かないからだ。デーモンは文句をログに書いて、それでも起動する。読み込みに失敗したプラグインはエラー状態を公開するが、それをあなたの前に持ってくるものは何もない。この記事全体は、ある意味で「互換レイヤに背負わせず、移行を終わらせよう」という主張にほかならない。[ctr20]

サポート表をきちんと読む

まずリリース表から始める。何かを決めている唯一の文書であり、そして体系的に読み違えられている文書でもあるからだ。containerd は 2 種類のブランチを運用している。通常リリースは 8 か月サポートされる。年に 1 つのリリースが LTS に指定され、最低 2 年サポートされる。そのうえで、一般のサポート期間が閉じたあとに、名前の挙がったメンテナによる延長サポートが個別のブランチに付くことがある——同じ列に同じラベルを付けた、まったく別のものだ。[ctrrel]

ブランチ状態EOLあなたにとっての意味
1.6EOL2025年8月23日1 年前からサポート外。セキュリティ修正を含め、もう何も来ない
1.7LTS、延長2026年9月延長サポートのみ。名前の挙がった 2 名のメンテナが提供し、GKE 上の Kubernetes 1.30–1.32 に限定
2.0LTS、延長2027年3月同じ形:GKE 上の Kubernetes 1.33 に限定した延長サポート。その 1.33 自体が EOL
2.1EOL2026年7月3日すでに終了。 多くの人が最初に上げた先であり、止まる場所としては最悪
2.2アクティブ2026年11月6日修正は来るが残り 10 週間。中継地点としては可、目的地としては不適
2.3LTS2028年4月30日目標。 現行の長期ブランチ。この先ほぼ 2 年のサポートがある
2.4今後暫定で 2027年4月通常の 8 か月リリース。LTS の代わりにはならない

次に Kubernetes のサポートマトリクスを重ねる。2 つのプロジェクトが交わる場所だ。containerd は Kubernetes のマイナーリリースごとに推奨バージョンの一覧を公開している。Kubernetes 1.36 に対するその一覧は 2.3.0+、2.2.0+ で、1.x のエントリはひとつもない。kubelet 側にこれを強制する仕組みはない。サポート外の組み合わせでも起動し、動作し、問題なさそうに見える——そうでなくなるまでは。そしてそのときは一人でデバッグすることになる。マトリクスは「何がテストされたか」の表明であり、そのテストこそが、あなたと「他の誰も踏んでいないランタイムのバグ」との間に立つ唯一のものだ。[k8srel]

Kubernetes推奨として掲載された containerd のバージョンKubernetes の EOL読み方
1.332.1.0+、2.0.4+、1.7.24+、1.6.36+2026年6月28日すでにサポート外。containerd 2.0 の延長が名指ししているのがまさにこの組み合わせ
1.342.1.3+、2.0.6+、1.7.28+、1.6.39+2026年10月27日残り 2 か月。しかも 4 つの containerd の選択肢のうち 2 つ——2.1 と 1.6——はそれ自体が EOL
1.352.2.0+、2.1.5+、1.7.28+2027年2月28日1.x のランタイムが登場する最後の行
1.362.3.0+、2.2.0+2027年6月28日1.x のエントリなし。 ここから移行は任意でなくなる

落とし穴の中の落とし穴が containerd 2.1 だ。2025年後半にアップグレードした人にとって当然の着地点であり、多くの社内ドキュメントには今もそう書かれている。そして 2026年7月3日に EOL を迎えた——2026年11月まで続く 2.2 より早く、現行 LTS で 2028年4月までサポートされる 2.3 よりはるかに早い。「2.x に上げる」は計画ではない。ゼロから狙う価値のあるブランチはちょうど 1 つ、2.3 である。

このノードに実際に入っているもの

何かに触れる前に、実際に何が入っているかを確定させる。ある程度の規模の環境なら、答えは 1 つのバージョンにはならない。重要なのは 3 つで、互いに独立している。デーモンのバージョン、設定ファイルのバージョン、そして実際に kubelet に提供されている CRI API のバージョンだ。忘れられがちなのは設定バージョンであり、そして「存在しない」ことがありうる唯一のものでもある。version 行のないファイルは version 1 のファイルとして扱われる。そしてここでは、プロジェクト自身の文書どうしが食い違っている。どちらにどう食い違っているかは知っておく価値がある。CRI 設定ガイドは version 1 が containerd 2.0 で削除されたと書いている一方、RELEASES.md は version 行がなければ version 1 として解釈され、過去のすべてのバージョンが移行によってサポートされると書いていて、ソースには今も v1 の移行関数が残っている。version 1 のファイルは、挙動を自信をもって推論できるものとしてではなく、見つけ次第直すものとして扱うのが正しい。[cfgtoml]

# The daemon, the client and the shim are three separate versions and they are
# allowed to disagree. Ask all three rather than assuming.
containerd --version
# containerd github.com/containerd/containerd/v2 v2.3.2 <revision>
ctr version                        # client and server, side by side
runc --version                     # the OCI runtime is a separate install now

# The configuration version is the single most useful number here. There is no
# `version` line in very old files: absent means version 1, which containerd
# 2.0 removed outright rather than migrating.
head -1 /etc/containerd/config.toml
# version = 2

# What the plugins are doing. A plugin in state "error" is the daemon telling
# you a migration went wrong; it does not stop the daemon from starting.
ctr plugins ls | awk '$4!="ok"'
# TYPE  ID  PLATFORMS  STATUS

# From the Kubernetes side, which is what actually matters:
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,RUNTIME:.status.nodeInfo.containerRuntimeVersion,'\
'KUBELET:.status.nodeInfo.kubeletVersion,OS:.status.nodeInfo.osImage'
# NODE     RUNTIME                KUBELET   OS
# node-01  containerd://1.7.28    v1.34.9   Ubuntu 24.04.3 LTS

# And the CRI API version the kubelet is really getting. containerd 2.0 removed
# v1alpha2; if anything on this node still speaks it, it stops working here.
crictl version
# RuntimeName:  containerd
# RuntimeApiVersion:  v1

次に、デーモンがずっと伝えようとしてきたことを聞き出す。1.6.27 と 1.7.12 以降、containerd は非推奨警告を introspection API 経由で公開している。まさにこの移行を「発見する」のではなく「計画する」ためだ。サブコマンドは ctr deprecations list——複数形である。少なくとも 1 つの公式ドキュメントが単数形で書いており、その形は存在しないので、わざわざ書いておく。--format json を付けて全ノードで実行する。健康診断書ではない。警告は使用時に出るので、前回の再起動以降 schema 1 のイメージを取得していないノードは何も報告しない。これは「すでに間違っているとわかっているもの」の出発点リストだ。[depsrc]

# containerd has been telling you what will break since 1.6.27 / 1.7.12, through
# the introspection API. Almost nobody reads it, because the warnings go into
# the daemon log rather than anywhere you look. Ask directly.
#
# Note the subcommand is `deprecations`, plural. Some documentation writes it
# in the singular; that form does not exist and returns a usage error.
ctr deprecations list
# ID                                          LAST OCCURRENCE   MESSAGE
# io.containerd.deprecation/pull-schema-1-image  2026-08-24...   Schema 1 image...
# io.containerd.deprecation/cri-registry-mirrors 2026-08-24...   `mirrors` is deprecated...

# Machine-readable, which is the form you want across a fleet:
ctr deprecations list --format json | jq -r '.[].id' | sort -u

# Run it on every node and count, rather than sampling. The warnings are
# emitted on use, so a node that has not pulled a schema 1 image since the last
# daemon restart will not report one - which is why this is a starting point
# and not a clean bill of health.
for n in $(kubectl get nodes -o name); do
  printf '%-22s ' "${n#node/}"
  kubectl debug "$n" -it --image=busybox --profile=general -- \
    chroot /host ctr deprecations list --format json 2>/dev/null \
    | jq -r '[.[].id] | join(",")' || echo '(unavailable)'
done

# Clean up afterwards. `kubectl debug node/...` names its pods
# node-debugger-<node>-<suffix> and applies no label of its own, so there is
# nothing to select on - match the name instead, or they accumulate silently.
kubectl get pods -n default -o name | grep '^pod/node-debugger-' | xargs -r kubectl delete

設定ファイル、version 2 から version 3 へ——そして今は 4

設定ファイルこそがこの移行の本体だ。そして最初に押さえるべきは「最新バージョン」が何を指すのかで、それは今年動いた。version 3 は containerd 2.0 で登場し、CRI プラグインを 2 つに分割したのがこれだ。version 4 は 2.3 で登場し、まったく別のことをしている。次の節で扱う。version 2 は今も読み込まれ、起動のたびにメモリ上で変換される——デーモンはその旨を 1 行ログに残すので、そのノードが本当に移行されたのか、単に大目に見られているだけなのかを知る最も安上がりな方法がこれだ。デーモンにはコンバータ containerd config migrate が同梱されていて、現在のファイルを読んで最新バージョンの内容を標準出力に書き出す。man ページには載っていない——containerd-config(8)default しか記載していない——ので、知っている人が少ないのはかなりの部分これで説明がつく。[cricfg][cfgsrc]

# containerd 2.x reads a version 2 file and converts it in memory on every start.
# That is a compatibility shim, not a plan: it costs startup time, it is where
# the registry bug below lives, and the daemon says so on every boot.
journalctl -u containerd | grep -m1 'Configuration migrated from version'
# Configuration migrated from version 2, use `containerd config migrate` to
# avoid migration

# `containerd config migrate` reads your current file and prints the LATEST
# version on stdout. It is not in the man page - only `default` is - but it has
# been in the binary since 2.0.
#
# Note which version "latest" means, because it moved. Version 3 arrived in
# containerd 2.0 and is the one that split the CRI plugin in two. Version 4
# arrived in 2.3 and moves the server sockets into plugins (see below).
containerd config migrate > /tmp/config.new.toml
head -1 /tmp/config.new.toml
# version = 4        <- on containerd 2.3. On 2.0-2.2 this says 3.

# Two things to know before you trust the output. First, `migrate` and `dump`
# share one implementation, so the result is the FULLY POPULATED configuration,
# defaults and all - not a minimal file. Every default you did not choose is now
# pinned in your file and stops following the daemon when upstream changes it.
wc -l /etc/containerd/config.toml /tmp/config.new.toml
#   41 /etc/containerd/config.toml
#  318 /tmp/config.new.toml

# Second, and this is upstream's own warning: migrating the file to the latest
# version limits which containerd versions can read it. A version 4 file needs
# 2.3.0 or newer. If you might want to roll the binary back tonight, write a
# version 3 file instead - 2.0 and later read it, and it still gets you the
# plugin split, which is the part that matters.

# So: use the output to learn the new names, then hand-write the short version.
# What did it actually change? Compare the keys, not the files.
grep -oE '^\s*\[[^]]+\]' /tmp/config.new.toml | tr -d ' []' | sort > /tmp/new.keys
grep -oE '^\s*\[[^]]+\]' /etc/containerd/config.toml | tr -d ' []' | sort > /tmp/old.keys
diff -u /tmp/old.keys /tmp/new.keys

# Validate before you restart anything. `config dump` loads the file the daemon
# would load, including everything pulled in by `imports`, and fails loudly on
# a file it cannot parse. Note that --config is a global flag: it goes BEFORE
# the subcommand. Putting it after `config dump` is a usage error, not a check -
# urfave/cli rejects it with "flag provided but not defined: -config".
containerd --config /tmp/config.new.toml config dump >/dev/null && echo 'parses'

# Keep the old one. It is the fastest rollback you have.
cp -a /etc/containerd/config.toml /etc/containerd/config.toml.v2.bak

その出力を本番の設定に上書きする前に、知っておくべきことが 2 つある。1 つ目、migratedump は同一のコードパスなので、返ってくるのは完全に展開された設定だ。デーモンが持つデフォルト値がすべて明示的に書き出される。40 行のファイルが 300 行になり、あなたが選んだ覚えのないデフォルト値がすべて自分のファイルに固定され、上流で変わっても追随しなくなる。出力は新しいキー名を学ぶために使い、短い版は自分で書く。2 つ目、何かを再起動する前に候補ファイルを検証する——ただし --config はサブコマンドのフラグではなくグローバルフラグなので、config dumpに置く。containerd --config <ファイル> config dump の順であり、後ろに付けたものは検証ではなく使い方の誤りになる。正しく実行すれば、デーモンが読むのと同じ手順でファイルを読み、imports を辿り、解析できないものがあれば明確に失敗する。タイプミスを見つける場所としては、戻ってこないノードよりはるかにましだ。[cfgman]

# Configuration version 4 (containerd 2.3 and later). It changes nothing about
# CRI: the whole of the plugin split above is version 3 work. What it moves is
# the daemon's own sockets, out of top-level tables and into server plugins.

# --- version 3 and earlier -------------------------------------------------
#   [grpc]
#     address = "/run/containerd/containerd.sock"
#     uid = 0
#     gid = 0
#   [ttrpc]
#     address = "/run/containerd/containerd.sock.ttrpc"
#   [metrics]
#     address = "127.0.0.1:1338"
#   [debug]
#     address = "/run/containerd/debug.sock"
#     level = "info"

# --- version 4 -------------------------------------------------------------
version = 4

[plugins.'io.containerd.server.v1.grpc']
  address = '/run/containerd/containerd.sock'
  uid = 0
  gid = 0

[plugins.'io.containerd.server.v1.ttrpc']
  address = '/run/containerd/containerd.sock.ttrpc'

[plugins.'io.containerd.server.v1.metrics']
  address = '127.0.0.1:1338'

[plugins.'io.containerd.server.v1.debug']
  address = '/run/containerd/debug.sock'

# `[debug]` does not disappear: level, format and log_trace_id stay at the top
# level. Only the socket fields move.
#
# One behaviour change hides in here. Before version 4, an unset ttrpc address
# was derived from the grpc address as "<grpc address>.ttrpc" and inherited its
# uid and gid. In version 4 the ttrpc plugin is independent and falls back to
# its own default. If anything of yours connects to that socket by path -
# a shim debugger, a monitoring agent - set it explicitly rather than assuming.

version 4 は単独で見ておく価値がある。この移行を扱った記事のほとんどが version 3 で止まっているうえ、切り戻しに制約を持ち込むからだ。CRI については何も変えない。変わるのはデーモン自身のソケットの置き場所で、トップレベルの [grpc][ttrpc][metrics][debug] テーブルから io.containerd.server.v1.* プラグインへ移る。帰結は 2 つある。挙動の面では、version 4 より前は ttrpc のアドレスを指定しないと gRPC のアドレスから <grpc address>.ttrpc として導出され、uid と gid もそこから継承していた。version 4 では ttrpc プラグインは独立していて、自前のデフォルトにフォールバックする——そのソケットにパスで接続しているものが手元にあるなら、これからは明示的に設定しておくべきだ。運用の面では、上流自身が警告を出している。ファイルを最新バージョンへ移行すると、それを読める containerd のバージョンが狭まる。version 4 のファイルには 2.3.0 以降が必要で、version 3 のファイルは 2.0 以降なら読める。同じ晩にバイナリを切り戻す可能性が計画に含まれているなら、version 3 で書くこと。肝心なプラグイン分割は version 3 で手に入る。[cfgver][srvmig]

1 つのプラグインが 2 つになり、設定も一緒に動いた

構造上の変更は、唯一だった CRI プラグインが 2 つに分割されたことだ。以前は io.containerd.grpc.v1.cri がすべてを抱えていた。version 3 ではストリーミングサーバのオプションだけが残り、中身は 2 つの新しい ID に移った。コンテナを走らせることに関するすべて——ランタイム、CNI、サンドボックス、SELinux、OOM の扱い——は io.containerd.cri.v1.runtime へ。イメージに関するすべて——snapshotter、レジストリ、ピン留めされたサンドボックスイメージ、取得の並列度——は io.containerd.cri.v1.images へ。以前より良い分け方であり、同時に、プラグイン ID を機械的に置換すると設定のおよそ半分が間違ったテーブルに入ることを意味する。[ctrarch]

# /etc/containerd/config.toml - containerd 1.7, the file most clusters have.
# Everything lives under one plugin ID: io.containerd.grpc.v1.cri
version = 2

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.k8s.io/pause:3.10"

  [plugins."io.containerd.grpc.v1.cri".containerd]
    snapshotter = "overlayfs"
    default_runtime_name = "runc"

    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
      runtime_type = "io.containerd.runc.v2"
      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
        SystemdCgroup = true

    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.gvisor]
      runtime_type = "io.containerd.runsc.v1"

  [plugins."io.containerd.grpc.v1.cri".cni]
    bin_dir = "/opt/cni/bin"
    conf_dir = "/etc/cni/net.d"

  # The block that causes the most trouble in this migration.
  [plugins."io.containerd.grpc.v1.cri".registry]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
      endpoint = ["https://mirror.internal.example.com"]
# /etc/containerd/config.toml - containerd 2.x. The single CRI plugin has been
# split in two, and the settings moved with the split: anything about running
# containers is now under io.containerd.cri.v1.runtime, anything about images
# under io.containerd.cri.v1.images. io.containerd.grpc.v1.cri still exists,
# but only for the streaming server options.
version = 3

[plugins.'io.containerd.cri.v1.images']
  snapshotter = 'overlayfs'                 # moved: was under ...cri.containerd

  [plugins.'io.containerd.cri.v1.images'.pinned_images]
    sandbox = 'registry.k8s.io/pause:3.10.2'  # replaces sandbox_image

  [plugins.'io.containerd.cri.v1.images'.registry]
    config_path = '/etc/containerd/certs.d'   # replaces the mirrors block

[plugins.'io.containerd.cri.v1.runtime']
  [plugins.'io.containerd.cri.v1.runtime'.containerd]
    default_runtime_name = 'runc'

    [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
      runtime_type = 'io.containerd.runc.v2'
      [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
        SystemdCgroup = true

    [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.gvisor]
      runtime_type = 'io.containerd.runsc.v1'

  [plugins.'io.containerd.cri.v1.runtime'.cni]
    bin_dirs = ['/opt/cni/bin']   # bin_dir is deprecated since 2.1: plural now
    conf_dir = '/etc/cni/net.d'

# Note what is NOT here: no registry.mirrors alongside config_path. Setting
# both is an error - "`mirrors` cannot be set when `config_path` is provided" -
# and the CRI plugin refuses to load rather than picking one.
#
# And note the clock on the old keys. registry.mirrors and registry.configs
# were deprecated in containerd 1.5, registry.auths in 1.3, and cni.bin_dir in
# 2.1. All four carry the same removal target: containerd 2.4. Converting them
# is not housekeeping you can defer past the next release.
containerd 1.x(version 2)containerd 2.x(version 3)備考
version = 2version = 3(2.0)/ version = 4(2.3)version 2 は今も読み込まれメモリ上で変換される。version 4 のファイルには 2.3.0 以降が必要
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.cri.v1.runtime'コンテナを走らせることに関するすべて:ランタイム、CNI、SELinux、OOM、サンドボックス
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.cri.v1.images'イメージに関するすべて:snapshotter、レジストリ、ピン留めイメージ、取得設定
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.grpc.v1.cri'今も存在するが、ストリーミングサーバのオプション専用
sandbox_image = "…"pinned_images.sandbox = '…'改名かつ移動。 失うと閉域ノードが registry.k8s.io を取りにいく
…cri".containerd.snapshotter…cri.v1.images'.snapshotter側が変わる。ランタイム側からイメージ側へ
…cri".containerd.runtimes.*…cri.v1.runtime'.containerd.runtimes.*パスのみ変更。runtime_type = io.containerd.runc.v2 は不変
…cri".registry.mirrors…cri.v1.images'.registry.config_path仕組みが違う。 テーブルではなく hosts.toml を並べたディレクトリ。削除予定は 2.4
…cri".registry.auths—(imagePullSecrets)意図的に代替なし。 認証情報はクラスタ側へ。削除予定は 2.4
…cri".cni.bin_dir…cri.v1.runtime'.cni.bin_dirs複数形で、しかもリスト。2.1 で非推奨、削除予定は 2.4
plugin_dir(Go プラグイン .so)—(プロキシまたはバイナリプラグイン)2.1 ですでに削除済み。非推奨にとどまらない

この分割の中にある 2 つの改名が被害の大半を生む。sandbox_imagepinned_images.sandbox になった。pause イメージを社内ミラーに向けていたクラスタは、静かに registry.k8s.io に戻る——ノードに外向きの経路がある間は何も起きない。もう 1 つは snapshotter で、ランタイム側からイメージ側へ移った。直感に反する程度には十分なので、想定ではなく確認するべきだ。下の表の残りはすべて、挙動ではなくパスの変更である。[ctrplug]

レジストリ:実際にクラスタを落とす部分

レジストリ設定は、この移行が「面倒」から「危険」に変わる場所であり、しかも期限が付いている。mirrorsconfigs は containerd 1.5 で、auths は 1.3 で非推奨になり、3 つとも削除予定は containerd 2.4——この記事が薦めるリリースの次だ。前者 2 つの代替はディレクトリツリーになる。単一の config_path の下に、レジストリのホスト名前空間ごとのサブディレクトリを置き、それぞれに hosts.toml を置く。ファイルは増え、魔法は大幅に減り、そして実際に良くなっている。ホストは順に試され、capabilities は明示され、レジストリごとの CA は特別扱いではなくファイル内の 1 行になる。3 つ目の auths にファイルでの代替がないのは意図的だ。認証情報は Kubernetes の imagePullSecret に置くべきもので、そのノード上のすべてのワークロードが継承するノードレベルの設定に置くものではない。[crireg][hosts]

# The mirrors / configs / auths properties are deprecated. The replacement is a
# directory of hosts.toml files, one per registry host namespace, pointed at by
# a single config_path. It is more files and considerably less magic.

# Directory naming, which is where this goes wrong silently. containerd looks
# for the host namespace in three forms, in order:
#     <host>_<port>_        e.g. registry.internal.example.com_5000_
#     <host>:<port>         e.g. registry.internal.example.com:5000
#     _default
# The first form is the portable one - a colon is not a legal filename on
# Windows - so prefer it. A directory named anything else looks perfectly
# correct and simply never matches.

mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'TOML'
server = "https://docker.io"

[host."https://mirror.internal.example.com"]
  capabilities = ["pull", "resolve"]

# Fall through to the real registry if the mirror does not have the layer.
# Order matters: hosts are tried top to bottom.
[host."https://registry-1.docker.io"]
  capabilities = ["pull", "resolve"]
TOML

# A private registry on a port, with its own CA:
mkdir -p /etc/containerd/certs.d/registry.internal.example.com_5000_
cat > /etc/containerd/certs.d/registry.internal.example.com_5000_/hosts.toml <<'TOML'
server = "https://registry.internal.example.com:5000"

[host."https://registry.internal.example.com:5000"]
  capabilities = ["pull", "resolve", "push"]
  ca = "/etc/containerd/certs.d/internal-ca.crt"
TOML

# registry.auths has no file equivalent, on purpose. Credentials belong in a
# Kubernetes imagePullSecret, not in the node's runtime configuration where
# every workload on the node inherits them.
kubectl create secret docker-registry regcred \
  --docker-server=registry.internal.example.com:5000 \
  --docker-username=ci --docker-password="$REG_PASSWORD"

# Verify resolution without restarting anything. --hosts-dir makes ctr read the
# same tree the CRI plugin will read.
ctr images pull --hosts-dir /etc/containerd/certs.d docker.io/library/alpine:3.22

# And confirm the daemon agrees once it has restarted:
containerd config dump | grep -A3 "cri.v1.images'.registry"
# config_path = '/etc/containerd/certs.d'

このバグは正確に述べておく価値がある。曖昧に述べると、探す場所を間違えるからだ。奇妙な設定を書いている必要はまったくない。registry.mirrors ブロックだけを含むごく普通の version 2 のファイルで containerd 2.2.0 を起動すると、メモリ上の移行がその隣にデフォルトの config_path を足す——そして CRI プラグインはこの組み合わせを真っ向から拒否する:`mirrors` cannot be set when `config_path` is provided。プラグインは読み込まれない。デーモンはそれでも起動する。そのノードにスケジュールされた Pod はすべて作成に失敗し、マシン上のあらゆるサービスのトップレベルのステータスは緑のままだ。自分のファイルを両方のキーで grep しても何も出てこない。片方は自分で書いていないからだ。これは 2.2.0 に対して報告され、pull request 12617 で修正された。この修正は 2.3.0 より前に入り、2.2 ブランチにもバックポートされている——だから現行の 2.3 や最近の 2.2 のパッチ版なら影響を受けず、2.0 と 2.1 なら受ける。どちらであっても変わらない指示は、変換を最後までやり切ることだ。certs.d のツリーを作り、config_path をそこへ向け、mirrors ブロックを削除する。そうすれば、移行が推測する余地はなくなる。[iss12612][pr12617]

イメージ取得の経路が足元で変わっていた

containerd 2.1 以降、CRI プラグインはプロセス内で取得するのではなく Transfer Service 経由でイメージを取得する。これはデフォルトであって、あなたが有効にしたオプションではない。それ自体は特筆すべきことではない。節を割く価値があるのはフォールバックのほうだ。CRI のイメージ設定に Transfer Service が満たせないものが含まれていると、containerd はノード全体に対して use_local_image_pull を設定し、警告を記録する。失敗はせず、使用時にも教えず、クラスタにも伝えない。[ctrxfer]

# Since 2.1 the CRI plugin pulls images through the Transfer Service instead of
# pulling in-process. This is not a flag you set; it is the default. What makes
# it worth knowing is the fallback: if the CRI image configuration contains
# anything the Transfer Service cannot honour, containerd silently switches the
# whole node back to local pull and logs a warning.
#
# The triggers, from the CRI config guide:
#   Registry.Mirrors set          Registry.Configs set      Registry.Auths set
#   MaxConcurrentDownloads != 3   DiscardUnpackedLayers = true
#   ImagePullWithSyncFs = true    DisableSnapshotAnnotations = false
#
# Which means the perfectly reasonable act of raising the download concurrency
# quietly changes the code path your images are pulled through.

journalctl -u containerd --since '10 min ago' \
  | grep -iE 'transfer|use_local_image_pull|falling back'

# If you want local pull, ask for it rather than triggering it by accident:
#   [plugins.'io.containerd.cri.v1.images']
#     use_local_image_pull = true
#
# If you want the Transfer Service, move the settings to where it reads them:
#   [plugins.'io.containerd.transfer.v1.local']
#     max_concurrent_downloads = 6

# Check which path a real pull took, end to end:
crictl pull registry.k8s.io/pause:3.10.2
crictl images | head
ctr -n k8s.io images ls | wc -l
設定項目ローカル取得Transfer Service(2.1 以降デフォルト)
snapshotter対応対応
ImagePullProgressTimeout対応対応
PinnedImages対応対応
Registry.Mirrors / Configs / Auths対応(いずれも非推奨)非対応——ローカル取得へのフォールバックを引き起こす
MaxConcurrentDownloadsCRI イメージ設定から読まれる移す必要があるplugins.'io.containerd.transfer.v1.local' へ。3 以外の値はフォールバックを引き起こす
DiscardUnpackedLayers対応非対応——フォールバックを引き起こす
ImagePullWithSyncFs対応非対応——フォールバックを引き起こす
DisableSnapshotAnnotations対応snapshotter プラグイン側で設定する。false はフォールバックを引き起こす

トリガーの一覧を一度読めば含意は明らかだ。max_concurrent_downloads を 3 から 6 に上げる——帯域の太いノードではごく普通で妥当な操作——だけで、そのノードのイメージ取得がすべて別のコードパスに移る。非推奨の mirrors ブロックを残しておくのも同じ結果になる。レジストリ変換を中途半端にせず終わらせるべき第二の理由がこれだ。ローカル取得が欲しいなら use_local_image_pull = true と明示して選ぶ。Transfer Service が欲しいなら、並列度の設定を実際に読まれる [plugins.'io.containerd.transfer.v1.local'] に移す。[cricfg]

本当に削除されたもの

次は削除だ。改名ではなく本当に無くなった部分である。一覧は短く、並んでいるものにはすべて文書化された代替がある。ただし要約ではなくバージョン番号のほうを読むこと——containerd 2.0 の移行文書と RELEASES.md は 2 か所で食い違っており、そのどちらでも人が引用するのは移行文書のほうだ。重要なのは Docker schema 1 の取得で、2.0 では無効化されて環境変数で戻せたが、2.1 で削除され、戻す手段はなくなった。ここでの目標は 2.3 なので、無くなったものとして扱う。これが効いてくるのは、schema 1 に残っているイメージとは、定義上、だいたい 2017年以降誰も再ビルドしていないイメージであり、つまり Dockerfile を持っている人もいないということだからだ。アップグレードの後ではなく前に探す。1.7.8 と 1.6.25 以降、変換されたイメージにはラベルが付くので検索できる。[ctr20][ctrrelmd]

# Docker schema 1 manifests. Get the timeline right, because it decides whether
# you have a workaround or a deadline:
#   containerd 2.0  pulling is DISABLED by default, and the environment variable
#                   CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1 re-enables it
#   containerd 2.1  support REMOVED. The variable does nothing. So does anything else.
# Since 2.3 is the target, treat this as removed and find the images NOW.
#
# Since 1.7.8 / 1.6.25 converted images carry a label, so they are findable:
ctr namespaces list --quiet | xargs -I{} -- \
  ctr --namespace={} image list \
    'labels."io.containerd.image/converted-docker-schema1"'

# On a node still running 1.7, the same list from the CRI side:
crictl images -o json | jq -r '.images[].repoTags[]' | sort -u > /tmp/node-images.txt

# For each one, ask the registry what media type it actually serves. A schema 1
# manifest answers with application/vnd.docker.distribution.manifest.v1+prettyjws.
# Anything that does needs rebuilding in schema 2 or OCI before the node moves.

# The runtime v1 shims were removed in 2.0. Anything still asking for them
# fails to start the container, with an error about an unknown runtime:
grep -rn 'io.containerd.runtime.v1.linux\|io.containerd.runc.v1' \
  /etc/containerd/ /etc/crio/ 2>/dev/null
kubectl get runtimeclass -o custom-columns=NAME:.metadata.name,HANDLER:.handler

# The AUFS snapshotter was removed. Almost nobody sets this, and the ones who
# do have a kernel from 2016 underneath it:
containerd config dump | grep -E "snapshotter\s*=" | sort -u

# LimitNOFILE is no longer set in the reference unit. On systemd 240 and newer
# the default is fine; below that the kernel default of 4096 applies, and
# containers inherit it.
systemctl show containerd -p LimitNOFILE -p LimitNOFILESoft
systemctl --version | head -1
機能非推奨になった版削除された版代わりに使うもの
Runtime V1、io.containerd.runtime.v1.linux1.42.0io.containerd.runc.v2
Runc V1、io.containerd.runc.v11.42.0io.containerd.runc.v2
組み込みの AUFS snapshotter1.52.0overlayfs
containerd.io/restart.logpath ラベル1.52.0containerd.io/restart.loguri
cri-containerd-*.tar.gz 同梱パッケージ1.62.0containerd、runc、CNI プラグインを個別にインストール
CRI v1alpha2 API1.72.0CRI v1 のみ。crictl versionRuntimeApiVersion: v1 を返すか確認
旧来の CRI podsandbox 実装2.02.0デフォルトであるサンドボックスコントローラ
Docker schema 1 イメージの取得1.72.1(2.0 で無効化)schema 2 / OCI で再ビルド。環境変数の抜け道は 2.1 で効かなくなった
Go プラグインライブラリ(*.so)によるランタイムプラグイン2.02.1外部プラグイン:プロキシまたはバイナリ
リファレンスユニット内の明示的な LimitNOFILE2.0systemd のデフォルトを使う。systemd 240 未満では 1024:524288 を手で設定
デフォルト seccomp プロファイル内の io_uring_*2.0明示的な seccomp プロファイルと、そもそも要るのかという議論

1 つだけ他より静かな削除があり、名指ししておく価値がある。リファレンスの containerd.service ユニットが LimitNOFILE を明示的に設定しなくなった。containerd の rlimit は起動するコンテナに継承されるので、これはデーモンだけの設定ではない。systemd 240 以降ならデフォルトは妥当で何も起きないが、それ未満ではカーネルのデフォルトである 4096 が適用され、そのホスト上のすべてのコンテナがそれを継承する。上流のそうしたホスト向けの案内は、LimitNOFILE=1024:524288 を手で設定し直すことだ。[pr8924][sdexec]

断りなく裏返ったデフォルト値

削除とは別に、いくつかのデフォルト値が値そのものを変えた。設定を 1 つも触っていないアップグレードのあとでノードの挙動が変わるのは、これらのせいだ。惰性で受け入れるのではなく、意識的に決める価値がある。[ctr20]

デフォルト値containerd 1.xcontainerd 2.xなぜ重要か
enable_unprivileged_portsfalsetrueコンテナが CAP_NET_BIND_SERVICE なしで 1024 未満にバインドできる
enable_unprivileged_icmpfalsetruepingCAP_NET_RAW なしで動く
enable_cdi無効true/etc/cdi/var/run/cdi の spec ファイルがデバイスアクセスを記述する。スイッチ自体は 2.2 で非推奨、2.4 で消える
NRI無効有効NRI ソケットがノードの攻撃面の一部になる
サンドボックス版 CRI旧 CRI サーバサンドボックスコントローラrunc では見えない。Kata と gVisor では試す価値がある
イメージ取得の経路プロセス内Transfer Service(2.1 以降)いくつかの設定で静かにローカル取得へ戻る
io_uring_* システムコール許可ブロックカーネル脆弱性が続いた末にデフォルト seccomp 許可リストから除外
サンドボックスイメージsandbox_imagepinned_images.sandbox値は同じ、キーが違う。書き直しで失いやすい
  • 非特権ポートと ICMP が有効。 CRI プラグインは、ホストのネットワーク名前空間もユーザー名前空間も使わないコンテナに対して net.ipv4.ip_unprivileged_port_start=0net.ipv4.ping_group_range=0 2147483647 を設定するようになった。1024 未満へのバインドに CAP_NET_BIND_SERVICE は不要になり、pingCAP_NET_RAW も不要になった。便利であり、同時にコンテナのセキュリティ姿勢の変更でもある。enable_unprivileged_portsenable_unprivileged_icmpfalse にすれば以前の挙動に戻る。
  • NRI が有効。 Node Resource Interface は、コンテナ作成時にプラグインがコンテナを書き換えることを可能にする。アクセス制御はシステム全体の NRI ソケットへのアクセス権で行われる。つまり NRI プラグインを 1 つも動かしていなくても、そのソケットはノードの攻撃面の一部になった。
  • CDI が有効。そしてそのスイッチ自体が退場しつつある。 Container Device Interface が有効で、cdi_spec_dirs のデフォルトは /etc/cdi/var/run/cdi だ。これらのディレクトリに spec ファイルを書けるものは何であれ、コンテナのデバイスアクセスを記述できる。なお enable_cdi というスイッチ自体が containerd 2.2 で非推奨になり、削除予定は 2.4 だ。それ以降 CDI は単に常時有効になる——無効にするつもりだったなら、その計画には期限がある。
  • io_uring がデフォルトの seccomp 許可リストから外れた。 io_uring_enterio_uring_registerio_uring_setup は、カーネルの脆弱性が十分に長く続いた結果、デフォルトで許可するのは安全でないとプロジェクトが判断して外された。io_uring を前提に作られたワークロードには明示的なプロファイルが必要になる——そして、それを与えるべきかどうかという議論も。
  • サンドボックス版の CRI 実装がデフォルト。 CRI プラグインは旧来の CRI サーバではなく、安定版のサンドボックスコントローラを使う。通常運用では見えず、Kata や gVisor のようなサンドボックス型ランタイムを動かしているときには非常によく見える——まさに全台展開の前に試すべきケースだ。

どれもアップグレードしない理由にはならない。1 台だけアップグレードして、見て、それからイメージビルドに変更を書き込む理由になる——それが移行と全台規模の不意打ちとの違いだ。[ctrnri][cdi]

ノードのアップグレード、順番どおりに

機械的な部分は短く、変わったのは中身よりやり方のほうだ。同梱パッケージ cri-containerd-cni-VERSION-OS-ARCH.tar.gz は 2.0 で削除されたので、containerd、runc、CNI プラグインはそれぞれ独立したインストールと独立したバージョン判断になった。より明示的で、手間は少し増え、そして「containerd を上げたつもりで runc も一緒に上がっていた」という長年の混乱の種がなくなる。跳躍そのものについては安心材料が 1 つある。containerd は連続したマイナーリリース間のアップグレードと、それとは別に連続した LTS リリース間の直接アップグレードをサポートしていて、1.7(LTS)から 2.3(LTS)は、リリース文書自身が挙げている例そのものだ。本来降り立つべき場所を飛ばしているわけではない。[ctrstart]

#!/usr/bin/env bash
# One node, from containerd 1.7 to 2.3 LTS. Run it on a drained node.
#
# 1.7 -> 2.3 is a supported jump. containerd supports sequential minor upgrades
# and, separately, direct upgrades between sequential LTS releases - and it
# names 1.7 (LTS) to 2.3 (LTS) as an example. That is exactly this path.
set -euo pipefail
VER=2.3.4                 # check https://containerd.io/releases/ before pinning
RUNC_VER=1.5.0
CNI_VER=1.9.1
ARCH=amd64

# 0. Get the workloads off, and keep the node out of rotation until verified.
#    (From the control plane, not from the node.)
#    kubectl drain node-01 --ignore-daemonsets --delete-emptydir-data

# 1. Back up everything the daemon owns. /var/lib/containerd is the image and
#    snapshot store; losing it means re-pulling every image on the node.
systemctl stop kubelet containerd
cp -a /etc/containerd/config.toml /etc/containerd/config.toml.bak
tar -C /var/lib -czf "/var/backups/containerd-lib-$(date +%F).tgz" containerd

# 2. Install the three components separately. The combined
#    cri-containerd-cni-*.tar.gz bundles were removed in 2.0; this is now the
#    supported route rather than a workaround.
curl -fsSLO "https://github.com/containerd/containerd/releases/download/v${VER}/containerd-${VER}-linux-${ARCH}.tar.gz"
curl -fsSLO "https://github.com/containerd/containerd/releases/download/v${VER}/containerd-${VER}-linux-${ARCH}.tar.gz.sha256sum"
sha256sum -c "containerd-${VER}-linux-${ARCH}.tar.gz.sha256sum"
tar -C /usr/local -xzf "containerd-${VER}-linux-${ARCH}.tar.gz"

#    runc and the CNI plugins, pinned deliberately rather than left behind.
#    These used to ride along inside the removed bundles; now they are yours
#    to choose, which also means yours to forget.
curl -fsSL -o /usr/local/sbin/runc \
  "https://github.com/opencontainers/runc/releases/download/v${RUNC_VER}/runc.${ARCH}"
chmod 755 /usr/local/sbin/runc

mkdir -p /opt/cni/bin
curl -fsSLO "https://github.com/containernetworking/plugins/releases/download/v${CNI_VER}/cni-plugins-linux-${ARCH}-v${CNI_VER}.tgz"
tar -C /opt/cni/bin -xzf "cni-plugins-linux-${ARCH}-v${CNI_VER}.tgz"

# 3. Refresh the systemd unit from the release, then reapply any drop-in of
#    your own. Note the reference unit no longer sets LimitNOFILE, and that the
#    directory does not exist on a host that came from a distribution package.
mkdir -p /usr/local/lib/systemd/system
curl -fsSL -o /usr/local/lib/systemd/system/containerd.service \
  "https://raw.githubusercontent.com/containerd/containerd/v${VER}/containerd.service"
systemctl daemon-reload

# 4. Put the new configuration in place - the hand-written one, not the
#    318-line dump - and prove it parses before anything restarts.
install -m 0644 /tmp/config.new.toml /etc/containerd/config.toml
containerd --config /etc/containerd/config.toml config dump >/dev/null

# 5. Bring it back, runtime first, kubelet second.
systemctl start containerd
sleep 3
ctr plugins ls | awk '$4!="ok"'          # must print only the header
systemctl start kubelet

# 6. Then verify from the cluster's point of view before uncordoning:
#    kubectl get node node-01 -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}'
#    kubectl uncordon node-01

順序について、急いでいると外しやすい点が 2 つ。始める前に /var/lib/containerd をバックアップする。アップグレードで壊れる公算が高いからではなく、そこがイメージとスナップショットの保管場所だからだ。失えばそのノードのすべてのイメージを取り直すことになり、大きなノードでは数十分と相当な外向き通信になる。そしてランタイムを kubelet より先に起動し、kubelet を起動する前に ctr plugins lsok でないプラグインがないか確認する。読み込みに失敗した CRI プラグインとは、Pod を受け入れて作成できないノードのことだ。[runc][cni]

同じ変更を Kubernetes 側から見る

クラスタ側にやることはなく、確認することが 1 つある。サポートマトリクスはプロジェクトの文書であってアドミッションポリシーではない。サポート外の containerd に対して起動を拒む kubelet は存在せず、どこにもイベントは出ない。だから監査は明示的にやるしかない。ノードを回って containerRuntimeVersion を読み、同じノードの kubelet バージョンに対応するマトリクスの行と突き合わせる。ノードプールのバージョンがまちまちなのは例外ではなく通常であり、特にノードイメージが独自の周期で進む環境ではそうだ。[k8sruntime]

# The support matrix is a project document, not a runtime check: nothing stops
# a kubelet from talking to an unsupported containerd. That is precisely the
# problem - you find out from a bug, not from a startup error. So audit it.

kubectl get nodes -o json | jq -r '
  .items[] | [.metadata.name,
              .status.nodeInfo.kubeletVersion,
              .status.nodeInfo.containerRuntimeVersion] | @tsv' \
| while IFS=$'\t' read -r node kubelet runtime; do
    ctd=${runtime#containerd://}
    case "${kubelet%.*}/${ctd%%.*}" in
      v1.36/1|v1.37/1) verdict='NOT LISTED - upgrade the runtime' ;;
      *)               verdict='check against containerd.io/releases' ;;
    esac
    printf '%-22s kubelet=%-9s containerd=%-9s %s\n' \
           "$node" "$kubelet" "$ctd" "$verdict"
  done

# Carry the cgroup driver across explicitly. Kubernetes 1.28 added the ability
# for the kubelet to read it from the CRI runtime instead of its own config file
# - but that arrived as an alpha feature behind the KubeletCgroupDriverFromCRI
# gate, and on the containerd side it needs 2.0 or later. Either way the
# containerd setting has to be right, so do not treat it as automatic:
grep -rn 'SystemdCgroup' /etc/containerd/config.toml
grep -E '^cgroupDriver:' /var/lib/kubelet/config.yaml

# Runtime handlers are the part most often lost, because they are the part
# somebody added by hand. Every handler referenced by a RuntimeClass must still
# exist in the rewritten configuration:
kubectl get runtimeclass -o jsonpath='{range .items[*]}{.handler}{"\n"}{end}' \
  | sort -u | while read -r h; do
      grep -q "runtimes\.${h}\b" /etc/containerd/config.toml \
        && echo "ok      $h" || echo "MISSING $h"
    done

# And the sandbox image. It moved from sandbox_image to pinned_images.sandbox,
# and if you had it pointed at an internal mirror, that is a setting to carry
# across rather than a default to accept.
crictl info | jq -r '.config.sandboxImage // .config.containerd.sandboxImage'

この移行で最も失われやすい設定はランタイムハンドラだ。最も手で追加されやすい設定だからである。クラスタ内のすべての RuntimeClass は、書き直した設定に存在していなければならないハンドラを名指ししている。存在しなければ、それを要求するワークロードだけが落ちる。つまり障害は gVisor や Kata を使っていたチームの中に閉じ、他の誰も 1 週間気づかない。新しいファイルに対して名前で突き合わせて確認する。cgroup ドライバも同じで、こちらは安心できるほうの筋書きが正確ではない。Kubernetes 1.28 で、kubelet が CRI ランタイムにどのドライバを使っているか問い合わせる機能が入ったが、それは KubeletCgroupDriverFromCRI ゲートの背後にあるアルファ機能であり、containerd 側は 2.0 以降が必要だ。つまり自動ではない。SystemdCgroup = true は今も書き直しを生き延びる必要があり、しかも今は別のパスにある。[k8srtc][k8skubeadm]

切り戻しと、切り戻せないもの

切り戻しについては、安心させる答えではなく率直な答えを出したい。バイナリと設定ファイルはきれいに戻る。どちらもディスク上のファイルであり、古い tarball と古い config.toml を残してあれば、出発点から 10 分の距離だ。これは率直に言って、たいていの移行が提供する以上のものである。[ctrgh]

# Rolling back is realistic here, which is not true of every migration on this
# site - but only if you kept the two things that matter and only within
# limits. Know which of these applies before you start the window.

# --- what rolls back cleanly ---------------------------------------------
# The binaries and the configuration file. Both are files on disk.
systemctl stop kubelet containerd
tar -C /usr/local -xzf /var/backups/containerd-1.7.28-linux-amd64.tar.gz
cp -a /etc/containerd/config.toml.v2.bak /etc/containerd/config.toml
systemctl daemon-reload && systemctl start containerd kubelet
containerd --version

# --- what does not ---------------------------------------------------------
# 1. The image and snapshot store, in the sense that nobody promises it will.
#    containerd's stability document puts file system layout, storage formats
#    and snapshot formats explicitly OUTSIDE its guarantees and says the project
#    may migrate these formats between minor versions. A downgrade against a
#    store that 2.x has already written to is therefore undefined rather than
#    documented-as-broken. Restore the tarball instead of finding out:
#      systemctl stop containerd
#      mv /var/lib/containerd /var/lib/containerd.v2
#      tar -C /var/lib -xzf /var/backups/containerd-lib-2026-08-24.tgz
#    (Container root filesystems are maintained on upgrade; it is the metadata
#     around them that has no promise attached.)
#
# 2. A configuration file you already migrated. A version 4 file needs
#    containerd 2.3.0 or newer, and a version 3 file needs 2.0 or newer. This
#    is why the config.toml backup matters as much as the binary one, and why
#    writing version 3 rather than 4 keeps your options open for a while.
#
# 3. Nothing about the Kubernetes control plane. This is a node-level change:
#    do NOT roll the cluster back because one node's runtime misbehaved.

# The honest limit on all of this: rollback buys you a night, not a quarter.
# containerd 1.7 leaves extended support in September 2026, and that extension
# only ever covered Kubernetes 1.30, 1.31 and 1.32 on GKE - all three of which
# are already out of support upstream.

そう簡単には戻らないものが 2 つある。1 つ目は状態ディレクトリで、率直に言えば「戻る」と約束している人はどこにもいない。containerd の安定性に関する文書は、ファイルシステムのレイアウト、ストレージ形式、スナップショット形式を明示的に保証の外に置き、マイナーバージョン間でこれらの形式を移行することがあると述べている。つまり 2.x がすでに書き込んだ /var/lib/containerd に対するダウングレードは、「壊れると文書化されている」のではなく「未定義」だ——午前 3 時に試して確かめる種類の区別ではない。tarball を復元するか、すべてのイメージを取り直すことを受け入れるかのどちらかにする。2 つ目は設定ファイルそのもので、すでに移行済みの場合が問題になる。version 4 のファイルは、読むだけでも containerd 2.3.0 以降を必要とする。そしてより大きな点ははっきり言っておく価値がある。切り戻しが買えるのは一晩であって、四半期ではない。1.7 ブランチは 2026年9月に延長サポートを離れ、それまでの間も特定ベンダーのマネージドサービス以外に対する修正は受け取らない。切り戻しは失敗したメンテナンス枠を安全に閉じる手段であって、判断を先送りする手段ではない。[ctrsec]

祈るのではなく検証する

検証は好みの問題ではないし、この移行では特有の形をしている。うまくいかないことのほとんどが、デーモンを動いたままにするのだ。だから「containerd が起動している」ことを確認しても何も証明されない。下のスクリプトは、静かに間違っていられるもの——設定バージョン、エラー状態のプラグインの有無、CRI が v1 で応答しているか、cgroup ドライバとランタイムハンドラが生き延びたか——を確認し、そのうえで、点検ではまったく確認できない 2 つのことをやる。[ctrcrictl]

#!/usr/bin/env bash
# Post-upgrade verification. Every check prints OK or explains itself; the exit
# code is the number of failures, so this can run straight from your config
# management after the node comes back.
fail=0
chk() { if eval "$2" >/dev/null 2>&1; then printf 'OK   %s\n' "$1";
        else printf 'FAIL %s\n' "$1"; fail=$((fail+1)); fi; }

chk 'containerd is 2.x'      'containerd --version | grep -qE " v2\."'
chk 'config is version 3+'   'grep -qE "^version = [34]$" /etc/containerd/config.toml'
chk 'no in-memory migration' '! journalctl -u containerd -b | grep -q "Configuration migrated from version"'
chk 'config parses'          'containerd --config /etc/containerd/config.toml config dump'
chk 'no plugin in error'     '[ "$(ctr plugins ls | awk "NR>1 && \$4!=\"ok\"" | wc -l)" -eq 0 ]'
chk 'CRI answers on v1'      'crictl version | grep -q "RuntimeApiVersion:  v1"'
chk 'runtime handler runc'   'containerd config dump | grep -q "runtimes.runc"'
chk 'systemd cgroup driver'  'containerd config dump | grep -q "SystemdCgroup = true"'
chk 'no legacy shims'        "! containerd config dump | grep -qE 'io\.containerd\.runtime\.v1\.linux|io\.containerd\.runc\.v1'"
chk 'mirrors not set'        "! containerd config dump | grep -q 'registry.mirrors'"
chk 'sandbox image pinned'   'containerd config dump | grep -q "pinned_images"'
chk 'kubelet is running'     'systemctl is-active --quiet kubelet'

# The two that are worth reading rather than counting. First: registry
# resolution has to be exercised, not inspected - a hosts.toml with the wrong
# directory name looks perfectly fine and simply never matches.
crictl pull registry.k8s.io/pause:3.10.2 >/dev/null 2>&1 \
  && echo 'OK   pull through the configured hosts' \
  || { echo 'FAIL pull through the configured hosts'; fail=$((fail+1)); }

# Second: the deprecation list should be shorter than it was before, not
# longer. A new entry here is something the migration introduced.
ctr deprecations list --format json 2>/dev/null | jq -r '.[].id' | sed 's/^/     still deprecated: /'

# And a real workload, because none of the above proves a container starts.
ctr run --rm docker.io/library/alpine:3.22 verify-"$$" /bin/true \
  && echo 'OK   container runs' \
  || { echo 'FAIL container runs'; fail=$((fail+1)); }

printf '\n%d failure(s)\n' "$fail"; exit "$fail" 

その 2 つとは、本物の取得と本物のコンテナだ。レジストリのホスト名前空間と名前が完全一致していないディレクトリに置かれた hosts.toml は、まったく正しく見えて、ただ一度もマッチしない。ファイルをいくら読み返してもそれは見えない。見えるのは取得だけだ。そして、あらゆる観点で妥当な設定であっても、ランタイムのバイナリがバージョン違いだったり shim が期待するパスになかったりすれば、コンテナは起動しない。2 台目に移る前に、1 台目で両方を実行すること。[critools]

どの順番でやるか

圧縮すれば、判断はこの記事よりずっと小さい。目標バージョンは 1 つ、2.3 だ——現行の LTS で、2028年4月までサポートされる。表の他はすべて、すでにサポート外か、数か月でサポート外になるか、他人のマネージドサービスに合わせて仕立てられた延長サポートブランチである。作業はバイナリの入れ替えではない。それは 15 分で終わる。作業は設定の書き直しであり、その中核はレジストリ変換だ。[ctrrel]

あなたの状況が…なら目標は…そして作業は…
Kubernetes 1.34 または 1.35 上の containerd 1.72.3 LTS へ一気に設定の全面書き直し。LTS から LTS への移動は明示的にサポートされており、2.2 で止まる理由はない
containerd 1.6、どこであれ2.3 LTS へ、急ぎで2025年8月からサポート外。しかも 1.6 から 2.3 は連続でも LTS 間でもない——1.7 を経由すること。保守案件ではなくセキュリティ案件として扱う
昨年上げた containerd 2.12.3 LTS設定はすでに version 3 なので、ほぼバイナリの入れ替えだけ——ただし 2.1 から 2.3 は 2.2 を飛ばすため、サポートされたアップグレード経路の外にある。想定で済ませず検証すること
containerd 2.2、最新に追随中2026年11月までに 2.3 LTS作業は最小。ただし放置しない——2.2 は次の LTS が来る前に終わる
すでに Kubernetes 1.36 なのに containerd 1.xこの作業枠で 2.3 LTS へテストされていない組み合わせで動いている。マトリクスの 1.36 の行に 1.x は無い
マネージドサービス(GKE、EKS、AKS)プロバイダが出すものノードイメージのリリースノートを読む。ランタイムは相手のもの、RuntimeClass はあなたのもの
  1. 計画の前に棚卸し。 ノードごとにデーモンのバージョン、設定ファイルのバージョン、CRI API のバージョン、kubelet のバージョンを記録する。次に全ノードで ctr deprecations list --format json を実行し、出力を保存する——それが「すでに間違っているとわかっているもの」の一覧だ。
  2. コンバータは辞書として使い、設定は手で書き直す。 containerd config migrate で新しいキー名を学び、短い version 3 のファイルは自分で書く。300 行のダンプをそのまま入れてはいけない。選んだ覚えのないデフォルト値をすべて固定してしまう。
  3. レジストリ変換は最初に、単独でやる。 certs.d のツリーを作り、ctr images pull --hosts-dir で証明し、mirrorsconfig_path が同じファイルに絶対に同居しないようにする。障害を引き起こすのはこの工程だ。
  4. 変わったデフォルト値は継承せずに決める。 非特権ポート、NRI、CDI、io_uring の seccomp 変更はいずれもセキュリティに関わる。選び、書き残し、イメージビルドに入れる。
  5. 1 台、次にプール、そして全台。 /var/lib/containerd をバックアップし、アップグレードし、検証スクリプトを走らせ、cordon を解除する。そのあとで初めてノードイメージに変更を書き込む——そして RuntimeClass が依存するランタイムハンドラを確認する。ほかに確認してくれるものはない。

これは同じ年に同じノード群へ降ってくる 4 つの変更のひとつで、まとめてやるほうが別々にやるより安く済む。cgroup v1 から cgroup v2 への移行 は、cgroup ドライバの設定が 2 度の書き直しを生き延びる必要があり、kubelet が今それをランタイムから読むようになったから。Docker Engine 29 の破壊的変更 は、同じコンテナ基盤を Docker 側から見たもの。ingress-nginx から Gateway API への移行 は、入口レイヤの変更のためにどのみちノードを作り直すなら。そもそもこれらがどれだけ必要なのかを量っているなら、Kubernetes を使うべきでないとき が議論のもう一方の側だ。

よくある質問

containerd 1.7 はまだサポートされていますか?

非常に狭い意味でだけです。containerd のリリース表は 1.7 を 2026年9月までの LTS として掲載していますが、重要なのは脚注です。コミッターによる一般サポートは 2026年3月に終了し、延長分は名前の挙がった 2 名のメンテナが提供していて、Google Kubernetes Engine 上の Kubernetes 1.32、1.31、1.30 での利用に的を絞っており、その用途に必要でない変更は拒否されうると明記されています。この 3 つの Kubernetes バージョンはいずれも上流ですでに EOL です。GKE 上でサポート切れの Kubernetes を動かしているのでない限り、1.7 は 9月ではなく今日の時点でサポート外として扱ってください。

containerd 2.x のどのバージョンに上げるべきですか?

2.3 です。現行の LTS ブランチで、2026年4月30日に始まり 2028年4月30日までサポートされます。ほかの選択肢はそれぞれ別の意味で劣ります。2.1 は 2026年7月3日に EOL、2.2 は 2026年11月6日までしかサポートされず、2.0 は 1.7 と同じくベンダー限定の延長サポートです。今 2.1 か 2.2 にいるなら、設定がすでに version 3 なので 2.3 への移動は短くて済みます。

config.toml を書き直す必要はありますか、それとも containerd 2.x が古いファイルを読んでくれますか?

version 2 のファイルは読み込まれ、起動のたびにメモリ上で変換されるので、強制されるものは何もありません。そして変換するたびに Configuration migrated from version 2, use `containerd config migrate` to avoid migration とログに書くので、これが環境全体を監査する最も手早い方法です。それでも書き直すべき理由は、既知の問題が住んでいるのがまさにこの互換経路だからです。とくに、レジストリの移行があなたの mirrors ブロックの隣に config_path を注入し、CRI プラグインが読み込まれなくなる問題があります。containerd config migrate で新しいキー名を学び、短いファイルを手で書いてください。バージョンは意識して選ぶこと。version 3 は containerd 2.0 以降で読め、version 4 には 2.3.0 以降が必要で、2.3 上の migrate が出力するのは version 4 です。

設定の version 4 とは何ですか、必要ですか?

version 4 は containerd 2.3 で登場しました。CRI については何も変えません——この移行の大半を占めるプラグイン分割は version 3 の仕事で、2.0 で導入されたものです。version 4 がやるのは、デーモン自身のソケットをトップレベルの [grpc][ttrpc][metrics][debug] テーブルから io.containerd.server.v1.grpc…v1.ttrpc…v1.metrics…v1.debug というプラグインのブロックへ移すことです。[debug]levelformatlog_trace_id をトップレベルに残します。見落としやすい挙動の変更が 1 つあります。version 4 より前は、ttrpc のアドレスを指定しないと gRPC のアドレスから <grpc address>.ttrpc として導出され、uid と gid もそこから継承していましたが、version 4 では ttrpc プラグインが自前のデフォルトを使います。version 4 が必要というわけではなく、しばらく version 3 を選ぶ理由もあります。version 4 のファイルは 2.0、2.1、2.2 では読めないので、書いた時点で切り戻しの選択肢が狭まります。

「`mirrors` cannot be set when `config_path` is provided」とは何を意味しますか?

CRI イメージサービスのプラグインが読み込みを拒否した、という意味です。レジストリ設定が、非推奨の mirrors テーブルと新しい config_path の両方を指定する形になったためです。containerd はそれでも起動し——デーモンは健全でプラグインは健全でない——そのノードにスケジュールされた Pod はすべて作成に失敗します。落とし穴は、両方を自分で書いたとは限らないことです。これは registry.mirrors ブロックだけを含むごく普通の version 2 のファイルを使って containerd 2.2.0 に対して報告されました。メモリ上の移行がその隣にデフォルトの config_path を足すからです。だから自分のファイルを grep しても何も出てきません。pull request 12617 で修正され、これは 2.3.0 より前に入って 2.2 にもバックポートされたので、現行の 2.3 と最近の 2.2 のパッチ版は影響を受けず、2.0 と 2.1 は受けます。いずれにせよ恒久的な対処は、2 つのうち片方だけを残すことです。certs.d のツリーを作り、config_path をそこへ向け、mirrors ブロックを削除します。

レジストリの mirrors ブロックは何に置き換わりましたか?

ディレクトリツリーです。[plugins.'io.containerd.cri.v1.images'.registry] の下に config_path を設定し——慣例では /etc/containerd/certs.d——レジストリのホスト名前空間ごとにサブディレクトリを作り、それぞれに hosts.toml を置きます。各ファイルは server と、明示的な capabilities を持つ 1 つ以上の [host."…"] エントリを記述し、上から順に試されるので、レイヤを持たないミラーから元のレジストリへフォールスルーできます。レジストリごとの CA やクライアント証明書も同じファイル内のキーです。ディレクトリ名はポートを含めてホスト名前空間と完全一致している必要があります——正しく見える設定が一度もマッチしない最も多い原因がこれです。

sandbox_image はどこへ行きましたか?

[plugins.'io.containerd.cri.v1.images'.pinned_images] の下の sandbox になりました。書き直しのあとは毎回これを手で確認する価値があります。障害が遅れて出るうえ、環境に依存するからです。インターネットに出られるノードは registry.k8s.io から pause イメージを平然と取得し、何もおかしく見えません。一方、閉域ノードや外向き通信が制限されたノードでは Pod が 1 つも作成できません。社内ミラーに向けていたなら、値を明示的に引き継いでください。

並列度を上げただけなのに、なぜイメージ取得の挙動が変わったのですか?

containerd 2.1 以降、CRI プラグインはデフォルトで Transfer Service 経由で取得し、Transfer Service は CRI イメージ設定から max_concurrent_downloads を読まないからです。Transfer Service が満たせない設定を見つけると、containerd はそのノードに use_local_image_pull = true を設定し、警告を記録して処理を続けます。トリガーの全一覧は Registry.MirrorsRegistry.ConfigsRegistry.Auths、3 以外の MaxConcurrentDownloadsDiscardUnpackedLayersImagePullWithSyncFsDisableSnapshotAnnotations = false です。コードパスを変えずに並列度を上げるには、[plugins.'io.containerd.transfer.v1.local'] の下に設定してください。

古いイメージは使えなくなりますか?

Docker schema 1 のイメージだけ、しかも取得時だけです。ただしバージョン番号を確認してください。この話題を扱った記事の多くが 1 リリース遅れています。サポートは containerd 2.0 でデフォルト無効になり、そこでは環境変数 CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1 で戻せましたが、2.1 で削除され、戻す手段はなくなりました。目標が 2.3 である以上、抜け道はありません。イメージを再ビルドするしかありません。アップグレード前に探してください。containerd 1.7.8 と 1.6.25 以降、schema 1 から変換されたイメージには io.containerd.image/converted-docker-schema1 ラベルが付くので、そのラベルを指定した ctr image list ですべての namespace を横断して見つけられます。解決策は schema 2 か OCI での再ビルドで、対象のイメージはたいてい十分に古いため、Dockerfile を見つけることが最も難しい部分になります。

containerd 2.x から 1.7 に切り戻せますか?

バイナリはきれいに戻ります——古い tarball を残しておけば 10 分の作業です。ただし複雑にする要素が 2 つあります。1 つ目は状態ディレクトリです。containerd の安定性に関する文書は、ファイルシステムのレイアウト、ストレージ形式、スナップショット形式を明示的に保証の外に置き、マイナーバージョン間でこれらの形式を移行することがあると述べています。つまり 2.x がすでに書き込んだ /var/lib/containerd に対するダウングレードは、単に危険なのではなく未定義です。アップグレード前に取得した tarball を復元するか、ノード上のすべてのイメージを取り直すことを受け入れてください。2 つ目は設定ファイルです。すでに移行済みなら、version 4 のファイルには 2.3.0 以降が、version 3 のファイルには 2.0 以降が必要なので、version 2 の原本を残しておいてください。切り戻しは移行を先送りする手段ではなく、失敗したメンテナンス枠を安全に閉じる手段として扱ってください。1.7 は 2026年9月に延長サポートを離れます。

containerd がサポート外のバージョンだと kubelet は動かなくなりますか?

いいえ。だからこそ明示的な監査が必要です。Kubernetes と containerd のサポートマトリクスは、両プロジェクトがどの組み合わせをテストしているかの表明であって、アドミッションポリシーではありません。kubelet はランタイムのバージョンを確認しませんし、イベントも出ませんし、サポート外の組み合わせでも起動して動いているように見えます。失われるのはテストです。Kubernetes 1.36 についてマトリクスが挙げているのは containerd 2.3.0+ と 2.2.0+ だけで、1.x のエントリはひとつもありません。つまり 1.36 クラスタ上の 1.7 ノードは、誰もあなたの代わりに動かしたことのない組み合わせです。ノードを回り、containerRuntimeVersion を読み、自分で突き合わせてください。

同じノードの Service データプレーンは別の時計で動いている。Kubernetes 1.37 は kube-proxy の ipvs モードを feature gate の裏で非推奨にし、1.40 で既定オフ、1.43 でコードそのものが消える。kube-proxy を IPVS から nftables へ移行するでは、カーネル 5.13 という下限、気づかないうちに変わる NodePort の挙動、そして誰も片付けなければトラフィックを飲み込む残留 kube-ipvs0 を扱っている。

リリース単位の補足をひとつ。1.37 の帳簿は、多くの記事が言っているものとは違う。実際に Pod を ContainerCreating で止めうるのは SELinuxMount の GA 昇格であって、cgroup v1 の失敗は 1.35、Static Pod の制限は 1.34、containerd の崖はこれから来る 1.38 だ。Kubernetes 1.37 へのアップグレードで実際に壊れるものでは、この三つの列を分けたうえで、アップグレードの後ではなく前に走らせるべき監査を示している。

参考資料

まず一次資料から。何が削除され、どのブランチがいつまで修正を受けるのかについて権威があるのは、containerd のリリースポリシーと 2.0 移行文書だけだ。改名されたキーの完全な一覧は CRI 設定ガイドだけにある。この記事が二次資料の言わないこと——2.1 はすでに EOL であること、1.7 の延長サポートはそれ自体サポート切れの Kubernetes バージョンに合わせて仕立てられていること——を書いている箇所で対立しているのは要約であって、プロジェクトではない。

  1. containerd - Versioning and release (RELEASES.md): the release-status table quoted throughout this article, including the end-of-life dates for 1.7, 2.0, 2.1, 2.2 and 2.3, the footnotes explaining that 1.7's and 2.0's extended support is scoped to specific Kubernetes versions on GKE, the Kubernetes/containerd support matrix, the daemon-configuration version table (version 3 needs 2.0, version 4 needs 2.3), the deprecation tables with their removal targets, the upgrade-path rules, and the "Not Covered" section that places storage and snapshot formats outside the stability guarantee
  2. containerd RELEASES.md on GitHub - the same document at its source, which is worth reading directly because the rendered docs site and the repository occasionally disagree with the older containerd-2.0 transition page (config version 1, and the deprecation release of the cri-containerd bundles, are two places where they do)
  3. containerd source, version/version.go on release/2.3: `const ConfigVersion = 4`. This is what `containerd config migrate` targets, and the reason a migrated file on 2.3 comes back as version 4 rather than version 3
  4. containerd source, the config migration table and serviceMigrate: the function that moves the top-level [grpc], [ttrpc], [metrics] and debug socket settings into io.containerd.server.v1.* plugins for version 4, including the note that an unset ttrpc address is no longer derived from the grpc address
  5. containerd 2.0 - what's new, what's breaking, what's changing: the single authoritative list of removals (CRI v1alpha2, the AUFS snapshotter, the runtime v1 shims, LimitNOFILE, the cri-containerd release bundles), the default flips, and the deprecation of the CRI registry properties
  6. CRI Plugin Config Guide - config versions 1, 2 and 3 side by side, the renamed plugin IDs, the full annotated default configuration, and the table of which image-pull options the Transfer Service does and does not support
  7. containerd - CRI registry configuration: how the deprecated mirrors, configs and auths properties map onto a certs.d directory tree, which is the conversion this migration actually turns on
  8. containerd - Registry Configuration (hosts.toml): the host-namespace directory layout under config_path, the capabilities key, and the per-host CA and client-certificate settings
  9. containerd-config(8): the manual page. Note that it documents only the `default` subcommand - `dump` and `migrate` exist in the binary but not in this page, which is why so few people know about them
  10. containerd-config.toml(5): the daemon configuration file itself - the version header, the plugins table, imports, and the state and root directories
  11. containerd source, cmd/containerd/command/config.go: the definition of `containerd config default`, `dump` and `migrate`. `migrate` and `dump` share one implementation, which is why the migrated file comes back fully populated with defaults instead of as a minimal diff
  12. containerd source, ctr deprecations: the subcommand is `deprecations` (plural), it takes --format json, and it sets CONTAINERD_SUPPRESS_DEPRECATION_WARNINGS while it runs. Some documentation writes it in the singular, which does not exist
  13. containerd issue 12612 - the CRI plugin fails to load when a version 2 config with registry.mirrors is migrated, because the result carries both config_path and mirrors: "`mirrors` cannot be set when `config_path` is provided". Reported against 2.2.0
  14. containerd pull request 12617 - the fix for the migration that emitted both config_path and mirrors. Worth checking against the exact patch release you are installing rather than assuming
  15. containerd - Plugins: the plugin model behind the renamed IDs, and the distinction between built-in, proxy and binary external plugins that matters if you still load Go plugin libraries from plugin_dir
  16. containerd - Ops: running the daemon, the systemd unit, the socket and state directories, and the configuration import mechanism
  17. containerd - Getting started: the officially supported installation route now that the cri-containerd bundles are gone, which is containerd, runc and the CNI plugins installed as three separate components
  18. containerd - Transfer service: the stable API that the CRI plugin uses for image pull by default from 2.1 onwards, and the reason a handful of registry settings now behave differently
  19. containerd - Snapshotters: overlayfs as the default and the replacement for the removed AUFS snapshotter, plus the blockfile, devmapper and erofs alternatives
  20. containerd - NRI, the Node Resource Interface: enabled by default from 2.0, which means access to the NRI socket is now part of your node's security surface whether or not you use it
  21. containerd - user namespaces in CRI: supported from 2.0 and requiring runc v1.2.0 or later, which is one of the reasons the runtime binary needs upgrading alongside the daemon
  22. containerd - CRI plugin architecture: how the kubelet, the CRI plugin, the snapshotters and the shims fit together, which is the mental model the renamed plugin IDs now reflect
  23. containerd - crictl: the CRI-level debugging tool, and the right way to confirm that the kubelet's view of the runtime matches yours
  24. containerd releases on GitHub: the binary tarballs, the checksums and the per-release notes. Also the place to confirm that the cri-containerd-(cni-)VERSION-OS-ARCH.tar.gz bundles really are gone rather than moved
  25. containerd - Security and audits: the project's security policy and advisory history, which is the argument for not staying on a branch that only accepts patches for someone else's managed service
  26. containerd pull request 8924 - the discussion behind removing the explicit LimitNOFILE from the reference systemd unit, including why hosts on systemd older than 240 must set it back by hand
  27. Kubernetes - Releases: the supported branches and their end-of-life dates. This is what turns containerd's extended-support footnotes into a dead end, because the Kubernetes versions they name are already out of support
  28. Kubernetes - Container runtimes: installing and configuring containerd for a cluster, including the cgroup driver requirement and the sandbox image setting
  29. Kubernetes - Container Runtime Interface: the API the kubelet speaks, and the reason the removal of CRI v1alpha2 in containerd 2.0 is a compatibility statement rather than an implementation detail
  30. Kubernetes - Configuring a cgroup driver: the kubelet side of the SystemdCgroup setting that has to be carried across when the containerd configuration is rewritten
  31. Kubernetes - Runtime Class: the resource that maps a pod onto one of the runtime handlers defined in the containerd configuration, which is the part of the config most likely to be hand-written and therefore most likely to be lost in a migration
  32. Kubernetes - User namespaces for pods: one of the capabilities that only exists once the node is on containerd 2.x with a recent enough runc
  33. Kubernetes - Pull an image from a private registry: the ImagePullSecrets mechanism that replaces the deprecated registry.auths block in the containerd configuration
  34. Kubernetes - Upgrading kubeadm clusters: the drain, upgrade, uncordon sequence this migration slots into, and the reminder that node components are upgraded one node at a time
  35. runc releases: the OCI runtime that has to be installed separately now that the combined containerd bundles are gone, and whose version gates CRI user namespaces
  36. CNI plugins releases: the third component of the install, previously bundled in cri-containerd-cni-*.tar.gz and now shipped on its own
  37. cri-tools: crictl and critest, the CRI-level client used throughout this article to verify that the runtime is answering on v1 and that images and pods survived the upgrade
  38. Container Device Interface: the specification behind enable_cdi and cdi_spec_dirs, both enabled by default from containerd 2.0
  39. Google Security Blog - learnings from the kCTF VRP: the exploit history that led to io_uring_enter, io_uring_register and io_uring_setup being dropped from containerd's default seccomp allowlist in 2.0
  40. OCI/Docker image manifest version 2, schema 2: the format that replaced the Docker schema 1 manifests whose pull support is disabled by default from containerd 2.0
  41. systemd.exec(5) - LimitNOFILE and the rest of the resource limits a unit inherits, which containers then inherit from containerd. Relevant because the reference unit stopped setting it explicitly

Was this useful?