Zum Inhalt springen
← Blog

containerd 1.x: die Zeit ist abgelaufen.

Der erweiterte Support für containerd 1.7 endet im September 2026 – und er galt ohnehin nur für Kubernetes-Versionen, die längst aus dem Support gefallen sind. Hier stehen die Umschreibung auf Konfigurationsversion 3, die Registry-Umstellung, die Cluster lahmlegt, und der einzige Branch, der als Ziel taugt.

·26 Min. Lesezeit
  • containerd
  • Kubernetes
  • Container
  • Linux

Es gibt eine Lesart dieser Migration, die nach Pflichtaufgabe klingt, und eine, die nach Frist klingt – und welche zutrifft, entscheidet eine Fußnote. Die Release-Tabelle von containerd führt den 1.7-Branch als LTS bis September 2026, was nach Luft klingt. Die Fußnote darunter stellt klar, dass dieser Support seit März 2026 von zwei namentlich genannten Maintainern geleistet wird und auf die Nutzung mit Kubernetes 1.32, 1.31 und 1.30 über Google Kubernetes Engine ausgerichtet ist – Änderungen, die dafür nicht gebraucht werden, können abgelehnt werden. Kubernetes 1.32 ist im Februar 2026 aus dem Support gefallen. Wer nicht ein nicht mehr unterstütztes Kubernetes auf GKE betreibt, für den war dieser Rettungsring nie gedacht.

Titelbild aus drei Feldern. Das linke Feld mit der Überschrift „containerd 1.x“ zeigt einen einzigen Konfigurationsblock namens io.containerd.grpc.v1.cri, der Runtimes, Snapshotter, sandbox_image und eine Registry-Mirror-Tabelle enthält, versehen mit einem roten End-of-Life-Stempel: „1.7 – erweiterter Support endet im September 2026“. Die mittlere Spalte mit der Überschrift „was sich verschiebt“ listet fünf Umstellungen auf: von Version 2 auf Version 3, io.containerd.grpc.v1.cri aufgeteilt in cri.v1.runtime und cri.v1.images, sandbox_image wird zu pinned_images.sandbox, registry.mirrors wird zu einem certs.d-Verzeichnis mit config_path, und die cri-containerd-Pakete werden zu drei getrennten Installationen. Das rechte Feld mit der Überschrift „containerd 2.3 LTS“ zeigt zwei Konfigurationsblöcke nebeneinander, einen für die Runtime und einen für Images, darüber ein Support-Balken bis April 2028.
Was sich tatsächlich bewegt: eine Konfigurationsversion, ein in zwei Teile zerlegtes Plugin, eine umbenannte Einstellung, ein durch einen Verzeichnisbaum ersetzter Registry-Block und ein durch drei getrennte ersetzter Sammel-Download.

Das ist also eine Migration mit einem echten Datum, und sie verdient mehr als eine hochgezählte Versionsnummer im Playbook. Was folgt, ist das Ganze: wie man die Release-Politik von containerd liest, ohne sich vom Wort LTS täuschen zu lassen, warum 2.1 der denkbar schlechteste Landeplatz ist, die Umschreibung der Konfiguration von Version 2 auf Version 3 mit den verschobenen Plugin-IDs, die Registry-Umstellung mit ihrem eigenen Bug und ihrer eigenen Ausfallklasse, der Image-Pull-Pfad, der sich in 2.1 leise geändert hat, alles, was 2.0 entfernt hat, ein Runbook pro Knoten, eine ehrliche Darstellung dessen, was ein Rollback wiederherstellen kann und was nicht, und ein Prüfskript, das bei Fehlern einen Rückgabewert ungleich null liefert.

Die Fehlerbilder – und warum keines nach containerd klingt

Keines dieser Fehlerbilder meldet sich als Problem der Runtime-Version, und genau deshalb wird es spät diagnostiziert. Ein Knoten kommt nach einem Upgrade zurück, und das CRI-Plugin ist schlicht nicht da: der Daemon läuft, systemctl status ist grün, und jeder Pod auf dem Knoten hängt in ContainerCreating. Ein Image, das vier Jahre lang aus dem internen Mirror kam, kommt plötzlich von Docker Hub, und das Erste, was auffällt, ist die Rechnung für ausgehenden Traffic. Eine RuntimeClass, die ein Team vor zwei Jahren von Hand angelegt hat, löst nicht mehr auf, und nur die Workloads scheitern, die sie nutzen. In jedem Fall läuft die Runtime – und die Runtime ist falsch.[ctrrel]

Was Sie sehenWas es meist bedeutetWo es behandelt wird
Alle Pods auf einem Knoten hängen in ContainerCreating, der Daemon ist gesundDas CRI-Plugin hat nicht geladen. containerd startet trotzdem und meldet den Fehler nur im eigenen LogRegistries
Images kommen plötzlich aus der öffentlichen Registry statt aus dem internen MirrorDer Block registry.mirrors hat die Umschreibung auf config_path nicht überlebtRegistries
Der Pause-Container zieht auf einem Knoten ohne Internetzugang von registry.k8s.iosandbox_image wurde nicht nach pinned_images.sandbox übernommenPlugin-Teilung
Nur die gVisor- oder Kata-Workloads scheitern, alles andere läuftEin von einer RuntimeClass genannter Runtime-Handler fehlt in der neuen KonfigurationKubernetes
Ein altes Image, das letzte Woche noch kam, scheitert jetzt mit einem Manifest-FehlerDer Pull von Docker-Schema-1-Images: in containerd 2.0 standardmäßig deaktiviert, in 2.1 ganz entferntEntfernungen
Image-Pulls verhalten sich anders nach einer Änderung, die niemand mit Pulls verbandEine Einstellung, die der Transfer Service nicht abbilden kann, hat den Knoten auf lokalen Pull zurückgestelltImage-Pull
Der Daemon protokolliert bei jedem Start „Configuration migrated from version 2“Die Datei wurde nie umgeschrieben. Die Kompatibilitätsschicht trägt sie – und genau dort sitzt der Registry-BugKonfiguration

Der gemeinsame Faden ist, dass containerd 2.x eine Konfigurationsdatei der Version 2 bewusst duldet: es liest sie, konvertiert sie im Speicher und startet. Beim Upgrade ist das eine Freundlichkeit, danach eine Last, denn es bedeutet, dass die Migration unbegrenzt halbfertig bleiben kann, ohne dass irgendetwas den Abschluss erzwingt. Der Daemon protokolliert seine Einwände und startet trotzdem; das Plugin, das nicht geladen hat, meldet einen Fehlerstatus, den Ihnen niemand zeigt. Dieser ganze Artikel ist in gewisser Weise ein Plädoyer dafür, die Migration abzuschließen, statt sie von der Kompatibilitätsschicht tragen zu lassen.[ctr20]

Die Support-Tabelle richtig lesen

