تجاوز إلى المحتوى
← المدونة

لم يعد أمام containerd 1.x طريق

ينتهي الدعم الممتد لـ containerd 1.7 في سبتمبر 2026، وهو دعم لم يكن يغطي أصلًا سوى إصدارات من Kubernetes خرجت من الدعم بالفعل. هنا إعادة الكتابة إلى الإصدار 3 من ملف الإعداد، وتحويل السجلّات الذي يُسقط العناقيد، والفرع الوحيد الذي يستحق الانتقال إليه.

·26 دقيقة قراءة
  • containerd
  • Kubernetes
  • الحاويات
  • لينكس

لهذا الترحيل قراءتان: واحدة تبدو كمهمة صيانة مؤجّلة، وأخرى تبدو كموعد نهائي، وأيّهما تنطبق عليك تحدّده حاشية صغيرة. جدول إصدارات containerd يعرض الفرع 1.7 على أنه LTS حتى سبتمبر 2026، وهو ما يبدو متّسعًا. الحاشية تحته توضّح أن هذا الدعم يقدّمه منذ مارس 2026 مشرفان اثنان بالاسم، وأنه موجَّه للاستخدام مع Kubernetes 1.32 و1.31 و1.30 عبر Google Kubernetes Engine، وأن التغييرات غير اللازمة لهذا الاستخدام قد تُرفض. و Kubernetes 1.32 انتهى عمرها في فبراير 2026. فإن لم تكن تشغّل Kubernetes خارج الدعم فوق GKE، فطوق النجاة هذا لم يُرمَ إليك أصلًا.

صورة غلاف من ثلاث لوحات. اللوحة اليسرى بعنوان «containerd 1.x» تُظهر كتلة إعداد واحدة اسمها io.containerd.grpc.v1.cri تضم runtimes و snapshotter و sandbox_image وجدول mirrors للسجل، وفوقها ختم أحمر لانتهاء العمر يقول «1.7 — ينتهي الدعم الممتد في سبتمبر 2026». العمود الأوسط بعنوان «ما الذي ينتقل» يسرد خمسة تحويلات: من الإصدار 2 إلى الإصدار 3، وتقسيم io.containerd.grpc.v1.cri إلى cri.v1.runtime و cri.v1.images، وتحوّل sandbox_image إلى pinned_images.sandbox، وتحوّل registry.mirrors إلى دليل certs.d مع config_path، وتحوّل حِزم cri-containerd إلى ثلاث عمليات تثبيت منفصلة. اللوحة اليمنى بعنوان «containerd 2.3 LTS» تُظهر كتلتَي إعداد متجاورتين، واحدة لـ runtime وأخرى للصور، فوق شريط دعم يمتد حتى أبريل 2028.
ما ينتقل فعلًا: رقم إصدار إعداد واحد، وإضافة مقسومة إلى نصفين، وإعداد تغيّر اسمه، وكتلة سجلّات استبدلتها شجرة أدلة، وتنزيل مجمّع واحد استبدلته ثلاثة منفصلة.

إذن هذا ترحيل بموعد حقيقي، ويستحق أكثر من زيادة رقم في دليل التشغيل. ما يلي هو الصورة كاملة: كيف تقرأ سياسة إصدارات containerd دون أن تخدعك كلمة LTS، ولماذا 2.1 أسوأ مكان للهبوط، وإعادة كتابة الإعداد من الإصدار 2 إلى الإصدار 3 مع معرّفات الإضافات التي انتقلت، وتحويل السجلّات بما له من عِلّة خاصة وصنف أعطال خاص، ومسار سحب الصور الذي تغيّر بهدوء في 2.1، وكل ما أُزيل في 2.0، ودليل تنفيذ لكل عقدة، وحديث صريح عمّا يستعيده التراجع وما لا يستعيده، ونص تحقّق يعيد رمز خروج غير صفري عند أي خلل.

الأعراض، ولماذا لا يذكر أيٌّ منها containerd

لا يعلن أيٌّ من هذه الأعطال عن نفسه كمشكلة في إصدار runtime، ولذلك يُشخَّص متأخرًا. تعود عقدة بعد ترقية وإضافة CRI ببساطة غير موجودة: الخفي (daemon) يعمل، و systemctl status أخضر، وكل Pod على العقدة عالق في ContainerCreating. صورة كانت تُسحب من الـ mirror الداخلي منذ أربع سنوات تبدأ فجأة بالسحب من Docker Hub، وأول ما ينبّهك هو فاتورة الخروج. و RuntimeClass أضافه فريق يدويًا قبل عامين لم يعد يُحَلّ، فتفشل وحدها الأحمال التي تستخدمه. في كل حالة، الـ runtime يعمل — والـ runtime خطأ.[ctrrel]

ما تراهما يعنيه عادةًأين يُعالَج
كل Pods العقدة عالقة في ContainerCreating والـ daemon سليمإضافة CRI لم تُحمَّل. containerd يقلع على أي حال ولا يبلّغ عن الفشل إلا في سجلّه الخاصالسجلّات
الصور تُسحب فجأة من السجل العام لا من الـ mirror الداخليكتلة registry.mirrors لم تنجُ من إعادة الكتابة إلى config_pathالسجلّات
حاوية pause تُسحب من registry.k8s.io على عقدة بلا منفذ إنترنتلم يُنقَل sandbox_image إلى pinned_images.sandboxقسمة الإضافة
تفشل أحمال gVisor أو Kata وحدها وكل شيء آخر سليممعالِج runtime تسمّيه إحدى فئات RuntimeClass مفقود من الإعداد الجديدKubernetes
صورة قديمة كانت تُسحب الأسبوع الماضي تفشل الآن بخطأ في البيانسحب صور Docker schema 1: معطّل افتراضيًا في containerd 2.0، ومُزال تمامًا في 2.1الإزالات
سحب الصور يتصرّف تصرّفًا مختلفًا بعد تغيير لم يربطه أحد بالصورإعداد لا يستطيع Transfer Service الوفاء به أعاد العقدة إلى السحب المحليسحب الصور
الـ daemon يسجّل «Configuration migrated from version 2» عند كل إقلاعلم تُعَد كتابة الملف قط. طبقة التوافق هي التي تحمله، وهناك تسكن علّة السجلّاتالإعداد

