Согласно документации ansible, начиная с Ansible 2.2, мы можем использовать бинарные модули, а значит мы можем написать модуль на любом компилируемом языке. (источник: https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#binary-modules)

Напишем базовый echo модуль на Python и Rust, а потом сравним их.

Python модуль

Писать модуль на python очень просто, в Ansible есть куча встроенных классов для работы с ними:

#!/usr/bin/python
# coding: utf-8

from __future__ import absolute_import, annotations, division, print_function
__metaclass__ = type

DOCUMENTATION = r"""
---
module: py_echo
author: abakanovskii
short_description: Echo some string
description:
    - This module echoes strings.
options:
    msg:
        description:
            - Message to echo.
        type: str
        required: true
"""

EXAMPLES = r"""
- name: Echo 123
  py_echo:
    msg: 123
"""

RETURN = r"""
msg:
  description: Message
  returned: always
  type: string
  sample: 123
"""

from ansible.module_utils.basic import AnsibleModule


def main():
    arg_spec = dict(
        msg=dict(type='str', required=True,),
    )

    module = AnsibleModule(
        supports_check_mode=True,
        argument_spec=arg_spec,
    )

    module.exit_json(msg=module.params['msg'])


if __name__ == '__main__':
    main()

Модуль очень простой, он просто берет msg в качестве обязательного апргумента и сразу же возвращает его.

Rust модуль

В Rust мы должны сами реализовать требуемые структуры и функции, но, к счастью, есть пример из самого Ansible. Модуль сначала читает из файла, потому что ansible запускает модули, передавая путь к файлу, который содержит все аргументы в формате JSON.

Создание проекта

Создадим проект и запишем код в src/main.rs:

cargo new ansible_mod

Код:

use serde::{Deserialize, Serialize};
use serde_json::Error;
use std::{
    env,
    process,
    fs::File,
    io::Read,
};


#[derive(Serialize, Deserialize)]
struct ModuleArgs {
    msg: String,
}

#[derive(Clone, Serialize, Deserialize)]
struct Response {
    msg: String,
    changed: bool,
    failed: bool,
}

fn exit_json(response_body: Response) {
    return_response(response_body)
}

fn fail_json(response_body: Response) {
    let failed_response: &mut Response = &mut response_body.clone();
    failed_response.failed = true;
    return_response(failed_response.clone())
}

fn return_response(resp: Response) {
    println!("{}", serde_json::to_string(&resp).unwrap());
    process::exit(resp.failed as i32);
}

fn read_file_contents(file_name: &str) -> Result<String, Box<std::io::Error>> {
    let mut json_string: String = String::new();
    File::open(file_name)?.read_to_string(&mut json_string)?;
    Ok(json_string)
}

fn parse_module_args(json_input: String) -> Result<ModuleArgs, Error> {
    Ok(
        ModuleArgs::from(
            serde_json::from_str(
                json_input.as_str()
            )?
        )
    )
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let program: &String = &args[0];

    let input_file_name: &str = match args.len() {
        2 => &args[1],
        _ => {
            eprintln!("Module '{program}' expects exactly one argument!");
            fail_json(Response {
                msg: "No module arguments file provided".to_string(),
                changed: false,
                failed: true,
            });
            ""
        }
    };

    let json_input: String = read_file_contents(input_file_name).map_err(|err| {
        eprintln!("Could not read file '{input_file_name}': {err}");
        fail_json(Response {
            msg: format!("Could not read input JSON file '{input_file_name}': {err}"),
            changed: false,
            failed: true,
        })
    }).unwrap();

    let module_args: ModuleArgs = parse_module_args(json_input).map_err(|err| {
        eprintln!("Error during parsing JSON module arguments: {err}");
        fail_json(Response {
            msg: format!("Malformed input JSON module arguments: {err}"),
            changed: false,
            failed: true,
        })
    }).unwrap();

    exit_json(Response {
        msg: format!("{}", &module_args.msg),
        changed: false,
        failed: false,
    });
}

Компиляция

Компилируем:

cargo build -r
cp target/release/ansible_mod .

Запуск и сравнение

Для запуска модулей локально без использования коллекций или ролей, нужно указать переменную окружения ANSIBLE_LIBRARY для каждой команды. Для простоты будем использовать подстановку команды pwd:

ANSIBLE_LIBRARY=$(pwd) ansible localhost -m py_echo -a msg=123
ANSIBLE_LIBRARY=$(pwd) ansible localhost -m ansible_mod -a msg=123

Результат: Ansible Rust Result

Теперь сравним производительность модулей через hyperfine: Ansible Rust Compare

Очевидно, что модуль Rust быстрее, и чем сложнее будет становится модуль, тем заметнее будет разница в скорости. (Ansible сам по себе тратит львиную долю времени на подключение, выполнение и т.д.)

Но как я уже упоминал ранее, писать модули на Python гораздо проще из-за обилия встроенных методов и классов для их работы. В коде Python я написал небольшую документацию для модуля, которую я могу просмотреть с помощью ansible-doc:

ANSIBLE_LIBRARY=$(pwd) ansible-doc -t module py_echo.py

Но я не смогу сделать то же самое с скомпилированным модулем Rust. Конечно, можно создать файл-заглушку ansible_mod.py только с переменными документации, но есть и другие инструменты, которые написаны только для python (например, ansible-lint)

Вывод

Для тяжелых операций можно переписать/написать модуль на компилированном языке, но переписывать все модули ради незначительного ускорения не имеет большого смысла, по ряду причин:

  • В Ansible у Python есть развитая экосистема и большое количество примеров
  • Python модули можно изменять на лету (пользователь может сам найти ошибку в коде и проверить)
  • Распространять модули в Python значительно проще и безопаснее

Если цель в том, чтобы значительно ускорить общее время выполнения плейбука, то в конечном итоге бутылочное горлышко будет в самом Ansible, а не в его отдельных модулях.

Была попытка переписать Ansible на Rust самим создателем Ansible, но вскоре проект был завершен, поскольку не нашел большого спроса в сообществе. (см. https://github.com/jetporch/jetporch)