تفاوت Ansible playbook و role؛ چه زمانی از کدام استفاده
تفاوت کاربردی Ansible playbook و role را بیاموزید. بفهمید چه زمانی ساختار تخت کافی است و چه زمانی باید با دستور ansible-galaxy init از نقشها استفاده کنید.
تفاوت Ansible playbook و role چیست
یک Ansible playbook فایلی است که آن را با ansible-playbook اجرا میکنید. این فایل گروهی از میزبانها را به کارهایی که باید انجام دهند، نگاشت میکند. یک Ansible role دایرکتوری با ساختار ثابت است که شامل taskها، templateها، handlerها و متغیرهای پیشفرض است و یک playbook آن را با نام فراخوانی میکند. نحو (syntax) دستورات در هر دو یکسان است، بنابراین این پرسش درباره آنچه میتوانید پیادهسازی کنید نیست؛ بلکه موضوع بر سر قابلیت استفاده مجدد است.
با یک playbook تخت (flat) شروع کنید. یک site.yml که شامل یک لیست tasks: باشد، ساختار مناسبی برای اولین اتوماسیون شماست و بیش از آنچه اکثر افراد تصور میکنند، کارآمد باقی میماند. زمانی به role روی بیاورید که همان بلوک از دستورات باید برای گروه دوم میزبانها اجرا شود، یا زمانی که فایل از حدود 100 خط فراتر رفت و دیگر نمیتوانید با اسکرول کردن، یک task خاص را پیدا کنید.
اگر هنوز اولین مورد خود را ننوشتهاید، با یک playbook اولیه روی یک VPS تکی شروع کنید و زمانی که حجم آن افزایش یافت، به اینجا بازگردید.
چه زمانی یک playbook تخت، انتخاب درستی است
یک playbook تخت زمانی مناسب است که کار فقط یک بار انجام شود، یا روی یک میزبان واحد باشد، یا قرار نباشد شخص دیگری آن را بخواند. آمادهسازی یک سرور برنامه واحد، یا وصلهکردن یک سیستم پیش از پنجرهٔ نگهداری: هیچکدام از این موارد نیازی به ساختار درختی دایرکتوری ندارند. یک role هفت دایرکتوری و یک لایه ارجاع غیرمستقیم اضافه میکند. اگر تنها فراخوانندهٔ آن، همان playbook کنارش باشد، این ارجاع غیرمستقیم هیچ سودی ندارد و تنها باعث میشود هر بار که میخواهید بدانید چه چیزی واقعاً اجرا میشود، مجبور به یک پرش اضافی شوید.
یک playbook تخت در لحظهٔ خاصی دیگر انتخاب درستی نخواهد بود و تشخیص آن لحظه آسان است. زمانی که یک بلوک از taskها را در یک playbook دوم کپی میکنید، آن کپی، نشانهٔ تغییر است. از آن لحظه به بعد، هر اصلاح باید دو بار انجام شود و یک روز فرا میرسد که آن اصلاح فقط یک بار اعمال خواهد شد.
محتوای واقعی یک دایرکتوری نقش (role)
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymlنقطه ورود است. زمانی که نقش فراخوانی میشود، Ansible این فایل را اجرا میکند و تمامی دایرکتوریهای دیگر اختیاری هستند.defaults/main.ymlمتغیرهایی را در خود جای میدهد که انتظار میرود فراخواننده آنها را بازنویسی (override) کند. این دایرکتوری پایینترین اولویت را در Ansible دارد، بنابراین تقریباً هر منبع دیگری بر آن مقدم است.vars/main.ymlمتغیرهایی را نگه میدارد که انتظار نمیرود فراخواننده آنها را بازنویسی کند. اولویت این دایرکتوری بالاتر از inventory است که یک اولویت بسیار قوی محسوب میشود. از آن به ندرت استفاده کنید.handlers/main.ymlوظایفی (tasks) را نگه میدارد که توسطnotifyفراخوانی میشوند. یک handler در پایان play، بدون توجه به تعداد دفعاتی که توسط وظایف مختلف فراخوانی شده، تنها یک بار اجرا میشود.files/فایلهایی را نگه میدارد که عیناً توسط ماژولcopyکپی میشوند وtemplates/شامل قالبهای Jinja2 است که توسط ماژولtemplateرندر میشوند. در داخل یک نقش، شما به هر دو فقط با نام فایل و بدون مسیر ارجاع میدهید، زیرا Ansible ابتدا دایرکتوریهای خودِ نقش را جستجو میکند.meta/main.ymlوابستگیهای نقش و متادیتایی که Ansible Galaxy میخواند را تعریف میکند.
این ساختار یک ترجیح سلیقهای نیست. Ansible دقیقاً در همین مسیرها جستجو میکند، بنابراین قالبی که در roles/common/template/ (به صورت مفرد) قرار دهید، هرگز پیدا نخواهد شد.
ساخت نقش عمومی با ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonاین دستور کل ساختار اسکلتی را در roles/common ایجاد میکند، که شامل دایرکتوریهایی است که از آنها استفاده نخواهید کرد و فایلهای main.yml که فقط حاوی --- هستند. مواردی را که خالی میگذارید، حذف کنید. یک vars/main.yml خالی برای Ansible بیضرر است، اما باعث میشود تشخیص اینکه کدام فایلها در نقش واقعاً اهمیت دارند، دشوار شود.
اکنون فایلهایی را که وظایف اصلی را انجام میدهند، تکمیل کنید. ابتدا با defaults شروع کنید، زیرا آنها رابط عمومی نقش هستند.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"مقادیر "no" و "yes" را داخل کوتیشن قرار دهید. Ansible فایلهای YAML را با PyYAML تجزیه میکند که یک no بدون کوتیشن را به عنوان مقدار بولی false میخواند؛ در نتیجه خط پیکربندی نهایی به PermitRootLogin False تبدیل شده و sshd آن را رد میکند. کوتیشنها باعث میشوند مقدار به صورت رشته باقی بماند.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}در Debian و Ubuntu، واحد systemd با نام ssh شناخته میشود، در حالی که در سیستمهای خانواده RHEL، این نام sshd است. هندلری که نام اشتباه را فراخوانی کند، تنها زمانی با خطا مواجه میشود که تغییری در قالب (template) ایجاد شود؛ به همین دلیل است که این مشکل معمولاً هفتهها بعد نمایان میشود.
خط validate مفیدترین بخش در آن task است. Ansible قالب را به یک فایل موقت تبدیل میکند، مسیر آن فایل را جایگزین %s کرده و دستور را اجرا میکند. فایل مقصد تنها در صورتی جایگزین میشود که دستور با کد خروجی 0 پایان یابد. یک دستور نامعتبر در قالب قرار دهید و دوباره اجرا کنید: task با خطای failed to validate شکست میخورد، فایل /etc/ssh/sshd_config.d/99-hardening.conf اصلی دستنخورده باقی میماند و شما همچنان به سرور دسترسی خواهید داشت. توجه داشته باشید که این بررسی، فراتر از سینتکس شما را تست میکند. اگر sshd -t نتواند کلیدهای میزبان را بخواند، با کد sshd: no hostkeys available -- exiting. خارج میشود و Ansible همان خطای failed to validate را گزارش میدهد؛ بنابراین پیش از مقصر دانستن قالب، msg مربوط به ماژول را مطالعه کنید.
نحوه فراخوانی یک نقش توسط پلیبوک
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlپلی باید در نهایت با failed=0 در خلاصه اجرا پایان یابد. پارامترها را در محل فراخوانی با استفاده از فرم بسطیافته ارسال کنید؛ این همان روشی است که یک نقش میتواند به دو گروه از میزبانها خدمترسانی کند:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordیک قانون ترتیببندی وجود دارد که تقریباً همه را غافلگیر میکند. یک پلی میتواند شامل pre_tasks، roles، tasks و post_tasks باشد و Ansible آنها را به همین ترتیب اجرا میکند، صرفنظر از اینکه شما آنها را با چه ترتیبی در فایل نوشته باشید. حتی اگر tasks: را بالاتر از roles: قرار دهید، نقشها همچنان اول اجرا میشوند. بنابراین اگر چیزی باید پیش از یک نقش اتفاق بیفتد، آن کار متعلق به pre_tasks: است، نه ابتدای tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postبرای فراخوانی یک نقش از داخل لیست وظایف (task list) بهجای استفاده از کلید roles:، از import_role یا include_role استفاده کنید.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role ایستا (static) است. Ansible نقش را در زمان تجزیه (parse time) میخواند و وظایف آن بخشی از پلی میشوند؛ بنابراین ansible-playbook --list-tasks site.yml آنها را فهرست میکند و یک تگ روی import به تمام وظایف داخل آن اعمال میشود. include_role پویا (dynamic) است. تا زمانی که وظیفه اجرا نشود، هیچچیز خوانده نمیشود؛ این همان قابلیتی است که به شما اجازه میدهد نام نقش را از طریق یک متغیر یا حلقه تعیین کنید. هزینه این کار این است که آن وظایف برای --list-tasks و --start-at-task نامرئی هستند.
یک تله در اینجا وجود دارد. یک when: روی یک وظیفه include_role پیش از آنکه defaults/main.yml نقشِ گنجاندهشده در محدوده (scope) قرار بگیرد، ارزیابی میشود. اگر when: common_packages | length > 0 را روی include بنویسید، اجرا با خطای 'common_packages' is undefined متوقف میشود، حتی اگر آن متغیر در همان نقشی که در حال گنجاندن آن هستید تعریف شده باشد. راهحل این است که این سوئیچ را از نقش خارج کنید: آن را در group_vars/all.yml قرار دهید، جایی که در همه جا در دسترس است، و مقادیر پیشفرض نقش را برای مواردی بگذارید که خودِ نقش از آنها استفاده میکند.
کدام متغیر اولویت دارد: defaults، group_vars، vars، یا extra vars
Ansible بیش از بیست سطح اولویت برای متغیرها تعریف کرده است. چهار مورد از آنها تقریباً هر بحثی را در عمل حلوفصل میکنند؛ در اینجا آنها را از ضعیفترین به قویترین مرتب کردهایم.
roles/<name>/defaults/main.ymlدر پایینترین سطح قرار دارد. تقریباً هر مقداری که در جای دیگری تعیین کنید، بر آن غلبه میکند؛ به همین دلیل است که این بخش، بهترین مکان برای تنظیمات قابلتغییر (tunable knobs) یک role است.group_vars/وhost_vars/در سطح میانی هستند. اینجاست که پاسخهای اختصاصی سایت شما قرار میگیرند و بهراحتی مقادیر پیشفرض role را بازنویسی (override) میکنند.roles/<name>/vars/main.ymlبالاتر ازhost_varsقرار دارد. مقداری که در اینجا قرار میدهید، از طریق inventory قابل بازنویسی نیست. این بخش را برای مواردی رزرو کنید که role برای حفظ سازگاری داخلی به آن نیاز دارد؛ مانند نام بستهای که باید با نام سرویس مطابقت داشته باشد.- پارامتر role که در محل فراخوانی ارسال میشود، بر
vars/main.ymlغلبه میکند و-eدر خط فرمان، بر همه چیز—از جمله پارامترهای role—پیروز میشود.
شما میتوانید این فرآیند حلوفصل را در حدود یک دقیقه مشاهده کنید. به یک role کوچک، یک مقدار پیشفرض و یک role var بدهید، سپس همان نامها را در host_vars تنظیم کنید.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliاجرای اول مقدار tunable=from-hostvars internal=from-rolevars را چاپ میکند. Inventory بر مقدار پیشفرض role غلبه کرد اما در برابر role var شکست خورد. اجرای دوم مقدار internal=from-cli را چاپ میکند، زیرا extra vars در بالاترین سطح قرار دارند و هیچچیز در سطوح پایینتر نمیتواند با آن مقابله کند. به همین دلیل است که -e برای یک اجرای موردی مناسب است، اما در اسکریپتی که نگهداری میکنید، اشتباه است: این روش بهطور بیصدا بر تمام تصمیمات سنجیدهشده در مخزن (repository) شما اولویت مییابد.
قانون عملی این است: اگر میخواهید مقداری قابلتغییر باشد، آن را در defaults/ قرار دهید. قرار دادن آن در vars/ به هر کاربر آیندهٔ این role میگوید که inventory ممکن است نتواند آن را تغییر دهد. گاهی اوقات این همان چیزی است که مد نظر شماست، اما معمولاً یک اشتباه سهوی است.
اثبات همتوان (Idempotent) بودن نقش: اجرای دوبار
یک اجرای قابلاعتماد Ansible باید در مرتبهٔ دوم همان نتیجه را تولید کند و گزارش دهد که هیچ تغییری ایجاد نشده است. Playbook را دو بار اجرا کنید و خلاصهٔ آن را بخوانید.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlخلاصهٔ دوم باید به این شکل باشد:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 به این معناست که هر ماژول وضعیت فعلی را بررسی کرده و متوجه شده است که کار از قبل انجام شده است. changed=2 در اجرای دوم نشان میدهد که دو تسک نمیتوانند تفاوت را تشخیص دهند، بنابراین آنها به بازنویسی فایلها و راهاندازی مجدد سرویسها برای همیشه ادامه خواهند داد. مقصر معمولاً command یا shell است، زیرا Ansible راهی برای دانستن اینکه یک دستور دلخواه چه کاری انجام داده است ندارد.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtآن Playbook را دو بار اجرا کنید، سپس خطوط دارای wc -l /tmp/grow.txt /tmp/guarded.txt را بشمارید. /tmp/grow.txt شامل دو خط و /tmp/guarded.txt شامل یک خط است. در اجرای دوم، تسک محافظتشده اصلاً اجرا نشد و نتیجهٔ آن حاوی پیام skipped, since /tmp/guarded.txt exists است، زیرا creates یک محصول قابلمشاهده به ماژول میدهد تا ابتدا آن را جستجو کند. هنگامی که یک دستور چنین محصولی باقی نمیگذارد، خروجی آن را ثبت کنید و خودتان با استفاده از changed_when تصمیم بگیرید.
ansible-playbook --check --diff site.yml تغییرات را بدون اعمال آنها پیشبینی میکند و --diff خطوط دقیقی را که یک template بازنویسی میکند، چاپ میکند. خروجی را با در نظر گرفتن یک نکته بخوانید: تسکهای shell و command در حالت check mode نادیده گرفته میشوند، بنابراین برنامهای که تمیز به نظر میرسد، همچنان میتواند کاری را پنهان کند.
Why does Ansible say the role was not found
Ansible looks for a roles/ directory next to the playbook file, then in roles_path. The search follows the playbook, not your shell.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyThat message means site.yml and roles/ have drifted apart, and it helpfully prints the paths it tried. Keep the two in the same directory. Running from a parent directory is fine, because the playbook path is what counts:
ansible-playbook -i infra/inventory.ini infra/site.ymlThere is a quieter version of the same problem. Ansible ignores an ansible.cfg in the current directory when that directory is world writable, because any user on the box could drop a config there and change what your run does.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Your roles_path and inventory settings are then silently absent, and the role lookup fails for a reason that has nothing to do with roles. ansible --version prints the config file it actually loaded, and ansible-config dump --only-changed prints every setting that differs from the built-in defaults. Check both whenever a run behaves as if your config does not exist.
اشتراکگذاری نقشها: requirements.yml و نسخه ثابت (pinned)
نقشی که توسط شخص دیگری نوشته شده است، باید نصب شود و نه کپی. آن را یکبار تعریف کنید:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesهمیشه version را تنظیم کنید. بدون آن، شما هر نسخهای که شاخه پیشفرض در روز اجرای دستور داشته باشد را دریافت میکنید؛ بنابراین استقراری که ماه گذشته کار میکرد، بدون هیچ تغییری در مخزن شما، از کار میافتد. مقدار roles_path را به دایرکتوری دانلود اشاره دهید و آن دایرکتوری را از git دور نگه دارید:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesنقشهای موجود در roles/ در کنار playbook همچنان پیدا میشوند، زیرا آن مسیر همیشه علاوه بر roles_path جستجو میشود. بنابراین نقشهای خودتان در مخزن باقی میمانند و بازبینی میشوند، در حالی که نقشهای شخص ثالث، دانلودهای قابل بازتولید هستند که به یک tag خاص متصل (pinned) شدهاند.
جایی که نقشها دیگر پاسخگو نیستند
یک نقش (role) واحدی برای استفادهٔ مجدد در یک اجرای Ansible است. این نقش سرورها یا رکوردهای DNS را در ارائهدهندهٔ خدمات شما ایجاد نمیکند و تلاش برای وادار کردن آن به انجام این کار، باعث میشود که playbookها به چیزی تبدیل شوند که نگهداری آن برای هیچکس مطلوب نیست. مطالعهٔ تقسیم کار بین Ansible و Terraform پیش از شروع، ارزشمند است. همچنین یک نقش جایگزین طراحی inventory نمیشود: هنگامی که تعداد ماشینها از چند عدد فراتر میرود، نحوهٔ گروهبندی و دسترسی به آن سرورها اهمیت بیشتری نسبت به نحوهٔ دستهبندی taskها پیدا میکند.
ایمنسازی (hardening) که این نقش common نصب میکند نیز نیازمند تصمیمگیریهای خاص خود است. تنظیمات ارائهشده در بالا فقط دو دستورالعمل را تعیین میکند و نه بیشتر؛ بنابراین پیش از آنکه تصمیم بگیرید چه چیزی برای هر میزبانی که در اختیار دارید در نقش قرار بگیرد، اینکه کدام تنظیمات SSH واقعاً ارزش تغییر دارند و چگونه Ubuntu را وادار به اعمال خودکار بهروزرسانیهای امنیتی کنیم را مطالعه کنید.
FAQ
چه زمانی باید یک Ansible playbook را به یک role تبدیل کنم؟
زمانی که همان بلوک از وظایف (tasks) باید در یک play دوم یا روی گروه دوم از میزبانها اجرا شود. کپی کردن وظایف بین playbookها یک هشدار است، زیرا از آن لحظه به بعد، هر اصلاح باید دو بار اعمال شود و یک روز حتماً فراموش میکنید یکی از آنها را اعمال کنید. یک playbook واحد با حدود کمتر از 100 خط که همیشه فقط یک گروه را هدف قرار میدهد، از تبدیل شدن به role سودی نمیبرد و دایرکتوریهای اضافی فقط خواندن آن را دشوارتر میکنند.
آیا roleها قبل از tasks در همان play اجرا میشوند؟
بله. Ansible ابتدا pre_tasks، سپس تمام موارد لیستشده در roles:، سپس tasks: و در نهایت post_tasks: را اجرا میکند و به ترتیبی که این کلیدها در فایل شما ظاهر شدهاند توجهی ندارد. نوشتن tasks: بالاتر از roles: باعث نمیشود آن وظایف زودتر اجرا شوند. اگر چیزی باید حتماً قبل از یک role اتفاق بیفتد، آن را در pre_tasks: قرار دهید.
چرا مقدار group_vars من، role را override نمیکند؟
بررسی کنید که آیا متغیر در vars/main.yml مربوط به role تنظیم شده است یا در defaults/main.yml. در ترتیب اولویت Ansible، vars/ بالاتر از group_vars و host_vars قرار دارد، بنابراین inventory نمیتواند آن را override کند. متغیر را به defaults/main.yml منتقل کنید که در انتهای لیست اولویت قرار دارد و جایگاه صحیح برای هر چیزی است که فراخواننده (caller) باید بتواند آن را تغییر دهد. برای اطمینان از اینکه علت، اولویت است و نه یک غلط تایپی، یک بار با -e name=value اجرا کنید که از تمام منابع دیگر اولویت بالاتری دارد.
چرا Ansible میگوید role پیدا نشد؟
جستجو از کنار فایل playbook شروع میشود، بنابراین site.yml و roles/ باید در یک دایرکتوری باشند. پیام خطا مسیرهایی را که تلاش کرده است چاپ میکند، مانند the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. اجرای playbook از یک دایرکتوری والد مشکلی ندارد، زیرا جستجو از مسیر playbook پیروی میکند و نه دایرکتوری کاری (working directory) شل شما. اگر به roles_path از ansible.cfg وابسته هستید، تأیید کنید که آن فایل با ansible --version بارگذاری شده است، زیرا اگر دایرکتوری کاری برای همه قابل نوشتن (world writable) باشد، Ansible آن را نادیده میگیرد.
آیا برای ایجاد یک role به ansible-galaxy init نیاز دارم؟
خیر. یک role فقط شامل دایرکتوریهایی با نامهای مورد انتظار است، بنابراین mkdir -p roles/common/tasks به همراه یک tasks/main.yml از قبل یک role فعال است. ansible-galaxy init --init-path roles common باعث صرفهجویی در تایپ میشود و اسکلت کامل، شامل meta/main.yml و یک نمونه README را به شما میدهد. دایرکتوریهایی که خالی میگذارید را حذف کنید، زیرا یک vars/main.yml خالی باعث میشود مشخص نباشد کدام فایلها در role واقعاً کاری انجام میدهند.