إدارة الإعدادات مع Ansible

مشروع: تهيئة أسطول من الخوادم

35 دقيقة الدرس 10 من 30

مشروع: تهيئة أسطول من الخوادم

كل ما بنيته في هذا الدرس التعليمي — تصميم المخزون، والأوامر العشوائية، والـ playbooks، والمتغيرات وقوالب Jinja2، والشروط والحلقات، والأدوار، وAnsible Vault، واستراتيجيات التوسع — تتقاطع في هذا المشروع الختامي. ستصمم وتنشر قاعدة كود Ansible كاملة وجاهزة للإنتاج تُهيّئ ثلاث مجموعات خوادم مختلفة: الويب (NGINX كـ reverse proxy)، والتطبيق (Node.js)، وقاعدة البيانات (PostgreSQL). هذا هو الهيكل الذي ستصادفه في الشركات الحقيقية التي تشغّل عشرات إلى مئات العقد.

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

هيكل المشروع: بنية الأدوار أولاً

الأساطيل الحقيقية منظمة حول الأدوار، لا الـ playbooks المسطحة. الهيكل الأساسي أدناه يفصل الاهتمامات بشكل نظيف: site.yml على مستوى الموقع هو نقطة الدخول؛ مخزونات لكل بيئة تعزل الإنتاج عن التجربة؛ الأدوار القابلة لإعادة الاستخدام تعيش تحت roles/؛ الأسرار مُشفّرة في group_vars.

fleet-config/ ├── ansible.cfg # إعدادات افتراضية على مستوى المشروع ├── site.yml # الـ playbook الرئيسي — يطبق جميع الأدوار ├── inventories/ │ ├── prod/ │ │ ├── hosts.ini # تعريفات مضيفي الإنتاج │ │ └── group_vars/ │ │ ├── all/ │ │ │ ├── vars.yml # متغيرات مشتركة (غير سرية) │ │ │ └── vault.yml # أسرار مشفّرة (ansible-vault) │ │ ├── web/ │ │ │ └── vars.yml # متغيرات خاصة بمجموعة الويب │ │ ├── app/ │ │ │ └── vars.yml │ │ └── db/ │ │ └── vars.yml │ └── staging/ │ └── ... # يعكس هيكل الإنتاج ├── roles/ │ ├── base/ # مطبّق على جميع المضيفين │ │ ├── tasks/main.yml │ │ ├── handlers/main.yml │ │ └── templates/ │ │ └── sshd_config.j2 │ ├── web/ │ │ ├── tasks/main.yml │ │ ├── handlers/main.yml │ │ └── templates/ │ │ └── nginx.conf.j2 │ ├── app/ │ │ ├── tasks/main.yml │ │ ├── handlers/main.yml │ │ └── templates/ │ │ └── app.env.j2 │ └── db/ │ ├── tasks/main.yml │ ├── handlers/main.yml │ └── templates/ │ └── pg_hba.conf.j2 └── requirements.yml # تبعيات مجموعات Galaxy
لماذا inventories/prod/group_vars/all/vault.yml بدلاً من vault على مستوى الجذر؟ الاحتفاظ بالـ vault داخل مجلد المخزون يعني أن تشغيل ansible-playbook -i inventories/prod site.yml يحمّل تلقائياً الأسرار الصحيحة لتلك البيئة. ملف vault واحد على مستوى الجذر سيكون مشتركاً عبر جميع البيئات، مما يجعل التعرض العرضي للأسرار عبر البيئات خطراً حقيقياً. ملفات vault لكل مخزون تُلغي الخطأ على المستوى الهيكلي.

المخزون: تجميع الأسطول

يُعرّف ملف hosts.ini ثلاث مجموعات خوادم بالإضافة إلى مجموعة مركّبة fleet تمتد عبر جميعها. المجموعة المركّبة يستخدمها دور base — مطبّق على كل عقدة بصرف النظر عن وظيفتها.

