وضع التحقق في Ansible: ماذا يثبت --check و--diff؟
افهم ما يثبته --check و--diff فعلاً، ولماذا قد يمنحك التشغيل الجاف إجابة خاطئة قبل التطبيق، خصوصاً مع الوحدات التي لا تدعم وضع التحقق.
ما الذي يفعله وضع التحقق في Ansible
وضع التحقق في Ansible هو تشغيل تجريبي: يتصل ansible-playbook --check بكل مضيف في الـplay، ويسأل كل module عمّا إذا كانت الحالة الحالية تطابق الحالة المطلوبة، ثم يعرض التغييرات التي ستحدث من دون كتابة أي شيء. أضف --diff ليعرض أيضاً المحتوى قبل التغيير وبعده للملفات التي سيعدّلها. يجيب الخياران معاً عن السؤال الذي يستحق طرحه قبل كل تشغيل فعلي: ما الذي سيتغير في هذه الخوادم؟
وضع التحقق ليس محاكاة لـplaybook. لا توجد أي صورة نموذجية للخادم. يُطلب من كل module ببساطة أن يقرأ بدلاً من أن يكتب. يقدّم module القادر على الإجابة للقراءة فقط تقريراً عن changed ثم يتابع التنفيذ. أما module غير القادر على الإجابة، فلا ينفّذ شيئاً ولا يعرض أي تقرير. توثّق Ansible ذلك في سطر واحد: "Modules التي لا تدعم وضع التحقق لا تعرض شيئاً ولا تنفّذ شيئاً." تكمن المشكلة في هذه الفجوة، إذ قد يمنحك التشغيل التجريبي إجابة خاطئة. لذلك يركّز معظم هذا الدليل على هذه الفجوة.
شغّل المحاكاة الجافة: --check و--diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C و-D هما الصياغتان المختصرتان للعلامتين. أما --limit فهو مقصود. إن كان الفرق بين حالة مضيف واحد وحالته المطلوبة، فيمكنك قراءته. أما فروق عشرين مضيفاً، فستتجاوزها بالتمرير.
تحمل أربع كلمات للنتائج التقرير بأكمله.
ok: [web1]تعني أن الوحدة تحققت من الحالة وأنها مطابقة بالفعل. لن يتغير شيء.changed: [web1]تعني أن الوحدة كانت ستكتب شيئاً. ومع--diff، تعرض الأسطر السابقة لها ما كانت ستكتبه.skipping: [web1]تعني أن المهمة لم تُقيَّم. إما أنwhenكانت خاطئة، أو أن الوحدة لا تعمل في وضع التحقق.fatal: [web1]تعني أن المهمة فشلت أثناء التحقق. اقرأ الرسالة قبل أن تفترض أن ملف playbook معطوب.
يطبع --diff فرقاً موحداً لوحدات الملفات، مع وضع علامة - على الأسطر المحذوفة وعلامة + على الأسطر المضافة، تحت ترويسة تبدأ أسطرها بـ--- before و+++ after وتسمّي مسار الوجهة. أما الوحدات التي لا تكتب ملفات، فتطبع حالتها قبل التغيير وبعده، لذلك يعرض ansible.builtin.user السمات التي كانت ستغيّرها بدلاً من محتوى الملف.
فعّل عرض الفرق دائماً في ansible.cfg حتى لا تنسى العلامة:
[diff]
always = true
context = 5يوجد فحصان أسرع ينبغي إجراؤهما قبل وضع التحقق. يحلل ansible-playbook site.yml --syntax-check صيغة YAML وبنية play من دون الاتصال بأي مضيف. ويطبع ansible-playbook site.yml --list-tasks المهام التي ستُشغَّل، وبذلك تعرف أن role ظننت أنها مميزة بعلامة ليست كذلك. لا يتصل أي منهما بالمضيفين، لذلك ينتهيان فوراً.
أما وضع التحقق نفسه فيتصل بالمضيفين. فهو يفتح SSH إلى كل مضيف يطابق النمط ويجمع facts، لذلك يفشل التشغيل التجريبي إذا كان أحد المضيفين متوقفاً. وهذه إشارة مفيدة بحد ذاتها، وهي أيضاً سبب أهمية تحديد ما ينبغي أن يفعله playbook بشأن المضيفين غير القابلين للوصول قبل إضافة التشغيل التجريبي إلى CI.
سبب فشل وضع التحقق على خادم جديد
هذه الخطة صحيحة. شغّلها باستخدام --check على خادم لا يحتوي على nginx بعد، وستفشل معظم مهامها.
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueتُبلغ المهمة apt عن changed، وهي محقة في ذلك: الحزمة غير موجودة، ولذلك ستثبّتها عملية التشغيل الفعلية. لكن وضع التحقق لم يثبتها. ثم تفشل المهمة template لأن /etc/nginx/conf.d/ غير موجود على هذا المضيف، ولم تنشئه أي مهمة. وتفشل المهمة service أيضاً لأنه لا توجد وحدة nginx يمكنها الاستعلام عنها. لا يُعد أي من هذين الفشلين خطأً في ملف التشغيل. فقد نفّذ التشغيل التجريبي دون الحالة التي يحتاج إليها. وهذا هو المقصود في الوثائق عند التحذير من أن وضع التحقق لا يستطيع إنتاج مخرجات مفيدة لمهمة يعتمد إدخالها على تغيير أجرته مهمة سابقة.
إذن الصياغة الدقيقة للقاعدة هي: يكون وضع التحقق دقيقاً مع مضيف سبق أن وصل ملف التشغيل إلى حالته المطلوبة، لكنه يعرض ضوضاء كثيرة مع مضيف جديد. ويعبّر تشغيل --check تُبلغ فيه كل مهمة عن ok فعلاً عن مضيف وصل إلى حالته المطلوبة، لأن ذلك يعني أنه لن يحدث أي تغيير. أما على مضيف جديد تماماً، فإن --check يخبرك غالباً بأن المضيف جديد. عند كتابة أول ملف تشغيل Ansible لك على VPS، توقّع أن يكون التشغيل التجريبي الأول مليئاً بالإخفاقات، وقيّم ملف التشغيل استناداً إلى التشغيل الثاني.
سبب تخطي مهام الأوامر وShell في وضع التحقق
لا يعرف ansible.builtin.command وansible.builtin.shell ما يفعله الأمر. لا توجد طريقة للقراءة فقط لتشغيل ملف ثنائي عشوائي، لذلك ترفض الوحدة تشغيله في وضع التحقق. تحتوي نتيجة المهمة على skipped: true، وتظهر الرسالة Command would have run if not in check mode، بينما يعرض الخرج skipping: [web1].
تصف وثائق الوحدة دعمها لوضع التحقق بأنه «جزئي»، وتسمّي الحل البديل creates وremoves. امنح المهمة مسار creates، وسيتمكن وضع التحقق على الأقل من تقييم اختبار الملف:
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/appإذا كان /opt/app/bin/app موجوداً بالفعل، فسيبلغ وضع التحقق عن Would not run command since '/opt/app/bin/app' exists، وهذه إجابة فعلية. وإذا كان المسار مفقوداً، فستحصل على Command would have run if not in check mode، وهذه أيضاً إجابة فعلية. من دون creates، تصبح هذه المهمة فراغاً في التشغيل التجريبي.
يكون الأثر التابع أسوأ من هذا الفراغ. تظل المهمة المتخطاة تسجّل نتيجة، لكن هذه النتيجة تكون نتيجة تخطٍّ ولا تحتوي على المفتاح stdout. لذلك يفشل شرط المهمة التالية أثناء تقييمه، مع ظهور خطأ قريب من 'dict object' has no attribute 'stdout'. يعمل playbook في التشغيل الفعلي، لكنه يفشل في التشغيل التجريبي. وهذا أكثر إخفاقات هذه الميزة إرباكاً.
check_mode: false، والمكان الوحيد الذي ينتمي إليه
تعني check_mode: false في إحدى المهام «نفّذ هذه المهمة فعلياً، حتى ضمن --check». وهذا هو إصلاح مشكلة الأمر الذي جرى تخطيه. ولا يكون آمناً إلا في مهمة للقراءة.
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: falseتكون هذه المهمة صادقة في كلا الوضعين. فهي تقرأ إصداراً ولا تكتب شيئاً. ويمنع changed_when: false المهمة من الإبلاغ عن تغيير لم تنفذه. ويجعل check_mode: false قيمة app_version.stdout موجودة أثناء التشغيل التجريبي، لذلك تظل الشروط المبنية عليها قابلة للتقييم.
اقرأ الكلمة المفتاحية حرفياً قبل نسخها إلى أي موضع آخر. فالمهمة التي تتضمن check_mode: false تكتب على خوادمك أثناء ansible-playbook --check. إذا وضعتها في مهمة apt أو مهمة template لجعل مخرجات التشغيل التجريبي أكثر ترتيباً، فلن يعود التشغيل التجريبي تجريبياً. عندما يتعذر جعل مهمة كتابة آمنة، قيّد تنفيذها بدلاً من ذلك:
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode متغير خاص يضبطه Ansible على true أثناء تشغيل التحقق. وتوجد الكلمة المفتاحية العكسية أيضاً. تفرض check_mode: true تشغيل المهمة في وضع التحقق دائماً، حتى أثناء التشغيل الفعلي، فتحوّلها إلى أداة لفحص الانحراف: سجّل النتيجة، ويعني تقرير changed أن المضيف لم يعد يطابق ما تطلبه المهمة.
لماذا تُبلغ مهمة عن التغيير في كل تشغيل
شغّل الـplaybook مرتين متتاليتين دون تنفيذ أي شيء بينهما. يجب أن تُبلغ كل مهمة عن ok في التشغيل الثاني. إذا استمرت أي مهمة في الإبلاغ عن changed، فهذا يعني أحد أمرين: أن الوحدة لا تستطيع رؤية الحالة التي تديرها، أو أن الإدخال الذي تمرّره إليها غير ثابت. يمكن إصلاح كلتا المشكلتين، ولا ينبغي كتم أي منهما باعتبارها ضجيجاً.
- يُبلغ
commandوshellمن دونcreatesأوremovesأوchanged_whenعنchangedفي كل مرة، لأن الوحدة لا تملك وسيلة لمعرفة ما إذا كان أي شيء قد حدث. أضفcreates، أو اضبطchanged_whenعلى سلسلة نصية في المخرجات. - يُبلغ
ansible.builtin.fileمعstate: touchعنchangedفي كل تشغيل حسب التصميم، لأن لمس ملف يحدّث طوابعه الزمنية. استخدمstate: fileإذا كان المطلوب فقط ضبط المالك أو الوضع. - يعيد
templateكتابة الملف في كل تشغيل إذا تغيّرت مخرجاته المُصيّرة. فتُنتج طابع زمني منansible_date_time، أو استدعاء إلىnow()، أو كلمة مرور تُولَّد حديثاً في كل مرة، بايتات مختلفة. لذلك تُبلغ الوحدة عن التغيير بشكل صحيح. أخرج القيمة المتغيرة من القالب. - يتغير
ansible.builtin.userمعpassword: "{{ pw | password_hash('sha512') }}"في كل تشغيل، لأنpassword_hashيختار salt عشوائياً في كل مرة يُستدعى فيها. لذلك لا يطابق hash الناتج القيمة الموجودة مسبقاً في/etc/shadow. مرّر salt صريحاً مشتقاً من قيمة ثابتة. - تُبلغ
state: latestفي وحدة حزم عنchangedكلما توفر تحديث. وهذا صحيح. وهو أيضاً سبب منحstate: latestإياك playbook لا يمكن التنبؤ بنتيجته. استخدمstate: presentونفّذ الترقية عمداً. - يعيد
ansible.builtin.unarchiveالموجّه إلى URL من دونcreatesالجلب والاستخراج. امنحه مسارcreates.
تُعد --diff أسرع طريقة للتمييز بين هذه الحالات. إذا قالت مهمة changed وأظهر الفرق بايتات مختلفة، فالإدخال غير ثابت. وإذا قالت changed ولم يُظهر الفرق أي شيء، فلا تستطيع الوحدة التعبير عما غيّرته. وهذا يعني عادةً وجود مهمة command أو كتابة لا تغيّر إلا البيانات الوصفية، مثل الطابع الزمني.
لا تستخدم changed_when: false لكتم مهمة كثيرة الإبلاغ. فهو يمنع التقرير، ولذلك لا يُطلَق notify ولا يعمل المعالج الذي يعيد تشغيل الخدمة. أصلح المهمة بدلاً من ذلك.
تقليص نطاق التأثير: --limit و--tags و--step
يوضح وضع الفحص ما الذي سيتغير. وتحدد هذه الخيارات عدد الأجهزة التي ستتلقى التغييرات في المرة الواحدة.
يضيّق --limit نطاق التنفيذ ليشمل مجموعة فرعية من الجرد. ويقبل الأنماط نفسها التي يقبلها hosts:، لذلك يعمل كل من --limit web1 و--limit 'webservers:!web3'. ضع النمط بين علامتي اقتباس. يؤدي استخدام ! من دون اقتباس في جلسة bash تفاعلية إلى تشغيل توسيع السجل عند علامة التعجب، ثم يعيد الصدَفة كتابة الأمر قبل أن يراه Ansible.
تحقق من النمط قبل الاعتماد عليه. يطبع ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts أسماء المضيفين المطابقين ثم يخرج من دون الاتصال بأيٍّ منها. ويُعد النمط الذي لا يطابق شيئاً آمناً، لأن Ansible لا يعود إلى استخدام الجرد بأكمله. بل يطبع تحذيراً يفيد بأنه تعذر العثور على تطابق لنمط المضيف، ثم يخرج بخطأ يفيد بأن المضيفين و--limit لا يطابقان أي مضيف. إن معرفة كيفية تعريف ملف الجرد لهذه المجموعات هي ما يجعل النمط قابلاً للتنبؤ من البداية.
يشغّل --tags deploy المهام الموسومة فقط، بينما يشغّل --skip-tags packages جميع المهام الأخرى. ويطبع --list-tags الوسوم المتاحة. تصبح الوسوم مفيدة عندما يتجاوز play الحجم الذي يجعلك مستعداً لتشغيله بالكامل، وهذا أحد أسباب تقسيم playbook طويل إلى أدوار.
يستأنف --start-at-task "Write the site config" التنفيذ الفاشل من مهمة مسمّاة. استخدمه للاسترداد، وافهم تكلفته: تُتخطى كل المهام التي تسبق تلك المهمة، بما في ذلك المهام التي تضبط facts أو تسجّل المتغيرات التي تقرؤها المهام اللاحقة.
يطالبك --step بإجابة قبل كل مهمة، وينتظر منك اختيار yes أو no أو continue. وهو بطيء، لكنه الأداة المناسبة عند تشغيل شيء مدمّر للمرة الأولى، لأنك تستطيع التوقف بين مهمتين بدلاً من التوقف بعد عشرين مهمة.
طرح التغيير بالتتابع
بشكل افتراضي، يشغّل Ansible مهمة واحدة على كل مضيف في الـplay قبل أن يبدأ المهمة التالية. هذا سريع، لكنه يعني أن المهمة الخاطئة تصل إلى جميع الخوادم في الثانية نفسها. وبحلول الوقت الذي تقرأ فيه الخطأ وتضغط Ctrl-C، يكون التغيير قد طُبِّق في كل مكان.
serial يقسّم الـplay إلى دفعات. يُشغَّل الـplay بالكامل على الدفعة الأولى، ثم على الدفعة التالية.
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webappتتكوّن الدفعة الأولى من مضيف واحد. إذا نجحت، تتكوّن الدفعة الثانية من 5 مضيفين، وتتكوّن كل دفعة لاحقة من 30 بالمئة من مضيفي الـplay. يوقف max_fail_percentage: 0 الـplay فور فشل أي مضيف في الدفعة، ولذلك يتوقف الإصدار المعطّل عند جهاز واحد. أما any_errors_fatal: true فهو الخيار الأكثر حدّة، إذ ينهي الـplay للجميع عند أول فشل لمضيف.
لا يُعد تشغيل التغيير على مضيف واحد أولاً مبالغة، والسبب محدد. تتغير مجموعات Inventory بمرور الوقت. قد يشغّل خادم أُضيف بعد 6 أشهر من بقية الخوادم إصداراً مختلفاً من التوزيعة، أو قد يحتوي على خدمة ثبّتها أحدهم يدوياً، أو قد تكون أقراصه مرتبة بطريقة مختلفة. يكون الـplaybook صحيحاً بالنسبة إلى المجموعة وخاطئاً بالنسبة إلى ذلك المضيف، ولن يكشف ذلك أي تشغيل تجريبي على مضيف متوافق مع بقية المجموعة. تُعد إدارة مجموعة من خوادم Linux في جوهرها ممارسة للعثور على المضيف غير المعتاد قبل أن يصل إليه التغيير.
ترتيب تنفيذ الخطوات
- يتحقق
ansible-playbook site.yml --syntax-checkمن أخطاء YAML وأخطاء البنية من دون أي اتصال بالشبكة. - يثبت
ansible-playbook site.yml --limit web1 --list-hostsأن النمط يطابق ما تتوقع أن يطابقه. - ينفّذ
ansible-playbook site.yml --limit web1 --check --diffعملية تشغيل تجريبية. اقرأ الفرق. - يطبّق
ansible-playbook site.yml --limit web1 --diffالتغييرات على ذلك المضيف فقط. - نفّذ الخطوة 4 مرة أخرى. يجب أن تُبلغ جميع النتائج عن
ok. وأي نتيجة لا تزالchangedتمثل مهمة يجب إصلاحها قبل تطبيقها على بقية مجموعة الخوادم. - يعرض
ansible-playbook site.yml --check --diffعبر كامل المخزون إجابة ذات معنى الآن، لأن المضيفين الذين أصبحت إعداداتهم متوافقة لا يصدرون نتائج، وما تبقى هو الفرق الفعلي.
تحذير بشأن الخطوة 3. يطبع --diff محتويات الملفات في الطرفية وفي سجل مهمة CI، لذلك فإن القالب الذي يعرض كلمة مرور قاعدة بيانات سيطبع كلمة المرور نفسها في السجل. عيّن diff: false في تلك المهمة لمنع إخراجها، أو استخدم no_log: true لإخفاء النتيجة بالكامل، واحتفظ بالقيمة نفسها في ملف Ansible Vault مشفّر بدلاً من وضعها في المستودع.
FAQ
هل يغيّر ansible-playbook --check أي شيء على الخادم؟
لا، باستثناء واحد تتحكم فيه. في وضع check، يُطلب من كل module إصدار تقرير بدلاً من الكتابة، أما modules التي لا تستطيع ذلك فلا تُصدر تقريراً ولا تنفّذ أي إجراء. الاستثناء هو الكلمة المفتاحية للمهمة check_mode: false، التي تفرض تنفيذ تلك المهمة فعلياً حتى أثناء تشغيل --check. ابحث في playbooks وroles عن check_mode: false قبل الوثوق بالتشغيل التجريبي، وتأكد من أن كل نتيجة هي مهمة تقرأ الحالة فقط.
ما الفرق بين --check و--diff؟
يحدد --check ما إذا كان أي شيء سيُنفَّذ فعلياً. ويحدد --diff مقدار التفاصيل التي تراها. يخبرك --check بمفرده بأن ملفاً سيتغير. أما --diff بمفرده فيطبّق التغيير ويعرض الأسطر التي غيّرها. استخدمهما معاً لإجراء تجريبي يمكنك قراءته فعلياً، واترك --diff مفعّلاً في عمليات التشغيل الفعلية أيضاً، وذلك بتعيين always = true ضمن [diff] في ansible.cfg.
لماذا تُبلغ مهمة Ansible لديّ عن حدوث تغيير في كل تشغيل؟
لأن module لا يستطيع رؤية الحالة التي يديرها، أو لأن القيمة التي تمررها إليه تختلف في كل مرة. تُبلغ command وshell عن changed دائماً، ما لم تضف creates أو changed_when. ويتغير file مع state: touch بحكم التصميم. إذا كان template ينشئ طابعاً زمنياً أو كلمة مرور جديدة، فسيُنتج بايتات مختلفة في كل تشغيل، ولذلك يُعاد فعلياً كتابة الملف. شغّل playbook مرتين متتاليتين: كل ما يبقى changed في المرور الثاني هو المهمة التي يجب إصلاحها.
لماذا يتم تخطي مهام command وshell لديّ أثناء التشغيل التجريبي؟
لأنه لا توجد طريقة للقراءة فقط لتشغيل أمر عشوائي. في وضع check، يعيّن module command القيمة skipped: true مع الرسالة Command would have run if not in check mode. أضف creates أو removes حتى يتمكن وضع check من تقييم اختبار الملف بدلاً من ذلك. بالنسبة إلى مهمة تقرأ الحالة فقط، عيّن check_mode: false مع changed_when: false، حتى تظل النتيجة المسجّلة موجودة أثناء التشغيل التجريبي، وتستمر الشروط المبنية عليها في العمل.
لماذا يفشل وضع check على خادم جديد، لكنه ينجح على خادم موجود؟
لأن وضع check لا ينشئ الحالة التي تعتمد عليها المهام اللاحقة. في التشغيل التجريبي على مضيف لا يحتوي على nginx، يُبلغ عن تثبيت nginx على أنه changed، ثم يفشل في المهمة التي تكتب إلى /etc/nginx/conf.d/، لأن ذلك الدليل لم يُنشأ قط. هذا سلوك متوقع. وضع check أداة لاكتشاف الانحراف في المضيفين الذين سبق أن أوصلهم playbook إلى الحالة المطلوبة. ولا يستطيع التحقق من التشغيل الأول. على مضيف جديد، طبّق playbook على جهاز واحد، ثم اقرأ نتيجة التشغيل الثاني.