Viết Custom Module Bằng Python
Ansible có hàng nghìn module, nhưng đôi khi không cái nào vừa nhu cầu của bạn — gọi một API nội bộ, thao tác một định dạng file đặc thù. Lúc đó bạn tự viết module. Đây là deep-dive khép lại vòng tròn: nhớ Bài 1, module chỉ là một chương trình Python nhận tham số (JSON vào) trả kết quả (JSON ra), được ship lên host chạy. Giờ ta viết một cái.
Khi nào cần viết module
Trước khi viết, hãy chắc không có module sẵn (tra ansible-doc -l, Galaxy). Viết module khi:
- Cần tích hợp một hệ thống nội bộ (API riêng của công ty).
- Logic phức tạp mà ghép
command/shellra vừa xấu vừa không idempotent (Bài 5). - Muốn một thao tác idempotent cho thứ chưa có module.
Nếu chỉ cần biến đổi dữ liệu trong Jinja2 hay lấy dữ liệu ngoài, có khi plugin (Bài 12) hợp hơn module.
Cấu trúc một module
Module là một script Python dùng lớp trợ giúp AnsibleModule. Đây là một module idempotent đảm bảo một file có nội dung cho trước:
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
import os
def run_module():
# 1. Khai báo tham số module nhận
module = AnsibleModule(
argument_spec=dict(
path=dict(type='str', required=True),
content=dict(type='str', required=True),
),
supports_check_mode=True, # khai báo có hỗ trợ --check
)
path = module.params['path']
content = module.params['content']
# 2. Đọc trạng thái HIỆN TẠI
current = None
if os.path.exists(path):
with open(path) as f:
current = f.read()
# 3. So sánh với MONG MUỐN → có cần đổi không?
changed = current != content
# 4. Chỉ thực hiện nếu cần VÀ không phải check mode
if changed and not module.check_mode:
with open(path, 'w') as f:
f.write(content)
# 5. Trả kết quả dạng JSON
module.exit_json(changed=changed, path=path)
if __name__ == '__main__':
run_module()
Năm phần này là khung của mọi module:
AnsibleModule(argument_spec=...)— khai báo các tham số module nhận (kiểu, bắt buộc, mặc định). Ansible tự kiểm tra tham số đầu vào theo spec này.- Đọc trạng thái hiện tại trên host.
- So sánh với trạng thái mong muốn → tính
changed. - Chỉ hành động khi cần và tôn trọng
check_mode(không đổi gì khi--check). exit_json(changed=..., ...)trả kết quả thành công (hoặcfail_json(msg=...)khi lỗi).
Bước 2–4 chính là pattern idempotency từ Bài 5, giờ bạn tự hiện thực nó.
Đặt module ở đâu
Ansible tìm custom module theo thứ tự: thư mục library/ cạnh playbook (tự tìm), hoặc đường dẫn khai báo trong ansible.cfg:
[defaults]
library = ./library
Lưu ý đã kiểm chứng: playbook tự tìm
library/cạnh nó, nhưng lệnh ad-hoc thì không — phải khai báolibrary = ./librarytrongansible.cfg(hoặc biến môi trườngANSIBLE_LIBRARY). Bỏ qua điều này sẽ gặp lỗiCannot resolve '<module>' to an action or module. Ở quy mô lớn, đóng gói module vào một collection (Bài 9) là cách phân phối chuẩn.
Chạy thử module
Đặt file trên ở library/ensure_file.py, khai báo library trong cfg, rồi chạy như bất kỳ module nào:
# Lần 1: file chưa tồn tại → phải tạo
ansible all -b -m ensure_file -a 'path=/tmp/custom.txt content=hello'
lab | CHANGED => { "changed": true, "path": "/tmp/custom.txt" }
# Lần 2: nội dung đã đúng → không làm gì
ansible all -b -m ensure_file -a 'path=/tmp/custom.txt content=hello'
lab | SUCCESS => { "changed": false, "path": "/tmp/custom.txt" }
Module của bạn idempotent: lần 1 CHANGED (tạo file), lần 2 SUCCESS/changed: false (đã đúng, không làm gì) — và cat /tmp/custom.txt trên host trả về hello. Vì khai báo supports_check_mode=True, nó cũng tôn trọng --check (báo sẽ đổi mà không áp).
Nhớ Bài 1: khi chạy, Ansible đóng gói ensure_file.py này (cùng module_utils) thành AnsiballZ, ship qua SSH lên host, chạy bằng Python của host, đọc JSON exit_json trả về. Module bạn vừa viết đi đúng vòng đời đó.
exit_json và fail_json
Hai cách kết thúc module:
module.exit_json(changed=True, path=path, msg="Đã ghi file") # thành công
module.fail_json(msg="Không ghi được: %s" % err) # lỗi (task FAILED)
Mọi key bạn truyền vào exit_json trở thành dữ liệu trả về của task — dùng được với register (Bài 6). Quy ước: luôn trả changed, và trả thêm thông tin hữu ích (đường dẫn, kết quả). Khi có lỗi, fail_json với msg rõ ràng để người dùng gỡ.
Documentation block: cho ansible-doc
Module chuyên nghiệp kèm ba khối tài liệu (chuỗi YAML) để ansible-doc <module> đọc được và để kiểm thử:
DOCUMENTATION = r'''
module: ensure_file
short_description: Đảm bảo file có nội dung cho trước
options:
path: { description: Đường dẫn file, required: true, type: str }
content: { description: Nội dung mong muốn, required: true, type: str }
'''
EXAMPLES = r'''
- name: Ghi file cấu hình
ensure_file: { path: /etc/app.conf, content: "key=value" }
'''
RETURN = r'''
path: { description: Đường dẫn file, type: str, returned: always }
'''
DOCUMENTATION còn được ansible-lint/sanity test kiểm tra (Bài 14, 15) — module thiếu nó bị coi là chưa hoàn chỉnh.
Mẹo viết module tốt
- Idempotent là bắt buộc: luôn đọc-so-sánh-rồi-mới-đổi, đừng hành động mù.
- Hỗ trợ check_mode: kiểm
module.check_modetrước khi thay đổi thật. - Kiểm tra đầu vào qua
argument_spec(kiểu,required,choices,default) thay vì tự validate. - Thông báo lỗi rõ qua
fail_json(msg=...). - Không in ra stdout ngoài JSON của
exit_json— Ansible đọc stdout làm JSON (Bài 1), in thừa sẽ làm hỏng.
🧹 Dọn dẹp
Module mẫu ở repo nghiadaulau/ansible-series, thư mục 11-custom-module/library.
Tổng kết
Tự viết module bằng Python với khung AnsibleModule: khai báo tham số qua argument_spec, đọc trạng thái hiện tại, so với mong muốn để tính changed, chỉ hành động khi cần và tôn trọng check_mode, rồi trả kết quả bằng exit_json/fail_json. Đặt module ở library/ (playbook tự tìm; ad-hoc cần khai library= trong cfg) hoặc đóng gói vào collection. Module bạn viết đi đúng vòng đời AnsiballZ ở Bài 1 — đến đây cơ chế Ansible khép kín. Kèm DOCUMENTATION để chuyên nghiệp.
Module mở rộng việc thực thi trên host. Bài 12 mở rộng bản thân Ansible: filter, lookup, callback plugins — extensibility ở một chiều khác.