# inventories/prod/hosts.ini [web] web-01.prod.example.com ansible_user=deploy web-02.prod.example.com ansible_user=deploy [app] app-01.prod.example.com ansible_user=deploy app-02.prod.example.com ansible_user=deploy app-03.prod.example.com ansible_user=deploy [db] db-primary.prod.example.com ansible_user=deploy db_role=primary db-replica-01.prod.example.com ansible_user=deploy db_role=replica db-replica-02.prod.example.com ansible_user=deploy db_role=replica # مجموعة مركّبة: جميع المضيفين المدارين [fleet:children] web app db

دور Base: تصليب كل عقدة

يفرض دور base خط الأساس على مستوى نظام التشغيل الذي يجب أن تشترك فيه كل عقدة في الأسطول — بصرف النظر عن تشغيل NGINX أو Node.js أو PostgreSQL. هنا تفرض تصليب SSH وauditd وضبط sysctl وNTP وعميل المراقبة المشترك. تطبيق خط أساس شامل عبر مجموعة مركّبة يمنع فئة الحادثة التي تكون فيها قاعدة البيانات الإنتاجية ذات إعداد SSH أضعف من طبقة الويب لأن شخصاً ما نسي تطبيق playbook التصليب عليها.

# roles/base/tasks/main.yml --- - name: Ensure essential packages are installed ansible.builtin.package: name: - curl - vim - htop - auditd - chrony - fail2ban state: present - name: Harden SSH daemon config ansible.builtin.template: src: sshd_config.j2 dest: /etc/ssh/sshd_config owner: root group: root mode: "0600" validate: /usr/sbin/sshd -t -f %s notify: Restart sshd - name: Set sysctl parameters for network performance ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" state: present reload: true loop: - { key: net.core.somaxconn, value: "65535" } - { key: net.ipv4.tcp_tw_reuse, value: "1" } - { key: vm.swappiness, value: "10" } - name: Ensure auditd is running and enabled ansible.builtin.service: name: auditd state: started enabled: true - name: Deploy deploy user authorized key ansible.posix.authorized_key: user: deploy key: "{{ deploy_ssh_public_key }}" exclusive: true # roles/base/handlers/main.yml --- - name: Restart sshd ansible.builtin.service: name: sshd state: restarted
معامل validate في قالب sshd_config غير قابل للتفاوض في الإنتاج. بدونه، خطأ في Jinja2 أو خطأ إملائي في القالب ينشر sshd_config غير صحيح نحوياً إلى القرص، ينبّه الـ handler لإعادة تشغيل sshd، يرفض sshd البدء، وأنت محظور من الدخول إلى العقدة. السطر validate: /usr/sbin/sshd -t -f %s يشغّل sshd -t (اختبار الإعداد) على الملف المُقدَّم قبل نقله إلى /etc/ssh/sshd_config. إذا فشل الاختبار، تفشل المهمة ويبقى الإعداد القديم في مكانه. هذا السطر الواحد أنقذ لا حصر لها من عمليات حظر SSH الإنتاجية.

دور Web: NGINX كـ Reverse Proxy

يثبّت دور الويب NGINX وينشر إعداد Virtual Host مُقدَّراً باستخدام Jinja2. يستخدم القالب متغيرات المجموعة لملء عناوين خوادم التطبيق الأمامية بشكل ديناميكي — لا عناوين IP مُشفَّرة في أي قالب.

# roles/web/tasks/main.yml --- - name: Install NGINX ansible.builtin.package: name: nginx state: present - name: Remove default NGINX site ansible.builtin.file: path: /etc/nginx/sites-enabled/default state: absent notify: Reload nginx - name: Deploy main NGINX config ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/sites-available/app.conf owner: root group: root mode: "0644" validate: /usr/sbin/nginx -t -c %s notify: Reload nginx - name: Enable app site ansible.builtin.file: src: /etc/nginx/sites-available/app.conf dest: /etc/nginx/sites-enabled/app.conf state: link notify: Reload nginx - name: Ensure NGINX is running and enabled ansible.builtin.service: name: nginx state: started enabled: true # roles/web/handlers/main.yml --- - name: Reload nginx ansible.builtin.service: name: nginx state: reloaded