Beginnen Sie mit der Release-Tabelle, denn sie ist das einzige Dokument, das etwas entscheidet, und sie wird regelmäßig falsch gelesen. containerd führt zwei Arten von Branch. Ein reguläres Release wird acht Monate lang unterstützt. Ein Release pro Jahr wird als LTS ausgewiesen und mindestens zwei Jahre lang unterstützt. Darüber hinaus können einzelne Branches nach Ablauf des allgemeinen Fensters erweiterten Support von namentlich genannten Maintainern erhalten – und das ist etwas anderes, das dasselbe Etikett in derselben Spalte trägt.[ctrrel]

BranchStatusEnd of LifeWas das für Sie bedeutet
1.6End of Life23. August 2025Seit einem Jahr ohne Support. Es kommt nichts mehr, auch keine Sicherheitsfixes
1.7LTS, erweitertSeptember 2026Nur erweiterter Support, von zwei namentlich genannten Maintainern, zugeschnitten auf Kubernetes 1.30–1.32 auf GKE
2.0LTS, erweitertMärz 2027Dieselbe Form: erweiterter Support, zugeschnitten auf Kubernetes 1.33 auf GKE, das selbst End of Life ist
2.1End of Life3. Juli 2026Bereits vorbei. Die Version, auf die viele zuerst aktualisiert haben, und der schlechteste Halteplatz
2.2Aktiv6. November 2026Erhält Patches, aber nur noch zehn Wochen Vorlauf. Als Zwischenstation brauchbar, als Ziel falsch
2.3LTS30. April 2028Das Ziel. Aktueller Langzeit-Branch mit fast zwei Jahren Support vor sich
2.4Zukünftigvoraussichtlich April 2027Ein reguläres Achtmonats-Release. Kein Ersatz für den LTS-Branch

Legen Sie nun die Kubernetes-Support-Matrix darüber, denn dort treffen sich die beiden Projekte. containerd veröffentlicht je Kubernetes-Minor-Release eine Liste empfohlener Versionen. Für Kubernetes 1.36 lautet sie 2.3.0+, 2.2.0+ – und es gibt überhaupt keinen 1.x-Eintrag. Nichts im kubelet erzwingt das: eine nicht unterstützte Kombination startet, läuft und wirkt in Ordnung, bis sie es nicht mehr ist, und dann debuggen Sie allein. Die Matrix ist eine Aussage darüber, was getestet wurde, und dieses Testen ist das Einzige, was zwischen Ihnen und einem Runtime-Bug steht, den sonst niemand gesehen hat.[k8srel]

KubernetesAls empfohlen gelistete containerd-VersionenEnd of Life von KubernetesLesart
1.332.1.0+, 2.0.4+, 1.7.24+, 1.6.36+28. Juni 2026Bereits ohne Support. Genau diese Paarung nennt die Verlängerung von containerd 2.0
1.342.1.3+, 2.0.6+, 1.7.28+, 1.6.39+27. Oktober 2026Zwei Monate übrig, und zwei der vier containerd-Optionen – 2.1 und 1.6 – sind selbst End of Life
1.352.2.0+, 2.1.5+, 1.7.28+28. Februar 2027Die letzte Zeile, in der überhaupt noch eine 1.x-Runtime auftaucht
1.362.3.0+, 2.2.0+28. Juni 2027Kein 1.x-Eintrag. Hier hört die Migration auf, optional zu sein

Die Falle in der Falle heißt containerd 2.1. Für alle, die in der zweiten Jahreshälfte 2025 aktualisiert haben, war das der naheliegende Landeplatz, in vielen internen Dokumenten steht es bis heute – und es ist am 3. Juli 2026 aus dem Support gefallen: vor 2.2, das bis November 2026 läuft, und lange vor 2.3, dem aktuellen LTS-Branch mit Support bis April 2028. „Auf 2.x wechseln“ ist kein Plan. Aus dem Stand gibt es genau einen sinnvollen Ziel-Branch, und das ist 2.3.

Was auf diesen Knoten tatsächlich installiert ist

