SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-21

قوالب Ansible والمعالجات: مثال عملي مع nginx

أنشئ إعداد nginx من قالب Jinja2، وأعد تحميل الخدمة عند تغيّر الملف فقط، ثم شغّل الـplaybook مرتين للتحقق من idempotence دون تغييرات زائدة.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 20, 2026.

ما الذي تضيفه قوالب Ansible والمعالجات إلى أول playbook لك

قوالب Ansible والمعالجات هما العنصران اللذان يحوّلان playbook ثابتاً إلى playbook عملي. يعرض القالب ملف إعدادات من متغيراتك، لذلك يغطي ملف واحد كل مضيف. ولا يعمل الـhandler إلا عندما تغيّر مهمة شيئاً فعلياً، لذلك تُعاد قراءة إعدادات الخدمة عند حدوث تغيير حقيقي في الإعدادات، وتُترك دون إجراء في بقية الأوقات.

يتابع هذا الدليل من النقطة التي يتوقف عندها أول playbook لك في Ansible على VPS. لديك بالفعل play يثبّت حزمة ويشغّل خدمة. تُنفَّذ جميع الخطوات أدناه على جهاز واحد، لأن الـplay يستهدف localhost عبر اتصال محلي. لا تحتاج إلى خادم ثانٍ لمتابعة الدليل. ويعمل الـplay نفسه مع مضيفي inventory الفعليين دون تغيير في المهام، بينما يوضّح القسم الأخير ما الذي يتغير.

إعداد دليل العمل

sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templates

وجود nginx هنا سببه أنه خدمة فعلية لها ملف إعداد وأمر لإعادة التحميل، وهذا كل ما يحتاج إليه المثال. يطبع ansible --version إصدار ansible-core ومفسّر Python الذي سيستخدمه. دوّن القيمتين. يستخدم ملف playbook أدناه أسماء وحدات مؤهلة بالكامل مثل ansible.builtin.template، وهي تتطلب Ansible 2.10 أو إصداراً أحدث، وأي حزمة حالية من التوزيعة تتجاوز ذلك بكثير.

أنشئ inventory.ini:

