ยกระดับการเขียน Playbook สู่ความเป็นมืออาชีพด้วย Variables, Jinja2 Templates, Roles และ Vault
องค์ประกอบพื้นฐานของ Ansible ที่เราได้เรียนรู้ไปแล้ว
เครื่องที่ติดตั้ง Ansible และใช้รันคำสั่งไปยังเครื่องอื่นๆ
ไฟล์รายชื่อเครื่องเป้าหมาย (Managed Nodes) ที่จัดกลุ่มไว้
สคริปต์ YAML ที่ระบุขั้นตอน (Tasks) ที่ต้องการให้ทำงาน
ชุดคำสั่งสำเร็จรูป เช่น apt, copy, service
การรักษาสถานะของระบบ (ซอฟต์แวร์, เซิร์ฟเวอร์, เครือข่าย) ให้อยู่ในรูปแบบที่สอดคล้องกับ "ความคาดหวัง" (Desired State) ที่กำหนดไว้อย่างสม่ำเสมอ
.conf, เปิด Service, สร้าง User"รันกี่ครั้ง ผลลัพธ์สุดท้ายก็ยังเหมือนเดิม และจะไม่ทำสิ่งที่ไม่จำเป็นซ้ำ"
Bash script สั่งเพิ่มข้อความต่อท้ายไฟล์ ถ้ารัน 5 ครั้ง ข้อความจะซ้ำกัน 5 บรรทัด
echo "export PATH=/new/path" >> ~/.bashrc
Ansible ตรวจสอบก่อนว่ามีบรรทัดนี้หรือยัง ถ้ามีแล้ว จะข้าม (Skip) ไม่ทำซ้ำ
- lineinfile:
path: ~/.bashrc
line: "export PATH=/new/path"
การใช้ command หรือ shell module ทำให้เสียคุณสมบัติ Idempotency
# แบบนี้ไม่ดี (จะแสดงสถานะ 'changed' ทุกครั้งที่รัน)
- name: Install git using shell
shell: apt-get install -y git
# แบบนี้ถูกต้อง (Ansible จะจัดการ Idempotency ให้)
- name: Install git using apt module
apt:
name: git
state: present
ถ้า "ต้อง" ใช้ shell module จริงๆ เราสามารถบอก Ansible ให้เช็คเงื่อนไขได้
- name: Run setup script only if file doesn't exist
shell: ./setup.sh
args:
creates: /opt/app/installed.txt # ถัามีไฟล์นี้แล้ว จะข้ามคำสั่งนี้ไป (ok)
- name: Check status
shell: myapp --status
register: app_status
changed_when: false # บังคับไม่ให้ขึ้นสถานะ changed เพราะแค่เช็คเฉยๆ
Variables ช่วยให้ Playbook ของเรานำกลับมาใช้ใหม่ได้ (Reusable) โดยไม่ต้อง Hardcode ค่าต่างๆ ลงไป
{{ variable_name }}---
- name: Install and configure web server
hosts: webservers
vars:
http_port: 8080
package_name: nginx
tasks:
- name: Install package
apt:
name: "{{ package_name }}"
state: present
เมื่อเราต้องการตั้งค่าให้แต่ละเครื่อง หรือแต่ละกลุ่มไม่เหมือนกัน
# ไฟล์ inventory.ini
[web_dev]
192.168.1.10 http_port=8080
[web_prod]
192.168.1.20 http_port=80
แทนที่จะใส่ในไฟล์ inventory นิยมแยกโฟลเดอร์:
ถ้าตั้งชื่อตัวแปรซ้ำกันในหลายที่ Ansible จะยึดค่าจากที่ไหน? (เรียงจากต่ำสุดไปสูงสุด)
-e ใน Command Line) (สูงสุด! ทับได้ทุกอย่าง)
ansible-playbook setup.yml -e "http_port=80"
ทุกครั้งที่รัน Playbook ขั้นตอนแรกคือ Gathering Facts (สังเกตใน log) Ansible จะดึงข้อมูลทั้งหมดของเครื่องเป้าหมายมาเก็บไว้ในตัวแปรอัตโนมัติ
OS Family: {{ ansible_os_family }} (เช่น Debian, RedHat)
IP Address: {{ ansible_default_ipv4.address }}
Total RAM: {{ ansible_memtotal_mb }}
💡 Use Case: สามารถเขียนเงื่อนไขติดตั้ง apt ถ้า OS เป็น Debian และติดตั้งด้วย yum ถ้า OS เป็น RedHat ได้
ในโลกความเป็นจริง ไฟล์ Configuration (เช่น nginx.conf, my.cnf) ของแต่ละสภาพแวดล้อม (Dev, Prod) จะไม่เหมือนกันเป๊ะ
copy ก๊อปปี้ไฟล์ Static ธรรมดา
Ansible ใช้เอนจิน Template ที่ชื่อว่า Jinja2 (ของ Python) นามสกุลไฟล์มักใช้ .j2
ใช้ปีกกาคู่เพื่อพิมพ์ค่า
ListenPort {{ http_port }}
ServerAdmin {{ admin_email }}
ใช้ปีกกาและเปอร์เซ็นต์ สำหรับ IF/FOR
{% if enable_ssl %}
listen 443 ssl;
{% endif %}
การปรับเนื้อหาไฟล์ตามค่าตัวแปร
# ไฟล์ nginx.conf.j2
server {
listen {{ http_port }};
server_name {{ domain_name }};
{% if enable_php %}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
}
{% else %}
# PHP is disabled
{% endif %}
}
มีประโยชน์มากเมื่อต้องการใส่รายชื่อหลายๆ อัน เช่น DNS Servers, หรือ Allow IPs
# ใน Playbook (ตัวแปร List)
dns_servers:
- 8.8.8.8
- 1.1.1.1
- 9.9.9.9
# ใน resolv.conf.j2
{% for dns in dns_servers %}
nameserver {{ dns }}
{% endfor %}
เมื่อเขียนไฟล์ .j2 เสร็จแล้ว ให้เรียกใช้ด้วยโมดูล template
- name: Configure Nginx using template
template:
src: templates/nginx.conf.j2 # ไฟล์ต้นทางที่เครื่องเรา
dest: /etc/nginx/nginx.conf # ไฟล์ปลายทางที่เครื่องเป้าหมาย
owner: root
group: root
mode: '0644'
notify: Restart Nginx # สั่ง Restart Service ทันทีถ้าไฟล์มีการเปลี่ยนแปลง
เมื่อโปรเจกต์ใหญ่ขึ้น ไฟล์ Playbook เดียวอาจมีขนาดใหญ่ถึง 1,000+ บรรทัด รวมงานติดตั้ง DB, Web, Cache ไว้ในไฟล์เดียว
Spaghetti Code!
Roles คือวิธีการจัดระเบียบและแบ่ง Playbook ขนาดใหญ่ออกเป็นส่วนย่อยๆ (Modular) ตามหน้าที่การทำงาน (เช่น Role สำหรับ Nginx, Role สำหรับ MySQL)
Role หนึ่งตัวจะมีโครงสร้างโฟลเดอร์มาตรฐานที่ Ansible เข้าใจอัตโนมัติ
roles/my_role/
├── tasks/ # เก็บไฟล์งาน (main.yml)
├── handlers/ # เก็บคำสั่ง service restart (main.yml)
├── templates/ # เก็บไฟล์ .j2 (Jinja2)
├── files/ # เก็บไฟล์ static ธรรมดา
├── vars/ # เก็บตัวแปรที่มี Priority สูง
├── defaults/ # เก็บตัวแปรค่าตั้งต้น (Priority ต่ำสุด)
└── meta/ # เก็บข้อมูลผู้สร้าง และ dependencies
จุดเริ่มต้นของ Role ไม่ต้องมี hosts: หรือ tasks: แล้ว ใส่รายการงานได้เลย
- name: Install nginx
apt:
name: nginx
ใส่ค่าตัวแปรเริ่มต้น ผู้ใช้ที่เรียก Role นี้สามารถตั้งค่าทับได้ง่ายๆ
nginx_port: 80
nginx_user: www-data
Handlers คือ Task พิเศษที่จะทำงานก็ต่อเมื่อถูกเรียก (Notified) เท่านั้น นิยมใช้กับ Service Restart
# ใน handlers/main.yml
- name: Restart Nginx
service:
name: nginx
state: restarted
# ใน tasks/main.yml
- name: Update Nginx Config
template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf
notify: Restart Nginx # ชื่อต้องตรงกันเป๊ะ
ไม่ต้องสร้างโฟลเดอร์เองทีละอัน Ansible มีคำสั่งสำหรับสร้างโครงสร้าง Role ให้ทันที
$ ansible-galaxy role init my_webserver
คำสั่งนี้จะสร้างโฟลเดอร์ my_webserver พร้อมโฟลเดอร์ย่อย (tasks, handlers, vars, ฯลฯ) ที่มีไฟล์ main.yml เตรียมไว้ให้คุณเข้าไปเขียนโค้ดได้เลย
เมื่อเรามี Role แล้ว Playbook หลัก (site.yml) ของเราจะสะอาดและอ่านง่ายมาก
# site.yml (Playbook หลัก)
---
- name: Configure Web Servers
hosts: webservers
become: true
roles:
- common_setup
- my_webserver
- name: Configure Database Servers
hosts: dbservers
become: true
roles:
- common_setup
- role: mysql_db
vars:
mysql_port: 3306 # ส่งค่าตัวแปรเข้าไปใน Role ได้
การเขียนรหัสผ่าน Database, API Keys, หรือ SSL Certificates เป็น Plain-text ลงใน Playbook และ Push ขึ้น Git (เช่น GitHub) เป็นความเสี่ยงระดับวิกฤต
db_password: "SuperSecretPassword123!"
Data Breach Risk
Ansible Vault คือฟีเจอร์ที่ช่วยให้เราสามารถ เข้ารหัส (Encrypt) ข้อมูลตัวแปร หรือไฟล์ทั้งไฟล์ ด้วยอัลกอริทึม AES256
$ ansible-vault create secrets.yml
$ ansible-vault edit secrets.yml
$ ansible-vault encrypt db_vars.yml
$ ansible-vault decrypt db_vars.yml
บางครั้งเราไม่อยากเข้ารหัสทั้งไฟล์ เราสามารถเข้ารหัสเฉพาะค่าของตัวแปร (Value) ได้
$ ansible-vault encrypt_string 'MySecretPass' --name 'db_password'
# นำผลลัพธ์ไปแปะในไฟล์ vars/main.yml ได้เลย:
db_password:
!vault |$ANSIBLE_VAULT;1.1;AES256
38366531393635383561336661333465353163353531636239333939616630323334346162353137
...
วิธีนี้ทำให้เรายังสามารถอ่าน Key name (เช่น db_password) ได้ออก แต่ค่า (Value) ปลอดภัย
เมื่อเรารัน Playbook ที่มีการอ้างอิงถึงไฟล์ Vault หรือตัวแปร Vault เราต้องส่งรหัสผ่านให้ Ansible ด้วย
ใช้ option --ask-vault-pass
ansible-playbook site.yml --ask-vault-pass
เก็บรหัสไว้ในไฟล์ (อย่าเอาขึ้น Git!) แล้วใช้ option --vault-password-file
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
ยกระดับโปรเจกต์ Ansible จากสัปดาห์ที่แล้ว ให้มีโครงสร้างระดับ Enterprise
ansible-galaxy init สร้าง Ansible Role สำหรับติดตั้ง Nginxnginx.conf.j2 โดยใช้ Jinja2 Template และดึงค่าจากตัวแปร (Variables)ยกระดับการเขียน Playbook สู่มาตรฐานการทำงานจริงด้วย Roles, Templates และ Vault
สร้างโครงสร้างโฟลเดอร์สำหรับโปรเจกต์ใหม่และเตรียมไฟล์ Inventory
# 1. สร้างโฟลเดอร์โปรเจกต์และเข้าไปในโฟลเดอร์
$ mkdir ansible-advanced-lab && cd ansible-advanced-lab
# 2. สร้างไฟล์ Inventory
$ nano hosts
# 3. ใส่ข้อมูลเครื่องเป้าหมาย (เปลี่ยน IP และ User ตามจริง)
[webservers]
192.168.x.x ansible_user=ubuntu
เราจะใช้คำสั่ง ansible-galaxy ในการสร้างโครงสร้างโฟลเดอร์สำหรับ Role โดยอัตโนมัติ
# 1. สร้างโฟลเดอร์ roles เพื่อเก็บ Role ทั้งหมด
$ mkdir roles && cd roles
# 2. สร้าง Role ใหม่ชื่อ nginx_web
$ ansible-galaxy role init nginx_web
- Role nginx_web was created successfully
# 3. กลับมาที่โฟลเดอร์หลัก
$ cd ..
roles/nginx_web/ เช่น tasks/, handlers/, templates/ เป็นต้น
กำหนดค่าพื้นฐานให้กับ Role ของเรา เพื่อให้นำไปใช้ซ้ำหรือปรับแก้ได้ง่าย
# แก้ไขไฟล์ roles/nginx_web/defaults/main.yml
$ nano roles/nginx_web/defaults/main.yml
---
# defaults file for nginx_web
http_port: 80
server_name: localhost
app_greeting: "Welcome to Ansible Roles & Vault!"
สร้างไฟล์ Configuration ของ Nginx ที่จะเปลี่ยนค่าไปตามตัวแปรที่เรากำหนด
# สร้างไฟล์ roles/nginx_web/templates/nginx.conf.j2
$ nano roles/nginx_web/templates/nginx.conf.j2
server {
listen {{ http_port }} default_server;
server_name {{ server_name }};
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
แทนที่จะใช้ไฟล์ Static ธรรมดา เราจะใช้ Template ฝังตัวแปรลงในหน้าเว็บ
# สร้างไฟล์ roles/nginx_web/templates/index.html.j2
$ nano roles/nginx_web/templates/index.html.j2
<!DOCTYPE html>
<html>
<body style="background-color: #1a202c; color: white; text-align: center; padding-top: 100px;">
<h1>{{ app_greeting }}</h1>
<p>Server Name: {{ server_name }}</p>
<p>Simulated DB Password: {{ db_password | default('Not Provided') }}</p>
</body>
</html>
เมื่อมีการแก้ไขไฟล์ Configuration ควรให้ Nginx สั่ง Restart ตัวเอง
# แก้ไขไฟล์ roles/nginx_web/handlers/main.yml
$ nano roles/nginx_web/handlers/main.yml
---
# handlers file for nginx_web
- name: Restart Nginx
service:
name: nginx
state: restarted
# แก้ไขไฟล์ roles/nginx_web/tasks/main.yml
---
- name: Install Nginx
apt: name=nginx state=present update_cache=yes
- name: Deploy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Restart Nginx # เรียกใช้ Handler เมื่อไฟล์เปลี่ยน
- name: Deploy Web Application (index.html)
template:
src: index.html.j2
dest: /var/www/html/index.html
- name: Ensure Nginx is running
service: name=nginx state=started enabled=yes
เราจะจำลองการเก็บรหัสผ่าน Database และใช้ Vault เพื่อเข้ารหัสไฟล์
# 1. สร้างโฟลเดอร์ group_vars สำหรับกลุ่ม webservers
$ mkdir -p group_vars/webservers
# 2. สร้างและเข้ารหัสไฟล์ vault
$ ansible-vault create group_vars/webservers/secrets.yml
New Vault password: (ตั้งรหัสผ่านของคุณ เช่น secret123)
Confirm New Vault password:
# 3. Text Editor จะเปิดขึ้นมา ให้พิมพ์ข้อมูลนี้ลงไป:
db_password: "SuperSecretPassword123!"
# (Save และ Exit ไฟล์)
คุณสามารถใช้ `cat group_vars/webservers/secrets.yml` เพื่อดูว่าไฟล์ถูกเข้ารหัสแล้ว (อ่านไม่รู้เรื่อง)
กลับมาที่โฟลเดอร์หลัก สร้างไฟล์ site.yml เพื่อเรียกใช้งาน Role
$ nano site.yml
---
- name: Configure Web Infrastructure
hosts: webservers
become: yes
roles:
- nginx_web
ต้องใช้ flag --ask-vault-pass เพื่อปลดล็อกตัวแปรความลับส่งไปให้ Jinja2 Template
$ ansible-playbook -i hosts site.yml -K --ask-vault-pass
Vault password: (พิมพ์รหัส vault ที่ตั้งไว้)
BECOME password: (พิมพ์รหัส sudo ของเครื่องปลายทาง)
# สังเกตผลลัพธ์ Tasks ที่รันจาก Role
changed: [192.168.x.x] => (item=Install Nginx)
changed: [192.168.x.x] => (item=Deploy Config)
นำ IP เครื่องเป้าหมายไปเปิดในเบราว์เซอร์ จะเห็นข้อความและ รหัสผ่านจาก Vault แสดงอยู่ (ในทางปฏิบัติจริง เราจะไม่ Print Password ออกหน้าเว็บ แต่ใน Lab นี้ทำเพื่อพิสูจน์ว่า Vault ทำงานได้)
รันคำสั่งเดิมซ้ำอีกครั้ง เพื่อดูความฉลาดของ Ansible
$ ansible-playbook -i hosts site.yml -K --ask-vault-pass
TASK [nginx_web : Install Nginx]
ok: [192.168.x.x]
TASK [nginx_web : Deploy Nginx configuration]
ok: [192.168.x.x]
PLAY RECAP ***************************************
192.168.x.x : ok=5 changed=0 unreachable=0 failed=0
*ผลคือ ok=5 changed=0 เพราะ Ansible รู้ว่าไม่มีอะไรต้องอัปเดต และ Handler "Restart Nginx" ก็จะไม่ถูกเรียกทำงาน!
roles/nginx_web)site.yml และ hostsgroup_vars/webservers/secrets.yml