Bevor Sie irgendetwas anfassen, stellen Sie fest, was tatsächlich installiert ist – in einer Flotte jeder Größe ist die Antwort nämlich nicht eine Version. Drei Dinge zählen, und sie sind voneinander unabhängig: die Version des Daemons, die Version der Konfigurationsdatei und die CRI-API-Version, die dem kubelet tatsächlich geliefert wird. Die Konfigurationsversion wird vergessen, und sie ist die, die fehlen kann: eine Datei ohne version-Zeile gilt als Datei der Version 1. An dieser Stelle widersprechen sich die Dokumente des Projekts, und es lohnt sich zu wissen, in welche Richtung: der CRI-Konfigurationsleitfaden schreibt, Version 1 sei in containerd 2.0 entfernt worden, während RELEASES.md festhält, eine fehlende Versionsangabe werde als Version 1 gelesen und alle früheren Versionen würden per Migration unterstützt – und im Quelltext liegt weiterhin eine v1-Migrationsfunktion. Behandeln Sie eine Version-1-Datei als etwas, das man beim ersten Anblick repariert, und nicht als etwas, über das sich verlässlich argumentieren lässt.[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

Fragen Sie danach den Daemon, was er Ihnen seit Langem sagen will. Seit 1.6.27 und 1.7.12 stellt containerd Veraltungshinweise über die Introspection-API bereit, ausdrücklich damit diese Migration geplant und nicht entdeckt werden muss. Der Unterbefehl heißt ctr deprecations list – im Plural, was der Erwähnung wert ist, weil mindestens ein offizielles Dokument ihn im Singular schreibt und diese Form nicht existiert. Führen Sie ihn mit --format json über die Flotte aus. Er ist kein Gesundheitszeugnis, denn die Hinweise entstehen bei Nutzung: ein Knoten, der seit dem letzten Neustart kein Schema-1-Image gezogen hat, meldet auch keines. Er ist die Anfangsliste dessen, wovon Sie bereits wissen, dass es falsch ist.[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

Die Konfigurationsdatei, von Version 2 auf Version 3 – und jetzt 4

Die Konfigurationsdatei ist der Kern der Migration, und als Erstes ist zu klären, was „die neueste Version“ überhaupt heißt, denn das hat sich dieses Jahr verschoben. Version 3 kam mit containerd 2.0 und ist die Version, die das CRI-Plugin in zwei geteilt hat. Version 4 kam mit 2.3 und tut etwas anderes – davon handelt der nächste Abschnitt. Version 2 wird nach wie vor bei jedem Start gelesen und im Speicher konvertiert, und der Daemon schreibt dazu eine Zeile ins Log: das ist der billigste Weg herauszufinden, ob ein Knoten tatsächlich migriert wurde oder nur geduldet wird. Der Daemon bringt einen Konverter mit, containerd config migrate, der Ihre aktuelle Datei liest und die neueste Version auf die Standardausgabe schreibt. In der Manpage steht er nicht – containerd-config(8) dokumentiert nur default –, und das erklärt zu großen Teilen, warum ihn so wenige kennen.[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

Zwei Dinge über diesen Konverter sollten Sie wissen, bevor Sie seine Ausgabe über die laufende Konfiguration schreiben. Erstens: migrate und dump sind derselbe Codepfad, deshalb kommt die vollständig ausgefüllte Konfiguration zurück – jeder Standardwert des Daemons explizit ausgeschrieben. Aus vierzig Zeilen werden dreihundert, und jeder Standardwert, den Sie nie gewählt haben, ist nun in Ihrer Datei festgeschrieben und folgt Upstream nicht mehr, wenn er sich ändert. Nutzen Sie die Ausgabe, um die neuen Schlüsselnamen zu lernen, und schreiben Sie die kurze Fassung von Hand. Zweitens: Prüfen Sie die Kandidatendatei, bevor irgendetwas neu startet – aber beachten Sie, dass --config ein globales Flag ist und keines des Unterbefehls. Es steht also vor config dump: containerd --config <datei> config dump. Hinten angehängt ergibt es einen Aufruffehler statt einer Prüfung. Richtig ausgeführt lädt der Befehl die Datei, die der Daemon laden würde, folgt den imports und scheitert laut an etwas, das er nicht parsen kann – ein deutlich besserer Ort für einen Tippfehler als ein Knoten, der nicht zurückkommt.[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 verdient einen eigenen Blick, weil fast jede Darstellung dieser Migration bei Version 3 aufhört und weil sie eine Einschränkung fürs Rollback mitbringt. An CRI ändert sie nichts: sie holt die Sockets des Daemons selbst aus den Top-Level-Tabellen [grpc], [ttrpc], [metrics] und [debug] heraus und verlegt sie in Plugins unter io.containerd.server.v1.*. Daraus folgen zwei Dinge. Das verhaltensrelevante: vor Version 4 wurde eine nicht gesetzte ttrpc-Adresse aus der gRPC-Adresse als <gRPC-Adresse>.ttrpc abgeleitet und erbte deren uid und gid, während das ttrpc-Plugin in Version 4 eigenständig ist und auf seinen eigenen Standard zurückfällt – alles, was bei Ihnen diesen Socket über den Pfad anspricht, sollte ihn jetzt ausdrücklich setzen. Das betriebliche ist die Warnung von Upstream selbst: eine Datei auf die neueste Version zu migrieren schränkt ein, welche containerd-Versionen sie überhaupt noch lesen können. Eine Version-4-Datei braucht 2.3.0 oder neuer, eine Version-3-Datei lesen 2.0 und alles danach. Wenn ein Binary-Rollback in derselben Nacht zu Ihrem Plan gehört, schreiben Sie Version 3 – die Plugin-Teilung, also den Teil, auf den es ankommt, bekommen Sie trotzdem.[cfgver][srvmig]

Aus einem Plugin wurden zwei – und die Einstellungen wanderten mit

Die strukturelle Änderung ist, dass das eine CRI-Plugin in zwei geteilt wurde. io.containerd.grpc.v1.cri enthielt früher alles; in Version 3 enthält es nur noch die Optionen des Streaming-Servers, und der Inhalt liegt unter zwei neuen IDs: io.containerd.cri.v1.runtime für alles rund um das Ausführen von Containern – Runtimes, CNI, Sandboxes, SELinux, OOM-Verhalten – und io.containerd.cri.v1.images für alles rund um Images: Snapshotter, Registry, das fixierte Sandbox-Image, die Download-Parallelität. Die Aufteilung ist besser als die vorige, und sie bedeutet, dass ein mechanisches Suchen-und-Ersetzen der Plugin-ID etwa die Hälfte Ihrer Einstellungen in die falsche Tabelle legt.[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)Anmerkung
version = 2version = 3 (2.0) / version = 4 (2.3)Version 2 wird weiter gelesen und im Speicher konvertiert. Eine Version-4-Datei braucht 2.3.0 oder neuer
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.cri.v1.runtime'Alles zum Ausführen von Containern: Runtimes, CNI, SELinux, OOM, Sandboxes
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.cri.v1.images'Alles zu Images: Snapshotter, Registry, fixierte Images, Pull-Einstellungen
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.grpc.v1.cri'Existiert weiter, aber nur für die Optionen des Streaming-Servers
sandbox_image = "…"pinned_images.sandbox = '…'Umbenannt und verschoben. Fehlt es, greift ein abgeschotteter Knoten nach registry.k8s.io
…cri".containerd.snapshotter…cri.v1.images'.snapshotterWechselt die Seite, von der Runtime zu den Images
…cri".containerd.runtimes.*…cri.v1.runtime'.containerd.runtimes.*Nur der Pfad ändert sich. runtime_type = io.containerd.runc.v2 bleibt
…cri".registry.mirrors…cri.v1.images'.registry.config_pathAnderer Mechanismus. Ein Verzeichnis mit hosts.toml-Dateien, keine Tabelle. Entfernung in 2.4 vorgesehen
…cri".registry.auths— (imagePullSecrets)Absichtlich kein Ersatz. Zugangsdaten wandern in den Cluster. Entfernung in 2.4 vorgesehen
…cri".cni.bin_dir…cri.v1.runtime'.cni.bin_dirsPlural, und eine Liste. Veraltet seit 2.1, Entfernung in 2.4 vorgesehen
plugin_dir (Go-Plugin .so)— (Proxy- oder Binary-Plugins)Bereits entfernt in 2.1, nicht nur veraltet

Zwei Umbenennungen innerhalb dieser Teilung richten den meisten Schaden an. Aus sandbox_image wurde pinned_images.sandbox, sodass ein Cluster, dessen Pause-Image auf einen internen Mirror zeigte, stillschweigend auf registry.k8s.io zurückfällt – unproblematisch bis zu dem Tag, an dem der Knoten keinen ausgehenden Zugriff hat. Und snapshotter ist von der Runtime-Seite auf die Image-Seite gewandert, was unintuitiv genug ist, um es zu prüfen statt anzunehmen. Alles andere in der folgenden Tabelle ist eine Pfadänderung, keine Verhaltensänderung.[ctrplug]

Registries: der Teil, der Cluster lahmlegt

Bei der Registry-Konfiguration wird diese Migration von mühsam zu riskant, und sie hat ein Datum: mirrors und configs gelten seit containerd 1.5 als veraltet, auths seit 1.3, und für alle drei ist die Entfernung in containerd 2.4 vorgesehen – also im Release nach dem, zu dem dieser Artikel rät. Der Ersatz für die ersten beiden ist ein Verzeichnisbaum: ein Unterverzeichnis je Registry-Host-Namensraum unter einem einzigen config_path, jeweils mit einer hosts.toml. Das sind mehr Dateien und deutlich weniger Magie, und es ist ehrlich besser – Hosts werden in Reihenfolge probiert, Capabilities sind explizit, und eine CA je Registry ist eine Zeile in einer Datei statt eines Sonderfalls. Die dritte Eigenschaft, auths, hat absichtlich keinen Datei-Ersatz: Zugangsdaten gehören in ein Kubernetes-Image-Pull-Secret, nicht in eine Knotenkonfiguration, die jeder Workload auf dem Knoten erbt.[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'

Den Bug sollte man präzise benennen, denn die ungenaue Fassung schickt einen an der falschen Stelle auf die Suche. Er setzt nicht voraus, dass Sie irgendetwas Ungewöhnliches geschrieben haben. Starten Sie containerd 2.2.0 mit einer gewöhnlichen Version-2-Datei, die einen registry.mirrors-Block enthält und sonst nichts, dann stellt die Migration im Speicher den Standard-config_path daneben – und das CRI-Plugin lehnt diese Kombination rundheraus ab: `mirrors` cannot be set when `config_path` is provided. Das Plugin lädt nicht. Der Daemon startet trotzdem. Jeder Pod, der danach auf diesen Knoten geplant wird, scheitert beim Erstellen, während der Top-Level-Status jedes Dienstes auf der Maschine grün bleibt. Die eigene Datei nach beiden Schlüsseln zu durchsuchen findet nichts, weil Sie einen davon nie geschrieben haben. Gemeldet wurde das gegen 2.2.0 und behoben in Pull Request 12617, der vor 2.3.0 landete und in den 2.2-Branch zurückportiert wurde – auf einem aktuellen 2.3 oder einem frischen 2.2-Patch sind Sie also nicht betroffen, auf 2.0 oder 2.1 schon. Die Anweisung, die in beiden Fällen gilt, lautet: Schließen Sie die Umstellung ab. Bauen Sie den certs.d-Baum, lassen Sie config_path darauf zeigen und löschen Sie den mirrors-Block, damit keine Migration etwas raten muss.[iss12612][pr12617]

Der Image-Pull-Pfad hat sich unter Ihnen geändert

Ab containerd 2.1 zieht das CRI-Plugin Images über den Transfer Service statt im eigenen Prozess. Das ist ein Standard und keine Option, die Sie eingeschaltet hätten, und für sich genommen unauffällig. Einen eigenen Abschnitt verdient es wegen des Rückfalls. Enthält die CRI-Image-Konfiguration etwas, das der Transfer Service nicht abbilden kann, setzt containerd use_local_image_pull für den gesamten Knoten und protokolliert eine Warnung. Es scheitert nicht, es sagt es Ihnen nicht im Moment der Nutzung, und es sagt es dem Cluster nicht.[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
EinstellungLokaler PullTransfer Service (Standard ab 2.1)
snapshotterUnterstütztUnterstützt
ImagePullProgressTimeoutUnterstütztUnterstützt
PinnedImagesUnterstütztUnterstützt
Registry.Mirrors / Configs / AuthsUnterstützt (alle veraltet)Nicht unterstützt – löst den Rückfall auf lokalen Pull aus
MaxConcurrentDownloadsWird aus der CRI-Image-Konfiguration gelesenMuss nach plugins.'io.containerd.transfer.v1.local' verschoben werden; jeder Wert außer 3 löst den Rückfall aus
DiscardUnpackedLayersUnterstütztNicht unterstützt – löst den Rückfall aus
ImagePullWithSyncFsUnterstütztNicht unterstützt – löst den Rückfall aus
DisableSnapshotAnnotationsUnterstütztStattdessen im Snapshotter-Plugin konfigurieren; false löst den Rückfall aus

Lesen Sie die Auslöserliste einmal, und die Folge liegt auf der Hand: max_concurrent_downloads von 3 auf 6 zu erhöhen – auf einem Knoten mit dicker Leitung eine gewöhnliche, vernünftige Maßnahme – verschiebt jeden Image-Pull auf diesem Knoten auf einen anderen Codepfad. Dasselbe gilt, wenn Sie den veralteten mirrors-Block behalten, was ein zweiter Grund ist, die Registry-Umstellung abzuschließen statt liegen zu lassen. Wenn Sie den lokalen Pull wollen, setzen Sie use_local_image_pull = true und stehen Sie dazu. Wenn Sie den Transfer Service wollen, verschieben Sie die Parallelitätseinstellung nach [plugins.'io.containerd.transfer.v1.local'], wo sie tatsächlich gelesen wird.[cricfg]

Was tatsächlich entfernt wurde

Nun die Entfernungen, also der Teil, der wirklich weg und nicht nur umbenannt ist. Die Liste ist kurz, und alles darauf hat einen dokumentierten Ersatz – lesen Sie aber die Versionsnummern statt der Zusammenfassungen: das Übergangsdokument zu containerd 2.0 und RELEASES.md widersprechen sich an zwei Stellen, und beide Male ist es das Übergangsdokument, das zitiert wird. Der wichtige Fall ist der Pull von Docker-Schema-1-Images: in 2.0 deaktiviert, wo eine Umgebungsvariable ihn zurückholte, und in 2.1 entfernt, wo nichts mehr hilft. Da das Ziel hier 2.3 heißt, gilt er schlicht als weg. Das zählt, denn die Images, die noch in Schema 1 vorliegen, sind per Definition Images, die seit etwa 2017 niemand neu gebaut hat, weshalb auch niemand das Dockerfile hat. Suchen Sie sie vor dem Upgrade, nicht danach: seit 1.7.8 und 1.6.25 tragen konvertierte Images ein Label, über das sie auffindbar sind.[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
FunktionVeraltet seitEntfernt inWas stattdessen
Runtime V1, io.containerd.runtime.v1.linux1.42.0io.containerd.runc.v2
Runc V1, io.containerd.runc.v11.42.0io.containerd.runc.v2
Eingebauter AUFS-Snapshotter1.52.0overlayfs
Label containerd.io/restart.logpath1.52.0containerd.io/restart.loguri
Pakete cri-containerd-*.tar.gz1.62.0containerd, runc und die CNI-Plugins getrennt installieren
CRI-API v1alpha21.72.0Nur CRI v1. Prüfen, dass crictl version RuntimeApiVersion: v1 meldet
Alte CRI-podsandbox-Implementierung2.02.0Der Sandbox-Controller, der ohnehin der Standard ist
Pull von Docker-Schema-1-Images1.72.1 (in 2.0 deaktiviert)In Schema 2 / OCI neu bauen. Der Notausgang über die Umgebungsvariable wirkt ab 2.1 nicht mehr
Runtime-Plugins als Go-Plugin-Bibliothek (*.so)2.02.1Externe Plugins: Proxy oder Binary
Explizites LimitNOFILE in der Referenz-Unit2.0systemd-Standard nutzen; unterhalb von systemd 240 1024:524288 von Hand setzen
io_uring_* im seccomp-Standardprofil2.0Ein explizites seccomp-Profil – und ein Gespräch darüber, ob Sie eines wollen

Eine Entfernung ist leiser als die übrigen und verdient eine ausdrückliche Erwähnung. Die Referenz-Unit containerd.service setzt LimitNOFILE nicht mehr explizit. Die rlimits von containerd werden von den gestarteten Containern geerbt, das ist also keine reine Daemon-Einstellung: ab systemd 240 ist der Standard vernünftig und die Sache belanglos, darunter greift der Kernel-Standard von 4096, und jeder Container auf dem Host erbt ihn. Die Empfehlung von Upstream für solche Hosts lautet, LimitNOFILE=1024:524288 von Hand wieder zu setzen.[pr8924][sdexec]

Standardwerte, die sich ungefragt gedreht haben

Unabhängig von den Entfernungen haben mehrere Standardwerte ihren Wert gewechselt. Das sind die, wegen derer sich ein Knoten nach einem Upgrade anders verhält, bei dem Sie keine einzige Einstellung angefasst haben – sie verdienen eine bewusste Entscheidung statt stillschweigender Übernahme.[ctr20]

Standardwertcontainerd 1.xcontainerd 2.xWarum es zählt
enable_unprivileged_portsfalsetrueContainer binden unterhalb von 1024 ohne CAP_NET_BIND_SERVICE
enable_unprivileged_icmpfalsetrueping funktioniert ohne CAP_NET_RAW
enable_cdiaustrueSpec-Dateien unter /etc/cdi und /var/run/cdi beschreiben Gerätezugriff. Der Schalter selbst ist seit 2.2 veraltet und in 2.4 weg
NRIdeaktiviertaktiviertDer NRI-Socket wird Teil der Angriffsfläche des Knotens
Sandbox-CRIalter CRI-ServerSandbox-ControllerBei runc unsichtbar, bei Kata und gVisor testenswert
Image-Pull-Pfadim ProzessTransfer Service (ab 2.1)Fällt bei mehreren Einstellungen still auf lokalen Pull zurück
io_uring_*-SyscallserlaubtblockiertNach wiederholten Kernel-Exploits aus der seccomp-Standardliste gestrichen
Sandbox-Imagesandbox_imagepinned_images.sandboxGleicher Wert, anderer Schlüssel. Bei der Umschreibung leicht zu verlieren
  • Unprivilegierte Ports und ICMP sind an. Das CRI-Plugin setzt jetzt net.ipv4.ip_unprivileged_port_start=0 und net.ipv4.ping_group_range=0 2147483647 für Container, die weder den Netzwerk-Namensraum des Hosts noch User-Namespaces nutzen. Ein Bind unterhalb von Port 1024 braucht kein CAP_NET_BIND_SERVICE mehr, und ping kein CAP_NET_RAW. Bequem – und eine Änderung Ihrer Container-Sicherheitslage: enable_unprivileged_ports und enable_unprivileged_icmp auf false zu setzen stellt das alte Verhalten wieder her.
  • NRI ist aktiviert. Das Node Resource Interface erlaubt Plugins, Container beim Erstellen zu verändern. Der Zugriff wird über den Zugriff auf den systemweiten NRI-Socket gesteuert – dieser Socket gehört damit zur Angriffsfläche Ihres Knotens, ob Sie ein einziges NRI-Plugin betreiben oder keines.
  • CDI ist aktiviert, und der Schalter dafür verschwindet. Das Container Device Interface ist an, mit cdi_spec_dirs standardmäßig auf /etc/cdi und /var/run/cdi. Alles, was eine Spec-Datei in diese Verzeichnisse schreiben kann, kann Gerätezugriff für Container beschreiben. Beachten Sie dabei, dass enable_cdi selbst in containerd 2.2 als veraltet markiert wurde und in 2.4 entfernt werden soll – danach ist CDI schlicht immer an. Wenn Ihr Plan war, es abzuschalten, hat dieser Plan ein Ablaufdatum.
  • io_uring steht nicht mehr auf der seccomp-Standardliste. io_uring_enter, io_uring_register und io_uring_setup wurden gestrichen, nachdem eine ausreichend lange Serie von Kernel-Exploits das Projekt zu dem Schluss brachte, sie standardmäßig nicht mehr zuzulassen. Ein auf io_uring gebauter Workload braucht ein explizites Profil – und ein Gespräch darüber, ob er es haben sollte.
  • Die Sandbox-Implementierung von CRI ist Standard. Das CRI-Plugin nutzt den stabilen Sandbox-Controller statt des alten CRI-Servers. Im Normalbetrieb ist das unsichtbar und bei einer Sandbox-Runtime wie Kata oder gVisor sehr sichtbar – genau der Fall, den man vor dem Flotten-Rollout testen sollte.

Keiner dieser Punkte ist ein Grund, nicht zu aktualisieren. Sie sind ein Grund, einen Knoten zu aktualisieren, ihn anzuschauen und die Änderung erst danach in den Image-Build zu schreiben – das ist der Unterschied zwischen einer Migration und einer Überraschung in Flottengröße.[ctrnri][cdi]

Das Knoten-Upgrade, der Reihe nach

Der mechanische Teil ist kurz, und es hat sich eher geändert, wie er gemacht wird, als was er tut. Die kombinierten Pakete cri-containerd-cni-VERSION-OS-ARCH.tar.gz wurden in 2.0 entfernt, also sind containerd, runc und die CNI-Plugins nun drei getrennte Installationen mit drei getrennten Versionsentscheidungen. Das ist expliziter und etwas mehr Arbeit, und es beseitigt eine langjährige Verwechslungsquelle, bei der Leute containerd aktualisierten und dabei unbemerkt runc mit aktualisierten. Eine Beruhigung zum Sprung selbst: containerd unterstützt Upgrades zwischen aufeinanderfolgenden Minor-Releases und, davon getrennt, direkte Upgrades zwischen aufeinanderfolgenden LTS-Releases – und 1.7 (LTS) auf 2.3 (LTS) ist genau das Beispiel, das das eigene Release-Dokument nennt. Sie überspringen nichts, auf dem Sie hätten landen müssen.[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

Zwei Hinweise zur Reihenfolge, die man unter Zeitdruck leicht falsch macht. Sichern Sie /var/lib/containerd, bevor Sie anfangen – nicht weil das Upgrade es wahrscheinlich beschädigt, sondern weil es der Image- und Snapshot-Speicher ist: es zu verlieren bedeutet, jedes Image auf dem Knoten neu zu ziehen, was auf einem großen Knoten in Dutzenden von Minuten und viel ausgehendem Traffic gemessen wird. Und bringen Sie die Runtime vor dem kubelet hoch und prüfen Sie ctr plugins ls auf alles, was nicht im Zustand ok ist, bevor Sie das kubelet überhaupt starten. Ein CRI-Plugin, das nicht geladen hat, ist ein Knoten, der Pods annimmt und sie nicht erstellen kann.[runc][cni]

Dieselbe Änderung von der Kubernetes-Seite

Von der Cluster-Seite ist nichts zu tun und eines zu prüfen. Die Support-Matrix ist ein Projektdokument und keine Zulassungsrichtlinie: kein kubelet verweigert den Start gegen ein nicht unterstütztes containerd, und irgendwo ein Event erscheint auch nicht. Die Prüfung muss also ausdrücklich erfolgen – die Knoten durchgehen, containerRuntimeVersion lesen und mit der Matrix für die kubelet-Version desselben Knotens abgleichen. Gemischte Node-Pools sind der Normalfall und nicht die Ausnahme, besonders dort, wo Node-Images ihrem eigenen Takt folgen.[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'

Die Einstellung, die bei dieser Migration am häufigsten verloren geht, ist der Runtime-Handler – weil es die Einstellung ist, die am häufigsten von Hand hinzugefügt wurde. Jede RuntimeClass im Cluster nennt einen Handler, den es in der umgeschriebenen Konfiguration geben muss; gibt es ihn nicht, scheitern nur die Workloads, die ihn anfordern, der Fehler bleibt also auf das Team beschränkt, das gVisor oder Kata nutzte, und sonst merkt es eine Woche lang niemand. Prüfen Sie sie namentlich gegen die neue Datei. Dasselbe gilt für den cgroup-Treiber, und hier stimmt die beruhigende Fassung der Geschichte nicht ganz: Kubernetes 1.28 hat die Möglichkeit ergänzt, dass das kubelet die CRI-Runtime nach dem verwendeten Treiber fragt – allerdings als Alpha-Funktion hinter dem Feature-Gate KubeletCgroupDriverFromCRI, und die containerd-Seite davon braucht 2.0 oder neuer. Automatisch ist das also nicht, SystemdCgroup = true muss die Umschreibung nach wie vor überleben – und liegt jetzt an einem anderen Pfad.[k8srtc][k8skubeadm]

Zurückrollen – und was sich nicht zurückrollen lässt

Das Rollback verdient eine klare statt einer beruhigenden Antwort. Die Binaries und die Konfigurationsdatei lassen sich sauber zurückrollen – beides sind Dateien auf der Platte, und wenn Sie den alten Tarball und die alte config.toml aufgehoben haben, sind Sie zehn Minuten von Ihrem Ausgangspunkt entfernt. Das ist ehrlich mehr, als die meisten Migrationen bieten.[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.

Zwei Dinge lassen sich nicht so leicht zurückrollen. Das erste ist das Zustandsverzeichnis, und die ehrliche Formulierung lautet: niemand verspricht, dass es geht. Das Stabilitätsdokument von containerd stellt Dateisystemlayout, Speicherformate und Snapshot-Formate ausdrücklich außerhalb seiner Zusagen und hält fest, dass das Projekt diese Formate zwischen Minor-Versionen migrieren darf. Ein Downgrade gegen ein /var/lib/containerd, in das 2.x bereits geschrieben hat, ist damit undefiniert und nicht etwa als kaputt dokumentiert – ein Unterschied, den man um drei Uhr morgens lieber nicht ausprobiert: Stellen Sie den Tarball wieder her oder nehmen Sie in Kauf, jedes Image neu zu ziehen. Das zweite ist die Konfigurationsdatei selbst, sofern Sie sie bereits migriert haben: eine Version-4-Datei braucht containerd 2.3.0 oder neuer, um überhaupt gelesen zu werden. Und der größere Punkt gehört unumwunden gesagt: Ein Rollback kauft Ihnen eine Nacht, kein Quartal. Der 1.7-Branch verlässt den erweiterten Support im September 2026 und erhält bis dahin keine Patches für irgendetwas außerhalb des Managed Service eines Anbieters. Ein Rollback ist eine Art, ein missratenes Wartungsfenster sauber zu beenden, keine Art, die Entscheidung zu vertagen.[ctrsec]

Nachweisen statt hoffen

Die Prüfung ist keine Geschmacksfrage, und bei dieser Migration hat sie eine besondere Gestalt: fast alles, was schiefgeht, lässt den Daemon laufen. Zu prüfen, dass containerd oben ist, beweist also gar nichts. Das folgende Skript prüft die Dinge, die still falsch sein können – die Konfigurationsversion, ob ein Plugin im Fehlerzustand ist, ob CRI auf v1 antwortet, ob der cgroup-Treiber und die Runtime-Handler überlebt haben – und tut dann die beiden Dinge, die sich durch Hinsehen überhaupt nicht feststellen lassen.[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" 

Diese beiden sind ein echter Pull und ein echter Container. Eine hosts.toml in einem Verzeichnis, dessen Name nicht exakt dem Registry-Host-Namensraum entspricht, sieht völlig korrekt aus und greift schlicht nie – und kein noch so aufmerksames Lesen der Datei zeigt Ihnen das; nur ein Pull tut es. Und eine Konfiguration kann in jeder Hinsicht gültig sein und trotzdem keinen Container starten, weil das Runtime-Binary die falsche Version hat oder nicht an dem Pfad liegt, den der Shim erwartet. Führen Sie beides auf dem ersten Knoten aus, bevor Sie zum zweiten gehen.[critools]

Die Reihenfolge, in der das gemacht wird

Verdichtet ist die Entscheidung kleiner als der Artikel. Es gibt eine Zielversion, und das ist 2.3 – der aktuelle LTS-Branch, unterstützt bis April 2028. Alles andere in der Tabelle ist entweder schon aus dem Support, binnen Monaten aus dem Support, oder ein erweiterter Support-Branch, der auf den Managed Service eines anderen zugeschnitten ist. Die Arbeit ist nicht der Binary-Tausch, der fünfzehn Minuten dauert; die Arbeit ist die Umschreibung der Konfiguration und darin die Registry-Umstellung.[ctrrel]

Wenn Ihre Lage ist …dann ist das Ziel …und die Arbeit ist …
containerd 1.7 auf Kubernetes 1.34 oder 1.352.3 LTS, in einem SprungDie vollständige Umschreibung der Konfiguration. LTS auf LTS ist ein ausdrücklich unterstützter Sprung, und es gibt keinen Grund, bei 2.2 zu halten
containerd 1.6, irgendwo2.3 LTS, dringendSeit August 2025 ohne Support, und 1.6 auf 2.3 ist weder aufeinanderfolgend noch LTS auf LTS – gehen Sie über 1.7. Behandeln Sie es als Sicherheits-, nicht als Wartungsthema
containerd 2.1, letztes Jahr aktualisiert2.3 LTSDie Konfiguration ist bereits Version 3, es ist also im Wesentlichen ein Binary-Tausch – aber 2.1 auf 2.3 überspringt 2.2 und liegt außerhalb des unterstützten Upgrade-Pfads, also testen statt annehmen
containerd 2.2, aktuell2.3 LTS vor November 2026Minimal, aber lassen Sie es nicht treiben – 2.2 endet, bevor der nächste LTS kommt
Bereits Kubernetes 1.36, dazu containerd 1.x2.3 LTS, in diesem FensterSie fahren eine ungetestete Paarung. Die Matrix hat für 1.36 keine 1.x-Zeile
Ein Managed Service (GKE, EKS, AKS)Was der Anbieter ausliefertLesen Sie dessen Node-Image-Release-Notes: die Runtime gehört ihm, die RuntimeClasses Ihnen
  1. Inventur vor der Planung. Daemon-Version, Version der Konfigurationsdatei, CRI-API-Version und kubelet-Version, je Knoten. Danach ctr deprecations list --format json über die Flotte, und heben Sie die Ausgabe auf – sie ist die Liste dessen, wovon Sie bereits wissen, dass es falsch ist.
  2. Schreiben Sie die Konfiguration von Hand um und nutzen Sie den Konverter als Wörterbuch. containerd config migrate ausführen, um die neuen Schlüsselnamen zu lernen, und dann selbst eine kurze Version-3-Datei schreiben. Installieren Sie nicht den dreihundertzeiligen Dump: er schreibt jeden Standardwert fest, den Sie nie gewählt haben.
  3. Machen Sie die Registry-Umstellung zuerst und getrennt. Bauen Sie den certs.d-Baum, weisen Sie ihn mit ctr images pull --hosts-dir nach, und stellen Sie sicher, dass mirrors und config_path nie in derselben Datei auftauchen. Dieser Schritt verursacht die Ausfälle.
  4. Entscheiden Sie über die geänderten Standardwerte, statt sie zu erben. Unprivilegierte Ports, NRI, CDI und die seccomp-Änderung bei io_uring sind sicherheitsrelevant. Entscheiden, aufschreiben, in den Image-Build übernehmen.
  5. Ein Knoten, dann ein Pool, dann die Flotte. /var/lib/containerd sichern, aktualisieren, das Prüfskript laufen lassen, uncordon. Erst danach die Änderung ins Node-Image schreiben – und die Runtime-Handler prüfen, von denen Ihre RuntimeClasses abhängen, denn sonst tut es nichts.

Das ist eine von vier Änderungen, die im selben Jahr auf denselben Knoten landen, und zusammen sind sie billiger als einzeln: die Migration von cgroup v1 auf cgroup v2, weil die Einstellung des cgroup-Treibers beide Umschreibungen überleben muss und das kubelet sie inzwischen von der Runtime liest; die Breaking Changes in Docker Engine 29, dasselbe Container-Fundament von der Docker-Seite aus gesehen; und der Wechsel von ingress-nginx zur Gateway API, falls der Knoten ohnehin für die Ingress-Umstellung neu gebaut wird. Wer abwägt, wie viel davon überhaupt nötig ist, findet in wann man Kubernetes nicht einsetzen sollte die andere Seite des Arguments.

Häufige Fragen

Wird containerd 1.7 noch unterstützt?

Nur in einem sehr engen Sinn. Die Release-Tabelle von containerd führt 1.7 als LTS bis September 2026, doch die Fußnote ist entscheidend: der allgemeine Support durch die Committer endete im März 2026, und die Verlängerung leisten zwei namentlich genannte Maintainer, ausgerichtet auf die Nutzung mit Kubernetes 1.32, 1.31 und 1.30 über Google Kubernetes Engine, wobei Änderungen abgelehnt werden können, wenn sie dafür nicht gebraucht werden. Alle drei Kubernetes-Versionen sind upstream bereits End of Life. Wer nicht auf GKE ein nicht mehr unterstütztes Kubernetes betreibt, sollte 1.7 heute als nicht unterstützt behandeln, nicht erst im September.

Auf welche containerd-2.x-Version sollte ich aktualisieren?

Auf 2.3. Das ist der aktuelle LTS-Branch, er begann am 30. April 2026 und wird bis zum 30. April 2028 unterstützt. Die Alternativen sind auf jeweils eigene Weise schlechter: 2.1 ist am 3. Juli 2026 End of Life geworden, 2.2 wird nur bis zum 6. November 2026 unterstützt, und 2.0 steht auf derselben Art herstellerbezogenem erweitertem Support wie 1.7. Wer heute auf 2.1 oder 2.2 ist, hat einen kleinen Weg zu 2.3, weil die Konfiguration bereits Version 3 ist.

Muss ich config.toml umschreiben, oder liest containerd 2.x meine alte Datei?

Es liest eine Datei der Version 2 und konvertiert sie bei jedem Start im Speicher, es zwingt Sie also nichts – und es protokolliert dabei jedes Mal Configuration migrated from version 2, use `containerd config migrate` to avoid migration, was der schnellste Weg ist, eine Flotte zu prüfen. Das Argument für die Umschreibung ist, dass die Kompatibilitätsroute genau dort verläuft, wo die bekannten Probleme sitzen, insbesondere die Registry-Migration, die config_path neben Ihren mirrors-Block setzt und damit das Laden des CRI-Plugins verhindert. Nutzen Sie containerd config migrate, um die neuen Schlüsselnamen zu lernen, und schreiben Sie eine kurze Datei von Hand. Und entscheiden Sie sich bewusst für eine Version: Version 3 lesen containerd 2.0 und alles danach, Version 4 braucht 2.3.0 oder neuer – und Version 4 ist das, was migrate auf 2.3 ausgibt.

Was ist Konfigurationsversion 4, und brauche ich sie?

Version 4 kam mit containerd 2.3. An CRI ändert sie nichts – die Plugin-Teilung, um die es in dieser Migration überwiegend geht, ist Arbeit aus Version 3 und kam mit 2.0. Was Version 4 tut, ist, die Sockets des Daemons selbst aus den Top-Level-Tabellen [grpc], [ttrpc], [metrics] und [debug] in die Plugin-Blöcke io.containerd.server.v1.grpc, …v1.ttrpc, …v1.metrics und …v1.debug zu verlegen; [debug] behält level, format und log_trace_id auf oberster Ebene. Eine Verhaltensänderung übersieht man leicht: vor Version 4 wurde eine nicht gesetzte ttrpc-Adresse aus der gRPC-Adresse als <gRPC-Adresse>.ttrpc abgeleitet und erbte deren uid und gid, in Version 4 nutzt das ttrpc-Plugin stattdessen seinen eigenen Standard. Brauchen müssen Sie Version 4 nicht, und es spricht einiges dafür, vorerst bei Version 3 zu bleiben: eine Version-4-Datei können 2.0, 2.1 und 2.2 nicht lesen, sie schmälert also Ihre Rollback-Optionen.

Was bedeutet „`mirrors` cannot be set when `config_path` is provided“?

Es bedeutet, dass das CRI-Image-Service-Plugin das Laden verweigert hat, weil die Registry-Konfiguration sowohl die veraltete mirrors-Tabelle als auch das neuere config_path angibt. containerd startet trotzdem – der Daemon ist gesund, das Plugin nicht – und jeder Pod, der auf diesen Knoten geplant wird, scheitert beim Erstellen. Die Falle ist, dass Sie beides gar nicht geschrieben haben müssen: gemeldet wurde es gegen containerd 2.2.0 mit einer gewöhnlichen Version-2-Datei, die nur einen registry.mirrors-Block enthielt, weil die Migration im Speicher den Standard-config_path danebenstellt. Die eigene Datei zu durchsuchen findet also nichts. Behoben wurde es in Pull Request 12617, der vor 2.3.0 landete und nach 2.2 zurückportiert wurde – ein aktuelles 2.3 und frische 2.2-Patches sind damit nicht betroffen, 2.0 und 2.1 schon. So oder so ist die dauerhafte Lösung, genau eines von beiden zu behalten: einen certs.d-Baum bauen, config_path darauf zeigen lassen und den mirrors-Block löschen.

Was ersetzt den Registry-Mirrors-Block?

Ein Verzeichnisbaum. Setzen Sie config_path unter [plugins.'io.containerd.cri.v1.images'.registry] – üblicherweise auf /etc/containerd/certs.d – und legen Sie je Registry-Host-Namensraum ein Unterverzeichnis mit einer hosts.toml an. Jede Datei nennt einen server und einen oder mehrere [host."…"]-Einträge mit expliziten capabilities, die der Reihe nach probiert werden – ein Mirror ohne den passenden Layer kann so auf die Upstream-Registry durchfallen. CA- und Client-Zertifikate je Registry sind Schlüssel in derselben Datei. Der Verzeichnisname muss dem Host-Namensraum exakt entsprechen, inklusive Port; das ist der häufigste Grund, warum eine korrekt aussehende Konfiguration nie greift.

Wo ist sandbox_image geblieben?

Es heißt jetzt sandbox unter [plugins.'io.containerd.cri.v1.images'.pinned_images]. Diesen Punkt sollte man nach jeder Umschreibung von Hand prüfen, weil der Fehler verzögert und umgebungsabhängig auftritt: ein Knoten mit Internetzugang zieht das Pause-Image klaglos von registry.k8s.io und nichts wirkt falsch, während ein abgeschotteter oder egress-beschränkter Knoten überhaupt keinen Pod erstellen kann. Wenn es auf einen internen Mirror zeigte, übernehmen Sie den Wert ausdrücklich.

Warum hat sich das Pull-Verhalten geändert, obwohl ich nur die Parallelität erhöht habe?

Weil das CRI-Plugin ab containerd 2.1 standardmäßig über den Transfer Service zieht und der Transfer Service max_concurrent_downloads nicht aus der CRI-Image-Konfiguration liest. Findet containerd eine Einstellung, die der Transfer Service nicht abbilden kann, setzt es use_local_image_pull = true für den Knoten, protokolliert eine Warnung und macht weiter. Die vollständige Auslöserliste lautet Registry.Mirrors, Registry.Configs, Registry.Auths, ein MaxConcurrentDownloads ungleich 3, DiscardUnpackedLayers, ImagePullWithSyncFs und DisableSnapshotAnnotations = false. Um die Parallelität ohne Wechsel des Codepfads zu erhöhen, setzen Sie sie stattdessen unter [plugins.'io.containerd.transfer.v1.local'].

Funktionieren meine alten Images nicht mehr?

Nur Docker-Schema-1-Images, und nur beim Ziehen – prüfen Sie dabei die Versionsnummern, denn vieles, was dazu geschrieben wurde, hinkt ein Release hinterher. Die Unterstützung wurde in containerd 2.0 standardmäßig deaktiviert, wo die Umgebungsvariable CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1 sie zurückholte, und in 2.1 entfernt, wo nichts mehr hilft. Da 2.3 das Ziel ist, gibt es keinen Notausgang: die Images müssen neu gebaut werden. Suchen Sie sie vor dem Upgrade: seit containerd 1.7.8 und 1.6.25 tragen aus Schema 1 konvertierte Images das Label io.containerd.image/converted-docker-schema1, sodass ctr image list mit diesem Label sie über alle Namespaces hinweg findet. Die eigentliche Lösung ist ein Neubau in Schema 2 oder OCI, und die betroffenen Images sind meist alt genug, dass das Auffinden des Dockerfile der schwierige Teil ist.

Kann ich von containerd 2.x auf 1.7 zurück?

Die Binaries lassen sich sauber zurückrollen – heben Sie den alten Tarball auf, dann ist es eine Zehnminutensache. Zwei Dinge machen es komplizierter. Zuerst das Zustandsverzeichnis: das Stabilitätsdokument von containerd stellt Dateisystemlayout, Speicherformate und Snapshot-Formate ausdrücklich außerhalb seiner Zusagen und hält fest, dass das Projekt diese Formate zwischen Minor-Versionen migrieren darf – ein Downgrade gegen ein /var/lib/containerd, in das 2.x bereits geschrieben hat, ist damit undefiniert und nicht bloß riskant. Stellen Sie den vor dem Upgrade gezogenen Tarball wieder her oder nehmen Sie in Kauf, jedes Image auf dem Knoten neu zu ziehen. Dann die Konfigurationsdatei: wenn Sie sie migriert haben, braucht eine Version-4-Datei 2.3.0 oder neuer und eine Version-3-Datei 2.0 oder neuer – heben Sie also das Version-2-Original auf. Betrachten Sie das Rollback als Weg, ein missratenes Wartungsfenster zu beenden, nicht als Weg, die Migration zu verschieben: 1.7 verlässt den erweiterten Support im September 2026.

Hört das kubelet auf zu funktionieren, wenn containerd eine nicht unterstützte Version ist?

Nein, und genau deshalb braucht es eine ausdrückliche Prüfung. Die Support-Matrix von Kubernetes und containerd ist eine Aussage darüber, welche Paarungen die Projekte testen, keine Zulassungsrichtlinie: nichts im kubelet prüft die Runtime-Version, es wird kein Event erzeugt, und eine nicht unterstützte Paarung startet und wirkt funktionsfähig. Was fehlt, ist das Testen: für Kubernetes 1.36 listet die Matrix nur containerd 2.3.0+ und 2.2.0+, ganz ohne 1.x-Eintrag, ein 1.7-Knoten in einem 1.36-Cluster ist also eine Kombination, die niemand für Sie durchgespielt hat. Gehen Sie die Knoten durch, lesen Sie containerRuntimeVersion und vergleichen Sie selbst.

Die Service-Datenebene auf denselben Knoten folgt ihrer eigenen Uhr: Kubernetes 1.37 hat den ipvs-Modus von kube-proxy hinter einem Feature Gate für veraltet erklärt, 1.40 schaltet ihn standardmäßig ab und 1.43 löscht den Code. die Umstellung von kube-proxy von IPVS auf nftables behandelt die Kernel-Untergrenze 5.13, das NodePort-Verhalten, das sich unbemerkt ändert, und das zurückbleibende kube-ipvs0, das Datenverkehr verschluckt, wenn es niemand aufräumt.

Eine Anmerkung auf Release-Ebene, denn die Bilanz für 1.37 sieht anders aus, als fast überall zu lesen ist: Was tatsächlich Pods in ContainerCreating hängen lassen kann, ist SELinuxMount auf GA - der cgroup-v1-Fehler kam in 1.35, die Static-Pod-Einschränkung in 1.34, und die containerd-Klippe liegt noch vor Ihnen, in 1.38. was beim Upgrade auf Kubernetes 1.37 wirklich kaputtgeht trennt die drei Spalten und liefert das Audit, das vor dem Upgrade gehört und nicht danach.

Quellen

Primärquellen zuerst. Die Release-Politik von containerd und das Übergangsdokument zu 2.0 sind die einzigen maßgeblichen Aussagen darüber, was entfernt wurde und wie lange ein Branch Patches erhält; der CRI-Konfigurationsleitfaden ist die einzige vollständige Liste der umbenannten Schlüssel. Wo dieser Artikel etwas sagt, das eine Sekundärquelle nicht sagt – dass 2.1 bereits End of Life ist, dass der erweiterte Support von 1.7 auf Kubernetes-Versionen zugeschnitten ist, die selbst aus dem Support sind –, richtet sich der Widerspruch gegen die Zusammenfassung, nicht gegen das Projekt.

  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

War das hilfreich?