يبني قالب Jinja2 المقابل كتلة upstream بشكل ديناميكي من المخزون الحي — هذا هو النمط الأقوى في إدارة إعدادات الأسطول. يُعاد إعداد NGINX تلقائياً عند إضافة أو إزالة خادم تطبيق؛ لا تلمس القالب أبداً.

{# roles/web/templates/nginx.conf.j2 #} upstream app_cluster { {% for host in groups['app'] %} server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }}; {% endfor %} } server { listen 80; listen [::]:80; server_name {{ web_server_name }}; location / { proxy_pass http://app_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 60s; } location /health { access_log off; return 200 "ok\n"; add_header Content-Type text/plain; } }

دور App: خادم تطبيق Node.js

يثبّت دور التطبيق Node.js، وينشر التطبيق من أرتفاكت مُصدَّر بإصدار، ويدير وحدة systemd التي تُبقيه يعمل. يتم حقن أسرار التطبيق (كلمة مرور قاعدة البيانات، JWT secret) في وقت النشر من Ansible Vault إلى ملف .env لا يُودَع أبداً في التحكم بالمصدر.

# roles/app/tasks/main.yml --- - name: Add NodeSource GPG key ansible.builtin.rpm_key: key: https://rpm.nodesource.com/gpgkey/ns-operations@nodesource.com.gpg.key state: present - name: Install Node.js 20 LTS ansible.builtin.package: name: nodejs state: present - name: Create app user ansible.builtin.user: name: "{{ app_user }}" system: true shell: /sbin/nologin create_home: false - name: Create app directory ansible.builtin.file: path: "{{ app_dir }}" state: directory owner: "{{ app_user }}" group: "{{ app_user }}" mode: "0755" - name: Deploy application environment file ansible.builtin.template: src: app.env.j2 dest: "{{ app_dir }}/.env" owner: "{{ app_user }}" group: "{{ app_user }}" mode: "0600" # أسرار: قراءة المالك فقط notify: Restart app - name: Deploy systemd unit ansible.builtin.template: src: app.service.j2 dest: /etc/systemd/system/{{ app_service_name }}.service owner: root group: root mode: "0644" notify: - Reload systemd - Restart app - name: Ensure app service is running and enabled ansible.builtin.service: name: "{{ app_service_name }}" state: started enabled: true

يسحب قالب .env من المتغيرات المشفّرة بـ Vault. وضع الملف 0600 يضمن أن عملية app_user فقط هي من تستطيع قراءته — انضباط يمنع تسرب الأسرار إلى السجلات المقروءة للجميع أو العمليات الأخرى على نفس المضيف.

دور DB: إعداد PostgreSQL بصرياً

دور قاعدة البيانات هو الأكثر حساسية. يجب أن يتعامل مع التمييز بين الأساسي والنسخة الاحتياطية (التي تم التقاطها كمتغير مضيف في المخزون)، ويُهيّئ PostgreSQL مرة واحدة فقط (لا في كل تشغيل playbook)، ويُعدّ pg_hba.conf للسماح باتصالات طبقة التطبيق مع حجب كل شيء آخر.

# roles/db/tasks/main.yml --- - name: Install PostgreSQL 16 ansible.builtin.package: name: - postgresql16-server - postgresql16 state: present - name: Check if PostgreSQL is already initialized ansible.builtin.stat: path: /var/lib/pgsql/16/data/PG_VERSION register: pg_initialized - name: Initialize PostgreSQL (first run only) ansible.builtin.command: /usr/pgsql-16/bin/postgresql-16-setup initdb when: not pg_initialized.stat.exists notify: Start postgresql - name: Deploy pg_hba.conf ansible.builtin.template: src: pg_hba.conf.j2 dest: /var/lib/pgsql/16/data/pg_hba.conf owner: postgres group: postgres mode: "0600" notify: Reload postgresql - name: Ensure postgresql is running and enabled ansible.builtin.service: name: postgresql-16 state: started enabled: true - name: Create application database community.postgresql.postgresql_db: name: "{{ db_name }}" state: present become_user: postgres when: db_role == 'primary' - name: Create application database user community.postgresql.postgresql_user: name: "{{ db_app_user }}" password: "{{ db_app_password }}" # من vault.yml priv: "{{ db_name }}.*:ALL" state: present become_user: postgres when: db_role == 'primary'

الـ Playbook الرئيسي: تجميع كل شيء

يُنسّق site.yml كل شيء. يطبق دور base على مجموعة fleet بالكامل أولاً، ثم يطبق أدوار خاصة بالطبقات في مسرحيات منفصلة. الترتيب مهم: يجب أن يكون التصليب الأساسي في مكانه قبل تثبيت أي خدمة، ويجب أن تكون قاعدة البيانات قابلة للوصول قبل أن تبدأ خوادم التطبيق.

# site.yml --- - name: Apply baseline hardening to all fleet nodes hosts: fleet become: true gather_facts: true roles: - base - name: Configure web tier (NGINX reverse proxy) hosts: web become: true gather_facts: true roles: - web - name: Configure app tier (Node.js application) hosts: app become: true gather_facts: true roles: - app - name: Configure database tier (PostgreSQL) hosts: db become: true gather_facts: true roles: - db
Fleet configuration: role-based architecture for web, app, and database tiers Control Node site.yml ansible-playbook WEB TIER role: base + web web-01 web-02 NGINX reverse proxy APP TIER role: base + app app-01 app-02 app-03 Node.js workers DB TIER role: base + db primary read-write replicas x2 read-only PostgreSQL 16 HTTP 80/443 Internet traffic proxy_pass :3000 PostgreSQL :5432 Plays in site.yml Play 1: fleet -> base role Play 2: web role Play 3: app role Play 4: db role
معمارية الأسطول: يطبّق site.yml الواحد دور base على جميع العقد، ثم يفوّض أدوار خاصة بالطبقة إلى مجموعات web وapp وdb بالتسلسل.

تشغيل الأسطول: سير عمل النشر الكامل

مع اكتمال قاعدة الكود، يتبع سير عمل النشر نمطاً صارماً من ثلاث مراحل: المعاينة، والتحقق، والتطبيق. لا تشغّل site.yml على الإنتاج أبداً بدون تمريرة جافة أولاً.

# 1. تثبيت تبعيات مجموعات Galaxy أولاً ansible-galaxy collection install -r requirements.yml # 2. فحص البنية — يكشف أخطاء YAML والمتغيرات غير المعرّفة قبل لمس المضيفين ansible-playbook -i inventories/prod site.yml --syntax-check # 3. تشغيل جاف على جميع مضيفي الإنتاج — راجع كل مهمة "changed" ansible-playbook -i inventories/prod site.yml \ --ask-vault-pass \ --check --diff # 4. التشغيل الحي — طبّق على staging أولاً، ثم الإنتاج ansible-playbook -i inventories/staging site.yml --ask-vault-pass ansible-playbook -i inventories/prod site.yml --ask-vault-pass # 5. استهداف طبقة واحدة للتغييرات التدريجية (مثل تحديث إعداد NGINX) ansible-playbook -i inventories/prod site.yml \ --ask-vault-pass \ --limit web \ --tags web # 6. حد الطوارئ: استهداف مضيف واحد أثناء فرز الحوادث ansible-playbook -i inventories/prod site.yml \ --ask-vault-pass \ --limit web-01.prod.example.com \ --tags web \ --check --diff

أنماط فشل الإنتاج وكيفية منعها

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

  • التطبيق الجزئي للأسطول عند خطأ الشبكة. يطبّق Ansible المهام مضيفاً تلو الآخر في دفعات متوازية. انقطاع SSH العابر على ثلاث عقد يضعها في UNREACHABLE، لكن العقد الأخرى تُهيَّأ. استخدم serial: "20%" في المسرحيات عالية نطاق التأثير حتى تكون الإخفاقات محدودة، وتحقق من عدد failed_hosts في AWX قبل المتابعة إلى الدفعة التالية.
  • عدم تطابق كلمة مرور Vault بين البيئات. vault التجربة والإنتاج يستخدمان كلمات مرور مختلفة. إذا استخدمت --vault-password-file مشيراً إلى الملف الخاطئ، تفكّ تشفير vault التجربة بمفتاح الإنتاج وتحصل على خطأ قيمة خاطئة صامت (المتغير يُحلّ لكن يحتوي على بيانات غير صالحة). استخدم علامات --vault-id منفصلة: --vault-id prod@~/.vault-prod يجعل مصدر كلمة المرور صريحاً والتعارض صاخباً.
  • تخطّي القالب لكتلة بصمت أثناء التقدير. حلقة Jinja2 {% for host in groups['app'] %} التي تشير إلى hostvars[host]['ansible_default_ipv4']['address'] تُولّد بصمت كتلة upstream فارغة إذا كان جمع الحقائق معطّلاً (gather_facts: false) للمجموعة app. شغّل دائماً gather_facts: true لأي مسرحية تشير قوالبها إلى hostvars، وأضف مهمة assert تتحقق من أن الملف المُقدَّر غير فارغ.
  • عدم إطلاق الـ handler بعد مهمة فاشلة. تُطلَق الـ handlers فقط إذا اكتملت المسرحية بدون فشل على ذلك المضيف. فشل مهمة في منتصف المسرحية يُكتم جميع الـ handlers المعلّقة — مما يعني نشر قالب لكن عدم إعادة تحميل الخدمة أبداً. استخدم force_handlers: true على مستوى المسرحية لـ handlers إعادة تحميل الخدمة لضمان إعادة تحميل بذل الجهود الكامل حتى عند الفشل الجزئي، وراقب صحة الخدمة بشكل منفصل.
ضع علامات على كل مهمة ودور للاستهداف الجراحي. ضع علامة على مهام دور base بـ tags: [base, hardening]، ومهام الويب بـ tags: [web, nginx]، وهكذا. في الإنتاج، لن تُعيد تشغيل site.yml الكامل أبداً تقريباً — ستشغّل --tags nginx لدفع تغيير إعداد NGINX، أو --tags hardening للاستجابة لـ CVE. وضع العلامات هو ما يفصل قاعدة كود Ansible يمكنك استخدامها بثقة في الإنتاج عن واحدة تُرهب فريقك لأن كل تغيير يتطلب تشغيل كل شيء.

التطبيق المستمر: الجدولة عبر AWX

في الإنتاج، تشغيلات ansible-playbook اليدوية مخصصة للتغييرات الفردية والاستجابة للحوادث. تطبيق خط الأساس المستمر يُعالَج بجدولة تشغيل site.yml الكامل كل 30 دقيقة في AWX. كل تشغيل مُسجَّل وناتجه قابل للبحث، والمضيفون الفاشلون يُطلقون تنبيهات PagerDuty عبر نظام إخطار AWX. يُقارب هذا ضمانات تصحيح الانجراف لأداة مبنية على Pull — بدون التعقيد التشغيلي لإدارة بنية Puppet.

الـ playbook الذي بنيته في هذا المشروع هو النمط الكامل الذي تستخدمه فرق البنية التحتية على نطاق واسع. من هذا الأساس يمكنك التوسع: إضافة دور monitoring ينشر مُصدِّر عقدة Prometheus، ودور logship يُعدّ Fluent Bit، ودور certs يُدوّر شهادات TLS عبر Vault PKI — كلها تتبع نفس هيكل الدور، ونفس اتفاقية المخزون، ونفس انضباط التشغيل الجاف أولاً.