Согласно документации 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
Результат:
Теперь сравним производительность модулей через hyperfine:
Очевидно, что модуль 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)