الخيط الجامع أن containerd 2.x متسامح عن قصد مع ملف إعداد من الإصدار 2: يقرأه، ويحوّله في الذاكرة، ثم يقلع. هذا لطفٌ لحظة الترقية وعبءٌ بعدها، لأنه يعني أن الترحيل قد يبقى نصف مُنجَز إلى ما لا نهاية دون أن يجبرك شيء على إتمامه. الـ daemon يسجّل اعتراضاته ويقلع على أي حال؛ والإضافة التي فشل تحميلها تعلن حالة خطأ لا يعرضها عليك شيء. هذا المقال كلّه، بمعنى ما، حُجّة لإتمام الترحيل بدل ترك طبقة التوافق تحمله عنك.[ctr20]

اقرأ جدول الدعم كما ينبغي

ابدأ من جدول الإصدارات، لأنه الوثيقة الوحيدة التي تحسم شيئًا، ولأنه يُقرأ خطأً بانتظام. يدير المشروع نوعين من الفروع. الإصدار العادي مدعوم ثمانية أشهر. وإصدار واحد كل سنة يُسمّى LTS ويُدعم سنتين على الأقل. وفوق ذلك، قد يحصل فرع بعينه على دعم ممتد من مشرفين بالاسم بعد إغلاق النافذة العامة — وهذا شيء آخر يحمل البطاقة نفسها في العمود نفسه.[ctrrel]

الفرعالحالةنهاية العمرماذا يعني لك
1.6انتهى العمر23 أغسطس 2025بلا دعم منذ عام. لن يأتي شيء بعد، بما في ذلك تصحيحات الأمان
1.7LTS، ممتدسبتمبر 2026دعم ممتد فقط، من مشرفَين بالاسم، مفصّل على Kubernetes 1.30–1.32 فوق GKE
2.0LTS، ممتدمارس 2027الشكل نفسه: دعم ممتد مفصّل على Kubernetes 1.33 فوق GKE، وهو نفسه منتهي العمر
2.1انتهى العمر3 يوليو 2026انتهى فعلًا. الإصدار الذي رقّى إليه كثيرون أولًا، وأسوأ مكان للتوقّف
2.2نشط6 نوفمبر 2026يتلقّى تصحيحات، لكن بمهلة عشرة أسابيع. يصلح محطةً لا وجهةً
2.3LTS30 أبريل 2028الهدف. فرع الدعم طويل الأمد الحالي، أمامه قرابة عامين
2.4قادمأبريل 2027 مبدئيًاإصدار عادي مدته ثمانية أشهر. ليس بديلًا عن LTS

الآن ضع فوقه مصفوفة دعم Kubernetes، وهي نقطة التقاء المشروعين. ينشر containerd قائمة إصدارات موصى بها لكل إصدار فرعي من Kubernetes. وبالنسبة إلى Kubernetes 1.36 تقول القائمة 2.3.0+, 2.2.0+ — ولا يوجد أي مدخل لـ 1.x على الإطلاق. لا شيء داخل kubelet يفرض ذلك: الاقتران غير المدعوم يقلع ويعمل ويبدو سليمًا، إلى أن يتوقف عن ذلك، وعندها تصحّح الخطأ وحدك. المصفوفة تصريح عمّا جرى اختباره، وهذا الاختبار هو الشيء الوحيد الفاصل بينك وبين علّة في الـ runtime لم يرها أحد غيرك.[k8srel]

Kubernetesإصدارات containerd المدرجة كموصى بهانهاية عمر Kubernetesالقراءة
1.332.1.0+, 2.0.4+, 1.7.24+, 1.6.36+28 يونيو 2026خارج الدعم أصلًا. وهذا بالضبط الاقتران الذي يسمّيه تمديد containerd 2.0
1.342.1.3+, 2.0.6+, 1.7.28+, 1.6.39+27 أكتوبر 2026بقي شهران، واثنان من خيارات containerd الأربعة — 2.1 و1.6 — منتهيا العمر هما نفساهما
1.352.2.0+, 2.1.5+, 1.7.28+28 فبراير 2027آخر سطر يظهر فيه runtime من 1.x أصلًا
1.362.3.0+, 2.2.0+28 يونيو 2027لا مدخل لـ 1.x. عند هذا السطر يتوقّف الترحيل عن كونه اختياريًا

المصيدة داخل المصيدة اسمها containerd 2.1. كانت المحطة البديهية لمن رقّى في النصف الثاني من 2025، ولا تزال كثير من الوثائق الداخلية تقول ذلك، وقد انتهى عمرها في 3 يوليو 2026: قبل 2.2 التي تمتد إلى نوفمبر 2026، وقبل 2.3 بكثير، وهي إصدار الدعم طويل الأمد الحالي المدعوم حتى أبريل 2028. «انتقل إلى 2.x» ليست خطة. من نقطة الصفر هناك فرع واحد بالضبط يستحق الاستهداف، وهو 2.3.

ما المثبَّت فعلًا على هذه العقد

قبل أن تلمس شيئًا، حدّد ما هو مثبَّت فعلًا، لأن الجواب في أي أسطول بأي حجم لن يكون إصدارًا واحدًا. ثلاثة أشياء تهمّ وهي مستقلة عن بعضها: إصدار الـ daemon، وإصدار ملف الإعداد، وإصدار واجهة CRI الذي يُقدَّم فعلًا إلى kubelet. إصدار الإعداد هو الذي يُنسى، وهو الوحيد الذي قد يكون غائبًا: ملف بلا سطر version يُعامَل على أنه ملف من الإصدار 1. وهنا تتناقض وثائق المشروع نفسها، ويستحق الأمر أن تعرف كيف: يقول دليل إعداد CRI إن الإصدار 1 أُزيل في containerd 2.0، بينما يقول RELEASES.md إن غياب سطر الإصدار يُقرأ على أنه الإصدار 1 وإن كل الإصدارات السابقة مدعومة عبر الترحيل، ولا تزال الشيفرة المصدرية تشحن دالة ترحيل من v1. عامِل ملف الإصدار 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

ثم اسأل الـ daemon عمّا ظلّ يحاول إخبارك به. منذ 1.6.27 و1.7.12 يعرض containerd تحذيرات الإهمال عبر واجهة الاستبطان، تحديدًا كي يمكن التخطيط لهذا الترحيل بدل اكتشافه. الأمر الفرعي هو ctr deprecations list — بصيغة الجمع، ويستحق التنبيه لأن وثيقة رسمية واحدة على الأقل تكتبه بالمفرد وهذه الصيغة غير موجودة. شغّله بـ --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