[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"

يخبر ansible_connection=local Ansible بتشغيل كل مهمة كعملية محلية بدلاً من فتح جلسة SSH مع نفسه. الإعداد الثاني ليس للزينة. عندما تكتب localhost في ملف inventory، يصبح مضيفاً عادياً ويفقد مفسّر Python الذي يمرّره Ansible تلقائياً إلى localhost الضمني، لذلك يعود إلى اكتشاف المفسّر وقد يختار Python مختلفاً عن المفسّر الذي يشغّل play. إن ansible_playbook_python هو المفسّر الذي يشغّل ansible-playbook الآن، وبذلك يبقى المفسّران متطابقين.

أنشئ ansible.cfg:

[defaults]
inventory = inventory.ini

من دون هذا الملف، تمرّر -i inventory.ini في كل أمر. وعند عدم وجود inventory إطلاقاً، يطبع Ansible [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'، ثم لا يطابق play الذي يحتوي على hosts: all أي مضيف. هناك أمر آخر يتعلق بـansible.cfg: يتجاهله Ansible عندما يكون موجوداً في دليل قابل للكتابة من جميع المستخدمين، لذلك أبقِ المشروع ضمن دليلك المنزلي. يحتوي ملف inventory على أكثر من قائمة بالمضيفين، وهذا هو أصغر ملف ينجز المهمة.

template مقابل copy، ومتى يكون استخدام كل منهما صحيحاً

ينقل ansible.builtin.copy الملف كما هو. يعالج ansible.builtin.template الملف أولاً باستخدام Jinja2، ثم ينقل النتيجة. يصف توثيق الوحدة template بأنها «وحدة افتراضية تُنفَّذ بالكامل على شكل إضافة إجراءات وتعمل على وحدة التحكم». ولهذا أثر مهم يجب تذكّره: يحدث العرض على الجهاز الذي كتبت عليه ansible-playbook. لا يرى المضيف الهدف متغيراتك، ولا يحتاج إلى تثبيت Jinja2.

استخدم copy عندما يكون الملف متطابقاً على كل مضيف. استخدم template بمجرد اختلاف قيمة واحدة لكل مضيف، أو عندما تحتاج إلى حلقة {% for %} أو كتلة {% if %}. تحتوي copy فعلاً على وسيطة content:، وتُستبدل المتغيرات داخلها مثل أي وسيطة مهمة أخرى، لكن لا توجد فيها حلقات ولا شروط. لذلك، ينتمي أي محتوى ذي بنية إلى قالب. تقبل الوحدتان خيارات الملفات نفسها، لأن كلاً منهما يضمّن مقاطع التوثيق نفسها. لذلك تعمل owner وgroup وmode وbackup وvalidate بالطريقة نفسها في كل منهما.

اكتب القالب: متغير واحد، وحلقة واحدة

احفظه باسم templates/app.conf.j2:

# {{ ansible_managed }}
upstream {{ app_name }}_backend {
{% for backend in app_backends %}
    server {{ backend.host }}:{{ backend.port }} weight={{ backend.weight }};
{% endfor %}
}

server {
    listen {{ app_listen_port }};
    server_name {{ app_server_name }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
    }
}

ينفّذ العمل هنا نوعان من وسوم Jinja2. {{ ... }} تعبير وتطبع قيمته. {% ... %} عبارة ولا تطبع شيئاً من تلقاء نفسها. app_backends قائمة من القواميس، لذلك تقرأ backend.host مفتاحاً واحداً من كل إدخال، وتكتب الحلقة سطراً واحداً من server لكل إدخال، مهما كان عدد الإدخالات التي تعرّفها.

هناك تفصيل يتعلق بالمسافات البيضاء قد يفاجئ من يعرف Jinja2 من بيئات أخرى. يضبط Ansible trim_blocks على yes افتراضياً، بينما لا يضبطه Jinja2 نفسه، لذلك يُحذف السطر الجديد الذي يلي وسم {% ... %} مباشرةً، ولا تترك الحلقة سطراً فارغاً خلفها. ويُبقي Ansible lstrip_blocks مضبوطاً على no، لذلك تُحفظ أي مسافات تضعها قبل وسم {% وتظهر في الملف الناتج. إذا ظهر في الناتج إزاحة زائدة، فاضبط lstrip_blocks: true في مهمة القالب.

يعرض {{ ansible_managed }} النص الحرفي Ansible managed افتراضياً. اتركه كما هو. يعيد بعض المستخدمين تعريف ansible_managed في ansible.cfg ليشمل تاريخاً، وعندها يختلف الملف الناتج في كل تشغيل، وتُبلغ المهمة عن تغيير في كل تشغيل، وتعيد الخدمة التحميل في كل تشغيل. يؤدي هذا الإعداد وحده إلى إتلاف الخاصية التي يدور حولها باقي هذا الدليل. ويُعد امتداد .j2 اصطلاحاً، ولا يتحقق Ansible منه.

دليل التنفيذ

احفظ هذا المحتوى باسم site.yml:

- name: Render an nginx site from a template
  hosts: local
  become: true

  vars:
    app_name: learn
    app_listen_port: 8080
    app_server_name: learn.example.com
    app_backends:
      - host: 127.0.0.1
        port: 9001
        weight: 3
      - host: 127.0.0.1
        port: 9002
        weight: 1

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Render the site configuration
      ansible.builtin.template:
        src: templates/app.conf.j2
        dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
        owner: root
        group: root
        mode: '0644'
        backup: true
      notify: nginx config changed

    - name: Make sure nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test the nginx configuration
      ansible.builtin.command:
        cmd: /usr/sbin/nginx -t
      changed_when: false
      listen: nginx config changed

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: nginx config changed

يُوضَع mode: '0644' بين علامتي اقتباس عمداً. توضّح وثائق خيارات الملف ضرورة وضع الأعداد الثمانية في علامات اقتباس «حتى يستلم Ansible قيمة نصية ويتمكن من تحويلها بنفسه من نص إلى عدد». عند عدم استخدام علامات الاقتباس، يقرأ محلل YAML القيمة 0644 كعدد عادي، وقد تحصل على أذونات لم تطلبها.

يسمّي notify: nginx config changed موضوعاً، وليس معالجاً. يحتوي كلا المعالجين على listen: nginx config changed، لذلك يصل إشعار واحد إلى كليهما. يمكنك إضافة معالج ثالث يحتوي على السطر نفسه listen لاحقاً، من دون تعديل مهمة القالب. يمنع cache_valid_time: 3600 تشغيل العملية مرة ثانية خلال الساعة نفسها، وبالتالي لا يعيد الاتصال بمرايا الحزم.

شغّله مرة واحدة، ثم اقرأ ما طبعه

ansible-playbook site.yml

إذا طلب sudo كلمة مرور، فأضف -K وسيطلبها Ansible منك.

اقرأ أسطر المهام أولاً، ثم اقرأ PLAY RECAP في الأسفل. تطبع كل مهمة changed: عندما يضطر Ansible إلى تنفيذ تغيير، أو ok: عندما يكون المضيف في الحالة المطلوبة مسبقاً. ويجمع الملخص إجمالي هذه العدادات لكل مضيف. بعد اكتمال كل مهمة في الـplay، وليس قبل ذلك، تحصل على RUNNING HANDLER [Test the nginx configuration] متبوعاً بـRUNNING HANDLER [Reload nginx].

تحقق الآن من الجهاز نفسه بدلاً من الوثوق بالمخرجات:

sudo cat /etc/nginx/conf.d/learn.conf
sudo /usr/sbin/nginx -t
curl -sI http://127.0.0.1:8080/

يطبع nginx -t القيمة nginx: configuration file /etc/nginx/nginx.conf test is successful عندما تكون الإعدادات المجمّعة قابلة للتحليل. ويعيد curl سطر حالة من nginx، بينما تكون 502 Bad Gateway هي الإجابة الصحيحة هنا، لأن server block يعمل ولا توجد عملية تستمع على المنفذين 9001 أو 9002. يوضح sudo tail /var/log/nginx/error.log السبب بعبارات واضحة: connect() failed (111: Connection refused) while connecting to upstream.

شغِّله مرة ثانية لإثبات قابلية التنفيذ المتكرر دون تغيير

ansible-playbook site.yml

هذا هو التشغيل المهم، لذلك قارن ناتجه بالتشغيل الأول سطراً بسطر. يجب أن تطبع مهمة القالب الآن ok: بدلاً من changed:، ويجب ألا يظهر أيٌّ من المعالجين في الناتج.

الآلية بسيطة وتستحق المعرفة، لأنك ستعتمد عليها أثناء تصحيح الأخطاء. يعرض template الملف على وحدة التحكم ويقارن checksum الناتج مع checksum للملف الموجود مسبقاً في dest. عندما يتطابق المحتوى والمالك والوضع، لا يبقى شيء لتنفيذه، لذلك تُبلغ المهمة عن ok، وبالتالي لا يُشغَّل notify، ولا يعمل المعالج. تعمل المعالجات عند changed فقط، ولا تعمل في أي حالة أخرى.

أثبت الاتجاه الآخر أيضاً. غيّر weight: 3 إلى weight: 1 في vars، ثم شغّل play مرة أخرى. ستُبلغ مهمة القالب عن changed، وسيعمل المعالجان، وسيعرض sudo cat /etc/nginx/conf.d/learn.conf القيمة الجديدة.

إذا ظل التشغيل الثاني المطابق يبلغ عن حدوث تغيير، فهذا يعني أن عملية العرض غير ثابتة. ابحث أولاً عن قيمة تعتمد على الوقت في الناتج، لأن ذلك هو السبب الشائع، وعادةً ما يكون ansible_managed مخصصاً هو السبب. بعد ذلك، تحقق من أن mode وowner في المهمة يطابقان ما هو موجود فعلياً على القرص، لأن عدم التطابق بينهما يُعد تغييراً حتى عندما تكون البايتات متطابقة.

شاهد التغيير قبل تطبيقه

ansible-playbook site.yml --check --diff

تنفّذ --check الـplay دون تغيير المضيف. تطبع --diff ما الذي كانت كل مهمة ستعدّله. وبالنسبة إلى template، يكون ذلك فرقاً سطراً بسطر بين المحتوى المُنشأ والملف الموجود على القرص. معاً، يجيبان عن السؤال: «ماذا سيفعل هذا التشغيل؟» من دون تنفيذ التغييرات. لوضع التحقق حدوده الخاصة، ولا سيما في المهام التي تعتمد نتيجتها على مهمة سابقة لم ينفذها وضع التحقق فعلياً.

لماذا تنتظر handlers حتى نهاية الـplay

توضح وثائق handlers ذلك مباشرة: «بشكل افتراضي، تُشغَّل handlers بعد اكتمال جميع المهام في play معيّن. وتُنفَّذ handlers التي جرى إشعارها تلقائياً بعد كل قسم من الأقسام التالية، بالترتيب التالي: pre_tasks، وroles/tasks، وpost_tasks».

السبب هو تجميع التغييرات. إذا أنشأ play أربعة ملفات إعداد لخدمة واحدة، فمن المفترض إعادة تشغيل الخدمة مرة واحدة في النهاية، بعد وضع الملفات الأربعة كلها في مكانها. أما إعادة التشغيل بعد كل ملف فستؤدي إلى إعادة تشغيلها أربع مرات، وستحمّل الخدمة في ثلاث من هذه المرات إعداداً غير مكتمل. وتذكر الصفحة نفسها الضمان بوضوح: «يؤدي إشعار handler نفسها عدة مرات إلى تنفيذها مرة واحدة فقط، بغض النظر عن عدد المهام التي أشعرتها».

الترتيب ثابت أيضاً: «تُنفَّذ handlers بالترتيب الذي عُرِّفت به في قسم handlers، وليس بالترتيب المدرج في عبارة notify». لذلك تأتي Test the nginx configuration قبل Reload nginx في playbook. يُجرى الاختبار أولاً لأنه كُتب أولاً، ولا يؤثر أي شيء في السطر notify في ذلك.

كيفية تشغيل المعالجات مبكراً، وكيفية تشغيلها بعد حدوث فشل

قد تحتاج مهمة لاحقة في الـplay نفسه إلى أن تكون الخدمة قد شُغِّلت بالإعداد الجديد. نفّذ المعالجات التي تلقت إشعارات عند تلك النقطة باستخدام الوحدة meta، التي تصفها الوثائق بأنها تجعل "Ansible يشغّل أي مهام معالجة تلقّت إشعاراً حتى الآن".

    - name: Run the notified handlers now instead of at the end of the play
      ansible.builtin.meta: flush_handlers

    - name: Wait for the new listener to accept connections
      ansible.builtin.wait_for:
        host: 127.0.0.1
        port: 8080
        timeout: 10

احذف سطر meta، وستُشغَّل المهمة wait_for بينما لا يزال nginx يقدّم الإعداد القديم. في التشغيل الأول، لا يوجد مستمع على المنفذ 8080 بعد، لذلك تنتظر المهمة مدة عشر ثوانٍ كاملة ثم تفشل.

الحالة الثانية هي الفشل. "إذا أرسلت مهمة إشعاراً إلى معالج، لكن فشلت مهمة أخرى لاحقاً في الـplay، فلن يعمل المعالج على ذلك المضيف افتراضياً، وقد يترك المضيف في حالة غير متوقعة." لذلك يترك الـplay الذي ينشئ ملف إعداد ثم يفشل بسبب مهمة غير مرتبطة الملف الجديد على القرص، بينما يظل الإعداد القديم محمّلاً في الخدمة قيد التشغيل. غيّر هذا السلوك باستخدام --force-handlers في سطر الأوامر، أو باستخدام force_handlers: true في الـplay. ويتوفر المفتاح نفسه باسم force_handlers = True ضمن [defaults] في ansible.cfg، وكذلك في متغير البيئة ANSIBLE_FORCE_HANDLERS. القيمة الافتراضية هي False.

تتعارض أسماء معالجات الأحداث، والخاسر لا يصدر أي رسالة

تنص الوثائق على القاعدة التالية: «يجب أن يكون لكل معالج اسم فريد على مستوى العالم. وإذا عُرِّفت عدة معالجات بالاسم نفسه، فلن يمكن إرسال الإشعار والتنفيذ إلا للمعالج الأخير الذي حُمّل في الـplay». ولا تكون المعالجات المعرّفة داخل role محصورة في ذلك الـrole أيضاً. إذ تُدرج في قائمة معالجات عامة واحدة تخص الـplay بأكمله، ولذلك إذا عرّف roleان كل منهما Restart nginx، فسيشير الاسم إلى واحد منهما فقط، ويحدد ترتيب التحميل أيّهما سيُستخدم، لا الـrole الذي أرسلت إليه الإشعار.

اختبر هذه القاعدة قبل الاعتماد عليها. احفظ ما يلي باسم handlers-dup.yml:

- name: Two handlers, one name
  hosts: local
  gather_facts: false

  tasks:
    - name: Notify the duplicated name
      ansible.builtin.command:
        cmd: /bin/true
      changed_when: true
      notify: Duplicated handler

  handlers:
    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-first
        state: touch
        mode: '0644'

    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-second
        state: touch
        mode: '0644'
rm -f /tmp/dup-first /tmp/dup-second
ansible-playbook handlers-dup.yml
ls -l /tmp/dup-first /tmp/dup-second

ينجح الـplay، ويظهر RUNNING HANDLER [Duplicated handler] مرة واحدة، ويطبع ls سطراً عن /tmp/dup-first وls: cannot access '/tmp/dup-second': No such file or directory عن الآخر. المعالج الذي نُفِّذ هو المكتوب أولاً، وليس الأخير الذي حُمّل، وهذا عكس ما تتوقعه تلك الجملة.

من المهم فهم هذا الفرق، لأن القاعدة الموثقة تتعلق بكتل المعالجات، لا بالأسطر الموجودة في ملف. المعالجات التي تصل من موضعين منفصلين، من role ثم من role آخر، تكون في كتلتين منفصلتين، وتحجب الكتلة اللاحقة الكتلة السابقة فعلاً. أما قائمة handlers: عادية داخل play فتُعد كتلة واحدة، ويجري البحث داخل الكتلة من الأعلى إلى الأسفل، ويتوقف عند أول اسم مطابق. لذلك تجيب التعريفات داخل ملف واحد بالتعريف الأول، بينما يتعذر الوصول إلى التعريف الثاني، وفي حالة الأدوار يعمل الحجب بالطريقة التي تصفها الوثائق. وفي كلتا الحالتين لن تتمكن من الوصول إلى كليهما، ولا ينبغي بناء الإعداد على أي من السلوكين.

هناك طريقتان واضحتان لحل المشكلة. امنح كل اسم معالج بادئة خاصة بالـrole، أو أرسل الإشعار إلى الصيغة المؤهلة role_name : handler_name، التي تذكرها الوثائق باعتبارها الطريقة «لضمان إرسال الإشعار إلى معالج من role بدلاً من معالج خارج الـrole له الاسم نفسه». وتُعد المسافات المحيطة بالنقطتين جزءاً من هذه الصيغة. تصبح هذه المشكلة عملية فور البدء في استخدام أدوار لم تكتبها بنفسك.

هناك قاعدة أخرى في الصفحة نفسها: «تجنب وضع المتغيرات في اسم المعالج. بما أن أسماء المعالجات تُعالج بالقوالب مبكراً، فقد لا تكون لدى Ansible قيمة متاحة لاسم معالج مثل هذا». يؤدي معالج باسم Restart {{ service_name }} إلى فشل الـplay بأكمله عندما لا يكون ذلك المتغير معرّفاً عند معالجة الاسم بالقالب. إن إبقاء أسماء المعالجات سلاسل ثابتة وتجميعها باستخدام listen يتجنب هذه المشكلة.

تحقّق: ارفض تثبيت ملف ناتج معطوب

يشغّل validate أمراً على الملف الناتج قبل أن ينقله Ansible إلى موضعه النهائي. توضّح الوثائق: "أمر التحقّق الذي يُشغَّل قبل نسخ الملف المحدّث إلى الوجهة النهائية. يُستخدم مسار ملف مؤقت للتحقّق، ويُمرَّر عبر %s الذي يجب أن يكون موجوداً كما في الأمثلة أدناه. كما يُمرَّر الأمر بأمان، لذلك لن تعمل ميزات الصَدَفة مثل التوسعة والأنابيب."

تنبثق قاعدتان مباشرة من هذا النص. إنّ %s إلزامي، وتؤدي سلسلة validate التي لا تتضمنه إلى فشل المهمة مع validate must contain %s. ولا توجد صدفة، لذلك لا تعمل الأنابيب وإعادة التوجيه وتوسعة الأنماط و&&. استخدم أمراً واحداً ومعامل ملف واحداً.

توضح أمثلة الوحدة الرسمية حالتين يعمل فيهما ذلك بصورة صحيحة تماماً:

- name: Copy a new sudoers file into place, after passing validation with visudo
  ansible.builtin.template:
    src: /mine/sudoers
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

- name: Update sshd configuration safely, avoid locking yourself out
  ansible.builtin.template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: /usr/sbin/sshd -t -f %s
    backup: yes

يعمل المثالان لأن كل أداة تحقّق تقبل ملفاً واحداً وتقيّمه وفق قواعدها الخاصة. يقرأ visudo -cf ملف sudoers. ويقرأ sshd -t -f ملف sshd_config كاملاً.

لماذا لا يستطيع validate التحقق من ملف nginx في هذا الدليل

أضف validate: /usr/sbin/nginx -t -c %s إلى مهمة template أعلاه، وستفشل المهمة. توضّح الرسالة السبب:

nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2

يتوقع nginx -t -c إعداداً كاملاً يبدأ من المستوى الأعلى بكتلتي events وhttp. الملف الذي تنشئه هذه الـplay هو جزء مقتطع، ويُضمَّن داخل كتلة http بواسطة include /etc/nginx/conf.d/*.conf; داخل /etc/nginx/nginx.conf. عند التعامل معه منفرداً، خارج هذا السياق، تكون upstream فعلاً directive في موضع غير صحيح، ولذلك يرفض nginx ملفاً صحيحاً تماماً في موضعه الفعلي. لقد مُرِّر إلى أداة التحقق جزء مقتطع، وطُلب منها التعامل معه كإعداد كامل.

الحل العملي هو الحل الموجود في الـplaybook أصلاً. ثبّت الجزء المقتطع، ثم تحقّق من الإعداد المُجمّع في handler معرّف قبل handler إعادة التحميل. وبما أن handlers تعمل بالترتيب الذي عُرِّفت به، يرى nginx -t ملف /etc/nginx/nginx.conf الفعلي مع تضمين الجزء المقتطع، ويؤدي الفشل هناك إلى فشل الـplay قبل استدعاء systemctl reload. انتبه إلى التكلفة: يكون الملف المعطوب على القرص عند فشل هذا التحقق، ويواصل nginx تقديم آخر إعداد حمّله إلى أن يعيد أحد تشغيله.

لهذا يستحق backup: true مكانه. فهو يكتب نسخة من الملف السابق بجوار الملف الأصلي قبل استبداله، ويسميها basename.PID.YYYY-MM-DD@HH:MM:SS~، ولذلك ينتهي الدليل بمدخلات مثل learn.conf.4127.2026-08-20@11:42:09~. شغّل sudo ls -l /etc/nginx/conf.d/ بعد إجراء تغيير، وستجد نسخة منها.

تفصيل التسمية هذا أهم مما يبدو. تكون النسخة الاحتياطية غير مؤذية في /etc/nginx/conf.d/ لأن الإعداد الرئيسي يضمّن conf.d/*.conf فقط، ولأن اسم النسخة الاحتياطية ينتهي بعلامة tilde. لكنها ليست غير مؤذية في دليل يُضمَّن باستخدام * مجردة، وفي Debian وUbuntu يضمّن /etc/nginx/nginx.conf ملف /etc/nginx/sites-enabled/* بهذه الطريقة تحديداً. إذا أنشأت القالب داخل sites-enabled باستخدام backup: true، فسيحمّل nginx النسخة الاحتياطية ككتلة server ثانية فعّالة، ولذلك تكتب هذه الـplay إلى conf.d بدلاً من ذلك.

تشغيل الـplay نفسه على مضيفي inventory الفعليين

غيّر hosts: local إلى اسم المجموعة الذي تستخدمه، ولا تغيّر أي شيء آخر في الـplay. يُنشئ القالب نسخة لكل مضيف، لذلك يمكن أن تأتي app_listen_port وapp_backends من group_vars وhost_vars، بينما يظل ملف القالب نفسه واحداً. هذه هي فائدة وضع القيم في المتغيرات بدلاً من وضعها في الملف.

يتغير شيئان. يحتاج become: true الآن إلى كلمة مرور sudo على كل هدف، ما لم يكن sudo بلا كلمة مرور مفعّلاً هناك، لذلك أضف -K. كذلك، يجب ألا يبقى أي سر في ذلك القالب، مثل كلمة مرور قاعدة بيانات أو رمز API، في صورة نص عادي داخل vars: في ملف تلتزم به في المستودع. شفّر هذه القيم باستخدام Ansible Vault، وأشر إليها بالاسم بالطريقة نفسها التي تستخدمها الآن، لأن القالب لا يهتم بمصدر المتغير.

عندما يتجاوز الـplay خدمة واحدة، تكون vars: وtemplates/ وhandlers: قد حصلت جميعاً على مكان قياسي جاهز لها. ونقلها إلى تلك الأماكن هو الهدف الكامل من الفصل بين playbook وrole.

FAQ

لماذا لم يُشغِّل معالج Ansible؟

يحدث ذلك تقريباً دائماً لأن المهمة التي تُخطره أبلغت عن ok بدلاً من changed. تُشغَّل المعالجات عند حدوث تغيير فقط، ولا تُشغَّل لسبب آخر. لذلك فإن مهمة قالب يتطابق ناتجها مع الملف الموجود على القرص لا تُخطر أي معالج. بعد ذلك، تحقّق من أربعة أمور. يجب أن تتطابق السلسلة في notify تماماً مع name الخاص بالمعالج أو مع موضوع listen، بما في ذلك حالة الأحرف والمسافات. تؤدي مهمة لاحقة فشلت على ذلك المضيف إلى منع المعالجات التي أُخطرت، ما لم تمرّر --force-handlers. لا يكون المعالج المعرّف في play مختلفة مرئياً من هذه الـplay. كما أن المهمة التي تُخطر المعالج، إذا تخطّتها حالة when، لا تُخطر أي معالج إطلاقاً.

لماذا يبلّغ ملف الـplaybook عن حدوث تغيير في كل تشغيل؟

النص الناتج ليس ثابتاً بين عمليات التشغيل. السبب الأكثر شيوعاً هو وجود طابع زمني في الناتج، وسلسلة ansible_managed مخصّصة تتضمن تاريخاً تفعل ذلك بالضبط. الأمر التالي الذي يجب التحقق منه هو mode وowner في المهمة. إذا لم تتطابق قيمتهما مع الملف الموجود على القرص، يصححهما Ansible ويبلّغ عن حدوث تغيير، حتى لو كان المحتوى متطابقاً. شغّل ansible-playbook site.yml --check --diff لمعرفة أيّ السببين ينطبق، لأن --diff يعرض الفرق الذي تنوي المهمة إجراؤه.

ما الفرق بين template وcopy في Ansible؟

ترسل ansible.builtin.copy الملف دون تغيير. أما ansible.builtin.template فتعالج الملف عبر Jinja2 على وحدة التحكم أولاً، ثم ترسل الناتج، ولذلك تُحل المتغيرات والحلقات قبل أن يصل الملف إلى المضيف الهدف. استخدم copy لملف متطابق بالبايتات على جميع المضيفات. استخدم template لأي ملف يختلف حسب المضيف. يشترك الخياران في خيارات الملفات نفسها، لذلك تعمل mode وowner وbackup وvalidate بالطريقة نفسها في كليهما.

كيف أجعل المعالج يعمل في منتصف الـplay؟

أضف ansible.builtin.meta: flush_handlers كمهمة عند النقطة التي تريد تشغيل المعالجات فيها. يؤدي ذلك إلى تشغيل كل معالج أُخطر حتى تلك اللحظة، ثم تتابع الـplay عملها بصورة طبيعية. استخدمه عندما تعتمد مهمة لاحقة في الـplay نفسها على أن تكون الخدمة قد شغّلت الإعداد الجديد، مثل wait_for على منفذ لا يصبح موجوداً إلا بعد إعادة التحميل. هذه هي الطريقة المعتمدة لتشغيل معالج قبل نهاية الـplay.

هل يمكنني استخدام validate مع جزء من إعدادات nginx؟

ليس مع nginx -t -c %s. يتوقع ذلك الأمر إعداداً كاملاً يبدأ بكتلتي events وhttp ذواتي المستوى الأعلى، لذلك يرفض جزء conf.d برسالة مثل "upstream" directive is not allowed here. يكون الجزء صالحاً داخل كتلة http، لكنه غير صالح بمفرده. ثبّت الملف، ثم شغّل nginx -t على الإعداد المجمّع داخل معالج معرّف قبل معالج إعادة التحميل. تُشغَّل المعالجات بالترتيب الذي عُرّفت به، لذلك يؤدي الإعداد غير الصالح إلى فشل الـplay قبل محاولة إعادة التحميل. اضبط backup: true في مهمة القالب حتى يبقى الملف السابق متاحاً لإعادته.

#ansible#jinja2#handlers#idempotence#automation