آموزش استفاده از Templates و Handlers در Ansible
در این راهنما یاد میگیرید چگونه با استفاده از Jinja2 فایل پیکربندی nginx را رندر کنید و با تعریف Handler، سرویس را فقط هنگام تغییر واقعی تنظیمات reload کنید.
نقش قالبها و هندلرها در بهبود اولین پلیبوک Ansible
قالبها (Templates) و هندلرها (Handlers) دو عنصری هستند که یک پلیبوک ایستا را به ابزاری کاربردی تبدیل میکنند. یک قالب، فایل پیکربندی را بر اساس متغیرهای شما رندر میکند؛ بنابراین یک فایل واحد برای تمام میزبانها قابل استفاده است. هندلر تنها زمانی اجرا میشود که یک تسک واقعاً تغییری ایجاد کرده باشد؛ در نتیجه، سرویس فقط هنگام تغییر واقعی پیکربندی بازنشانی (reload) میشود و در سایر مواقع بدون تغییر باقی میماند.
این راهنما دقیقاً از جایی ادامه مییابد که اولین پلیبوک Ansible شما روی یک VPS به پایان رسید. شما هماکنون پلیبوکی دارید که یک پکیج را نصب و یک سرویس را اجرا میکند. تمام موارد زیر روی یک ماشین اجرا میشوند، زیرا پلیبوک از طریق یک اتصال محلی، localhost را هدف قرار میدهد. برای دنبال کردن این آموزش نیازی به سرور دوم ندارید. همین پلیبوک بدون هیچ تغییری در تسکها، روی میزبانهای واقعی موجود در inventory نیز اجرا میشود و بخش آخر به بررسی تغییرات لازم برای آن حالت میپردازد.
تنظیم دایرکتوری کاری
sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templatesنرمافزار nginx در اینجا تنها به این دلیل استفاده شده است که یک سرویس واقعی با فایل پیکربندی و دستور reload است؛ یعنی دقیقاً همان چیزی که این مثال به آن نیاز دارد. دستور ansible --version نسخه ansible-core و مفسر Python مورد استفاده آن را نمایش میدهد. هر دو را یادداشت کنید. پلیبوک زیر از نامهای کامل ماژولها مانند 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 مینویسید، آن به یک میزبان عادی تبدیل میشود و مفسری را که Ansible بهطور پیشفرض به localhost ضمنی اختصاص میدهد از دست میدهد؛ در نتیجه به قابلیت کشف مفسر (interpreter discovery) متوسل شده و ممکن است پایتونی متفاوت از آنچه پلی را اجرا میکند، انتخاب کند. 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' را چاپ میکند و یک پلی با hosts: all با هیچچیز مطابقت نخواهد داشت. یک نکته دیگر درباره ansible.cfg: اگر این فایل در دایرکتوری با دسترسی نوشتن برای همه (world-writable) قرار داشته باشد، Ansible آن را نادیده میگیرد؛ بنابراین پروژه را در دایرکتوری home خود نگه دارید. یک فایل inventory بیش از یک لیست از میزبانها را در خود جای میدهد و این کوچکترین فایلی است که این کار را انجام میدهد.
تفاوت template و copy، و زمان مناسب برای استفاده از هر کدام
ansible.builtin.copy یک فایل را همانطور که هست منتقل میکند. ansible.builtin.template ابتدا فایل را از طریق Jinja2 پردازش کرده و نتیجه را منتقل میکند. مستندات ماژول، template را به عنوان «یک ماژول مجازی که کاملاً به صورت یک action plugin پیادهسازی شده و روی controller اجرا میشود» توصیف میکند، که نتیجهای مهم دارد: رندر کردن (rendering) روی ماشینی انجام میشود که دستور ansible-playbook را در آن تایپ کردهاید. میزبان مقصد هرگز متغیرهای شما را نمیبیند و نیازی به نصب بودن Jinja2 روی آن ندارد.
زمانی که فایل در تمام میزبانها یکسان است، از copy استفاده کنید. به محض اینکه یک مقدار در هر میزبان متفاوت باشد، یا به یک حلقه {% for %} یا یک بلوک {% if %} نیاز داشته باشید، از template استفاده کنید. copy دارای پارامتر content: است و متغیرهای درون آن مانند سایر آرگومانهای task جایگذاری میشوند، اما در آنجا خبری از حلقهها و شرطها نیست؛ بنابراین هر چیزی که ساختار دارد، متعلق به یک template است. هر دو ماژول از گزینههای فایل یکسانی پشتیبانی میکنند، زیرا هر دو از قطعهکدهای مستندات مشابهی استفاده میکنند؛ بنابراین 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 مینویسد، فارغ از اینکه چند ورودی تعریف کرده باشید.
یک نکته درباره فضای خالی (whitespace) وجود دارد، زیرا کسانی که Jinja2 را از جای دیگری میشناسند، از آن تعجب میکنند. Ansible بهطور پیشفرض trim_blocks را روی yes تنظیم میکند، در حالی که خود Jinja2 این کار را نمیکند؛ بنابراین خط جدید بلافاصله بعد از تگ {% ... %} حذف میشود و حلقه، خط خالی پشت سر خود باقی نمیگذارد. Ansible مقدار lstrip_blocks را روی no نگه میدارد، بنابراین هر فضایی که قبل از تگ {% قرار دهید، حفظ شده و در فایل نهایی ظاهر میشود. اگر خروجی شما دارای تورفتگیهای ناخواسته است، lstrip_blocks: true را در task مربوط به قالب تنظیم کنید.
{{ ansible_managed }} بهطور پیشفرض به صورت متن تحتاللفظی Ansible managed رندر میشود. آن را به همان حالت باقی بگذارید. افراد اغلب ansible_managed را در ansible.cfg بازتعریف میکنند تا تاریخ را شامل شود، و به محض انجام این کار، فایل رندر شده در هر اجرا تغییر میکند، task در هر اجرا گزارش تغییر میدهد و سرویس در هر اجرا reload میشود. این یک تنظیم، ویژگیای را که بقیه این راهنما درباره آن است، از بین میبرد. پسوند .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 نام یک موضوع (topic) است، نه یک هندلر (handler). هر دو هندلر دارای listen: nginx config changed هستند، بنابراین یک دستور notify هر دوی آنها را اجرا میکند. بعداً میتوانید یک هندلر سوم با همان خط listen اضافه کنید و تسک مربوط به template نیازی به ویرایش نخواهد داشت. عبارت cache_valid_time: 3600 از اجرای مجدد تسک در طول یک ساعت و اتصال دوباره به مخازن پکیج جلوگیری میکند.
یکبار آن را اجرا کنید و خروجی را بخوانید
ansible-playbook site.ymlاگر دستور sudo از شما رمز عبور خواست، -K را اضافه کنید تا Ansible آن را از شما بپرسد.
ابتدا خطوط مربوط به هر task را بخوانید و سپس PLAY RECAP را در پایین بررسی کنید. هر task در صورتی که Ansible تغییری ایجاد کرده باشد، changed: و اگر وضعیت میزبان (host) از قبل مطابق خواسته شما بوده باشد، ok: را چاپ میکند؛ خلاصه نهایی نیز مجموع این شمارندهها را به تفکیک هر میزبان نشان میدهد. پس از پایان تمام taskها در 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 پاسخ صحیح در اینجا است، زیرا بلاک سرور فعال است و هیچ سرویسی روی پورتهای 9001 یا 9002 گوش نمیدهد. sudo tail /var/log/nginx/error.log دلیل را به زبان ساده بیان میکند: connect() failed (111: Connection refused) while connecting to upstream.
اجرای مجدد برای اثبات خاصیت Idempotence
ansible-playbook site.ymlاین اجرا، اجرای اصلی است؛ بنابراین خروجی آن را خطبهخط با اجرای اول مقایسه کنید. تسک template اکنون باید به جای changed:، عبارت ok: را چاپ کند و هیچکدام از handlerها نباید در خروجی ظاهر شوند.
این مکانیزم ساده است و ارزش یادگیری دارد، چرا که مبنای عیبیابی شماست. template فایل را روی controller رندر میکند و checksum نتیجه را با checksum فایلی که از قبل در dest قرار دارد، مقایسه میکند. تطابق محتوا، مالکیت و mode به این معناست که کاری برای انجام دادن وجود ندارد؛ بنابراین تسک وضعیت ok را گزارش میدهد، در نتیجه notify هرگز فعال نمیشود و handler اجرا نمیگردد. Handlerها فقط در وضعیت changed فعال میشوند و نه در هیچ وضعیت دیگری.
جهت دیگر را نیز امتحان کنید. مقدار weight: 3 را در vars به weight: 1 تغییر دهید و play را دوباره اجرا کنید؛ تسک template وضعیت changed را گزارش میدهد، هر دو handler اجرا میشوند و sudo cat /etc/nginx/conf.d/learn.conf مقدار جدید را نشان میدهد.
اگر در اجرای دومِ کاملاً مشابه، همچنان وضعیت تغییر (change) گزارش شد، رندر پایدار نیست. ابتدا به دنبال موارد وابسته به زمان در خروجی بگردید، زیرا این مورد دلیل رایجی است و معمولاً یک ansible_managed سفارشیشده عامل آن است. پس از آن، بررسی کنید که mode و owner در تسک با آنچه واقعاً روی دیسک وجود دارد مطابقت داشته باشد، زیرا عدم تطابق در این موارد، حتی در صورت یکسان بودن بایتها، به عنوان تغییر تلقی میشود.
مشاهده تغییرات پیش از اعمال آنها
ansible-playbook site.yml --check --diff--check پلیبوک را بدون ایجاد تغییر در میزبان اجرا میکند. --diff آنچه را که هر تسک تغییر میداد چاپ میکند، که برای template شامل نمایش تفاوت خطبهخط بین فایل رندر شده و فایل موجود روی دیسک است. این دو در کنار هم، بدون اجرای واقعی عملیات، به این پرسش پاسخ میدهند که «این اجرا چه کاری انجام خواهد داد». حالت Check mode محدودیتهای خاص خود را دارد، بهویژه در تسکهایی که نتیجه آنها به تسک قبلی وابسته است؛ تسکی که در حالت Check mode بهصورت واقعی اجرا نشده است.
چرا هندلرها تا پایان پلی منتظر میمانند
مستندات هندلرها در این باره صریح هستند: «بهطور پیشفرض، هندلرها پس از تکمیل تمام تسکها در یک پلی (play) خاص اجرا میشوند. هندلرهای فراخوانیشده بهطور خودکار پس از هر یک از بخشهای زیر و به ترتیب مشخصشده اجرا میشوند: pre_tasks، roles/tasks و post_tasks.»
دلیل این امر دستهبندی (batching) است. پلیای که چهار فایل پیکربندی را برای یک سرویس ایجاد میکند، باید آن سرویس را تنها یکبار در پایان و پس از قرارگیری هر چهار فایل، راهاندازی مجدد کند. راهاندازی مجدد پس از هر فایل باعث میشود سرویس چهار بار ریاستارت شود و سه مورد از این دفعات، پیکربندی ناقص را بارگذاری کنند. همان صفحه این تضمین را بهوضوح بیان میکند: «فراخوانی چندباره یک هندلر یکسان، منجر به اجرای آن هندلر تنها برای یکبار میشود، فارغ از اینکه چند تسک آن را فراخوانی کرده باشند.»
ترتیب اجرا نیز ثابت است: «هندلرها به ترتیبی که در بخش handlers تعریف شدهاند اجرا میشوند، نه به ترتیبی که در دستور notify ذکر شدهاند.» به همین دلیل است که در پلیبوک، Test the nginx configuration بالاتر از Reload nginx قرار میگیرد. تست ابتدا اجرا میشود چون ابتدا نوشته شده است و هیچچیز در خط notify بر آن تأثیری ندارد.
نحوه اجرای زودهنگام هندلرها و اجرای آنها پس از بروز خطا
گاهی اوقات یک تسک بعدی در همان play نیاز دارد که سرویس از قبل با پیکربندی جدید در حال اجرا باشد. هندلرهای اطلاعرسانیشده را در آن نقطه با استفاده از ماژول meta تخلیه (flush) کنید؛ مستندات این ماژول را به این صورت توصیف میکنند که باعث میشود «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 همچنان در حال ارائه پیکربندی قدیمی است. در اجرای اول، اصلاً هیچ listener روی پورت 8080 وجود ندارد، بنابراین تسک به مدت کامل 10 ثانیه منتظر میماند و سپس با خطا مواجه میشود.
مورد دوم، بروز خطا است. «اگر یک تسک، هندلری را اطلاعرسانی کند اما تسک دیگری بعداً در همان play با خطا مواجه شود، بهصورت پیشفرض هندلر روی آن میزبان اجرا نمیشود که این امر ممکن است میزبان را در وضعیتی غیرمنتظره رها کند.» بنابراین، playای که یک پیکربندی را رندر میکند و سپس در یک تسک نامرتبط دچار مشکل میشود، فایل جدید را روی دیسک باقی میگذارد در حالی که پیکربندی قدیمی همچنان در سرویس در حال اجرا بارگذاری شده است. این رفتار را با استفاده از --force-handlers در خط فرمان یا با force_handlers: true در play بازنویسی کنید. همین سوئیچ به عنوان force_handlers = True تحت [defaults] در ansible.cfg و همچنین به عنوان متغیر محیطی ANSIBLE_FORCE_HANDLERS وجود دارد. مقدار پیشفرض False است.
تداخل نامهای handler و سکوتِ بازنده
مستندات این قانون را بیان میکنند: «هر handler باید نامی منحصربهفرد در سطح جهانی داشته باشد. اگر چندین handler با نام یکسان تعریف شوند، فقط آخرین موردی که در play بارگذاری شده است، قابل فراخوانی و اجرا خواهد بود.» handlerهایی که درون یک role تعریف میشوند نیز محدود به همان role نیستند. آنها در یک لیست سراسری handler برای کل play درج میشوند؛ بنابراین دو role که هر کدام Restart nginx را تعریف میکنند، نامی را برای شما باقی میگذارند که دقیقاً به یکی از آنها اشاره دارد و ترتیب بارگذاری—نه roleای که از آن اعلان (notify) کردهاید—تعیین میکند کدامیک اجرا شود.
پیش از تکیه بر این قانون، آن را آزمایش کنید. این فایل را با نام 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 برای دیگری چاپ میکند. handlerای که اجرا شده، موردی است که اول نوشته شده است، نه آخرین موردی که بارگذاری شده؛ که این دقیقاً خلاف پیشبینی آن جمله است.
درک این تفاوت ضروری است، زیرا قانون مستندشده درباره بلوکهای handler است، نه خطوط داخل یک فایل. handlerهایی که از مکانهای جداگانه (یک role و سپس دیگری) میآیند، بلوکهای مجزایی هستند و بلوک بعدی، بلوک قبلی را shadow میکند. یک لیست ساده handlers: در یک play، یک بلوک واحد است و جستجو درون یک بلوک از بالا به پایین انجام شده و در اولین نام منطبق متوقف میشود. بنابراین درون یک فایل، اولین تعریف پاسخ میدهد و دومی غیرقابلدسترس است، در حالی که بین roleها، عمل shadowing به شکلی که مستندات توصیف کردهاند رخ میدهد. در هر صورت، شما هرگز نمیتوانید به هر دو دسترسی داشته باشید و هیچکدام از این دو روش، مبنای مناسبی برای طراحی نیست.
دو راه حل تمیز وجود دارد. به نام هر handler یک پیشوند اختصاصی برای همان role بدهید، یا از فرم واجد شرایط role_name : handler_name برای notify استفاده کنید؛ که مستندات آن را به عنوان روشی برای «اطمینان از اینکه یک handler از یک role فراخوانی میشود، در مقابل موردی از خارج role با همان نام» معرفی کرده است. فاصلههای اطراف دونقطه (colon) بخشی از این سینتکس هستند. این موضوع به محض اینکه شروع به استفاده از roleهایی کنید که خودتان ننوشتهاید، به یک مشکل جدی تبدیل میشود.
یک قانون دیگر از همان صفحه: «از قرار دادن متغیرها در نام handler خودداری کنید. از آنجا که نامهای handler در مراحل اولیه قالببندی (template) میشوند، ممکن است Ansible در آن لحظه مقداری برای نام handler نداشته باشد.» یک handler با نام Restart {{ service_name }} باعث شکست کل play میشود، زمانی که آن متغیر در لحظه قالببندی نام تعریف نشده باشد. ثابت نگهداشتن نام handlerها و گروهبندی آنها با listen، این مسئله را برطرف میکند.
اعتبارسنجی: جلوگیری از نصب فایل پیکربندی معیوب
validate یک دستور را روی فایل رندر شده اجرا میکند، پیش از آنکه Ansible آن را به مقصد نهایی منتقل کند. مستندات میگویند: «دستور اعتبارسنجی که باید پیش از کپی کردن فایل بهروزرسانی شده در مقصد نهایی اجرا شود. یک مسیر فایل موقت برای اعتبارسنجی استفاده میشود که باید از طریق %s در دستور گنجانده شود؛ همانطور که در مثالهای زیر آمده است. همچنین، دستور بهصورت امن ارسال میشود، بنابراین قابلیتهای shell مانند بسط (expansion) و pipeها کار نخواهند کرد.»
دو قانون مستقیماً از این متن استخراج میشود. استفاده از %s الزامی است و رشتهٔ اعتبارسنجی بدون آن، تسک را با خطای validate must contain %s مواجه میکند. همچنین shell در دسترس نیست، بنابراین pipeها، تغییر مسیر (redirection)، globbing و && کار نمیکنند. فقط یک دستور و یک آرگومان فایل مجاز است.
مثالهای رسمی ماژول، دو موردی هستند که این قابلیت در آنها بهخوبی کار میکند:
- 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 کامل را میخواند.
چرا در این راهنما امکان اعتبارسنجی فایل 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 شروع شود. فایلی که این پلیبوک رندر میکند، تنها یک قطعه (fragment) است که توسط include /etc/nginx/conf.d/*.conf; در داخل بلوک http به فایل /etc/nginx/nginx.conf فراخوانی میشود. اگر این قطعه را بهتنهایی و خارج از آن زمینه بررسی کنید، upstream واقعاً یک دستورالعمل در جای اشتباه است؛ بنابراین nginx فایلی را رد میکند که در محل اصلی خود کاملاً صحیح است. به ابزار بررسی، یک قطعه داده شده و از آن خواسته شده که با آن مانند یک پیکربندی کامل برخورد کند.
راهکار عملی همان چیزی است که در پلیبوک آمده است. ابتدا قطعه را نصب کنید، سپس پیکربندی مونتاژ شده را در یک handler که پیش از handler بارگذاری مجدد (reload) تعریف شده، بررسی کنید. از آنجا که handlerها به ترتیبی که تعریف شدهاند اجرا میشوند، nginx -t فایل /etc/nginx/nginx.conf واقعی را به همراه قطعه شما میبیند و بروز خطا در آن مرحله، باعث توقف پلیبوک پیش از فراخوانی systemctl reload میشود. به هزینه این کار توجه داشته باشید: هنگام شکست خوردن بررسی، فایل معیوب روی دیسک قرار دارد و nginx تا زمانی که کسی آن را restart نکند، به سرویسدهی با آخرین پیکربندی معتبر خود ادامه میدهد.
به همین دلیل است که 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/* میشود. اگر با استفاده از backup: true در sites-enabled قالببندی کنید، nginx فایل پشتیبان را به عنوان یک بلوک server دوم و فعال بارگذاری میکند؛ به همین دلیل است که این پلیبوک در conf.d مینویسد.
اجرای همان پلیبوک روی میزبانهای واقعی موجودی
مقدار hosts: local را به نام گروهی که استفاده میکنید تغییر دهید؛ در این صورت هیچ بخش دیگری از پلیبوک نیاز به تغییر ندارد. قالب (template) برای هر میزبان یکبار رندر میشود، بنابراین app_listen_port و app_backends میتوانند از group_vars و host_vars فراخوانی شوند، در حالی که خود فایل قالب ثابت باقی میماند. این همان مزیت قرار دادن مقادیر در متغیرها بهجای درج مستقیم در فایل است.
دو مورد تغییر میکنند. become: true اکنون روی هر میزبان مقصد به رمز عبور sudo نیاز دارد، مگر اینکه دسترسی sudo بدون رمز عبور داشته باشید؛ بنابراین -K را اضافه کنید. همچنین، هرگونه دادهٔ حساس در آن قالب، مانند رمز عبور پایگاه داده یا توکن API، نباید بهصورت متن ساده در vars: و فایلی که commit میکنید قرار بگیرد. این مقادیر را با Ansible Vault رمزنگاری کنید و دقیقاً مانند قبل با نام به آنها ارجاع دهید، زیرا قالب اهمیتی نمیدهد که متغیر از کجا آمده است.
هنگامی که پلیبوک از یک سرویس فراتر میرود، vars:، templates/ و handlers: همگی جایگاه استانداردی دارند که از پیش برای آنها در نظر گرفته شده است. انتقال آنها به آن جایگاه، هدف اصلی تفکیک بین یک پلیبوک و یک نقش (role) است.
FAQ
چرا هندلر Ansible من اجرا نشد؟
تقریباً همیشه به این دلیل است که تسکی که آن را فراخوانی (notify) میکند، وضعیت ok را گزارش کرده است، نه changed. هندلرها فقط در صورت بروز تغییر (change) اجرا میشوند و نه در هیچ حالت دیگری؛ بنابراین اگر خروجی یک تسک template با فایلی که از قبل روی دیسک موجود است یکسان باشد، هیچ هندلری فراخوانی نمیشود. پس از آن، این چهار مورد را بررسی کنید: رشتهٔ موجود در notify باید دقیقاً با نام هندلر در name یا یک موضوع listen مطابقت داشته باشد (شامل حروف کوچک و بزرگ و فاصلهها). اگر تسک بعدی در همان میزبان با خطا مواجه شود، هندلرهای فراخوانیشده اجرا نمیشوند، مگر اینکه از فلگ --force-handlers استفاده کنید. هندلری که در یک play متفاوت تعریف شده باشد، از این play قابل مشاهده نیست. همچنین تسکی که به دلیل شرط when نادیده گرفته (skip) شود، هرگز هیچ هندلری را فراخوانی نمیکند.
چرا پلیبوک من در هر اجرا وضعیت changed را گزارش میکند؟
متن رندرشده بین اجراها پایدار نیست. رایجترین علت، وجود یک timestamp در خروجی است و یک رشتهٔ سفارشی 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 روی پورتی که تنها پس از reload ایجاد میشود. این روشِ پشتیبانیشده برای اجرای هندلر پیش از پایان play است.
آیا میتوانم از validate با یک قطعه (fragment) از پیکربندی nginx استفاده کنم؟
خیر، با nginx -t -c %s نمیتوانید. این دستور انتظار یک پیکربندی کامل را دارد که با بلوکهای سطح بالای events و http شروع شود، بنابراین یک قطعه conf.d را با پیامی مانند "upstream" directive is not allowed here رد میکند. این قطعه در داخل بلوک http معتبر است و به تنهایی نامعتبر محسوب میشود. فایل را نصب کنید، سپس دستور nginx -t را روی پیکربندی نهایی (assembled) در هندلری که قبل از هندلر reload تعریف شده، اجرا کنید. هندلرها به ترتیبی که تعریف شدهاند اجرا میشوند، بنابراین پیکربندی نادرست باعث شکست play قبل از تلاش برای reload میشود. گزینه backup: true را روی تسک template تنظیم کنید تا فایل قبلی برای بازگردانی در دسترس باشد.