ملف الإعداد، من الإصدار 2 إلى الإصدار 3 — والآن 4

ملف الإعداد هو جوهر الترحيل، وأول ما ينبغي تحديده هو ما تعنيه عبارة «أحدث إصدار»، لأنها تحرّكت هذا العام. جاء الإصدار 3 مع containerd 2.0 وهو الذي قسم إضافة CRI إلى اثنتين. وجاء الإصدار 4 مع 2.3 ويفعل شيئًا مختلفًا يتناوله القسم التالي. ولا يزال الإصدار 2 يُقرأ ويُحوَّل في الذاكرة عند كل إقلاع — ويسجّل الـ daemon سطرًا يقول ذلك، وهو أرخص وسيلة لمعرفة ما إذا كانت العقدة قد رُحِّلت فعلًا أم أنها محتمَلة على مضض فحسب. ويأتي الـ daemon بمحوِّل، هو containerd config migrate، يقرأ ملفك الحالي ويطبع أحدث إصدار إلى المخرج القياسي. لا وجود له في صفحة الدليل — إذ لا توثّق 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

أمران عن هذا المحوِّل يستحقان المعرفة قبل توجيه مخرجاته فوق إعدادك العامل. أولًا، migrate و dump مسار واحد في الشيفرة، فما يعود إليك هو الإعداد كاملًا مُعبَّأ: كل قيمة افتراضية لدى الـ daemon مكتوبة صراحةً. ملف من أربعين سطرًا يصير ثلاثمئة، وكل قيمة افتراضية لم تخترها صارت مثبَّتة في ملفك ولن تتبع المنبع حين يتغيّر. استعمل المخرجات لتتعلّم أسماء المفاتيح الجديدة، ثم اكتب النسخة القصيرة بيدك. ثانيًا، تحقّق من صحّة الملف المرشَّح قبل إعادة تشغيل أي شيء — لكن انتبه إلى أن --config راية عامة لا راية أمر فرعي، فموضعها قبل config dump، ووضعها بعده خطأ في الاستعمال لا فحصًا. وحين يُشغَّل على الوجه الصحيح فإنه يحمّل الملف كما سيحمّله الـ daemon، ويتبع 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.

يستحق الإصدار 4 نظرة خاصة، لأن كل ما كُتب تقريبًا عن هذا الترحيل يتوقّف عند الإصدار 3، ولأنه يحمل قيدًا على التراجع. هو لا يغيّر شيئًا في CRI: إنما ينقل مقابس الـ daemon نفسه خارج الجداول العليا [grpc] و [ttrpc] و [metrics] و [debug] إلى إضافات io.containerd.server.v1.*. ويتبع ذلك أثران. الأثر السلوكي: قبل الإصدار 4 كان عنوان ttrpc غير المضبوط يُشتقّ من عنوان gRPC بالصيغة <grpc address>.ttrpc ويرث منه uid و gid، أما في الإصدار 4 فإضافة ttrpc مستقلة وترجع إلى قيمتها الافتراضية الخاصة — فأي شيء لديك يتّصل بذلك المقبس عبر مساره ينبغي أن يضبطه صراحةً الآن. والأثر التشغيلي هو تحذير المنبع نفسه: ترحيل الملف إلى أحدث إصدار يحدّ من إصدارات containerd القادرة على قراءته. فملف الإصدار 4 يحتاج 2.3.0 أو أحدث، وملف الإصدار 3 يقرأه 2.0 وما بعده. وإن كان التراجع الثنائي في الليلة نفسها جزءًا من خطتك، فاكتب الإصدار 3 — فستحصل مع ذلك على قسمة الإضافة، وهي الجزء الذي يهمّ.[cfgver][srvmig]

إضافة واحدة صارت اثنتين، وانتقلت الإعدادات معها

