Viết Custom Module Bằng Python

K
Kai··5 min read

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/shell ra 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:

  1. 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.
  2. Đọc trạng thái hiện tại trên host.
  3. So sánh với trạng thái mong muốn → tính changed.
  4. Chỉ hành động khi cầntôn trọng check_mode (không đổi gì khi --check).
  5. exit_json(changed=..., ...) trả kết quả thành công (hoặc fail_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áo library = ./library trong ansible.cfg (hoặc biến môi trường ANSIBLE_LIBRARY). Bỏ qua điều này sẽ gặp lỗi Cannot 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_mode trướ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.