Ansible template ও handler দিয়ে nginx config তৈরি
Jinja2 template থেকে nginx config তৈরি করুন, সত্যিই পরিবর্তন হলে handler দিয়ে reload চালান, এবং play দুবার চালিয়ে idempotence যাচাই করুন।
প্রথম playbook-এ Ansible template ও handler কী যোগ করে
Ansible template ও handler হলো সেই দুটি উপাদান, যা একটি static playbook-কে কার্যকর করে তোলে। Template আপনার variable থেকে একটি configuration file তৈরি করে, তাই একটি file দিয়েই প্রতিটি host পরিচালনা করা যায়। কোনো task সত্যিই কিছু পরিবর্তন করলেই handler চলে। ফলে প্রকৃত configuration পরিবর্তনের পরে service reload হয়, আর অন্য সময় service অপরিবর্তিত থাকে।
এই guide-টি আপনার VPS-এ প্রথম Ansible playbook যেখানে শেষ হয়েছে, ঠিক সেখান থেকেই শুরু হচ্ছে। আপনার কাছে ইতিমধ্যে এমন একটি play আছে, যা একটি package install করে এবং একটি service start করে। নিচের সবকিছু একটি machine-এ চলে, কারণ play-টি local connection ব্যবহার করে localhost-কে target করে। এটি অনুসরণ করতে আপনার দ্বিতীয় server প্রয়োজন নেই। একই play-টি task পরিবর্তন না করেই প্রকৃত inventory host-এর বিরুদ্ধে চালানো যায়। শেষ section-এ কোন বিষয়গুলো পরিবর্তন করতে হবে তা দেখানো হয়েছে।
কাজের ডিরেক্টরি সেট আপ করুন
sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templatesএখানে nginx শুধু একটি বাস্তব service হিসেবে ব্যবহৃত হয়েছে। এর configuration file এবং reload command আছে, যা এই উদাহরণের জন্য যথেষ্ট। ansible --version ansible-core-এর version এবং এটি যে Python interpreter ব্যবহার করবে তা দেখায়। দুটিই নোট করুন। নিচের playbook-এ ansible.builtin.template-এর মতো fully qualified module name ব্যবহার করা হয়েছে। এগুলোর জন্য Ansible 2.10 বা পরবর্তী version প্রয়োজন। বর্তমান যেকোনো distribution package-ই এর চেয়ে অনেক নতুন।
inventory.ini তৈরি করুন:
[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"ansible_connection=local Ansible-কে প্রতিটি task নিজের কাছে SSH session না খুলে local process হিসেবে চালাতে বলে। দ্বিতীয় setting-টি শুধু আনুষ্ঠানিকতা নয়। কোনো inventory file-এ localhost লিখলে এটি একটি সাধারণ host হয়ে যায়। তখন Ansible implicit localhost-কে স্বয়ংক্রিয়ভাবে দেওয়া interpreter আর এটি পায় না। ফলে interpreter discovery চালু হয় এবং play চালানো Python-এর চেয়ে ভিন্ন কোনো Python বেছে নিতে পারে। ansible_playbook_python হলো এই মুহূর্তে ansible-playbook চালানো interpreter। এতে দুটির মধ্যে সামঞ্জস্য থাকে।
ansible.cfg তৈরি করুন:
[defaults]
inventory = inventory.iniএই file না থাকলে প্রতিটি command-এ -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 থাকা play কোনো host-এর সঙ্গে মেলে না। ansible.cfg সম্পর্কে আরও একটি বিষয় মনে রাখুন: এটি world-writable directory-তে থাকলে Ansible উপেক্ষা করে। তাই project-টি আপনার home directory-র অধীনে রাখুন। inventory file-এ শুধু host-এর তালিকা থাকে না, এবং কাজটি সম্পন্ন করার জন্য এটিই সবচেয়ে ছোট inventory file।
template বনাম copy: কোন পরিস্থিতিতে কোনটি সঠিক
ansible.builtin.copy একটি ফাইল অপরিবর্তিত অবস্থায় স্থানান্তর করে। ansible.builtin.template প্রথমে ফাইলটি Jinja2-এর মাধ্যমে প্রক্রিয়া করে, তারপর ফলাফল স্থানান্তর করে। Module source-এ template-কে “সম্পূর্ণভাবে action plugin হিসেবে বাস্তবায়িত এবং controller-এ চলা একটি virtual module” বলা হয়েছে। এর একটি গুরুত্বপূর্ণ ফল মনে রাখুন: rendering সেই মেশিনে হয়, যেখানে আপনি ansible-playbook টাইপ করেছেন। Target host আপনার variables কখনো দেখে না এবং সেখানে Jinja2 ইনস্টল থাকা প্রয়োজন হয় না।
প্রতিটি host-এ ফাইলটি অভিন্ন হলে copy ব্যবহার করুন। প্রতি host-এ কোনো value ভিন্ন হলে, অথবা {% for %} loop কিংবা {% if %} block প্রয়োজন হলে template ব্যবহার করুন। copy-এ content: parameter আছে, এবং এর ভেতরের variables অন্য যেকোনো task argument-এর মতো substitute হয়। তবে সেখানে loop বা conditional নেই। তাই structure থাকা যেকোনো বিষয় template-এ রাখতে হবে। উভয় module একই file options গ্রহণ করে, কারণ উভয়ই একই documentation fragments ব্যবহার করে। তাই owner, group, mode, backup এবং validate উভয় module-এই একইভাবে কাজ করে।
টেমপ্লেট লিখুন: একটি ভেরিয়েবল, একটি লুপ
এটি 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 tag কাজ করে। {{ ... }} একটি expression এবং এর মান প্রদর্শন করে। {% ... %} একটি statement এবং নিজে কোনো output প্রদর্শন করে না। app_backends হলো dictionaries-এর একটি list। তাই backend.host প্রতিটি entry থেকে একটি key পড়ে, এবং loop-টি আপনি যতগুলো entry সংজ্ঞায়িত করবেন, ততগুলো server line লিখবে।
Whitespace সম্পর্কে একটি বিষয় উল্লেখ করা দরকার, কারণ অন্য পরিবেশে Jinja2 ব্যবহার করা ব্যক্তিদের কাছে এটি অপ্রত্যাশিত হতে পারে। Ansible ডিফল্টভাবে trim_blocks-এর মান yes সেট করে, কিন্তু Jinja2 নিজে তা করে না। তাই {% ... %} tag-এর ঠিক পরের newline সরিয়ে দেওয়া হয় এবং loop-টি অতিরিক্ত blank line রাখে না। Ansible lstrip_blocks-এর মান no রাখে। তাই {% tag-এর আগে যে spaces রাখবেন, সেগুলো অক্ষুণ্ণ থাকে এবং rendered file-এ দেখা যায়। Output-এ অপ্রত্যাশিত indentation থাকলে template task-এ lstrip_blocks: true সেট করুন।
ডিফল্টভাবে {{ ansible_managed }} আক্ষরিক text Ansible managed হিসেবে render হয়। এটিই রাখুন। অনেকে ansible.cfg-এ ansible_managed পুনরায় সংজ্ঞায়িত করে তার মধ্যে date যোগ করেন। এটি করলে প্রতিবার run-এ rendered file ভিন্ন হয়, task প্রতিবার change report করে, এবং service প্রতিবার reload হয়। এই একটি setting এই guide-এর মূল উদ্দেশ্যের স্থায়িত্ব নষ্ট করে। .j2 extension-টি একটি convention, এবং 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 changedmode: '0644' ইচ্ছাকৃতভাবে উদ্ধৃত করা হয়েছে। ফাইলের options documentation-এ octal number উদ্ধৃত করতে বলা হয়েছে, যাতে “Ansible একটি string পায় এবং string থেকে number-এ নিজের conversion করতে পারে”। উদ্ধৃতি না দিলে YAML parser 0644-কে সাধারণ number হিসেবে পড়ে। ফলে আপনার নির্ধারিত নয় এমন permission সেট হয়ে যেতে পারে।
notify: nginx config changed একটি topic-এর নাম, handler-এর নাম নয়। উভয় handler-এ listen: nginx config changed রয়েছে, তাই একটি notify দুটিতেই পৌঁছে যায়। পরে একই listen line-সহ তৃতীয় handler যোগ করলেও template task পরিবর্তন করতে হয় না। cache_valid_time: 3600 একই hour-এর মধ্যে দ্বিতীয় run হলে package mirror-এ আবার request পাঠানো বন্ধ করে।
একবার চালিয়ে আউটপুটে কী দেখায় তা পড়ুন
ansible-playbook site.ymlআপনার sudo পাসওয়ার্ড চাইলে -K যোগ করুন। Ansible তখন পাসওয়ার্ড চাইবে।
প্রথমে প্রতিটি task-এর লাইন পড়ুন। এরপর নিচের PLAY RECAP পড়ুন। Ansible-কে কোনো পরিবর্তন করতে হলে প্রতিটি task changed: প্রিন্ট করে। Host ইতিমধ্যে প্রত্যাশিত অবস্থায় থাকলে ok: প্রিন্ট করে। Recap প্রতিটি host-এর জন্য এই কাউন্টারগুলোর মোট দেখায়। Play-এর প্রতিটি task সম্পূর্ণ হওয়ার পরে, তার এক মুহূর্ত আগেও নয়, আপনি 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/Assembled configuration সঠিকভাবে parse হলে nginx -t, nginx: configuration file /etc/nginx/nginx.conf test is successful প্রিন্ট করে। curl nginx থেকে একটি status line ফেরত দেয়। এখানে সঠিক উত্তর হলো 502 Bad Gateway, কারণ server block সক্রিয় এবং 9001 বা 9002 port-এ কোনো কিছু listening করছে না। sudo tail /var/log/nginx/error.log সহজ ভাষায় কারণটি জানায়: connect() failed (111: Connection refused) while connecting to upstream।
Idempotence প্রমাণ করতে এটি দ্বিতীয়বার চালান
ansible-playbook site.ymlএই run-টিই গুরুত্বপূর্ণ। তাই এর output প্রথম run-এর সঙ্গে line by line তুলনা করুন। Template task-এ এখন ok: দেখানোর কথা, যেখানে আগে changed: দেখিয়েছিল। এছাড়া output-এর কোথাও কোনো handler দেখা যাবে না।
প্রক্রিয়াটি সহজ এবং জানা দরকার, কারণ debugging-এর সময় এটিই ভিত্তি হিসেবে ব্যবহার করবেন। template controller-এ file render করে এবং ফলাফলের checksum dest-এ থাকা file-এর checksum-এর সঙ্গে তুলনা করে। Content, ownership এবং mode একই হলে কোনো কাজ করার থাকে না। তাই task ok report করে, notify কখনো trigger হয় না, এবং handler-ও run করে না। Handler শুধু changed-এ trigger হয়, অন্য কোনো অবস্থায় নয়।
উল্টো দিকটিও প্রমাণ করুন। vars-এ weight: 3-কে weight: 1-এ পরিবর্তন করে play আবার run করুন। Template task changed report করবে, উভয় handler run করবে, এবং sudo cat /etc/nginx/conf.d/learn.conf নতুন value দেখাবে।
দ্বিতীয়বার একইভাবে run করার পরও যদি change report হয়, তাহলে render stable নয়। প্রথমে output-এ time-based কোনো বিষয় আছে কি না দেখুন। এটিই সাধারণ কারণ, এবং একটি customised ansible_managed সাধারণত এর জন্য দায়ী। এরপর task-এ mode এবং owner-এর value disk-এ বাস্তবে থাকা value-এর সঙ্গে মেলে কি না পরীক্ষা করুন। সেখানে mismatch থাকলে bytes একই হলেও সেটি change হিসেবে গণ্য হয়।
পরিবর্তন করার আগে তা দেখুন
ansible-playbook site.yml --check --diff--check host-এ কোনো পরিবর্তন না করে play চালায়। --diff প্রতিটি task কী পরিবর্তন করত তা দেখায়। template-এর ক্ষেত্রে এটি render করা বিষয়বস্তু এবং disk-এ থাকা ফাইলের মধ্যে লাইন-ভিত্তিক পার্থক্য দেখায়। এগুলো একসঙ্গে “এই run কী করবে” প্রশ্নের উত্তর দেয়, কোনো পরিবর্তন না করেই। Check mode-এর নিজস্ব কিছু সীমাবদ্ধতা আছে, বিশেষ করে যেসব task-এর ফল আগের কোনো task-এর ওপর নির্ভর করে, কিন্তু check mode সেই আগের task বাস্তবে চালায়নি।
কেন handlers play-এর শেষ পর্যন্ত অপেক্ষা করে
handlers-এর documentation বিষয়টি সরাসরি বলেছে: “ডিফল্টভাবে, কোনো নির্দিষ্ট play-এর সব task সম্পন্ন হওয়ার পরে handlers চলে। Notified handlers স্বয়ংক্রিয়ভাবে নিচের প্রতিটি section-এর পরে, এই ক্রমে চলে: pre_tasks, roles/tasks এবং post_tasks।”
এর কারণ হলো batching। একটি service-এর জন্য কোনো play চারটি configuration file তৈরি করলে সেই service-কে সব file তৈরি হওয়ার পরে শেষে একবার restart করা উচিত। প্রতিটি file-এর পরে restart করলে service চারবার restart হবে, এবং ওই restart-এর তিনটিতে অর্ধসমাপ্ত configuration load হবে। একই page-এ guarantee-টিও স্পষ্টভাবে বলা আছে: “একই handler-কে একাধিকবার notify করলে, যতগুলো task সেটিকে notify করুক না কেন, handler কেবল একবারই চলবে।”
Execution order-ও নির্দিষ্ট: “Handlers handlers section-এ যেভাবে সংজ্ঞায়িত করা হয়েছে, সেই ক্রমে চলে; notify statement-এ যেভাবে তালিকাভুক্ত আছে, সেই ক্রমে নয়।” তাই playbook-এ Test the nginx configuration, Reload nginx-এর উপরে আছে। এটি আগে লেখা হয়েছে বলেই test প্রথমে চলে; notify line-এর কোনো বিষয় এতে প্রভাব ফেলে না।
কীভাবে handler আগে চালাবেন এবং ব্যর্থতার পরে কীভাবে চালাবেন
কখনও একই play-এর পরের কোনো task-এর জন্য নতুন configuration-সহ service ইতিমধ্যে চালু থাকা প্রয়োজন হয়। সেই পর্যায়ে meta module ব্যবহার করে notified handler-গুলো flush করুন। ডকুমেন্টেশন অনুযায়ী, এটি “এখন পর্যন্ত notify করা যেকোনো handler task চালানোর জন্য 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 line বাদ দিলে wait_for task-টি nginx-এর পুরোনো configuration চালু থাকা অবস্থায়ই চলে। প্রথমবার চালানোর সময় port 8080-এ কোনো listener থাকে না। তাই task-টি পুরো দশ সেকেন্ড অপেক্ষা করে এবং শেষে ব্যর্থ হয়।
দ্বিতীয় পরিস্থিতিটি হলো failure। “কোনো task handler-কে notify করার পর play-এর পরে অন্য কোনো task ব্যর্থ হলে, default-ভাবে ওই host-এ handler চলে না। এর ফলে host অপ্রত্যাশিত অবস্থায় থাকতে পারে।” তাই কোনো play configuration render করার পর সম্পর্কহীন একটি task-এ ব্যর্থ হলে, নতুন file-টি disk-এ থেকে যায়, কিন্তু running service-এ পুরোনো configuration-ই loaded থাকে। Command line-এ --force-handlers ব্যবহার করে, অথবা play-এ force_handlers: true ব্যবহার করে এই আচরণ পরিবর্তন করুন। একই switch [defaults]-এর অধীনে ansible.cfg-এ force_handlers = True হিসেবে এবং ANSIBLE_FORCE_HANDLERS environment variable হিসেবেও রয়েছে। Default হলো False।
Handler-এর নাম সংঘর্ষ করে, কিন্তু পরাজিত handler নীরবে অকার্যকর থাকে
Documentation-এ নিয়মটি স্পষ্টভাবে বলা আছে: “প্রতিটি handler-এর globally unique নাম থাকা উচিত। একই নামে একাধিক handler সংজ্ঞায়িত হলে, play-এ সর্বশেষ load হওয়া handler-ই notify ও execute করা যায়।” কোনো role-এর ভেতরে সংজ্ঞায়িত handler-ও সেই role-এর মধ্যে সীমাবদ্ধ থাকে না। এগুলো পুরো play-এর জন্য একটি global handler list-এ যোগ হয়। তাই দুটি role-এ যদি একই Restart nginx সংজ্ঞায়িত থাকে, নামটি শেষ পর্যন্ত তাদের একটির সঙ্গেই resolve হয়। আপনি যে role-কে notify করেছেন তা নয়, load order নির্ধারণ করে কোনটি ব্যবহৃত হবে।
এই নিয়মের ওপর নির্ভর করার আগে এটি পরীক্ষা করুন। নিচের বিষয়বস্তু 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-secondplay সফল হয়, RUNNING HANDLER [Duplicated handler] একবার দেখা যায়, এবং ls একটি handler-এর জন্য /tmp/dup-first ও অন্যটির জন্য ls: cannot access '/tmp/dup-second': No such file or directory-এর একটি line print করে। যে handler চলে, সেটি সর্বশেষ load হওয়া handler নয়; বরং প্রথমে লেখা handler। অর্থাৎ ওই বাক্যে যে আচরণ বলা হয়েছে, পরীক্ষার ফল তার বিপরীত।
এই পার্থক্য বোঝা গুরুত্বপূর্ণ। কারণ documentation-এর নিয়মটি একটি file-এর line নয়, handler block নিয়ে। আলাদা স্থান থেকে আসা handler—প্রথমে একটি role, পরে আরেকটি role—আলাদা block হিসেবে গণ্য হয়। পরে আসা block আগের block-কে shadow করে। কিন্তু play-এ থাকা সাধারণ handlers: list একটি single block। সেই block-এর ভেতরে search উপর থেকে নিচে চলে এবং প্রথম মিলে যাওয়া নামেই থেমে যায়। তাই একই file-এ প্রথম definition কার্যকর হয় এবং দ্বিতীয়টি অপ্রাপ্য থাকে। অন্যদিকে role-এর মধ্যে shadowing documentation-এ বর্ণিত নিয়ম অনুযায়ী কাজ করে। যেভাবেই হোক, দুটি handler-ই কখনো reach করা যায় না। কোনো আচরণের ওপরই নির্ভর করে configuration তৈরি করা উচিত নয়।
এড়ানোর দুটি পরিষ্কার উপায় আছে। প্রতিটি handler-এর নামে সংশ্লিষ্ট role-এর জন্য নির্দিষ্ট prefix দিন। অথবা qualified form role_name : handler_name notify করুন। Documentation-এ এটিই এমন উপায় হিসেবে দেওয়া আছে, যা “একই নামের role-এর বাইরের handler-এর বদলে role-এর handler notify করা নিশ্চিত করে”। এই syntax-এ colon-এর দুই পাশের spaces-ও প্রয়োজনীয়। আপনি নিজের লেখা নয় এমন role ব্যবহার করা শুরু করলেই এটি বাস্তব সমস্যা হয়ে দাঁড়ায়।
একই page-এর আরেকটি নিয়ম হলো: “handler-এর নামে variable ব্যবহার করা এড়িয়ে চলুন। Handler-এর নাম early stage-এ templated হয়। তাই এ ধরনের handler name-এর জন্য Ansible-এর কাছে প্রয়োজনীয় value নাও থাকতে পারে।” কোনো variable undefined থাকা অবস্থায় নাম templated হলে Restart {{ service_name }} নামে handler পুরো play ব্যর্থ করে। Handler-এর নাম fixed string হিসেবে রাখুন এবং listen দিয়ে সেগুলো group করুন। এতে এই সমস্যা এড়ানো যায়।
validate: ত্রুটিপূর্ণ render ইনস্টল করতে অস্বীকার করুন
validate Ansible ফাইলটি নির্দিষ্ট স্থানে সরানোর আগে rendered ফাইলের ওপর একটি command চালায়। Documentation-এ বলা হয়েছে: “Updated ফাইলটি final destination-এ copy করার আগে যে validation command চালাতে হবে। Validation-এর জন্য একটি temporary file path ব্যবহার করা হয় এবং সেটি %s-এর মাধ্যমে pass করা হয়। নিচের উদাহরণগুলোর মতো %s উপস্থিত থাকতেই হবে। Command-টি নিরাপদভাবে pass করা হয়, তাই expansion এবং pipe-এর মতো shell feature কাজ করবে না।”
এই বক্তব্য থেকে দুটি নিয়ম সরাসরি পাওয়া যায়। %s বাধ্যতামূলক। এটি ছাড়া validate string ব্যবহার করলে validate must contain %s দিয়ে task ব্যর্থ হয়। এখানে কোনো shell নেই। তাই pipe, redirection, globbing এবং && কাজ করে না। একটি command এবং একটি file argument ব্যবহার করতে হবে।
Official module-এর উদাহরণে এই ব্যবহারের দুটি ক্ষেত্র দেখানো হয়েছে:
- 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দুটিই কাজ করে, কারণ প্রতিটি checker একটি file গ্রহণ করে এবং সেটিকে নিজস্ব নিয়মে যাচাই করে। visudo -cf একটি sudoers file পড়ে। sshd -t -f একটি সম্পূর্ণ sshd_config পড়ে।
এই গাইডে validate কেন nginx ফাইল পরীক্ষা করতে পারে না
টেমপ্লেট task-এ validate: /usr/sbin/nginx -t -c %s যোগ করলে task ব্যর্থ হয়। বার্তাটিতে কারণটি উল্লেখ থাকে:
nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2nginx -t -c এমন একটি সম্পূর্ণ configuration প্রত্যাশা করে, যা top level-এ events দিয়ে শুরু হয় এবং এতে http block থাকে। এই play যে file render করে, সেটি একটি fragment। /etc/nginx/nginx.conf-এর ভেতরে include /etc/nginx/conf.d/*.conf; directive-এর মাধ্যমে fragment-টি http block-এ অন্তর্ভুক্ত হয়। ওই context ছাড়া আলাদাভাবে পরীক্ষা করলে upstream সত্যিই ভুল স্থানে থাকা একটি directive। তাই nginx এমন একটি file প্রত্যাখ্যান করে, যা প্রকৃত অবস্থানে সম্পূর্ণ সঠিক। checker-কে একটি fragment দেওয়া হয়েছে, কিন্তু সেটিকে সম্পূর্ণ configuration হিসেবে পরীক্ষা করতে বলা হয়েছে।
কার্যকর সমাধানটি playbook-এই আছে। প্রথমে fragment install করুন। এরপর reload handler-এর আগে সংজ্ঞায়িত একটি handler-এ assembled configuration পরীক্ষা করুন। Handler-গুলো সংজ্ঞায়িত ক্রমে চলে। তাই nginx -t আপনার fragment-সহ প্রকৃত /etc/nginx/nginx.conf দেখতে পায়। সেখানে পরীক্ষা ব্যর্থ হলে systemctl reload কখনো চালানোর আগেই play ব্যর্থ হয়। তবে এর খরচটি স্পষ্টভাবে বুঝুন: পরীক্ষা ব্যর্থ হলে ত্রুটিপূর্ণ file-টি disk-এ থেকে যায়, আর nginx কেউ restart না করা পর্যন্ত সর্বশেষ load করা configuration দিয়েই service চালাতে থাকে।
এই কারণেই backup: true ব্যবহার করা হয়। এটি overwrite করার আগে আগের file-এর একটি copy মূল file-এর পাশে লিখে রাখে। Copy-টির নাম হয় basename.PID.YYYY-MM-DD@HH:MM:SS~। ফলে directory-তে learn.conf.4127.2026-08-20@11:42:09~-এর মতো entry দেখা যায়। কোনো পরিবর্তনের পর sudo ls -l /etc/nginx/conf.d/ চালালে এমন একটি file দেখতে পাবেন।
এই naming detail-টি যতটা মনে হয়, তার চেয়ে বেশি গুরুত্বপূর্ণ। /etc/nginx/conf.d/-এ backup নিরাপদ, কারণ মূল config শুধু conf.d/*.conf অন্তর্ভুক্ত করে এবং backup-এর নাম tilde দিয়ে শেষ হয়। কিন্তু bare * দিয়ে include করা directory-তে এটি নিরাপদ নয়। Debian এবং Ubuntu-তে /etc/nginx/nginx.conf ঠিক এইভাবে /etc/nginx/sites-enabled/* অন্তর্ভুক্ত করে। sites-enabled-এ backup: true দিয়ে template করলে nginx backup-টিকে দ্বিতীয় live server block হিসেবে load করে। এই কারণেই play-টি পরিবর্তে conf.d-এ লেখে।
বাস্তব inventory host-এর বিরুদ্ধে একই play চালানো
hosts: local-কে আপনার ব্যবহৃত group name দিয়ে পরিবর্তন করুন। play-এর অন্য কিছু পরিবর্তন হবে না। Template প্রতিটি host-এর জন্য একবার করে render হয়। তাই app_listen_port এবং app_backends, group_vars ও host_vars থেকে আসতে পারে, কিন্তু template file একটিই থাকে। File-এর পরিবর্তে variable-এ value রাখার সুবিধা এটাই।
দুটি বিষয় পরিবর্তন হয়। become: true-এর এখন প্রতিটি target-এ sudo password প্রয়োজন হবে, যদি সেখানে password ছাড়া sudo ব্যবহারের ব্যবস্থা না থাকে। তাই -K যোগ করুন। এছাড়া সেই template-এ থাকা যেকোনো secret, যেমন database password বা API token, আপনি commit করেন এমন কোনো file-এ plain vars: হিসেবে রাখা যাবে না। সেই value-গুলো Ansible Vault দিয়ে encrypt করুন এবং এখন যেভাবে করেন, ঠিক সেভাবেই name দিয়ে reference করুন। কারণ variable কোথা থেকে এসেছে, template তা বিবেচনা করে না।
play একটি service-এর সীমা ছাড়িয়ে বড় হলে vars:, templates/ এবং handlers: রাখার জন্য আগে থেকেই standard স্থান থাকে। এগুলো সেখানে সরিয়ে রাখাই playbook ও role আলাদা করার মূল উদ্দেশ্য।
FAQ
আমার Ansible handler কেন চালানো হয়নি?
প্রায় সব ক্ষেত্রেই কারণ হলো, যে task এটি notify করে সেটি ok-এর বদলে changed report করেছে। Handler শুধু change হলে চালু হয়; অন্য কোনো অবস্থায় নয়। তাই render করা template-এর ফলাফল disk-এ থাকা ফাইলের সঙ্গে মিলে গেলে template task কোনো notification পাঠায় না। এরপর চারটি বিষয় পরীক্ষা করুন। notify-এর string-টি case ও spacing-সহ handler-এর name অথবা কোনো listen topic-এর সঙ্গে হুবহু মিলতে হবে। ওই host-এ পরে কোনো task ব্যর্থ হলে notified handler-গুলো চালানো হয় না, যদি না আপনি --force-handlers pass করেন। অন্য play-তে সংজ্ঞায়িত handler এই play থেকে দৃশ্যমান নয়। আর when condition-এর কারণে কোনো notifying task skipped হলে সেটি কখনো notify করে না।
আমার playbook প্রতিবার চালালে changed report করে কেন?
প্রতিবার চালানোর মধ্যে render করা text একই থাকছে না। সবচেয়ে সাধারণ কারণ হলো output-এ timestamp থাকা। তারিখসহ একটি customised ansible_managed string ঠিক এ ধরনের পরিবর্তন ঘটায়। এরপর task-এর mode এবং owner পরীক্ষা করুন। এগুলো disk-এ থাকা ফাইলের সঙ্গে না মিললে content একই হলেও Ansible সেগুলো সংশোধন করে এবং change report করে। কোনটি ঘটছে তা দেখতে ansible-playbook site.yml --check --diff চালান, কারণ --diff task যে পরিবর্তন করতে চায় তার পার্থক্য দেখায়।
Ansible-এ template এবং copy-এর মধ্যে পার্থক্য কী?
ansible.builtin.copy কোনো file অপরিবর্তিত অবস্থায় পাঠায়। ansible.builtin.template controller-এ প্রথমে Jinja2 দিয়ে file render করে, তারপর ফলাফল পাঠায়। তাই file target host-এ পৌঁছানোর আগেই variables এবং loops resolve হয়ে যায়। যেসব host-এ file byte-for-byte একই, সেসব ক্ষেত্রে copy ব্যবহার করুন। Host অনুযায়ী যেসব file পরিবর্তিত হয়, সেগুলোর জন্য template ব্যবহার করুন। উভয়ই একই file options সমর্থন করে। তাই mode, owner, backup এবং validate উভয় ক্ষেত্রেই একইভাবে কাজ করে।
Play-এর মাঝখানে handler কীভাবে চালাব?
যে স্থানে handler-গুলো চালাতে চান, সেখানে task হিসেবে ansible.builtin.meta: flush_handlers যোগ করুন। এটি তখন পর্যন্ত notify করা প্রতিটি handler চালায়, তারপর play স্বাভাবিকভাবে চলতে থাকে। একই play-এর পরের কোনো task যদি নতুন configuration-সহ service আগে থেকেই চালু থাকার ওপর নির্ভর করে, তখন এটি ব্যবহার করুন। উদাহরণস্বরূপ, reload-এর পর চালু হওয়া port-এ একটি wait_for। Play শেষ হওয়ার আগেই handler চালানোর জন্য এটিই সমর্থিত পদ্ধতি।
nginx config fragment-এর সঙ্গে কি validate ব্যবহার করা যায়?
nginx -t -c %s-এর সঙ্গে নয়। ওই command-টি শীর্ষস্তরের events এবং http block দিয়ে শুরু হওয়া সম্পূর্ণ configuration প্রত্যাশা করে। তাই এটি conf.d fragment প্রত্যাখ্যান করে এবং "upstream" directive is not allowed here-এর মতো message দেখায়। Fragment-টি http block-এর ভিতরে বৈধ, কিন্তু একা বৈধ নয়। File install করুন, তারপর reload handler-এর আগে সংজ্ঞায়িত একটি handler-এ assembled configuration-এর বিরুদ্ধে nginx -t চালান। Handler যে ক্রমে সংজ্ঞায়িত করা হয়েছে, সেই ক্রমেই চলে। তাই reload করার আগে invalid configuration-এর কারণে play ব্যর্থ হবে। Template task-এ backup: true সেট করুন, যাতে আগের file-টি ফিরিয়ে আনার জন্য উপলব্ধ থাকে।