التغيير البنيوي أن إضافة CRI الوحيدة قُسِمت إلى اثنتين. كان io.containerd.grpc.v1.cri يحوي كل شيء؛ وفي الإصدار 3 لم يبقَ فيه سوى خيارات خادم البثّ، وانتقل الجوهر إلى معرّفَين جديدين: io.containerd.cri.v1.runtime لكل ما يخصّ تشغيل الحاويات — الـ runtimes و CNI و sandboxes و SELinux ومعالجة OOM — و io.containerd.cri.v1.images لكل ما يخصّ الصور: الـ snapshotter والسجل وصورة sandbox المثبّتة وتوازي التنزيل. القسمة أفضل من سابقتها، وهي تعني أن بحثًا واستبدالًا آليًّا لمعرّف الإضافة سيضع نحو نصف إعداداتك في الجدول الخطأ.[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 (الإصدار 2)containerd 2.x (الإصدار 3)ملاحظة
version = 2version = 3 (2.0) / version = 4 (2.3)لا يزال الإصدار 2 يُقرأ ويُحوَّل في الذاكرة. وملف الإصدار 4 يحتاج 2.3.0 أو أحدث
plugins."io.containerd.grpc.v1.cri"plugins.'io.containerd.cri.v1.runtime'كل ما يخصّ تشغيل الحاويات: الـ runtimes و CNI و SELinux و OOM و sandboxes
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انتقل عبر القسمة، من جهة runtime إلى جهة الصور
…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، لا مجرد مهجور

تسميتان جديدتان داخل هذه القسمة تُحدثان معظم الضرر. صار sandbox_image هو pinned_images.sandbox، فالعنقود الذي كان يوجّه صورة pause إلى mirror داخلي يعود بصمت إلى registry.k8s.io — وهو أمر لا يضرّ حتى اليوم الذي لا يكون فيه للعقدة منفذ خروج. أما snapshotter فانتقل من جهة runtime إلى جهة الصور، وهو أمر غير بديهي بما يكفي ليُتحقَّق منه بدل افتراضه. وكل ما تبقّى في الجدول أدناه تغييرُ مسار لا تغييرُ سلوك.[ctrplug]

السجلّات: الجزء الذي يُسقط العناقيد

إعداد السجلّات هو الموضع الذي ينتقل فيه هذا الترحيل من مُملّ إلى خطِر، وله موعد محدّد: هُجِر mirrors و configs في containerd 1.5، و auths في 1.3، وللثلاثة جميعًا هدف إزالة هو containerd 2.4 — أي الإصدار التالي لذلك الذي يوصي به هذا المقال. وبديل الأوّلَين شجرة أدلة: دليل فرعي لكل فضاء أسماء مضيف سجل تحت config_path واحد، في كل منها ملف hosts.toml. ملفات أكثر وسحر أقل بكثير، وهو تحسّن حقيقي: المضيفون يُجرَّبون بالترتيب، والقدرات مكتوبة صراحة، وشهادة المرجع لكل سجل صارت سطرًا في ملف بدل أن تكون حالة خاصة. أما الخاصية الثالثة، auths، فليس لها بديل ملفّي عن قصد: بيانات الاعتماد مكانها سرّ سحب الصور في Kubernetes، لا إعداد على مستوى العقدة يرثه كل حِمل يعمل عليها.[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'

تستحق العلّة صياغة دقيقة، لأن الصيغة غير الدقيقة منها ترسلك للبحث في المكان الخطأ. وهي لا تشترط أن تكون قد كتبت شيئًا غريبًا. شغّل containerd 2.2.0 بملف عادي من الإصدار 2 يحتوي كتلة registry.mirrors ولا شيء غيرها، فيضيف الترحيل في الذاكرة قيمة config_path الافتراضية إلى جانبها — وترفض إضافة CRI هذا الاقتران رفضًا قاطعًا: `mirrors` cannot be set when `config_path` is provided. الإضافة لا تُحمَّل. والـ daemon يقلع على أي حال. وكل Pod يُجدوَل على تلك العقدة يفشل في الإنشاء، بينما الحالة العليا لكل خدمة على الجهاز تبقى خضراء. والبحث في ملفك أنت عن المفتاحين معًا لا يجد شيئًا، لأنك لم تكتب أحدهما أصلًا. بُلِّغ عن ذلك على 2.2.0 وصُحِّح في طلب السحب 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مدعوم (وكلها مهجورة)غير مدعوم — يُطلِق التراجع إلى السحب المحلي
MaxConcurrentDownloadsيُقرأ من إعداد صور CRIيجب نقله إلى 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 تختلفان في موضعين، وفي المرتين تكون وثيقة الانتقال هي التي يقتبسها الناس. وسحب صور 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
snapshotter من نوع AUFS المدمج1.52.0overlayfs
الوسم containerd.io/restart.logpath1.52.0containerd.io/restart.loguri
حِزم cri-containerd-*.tar.gz1.62.0ثبّت containerd و runc وإضافات CNI كلًّا على حدة
واجهة CRI v1alpha21.72.0CRI v1 فقط. تحقّق أن crictl version يعرض RuntimeApiVersion: v1
تنفيذ podsandbox القديم في CRI2.02.0متحكّم sandbox، وهو الافتراضي
سحب صور Docker schema 11.72.1 (عُطِّل في 2.0)أعِد البناء بصيغة schema 2 أو OCI. توقّف مخرج متغيّر البيئة عن العمل في 2.1
إضافات runtime بمكتبة Go (*.so)2.02.1إضافات خارجية: وسيطة أو تنفيذية
LimitNOFILE الصريح في الوحدة المرجعية2.0استخدم قيمة systemd الافتراضية؛ ودون systemd 240 اضبط 1024:524288 يدويًا
io_uring_* في ملف seccomp الافتراضي2.0ملف seccomp صريح، وحوار حول ما إذا كنت تريده

إزالة واحدة أهدأ من البقية وتستحق التنبيه. لم تعد وحدة containerd.service المرجعية تضبط LimitNOFILE صراحةً. وحدود موارد containerd ترثها الحاويات التي يشغّلها، فهذا ليس إعدادًا يخصّ الـ daemon وحده: على systemd 240 وما بعده تكون القيمة الافتراضية معقولة والأمر بلا أثر، وأما دونها فتسري قيمة النواة الافتراضية وهي 4096، وترثها كل حاوية على المضيف. وتوصية المنبع لتلك المضيفات هي إعادة ضبط LimitNOFILE=1024:524288 يدويًا.[pr8924][sdexec]

قيم افتراضية انقلبت دون استئذان

بمعزل عن الإزالات، غيّرت عدة قيم افتراضية قيمتها. هذه هي التي تجعل عقدة تتصرّف تصرّفًا مختلفًا بعد ترقية لم تُغيّر فيها إعدادًا واحدًا، وهي تستحق قرارًا واعيًا لا قبولًا بالتقادم.[ctr20]

القيمة الافتراضيةcontainerd 1.xcontainerd 2.xلماذا يهمّ
enable_unprivileged_portsfalsetrueالحاويات تربط تحت 1024 دون CAP_NET_BIND_SERVICE
enable_unprivileged_icmpfalsetrueping يعمل دون CAP_NET_RAW
enable_cdiمُطفأtrueملفات المواصفات في /etc/cdi و /var/run/cdi تصف الوصول إلى الأجهزة. والمفتاح نفسه مهجور في 2.2، وزائل في 2.4
NRIمعطّلةمفعّلةمقبس NRI يصير جزءًا من سطح الهجوم للعقدة
CRI المعتمد على sandboxخادم CRI القديممتحكّم sandboxغير ملحوظ مع runc، ويستحق الاختبار مع Kata و gVisor
مسار سحب الصورداخل العمليةTransfer Service (منذ 2.1)يتراجع بصمت إلى السحب المحلي مع عدة إعدادات
استدعاءات io_uring_*مسموحةمحظورةأُسقطت من قائمة seccomp الافتراضية بعد ثغرات نواة متكرّرة
صورة sandboxsandbox_imagepinned_images.sandboxالقيمة نفسها، مفتاح مختلف. يسهل ضياعه في إعادة الكتابة
  • المنافذ غير المميّزة و ICMP مفعّلة. صارت إضافة CRI تضبط net.ipv4.ip_unprivileged_port_start=0 و net.ipv4.ping_group_range=0 2147483647 للحاويات التي لا تستخدم فضاء أسماء شبكة المضيف ولا فضاءات أسماء المستخدمين. لم يعد الربط تحت المنفذ 1024 يحتاج CAP_NET_BIND_SERVICE، ولا ping يحتاج CAP_NET_RAW. مريح، وهو أيضًا تغيّر في وضعك الأمني للحاويات — وضبط enable_unprivileged_ports و enable_unprivileged_icmp على false يعيد السلوك القديم.
  • NRI مفعّلة. تتيح واجهة موارد العقدة للإضافات تعديل الحاويات أثناء إنشائها. ويُضبط الوصول عبر الوصول إلى مقبس NRI على مستوى النظام، أي أن هذا المقبس صار جزءًا من سطح الهجوم لعقدتك سواء شغّلت إضافة NRI واحدة أم لا.
  • CDI مفعّلة، والمفتاح في طريقه إلى الزوال. واجهة أجهزة الحاويات تعمل، و cdi_spec_dirs تشير افتراضيًا إلى /etc/cdi و /var/run/cdi، فكل ما يقدر على كتابة ملف مواصفات في هذين الدليلين يقدر على وصف وصول الأجهزة للحاويات. ولاحظ أن enable_cdi نفسه هُجِر في containerd 2.2 بهدف إزالة في 2.4، وبعدها تصير CDI مفعّلة دائمًا ببساطة — فإن كانت خطتك أن تطفئها، فلخطتك تاريخ انتهاء.
  • io_uring لم تعد في قائمة seccomp الافتراضية. أُسقطت io_uring_enter و io_uring_register و io_uring_setup بعد سلسلة من ثغرات النواة طالت بما يكفي ليعتبرها المشروع غير آمنة افتراضيًا. والحِمل المبني حول io_uring سيحتاج ملف تعريف صريحًا — وحوارًا حول ما إذا كان يستحقّه.
  • تنفيذ CRI المعتمد على sandbox صار الافتراضي. تستخدم إضافة CRI متحكّم sandbox المستقرّ بدل خادم CRI القديم. لا يُلحَظ ذلك في التشغيل العادي ويُلحَظ بوضوح مع runtime يعتمد sandbox مثل Kata أو gVisor — وهي بالضبط الحالة التي يجدر اختبارها قبل النشر على الأسطول.

لا شيء من هذا سببٌ لعدم الترقية. كلها أسباب لترقية عقدة واحدة، والنظر إليها، ثم كتابة التغيير في بناء الصورة بعد ذلك فقط — وهذا هو الفرق بين ترحيل ومفاجأة على حجم الأسطول.[ctrnri][cdi]

ترقية العقدة، بالترتيب

الجزء الميكانيكي قصير، والتغيير في الطريقة أكثر منه في المضمون. أُزيلت الحِزم المجمّعة cri-containerd-cni-VERSION-OS-ARCH.tar.gz في 2.0، فصار containerd و runc وإضافات CNI ثلاث عمليات تثبيت منفصلة بثلاثة قرارات إصدار منفصلة. أوضح، وعمل أكثر قليلًا، ويزيل مصدر لبس قديمًا كان الناس فيه يرقّون containerd فيرقّون runc معه دون أن ينتبهوا. وطمأنة واحدة بشأن القفزة نفسها: يدعم 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

ملاحظتان عن الترتيب يسهل الإخلال بهما تحت ضغط الوقت. خذ نسخة احتياطية من /var/lib/containerd قبل البدء، لا لأن الترقية يُرجَّح أن تُفسده بل لأنه مخزن الصور واللقطات: فقدانه يعني إعادة سحب كل صورة على العقدة، وهو ما يُقاس على عقدة كبيرة بعشرات الدقائق وبكمّ كبير من حركة الخروج. وارفع الـ runtime قبل kubelet، ثم افحص ctr plugins ls بحثًا عن أي إضافة ليست في الحالة ok قبل تشغيل kubelet أصلًا. إضافة CRI لم تُحمَّل تعني عقدة تقبل Pods ولا تستطيع إنشاءها.[runc][cni]

التغيير نفسه من جهة Kubernetes

من جهة العنقود لا شيء يُفعل وشيء واحد يُفحص. مصفوفة الدعم وثيقة مشروع لا سياسة قبول: لا kubelet يرفض الإقلاع أمام containerd غير مدعوم، ولا يظهر أي حدث في أي مكان. فالتدقيق يجب أن يكون صريحًا: مرّ على العقد، اقرأ 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'

الإعداد الأكثر ضياعًا في هذا الترحيل هو معالِج runtime، لأنه الإعداد الأكثر إضافةً باليد. كل RuntimeClass في العنقود يسمّي معالِجًا يجب أن يوجد في الإعداد المُعاد كتابته؛ وإن لم يوجد فلن تفشل إلا الأحمال التي تطلبه، أي أن العطل ينحصر في الفريق الذي كان يستعمل gVisor أو Kata ولا يلاحظه أحد غيره لأسبوع. تحقّق منها بالاسم مقابل الملف الجديد. والأمر نفسه ينطبق على مشغّل cgroup، وهنا الرواية المطمئنة ليست دقيقة تمامًا: أضافت Kubernetes 1.28 قدرة kubelet على سؤال runtime الخاص بـ CRI عن المشغّل الذي يستعمله، لكن كميزة ألفا خلف البوابة KubeletCgroupDriverFromCRI، ويحتاج الجانب المقابل في containerd إلى 2.0 أو أحدث. فالأمر ليس تلقائيًا، ولا يزال على SystemdCgroup = true أن ينجو من إعادة الكتابة، وهو الآن في مسار مختلف.[k8srtc][k8skubeadm]

التراجع، وما لا يمكن التراجع عنه

يستحق التراجع جوابًا صريحًا لا مطمئنًا. الملفات التنفيذية وملف الإعداد يتراجعان بنظافة — كلاهما ملفات على القرص، وإن احتفظت بالأرشيف القديم و config.toml القديم فأنت على بُعد عشر دقائق من نقطة البداية. وهذا بصدق أكثر مما تقدّمه أغلب عمليات الترحيل.[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.

شيئان لا يتراجعان بهذه السهولة. الأول دليل الحالة، والصياغة الصادقة أن لا أحد يَعِد بأنه سيتراجع: تضع وثيقة الاستقرار في containerd تخطيط نظام الملفات وصيغ التخزين وصيغ اللقطات صراحةً خارج ضماناتها، وتقول إن المشروع قد ينقل هذه الصيغ بين الإصدارات الفرعية. فخفض الإصدار فوق /var/lib/containerd كتب فيه 2.x بالفعل أمر غير معرَّف لا أمر موثَّق على أنه معطوب، وهذا فرق لا يستحق اختباره في الثالثة فجرًا — استعِد الأرشيف أو اقبل إعادة سحب كل صورة. والثاني ملف الإعداد نفسه إن كنت قد رحّلته: فملف الإصدار 4 يحتاج containerd 2.3.0 أو أحدث ليُقرأ أصلًا. والنقطة الأوسع تستحق الصراحة: التراجع يشتري لك ليلة، لا فصلًا من السنة. يغادر الفرع 1.7 الدعم الممتد في سبتمبر 2026، وهو حتى ذلك الحين لا يتلقّى تصحيحات لأي شيء خارج الخدمة المُدارة لمزوّد بعينه. التراجع طريقة لإنهاء نافذة صيانة سيّئة بأمان، لا طريقة لتأجيل القرار.[ctrsec]

أن تتحقّق بدل أن تأمل

التحقّق ليس مسألة ذوق، وله في هذا الترحيل شكل محدّد: كل ما يعطب تقريبًا يترك الـ daemon يعمل. فالتأكد من أن containerd يعمل لا يثبت شيئًا. النص أدناه يفحص ما يمكن أن يكون خطأً بصمت — إصدار الإعداد، ووجود إضافة في حالة خطأ، وهل تجيب CRI على v1، وهل نجا مشغّل cgroup ومعالِجات runtime — ثم يفعل الشيئين اللذين لا يمكن إثباتهما بالفحص إطلاقًا.[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" 

هذان الشيئان هما سحب حقيقي وحاوية حقيقية. ملف hosts.toml في دليل لا يطابق اسمه فضاء أسماء مضيف السجل مطابقةً تامّة يبدو سليمًا تمامًا وببساطة لا يطابق شيئًا أبدًا، ولن تكشف لك ذلك أي قراءة للملف؛ يكشفه السحب وحده. كما أن إعدادًا صحيحًا من كل وجه قد لا يشغّل حاوية، لأن الملف التنفيذي لـ runtime من إصدار خاطئ أو ليس في المسار الذي يتوقّعه الـ shim. نفّذ الاثنين، على العقدة الأولى، قبل الانتقال إلى الثانية.[critools]

الترتيب الذي يُنفَّذ به هذا كلّه

مضغوطًا، القرار أصغر من المقال. هناك إصدار هدف واحد وهو 2.3 — إصدار الدعم طويل الأمد الحالي، المدعوم حتى أبريل 2028. وكل ما عداه في الجدول إما خارج الدعم أصلًا، أو خارجه خلال أشهر، أو فرع دعم ممتد مفصّل على مقاس خدمة مُدارة تخصّ غيرك. والعمل ليس تبديل الملف التنفيذي، فذلك خمس عشرة دقيقة؛ العمل هو إعادة كتابة الإعداد، وفي قلبها تحويل السجلّات.[ctrrel]

إن كان وضعك…فالهدف…والعمل…
containerd 1.7 فوق Kubernetes 1.34 أو 1.352.3 LTS، بقفزة واحدةإعادة كتابة الإعداد كاملًا. والانتقال من LTS إلى LTS قفزة مدعومة صراحةً، ولا سبب للتوقّف عند 2.2
containerd 1.6، في أي مكان2.3 LTS، بشكل عاجلبلا دعم منذ أغسطس 2025، والانتقال من 1.6 إلى 2.3 ليس متتاليًا ولا من LTS إلى LTS — فمُرّ عبر 1.7. عامله كبند أمني لا كبند صيانة
containerd 2.1، رُقّي العام الماضي2.3 LTSالإعداد بالإصدار 3 أصلًا، فالأمر في معظمه تبديل ملف تنفيذي — لكن الانتقال من 2.1 إلى 2.3 يتخطّى 2.2 وهو خارج مسار الترقية المدعوم، فاختبره بدل افتراضه
containerd 2.2، محدَّث2.3 LTS قبل نوفمبر 2026الحد الأدنى، لكن لا تدعه ينجرف — تنتهي 2.2 قبل وصول LTS التالية
Kubernetes 1.36 بالفعل مع containerd 1.x2.3 LTS، في هذه النافذةأنت على اقتران غير مُختبَر. المصفوفة بلا سطر 1.x عند 1.36
خدمة مُدارة (GKE أو EKS أو AKS)ما يشحنه المزوّداقرأ ملاحظات إصدار صور العقد لديه: الـ runtime له، وفئات RuntimeClass لك
  1. جَرْد قبل التخطيط. إصدار الـ daemon، وإصدار ملف الإعداد، وإصدار واجهة CRI، وإصدار kubelet، لكل عقدة. ثم ctr deprecations list --format json على الأسطول كله، واحتفظ بالمخرجات: فهي قائمة ما تعرف أصلًا أنه خطأ.
  2. أعِد كتابة الإعداد بيدك، مستعملًا المحوِّل كقاموس. شغّل containerd config migrate لتتعلّم أسماء المفاتيح الجديدة، ثم اكتب أنت ملفًا قصيرًا بالإصدار 3. لا تثبّت الملف المُفرَّغ ذا الثلاثمئة سطر: فهو يثبّت كل قيمة افتراضية لم تخترها قط.
  3. نفّذ تحويل السجلّات أولًا وعلى حدة. ابنِ شجرة certs.d، وأثبتها بـ ctr images pull --hosts-dir، وتأكّد أن mirrors و config_path لا يظهران أبدًا في ملف واحد. هذه هي الخطوة التي تسبّب الانقطاعات.
  4. اتّخذ قرارًا في القيم الافتراضية المتغيّرة بدل أن ترثها. المنافذ غير المميّزة و NRI و CDI وتغيير seccomp الخاص بـ io_uring كلها مسائل أمنية. اختر، ودوّن، وأدخِله في بناء الصورة.
  5. عقدة واحدة، ثم مجمّع، ثم الأسطول. نسخة احتياطية من /var/lib/containerd، ثم الترقية، ثم نص التحقّق، ثم رفع العزل. وبعد ذلك فقط اكتب التغيير في صورة العقدة — وافحص معالِجات runtime التي تعتمد عليها فئات RuntimeClass لديك، لأن لا شيء آخر سيفعل ذلك.

هذا واحد من أربعة تغييرات تهبط على العقد نفسها في السنة نفسها، وهي مجتمعة أرخص منها متفرّقة: الانتقال من cgroup v1 إلى cgroup v2، لأن إعداد مشغّل cgroup يجب أن ينجو من عمليتَي إعادة الكتابة ولأن kubelet صار يقرأه من الـ runtime؛ والتغييرات الكاسرة في Docker Engine 29، وهي طبقة الحاويات نفسها منظورًا إليها من جهة Docker؛ والانتقال من ingress-nginx إلى Gateway API، إن كانت العقدة سيُعاد بناؤها أصلًا من أجل تغيير طبقة الدخول. وإن كنت تزن كم تحتاج من هذا كله، فإن متى لا تستخدم Kubernetes هو الوجه الآخر للحجّة.

أسئلة شائعة

هل ما زال containerd 1.7 مدعومًا؟

بمعنى ضيّق جدًا فقط. يعرض جدول إصدارات containerd الفرع 1.7 على أنه LTS حتى سبتمبر 2026، لكن الحاشية هي المهمّة: انتهى الدعم العام من المساهمين ذوي صلاحية الدمج (committers) في مارس 2026، والتمديد يقدّمه مشرفان اثنان بالاسم، وهو موجَّه للاستخدام مع Kubernetes 1.32 و1.31 و1.30 عبر Google Kubernetes Engine، مع إمكان رفض التغييرات غير اللازمة لهذا الاستخدام. وإصدارات Kubernetes الثلاثة هذه منتهية العمر في المنبع أصلًا. فإن لم تكن على GKE بإصدار Kubernetes خارج الدعم، فعامِل 1.7 على أنها غير مدعومة اليوم لا في سبتمبر.

إلى أي إصدار من containerd 2.x أرقّي؟

إلى 2.3. هي فرع الدعم طويل الأمد الحالي، بدأت في 30 أبريل 2026 وتُدعم حتى 30 أبريل 2028. والبدائل أسوأ كلٌّ بطريقته: انتهى عمر 2.1 في 3 يوليو 2026، و2.2 مدعومة حتى 6 نوفمبر 2026 فقط، و2.0 على النوع نفسه من الدعم الممتد المفصّل على مزوّد بعينه مثل 1.7. وإن كنت اليوم على 2.1 أو 2.2 فالانتقال إلى 2.3 قصير، لأن إعدادك بالإصدار 3 أصلًا.

هل يجب أن أعيد كتابة config.toml أم سيقرأ containerd 2.x ملفي القديم؟

سيقرأ ملفًا من الإصدار 2 ويحوّله في الذاكرة عند كل إقلاع، فلا شيء يجبرك — ويسجّل في كل مرة Configuration migrated from version 2, use `containerd config migrate` to avoid migration، وهي أسرع وسيلة لتدقيق أسطول كامل. والحجّة لإعادة الكتابة رغم ذلك أن مسار التوافق هو حيث تسكن المشكلات المعروفة، وخصوصًا ترحيل السجلّات الذي يحقن config_path إلى جانب كتلة mirrors فيمنع تحميل إضافة CRI. استعمل containerd config migrate لتتعلّم أسماء المفاتيح الجديدة، واكتب بيدك ملفًا قصيرًا. واختر الإصدار عن قصد: الإصدار 3 يقرأه containerd 2.0 وما بعده، والإصدار 4 يحتاج 2.3.0 أو أحدث، والإصدار 4 هو ما يُخرجه migrate على 2.3.

ما هو إصدار الإعداد 4، وهل أحتاجه؟

وصل الإصدار 4 مع containerd 2.3. وهو لا يغيّر شيئًا في CRI — فقسمة الإضافة التي يدور حولها معظم هذا الترحيل من عمل الإصدار 3، وقد جاءت في 2.0. ما يفعله الإصدار 4 هو نقل مقابس الـ daemon نفسه خارج الجداول العليا [grpc] و [ttrpc] و [metrics] و [debug] إلى كتل إضافات io.containerd.server.v1.grpc و …v1.ttrpc و …v1.metrics و …v1.debug؛ ويُبقي [debug] المفاتيح level و format و log_trace_id في المستوى الأعلى. وثمّة تغيّر سلوكي واحد يسهل إغفاله: قبل الإصدار 4 كان عنوان ttrpc غير المضبوط يُشتقّ من عنوان gRPC بالصيغة <grpc address>.ttrpc ويرث منه uid و gid، وفي الإصدار 4 تستعمل إضافة ttrpc قيمتها الافتراضية الخاصة بدلًا من ذلك. أنت لست بحاجة إلى الإصدار 4، وثمّة سبب لتفضيل الإصدار 3 لبعض الوقت: ملف الإصدار 4 لا تستطيع 2.0 ولا 2.1 ولا 2.2 قراءته، فكتابته تضيّق خيارات تراجعك.

ماذا تعني «`mirrors` cannot be set when `config_path` is provided»؟

تعني أن إضافة خدمة صور CRI رفضت التحميل لأن إعداد السجلّات انتهى إلى تحديد جدول mirrors المهجور و config_path الأحدث معًا. ويقلع containerd على أي حال — الـ daemon سليم والإضافة ليست كذلك — ويفشل كل Pod يُجدوَل على تلك العقدة في الإنشاء. والمصيدة أنك قد لا تكون كتبت الاثنين: بُلِّغ عن ذلك على containerd 2.2.0 بملف عادي من الإصدار 2 لا يحتوي سوى كتلة registry.mirrors، لأن الترحيل في الذاكرة يضيف config_path الافتراضي إلى جانبها. فالبحث في ملفك أنت لا يجد شيئًا. وقد صُحِّح في طلب السحب 12617 الذي وصل قبل 2.3.0 ونُقل إلى 2.2، فإصدارات 2.3 الحالية وترقيعات 2.2 الحديثة ليست معرّضة بينما 2.0 و2.1 معرّضتان. وفي الحالتين الحل الدائم أن تُبقي واحدًا منهما بالضبط: ابنِ شجرة certs.d، ووجّه config_path إليها، واحذف كتلة mirrors.

ما الذي حلّ محلّ كتلة mirrors في السجل؟

شجرة أدلة. اضبط config_path تحت [plugins.'io.containerd.cri.v1.images'.registry] — والعرف أن يكون /etc/containerd/certs.d — وأنشئ دليلًا فرعيًا لكل فضاء أسماء مضيف سجل، في كل منها hosts.toml. يذكر كل ملف server وواحدًا أو أكثر من مداخل [host."…"] بقدرات capabilities مكتوبة صراحةً، تُجرَّب بالترتيب، فتستطيع نسخة mirror تنقصها طبقةٌ أن تسقط إلى السجل الأصلي. وشهادات المرجع وشهادات العميل لكل سجل مفاتيح في الملف نفسه. ويجب أن يطابق اسم الدليل فضاء أسماء المضيف مطابقةً تامّة بما في ذلك المنفذ — وهذا أشيع سبب لإعداد يبدو صحيحًا ولا يطابق شيئًا أبدًا.

أين ذهب sandbox_image؟

صار sandbox تحت [plugins.'io.containerd.cri.v1.images'.pinned_images]. ويستحق هذا فحصًا يدويًا بعد أي إعادة كتابة، لأن العطل مؤجَّل ومرتبط بالبيئة: عقدة لها منفذ إنترنت ستسحب صورة pause من registry.k8s.io بلا شكوى ولن يبدو شيء خطأ، بينما عقدة معزولة أو مقيّدة الخروج لن تنشئ Pod واحدًا. فإن كنت توجّهه إلى mirror داخلي فانقل القيمة صراحةً.

لماذا تغيّر سلوك سحب الصور وأنا لم أغيّر سوى مستوى التوازي؟

لأن إضافة CRI منذ containerd 2.1 تسحب عبر Transfer Service افتراضيًا، و Transfer Service لا يقرأ max_concurrent_downloads من إعداد صور CRI. وحين يجد containerd إعدادًا لا يستطيع Transfer Service الوفاء به يضبط use_local_image_pull = true للعقدة، ويسجّل تحذيرًا، ثم يواصل. وقائمة المُطلِقات كاملةً هي Registry.Mirrors و Registry.Configs و Registry.Auths و MaxConcurrentDownloads بقيمة غير 3 و DiscardUnpackedLayers و ImagePullWithSyncFs و DisableSnapshotAnnotations = false. ولرفع التوازي دون تغيير مسار الشيفرة، اضبطه تحت [plugins.'io.containerd.transfer.v1.local'] بدلًا من ذلك.

هل ستتوقّف صوري القديمة عن العمل؟

صور Docker schema 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 بذلك الوسم عبر كل فضاءات الأسماء. وإعادة البناء بصيغة schema 2 أو OCI هي الحل، وهذه الصور عادةً من القِدم بحيث يكون العثور على ملف Dockerfile هو الجزء الصعب.

هل أستطيع التراجع من containerd 2.x إلى 1.7؟

تتراجع الملفات التنفيذية بنظافة — احتفظ بالأرشيف القديم وتصير العملية عشر دقائق. وشيئان يعقّدان الأمر. دليل الحالة أولًا: تضع وثيقة الاستقرار في containerd تخطيط نظام الملفات وصيغ التخزين وصيغ اللقطات صراحةً خارج ضماناتها، وتقول إن المشروع قد ينقل هذه الصيغ بين الإصدارات الفرعية، فخفض الإصدار فوق /var/lib/containerd كتب فيه 2.x بالفعل أمر غير معرَّف لا مجرد محفوف بالمخاطر. استعِد الأرشيف الذي أخذته قبل الترقية، أو اقبل إعادة سحب كل صورة على العقدة. وملف الإعداد ثانيًا: إن كنت قد رحّلته، فملف الإصدار 4 يحتاج 2.3.0 أو أحدث وملف الإصدار 3 يحتاج 2.0 أو أحدث، فاحتفظ بأصل الإصدار 2. اعتبر التراجع وسيلة لإنهاء نافذة صيانة سيّئة، لا وسيلة لتأجيل الترحيل: تغادر 1.7 الدعم الممتد في سبتمبر 2026.

هل يتوقّف kubelet عن العمل إن كان إصدار containerd غير مدعوم؟

لا، ولهذا بالضبط يلزم تدقيق صريح. مصفوفة دعم Kubernetes و containerd تصريح عن الاقترانات التي يختبرها المشروعان، لا سياسة قبول: لا شيء في kubelet يفحص إصدار الـ runtime، ولا يُطلَق أي حدث، والاقتران غير المدعوم يقلع ويبدو عاملًا. ما تفقده هو الاختبار: لأجل Kubernetes 1.36 تُدرِج المصفوفة containerd 2.3.0+ و2.2.0+ فقط، بلا أي مدخل لـ 1.x، فعقدة على 1.7 داخل عنقود 1.36 هي تركيبة لم يجرّبها أحد نيابةً عنك. مُرّ على العقد، واقرأ containerRuntimeVersion، وقارن بنفسك.

مستوى بيانات الـ Service على العُقد نفسها يسير على ساعته الخاصة: أعلن Kubernetes 1.37 إهمال وضع ipvs في kube-proxy خلف feature gate، ويُعطّله 1.40 افتراضيًا، ويحذف 1.43 الشيفرة. نقل kube-proxy من IPVS إلى nftables يغطي حد النواة 5.13، وسلوك NodePort الذي يتغيّر من تحتك، وواجهة kube-ipvs0 المتبقية التي تبتلع حركة المرور إن لم ينظّفها أحد.

ملاحظة على مستوى الإصدار، لأن دفتر حساب 1.37 ليس كما تصفه معظم التغطية: التغيير الذي قد يترك الحاويات فعليًا في ContainerCreating هو وصول SELinuxMount إلى GA، بينما جاء فشل cgroup v1 في 1.35، وقيد الـ static pods في 1.34، أما حافة containerd فما زالت أمامك في 1.38. ما ينكسر فعليًا عند الترقية إلى Kubernetes 1.37 يفصل الأعمدة الثلاثة ويقدّم التدقيق الذي يجب تنفيذه قبل الترقية لا بعدها.

المصادر

المصادر الأولية أولًا. سياسة إصدارات containerd ووثيقة الانتقال إلى 2.0 هما التصريحان الوحيدان الموثوقان بشأن ما أُزيل وإلى متى يُصحَّح كل فرع؛ ودليل إعداد CRI هو القائمة الكاملة الوحيدة للمفاتيح التي تغيّرت أسماؤها. وحيث يقول هذا المقال ما لا يقوله مصدر ثانوي — أن 2.1 انتهى عمرها بالفعل، وأن الدعم الممتد لـ 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?