Automação com Ansible e Python: Criando Playbooks Dinâmicos e Reutilizáveis

A Simbiose Poderosa: Ansible e Python

ansible, com sua abordagem declarativa baseada em YAML, simplifica drasticamente a automação de infraestrutura e o gerenciamento de configurações. No entanto, em cenários de alta complexidade, a rigidez do YAML pode se tornar um fator limitante. É neste ponto que a integração com Python se revela não apenas uma opção, mas uma necessidade estratégica para engenheiros de devops e SREs. Sendo o próprio Ansible construído em Python, a sinergia entre as duas tecnologias é nativa e extremamente poderosa.

A combinação permite transcender as limitações da lógica declarativa, introduzindo o poder imperativo do Python para manipular dados complexos, interagir com APIs de terceiros que não possuem módulos nativos, implementar lógicas de negócio específicas e criar inventários que se adaptam dinamicamente a ambientes em nuvem ou on-premises. Este artigo explora tecnicamente como utilizar Python para estender o Ansible, criando playbooks que são verdadeiramente dinâmicos, modulares e reutilizáveis.

Módulos Customizados em Python: Estendendo o Core do Ansible

A forma mais direta de estender o Ansible é através da criação de módulos customizados. Um módulo é uma unidade de trabalho reutilizável que o Ansible executa nos nós gerenciados. Embora a biblioteca padrão do Ansible seja vasta, sempre haverá casos de uso específicos, como a interação com uma API interna ou um dispositivo de hardware proprietário, que exigem uma solução sob medida.

Estrutura de um Módulo Ansible em Python

Um módulo Ansible escrito em Python segue uma estrutura padrão para garantir a integração com o motor de execução. O componente central é a classe AnsibleModule, importada de ansible.module_utils.basic.

A estrutura fundamental inclui:

  • Importações: A principal importação é from ansible.module_utils.basic import AnsibleModule.
  • Função main(): O ponto de entrada da execução do módulo.
  • argument_spec: Um dicionário que define os parâmetros que o módulo aceita, seus tipos, se são obrigatórios e valores padrão.
  • Instanciação do AnsibleModule: Cria um objeto que gerencia os parâmetros de entrada, a execução e a saída.
  • Lógica do Módulo: O código que realiza a tarefa desejada.
  • Saída: O módulo deve retornar um estado final utilizando module.exit_json(changed=True/False, ...) em caso de sucesso ou module.fail_json(msg="...") em caso de erro.

Exemplo Prático: Módulo para Validar SemVer

Imagine a necessidade de validar se a versão de um artefato de software, obtida de um registro, segue o versionamento semântico (SemVer). Poderíamos criar um módulo para isso.

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
import re

SEMVER_REGEX = re.compile(r'^(0|[1-9]d*).(0|[1-9]d*).(0|[1-9]d*)(?:-((?:0|[1-9]d*|d*[a-zA-Z-][0-9a-zA-Z-]*)(?:.(?:0|[1-9]d*|d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:+([0-9a-zA-Z-]+(?:.[0-9a-zA-Z-]+)*))?$')

def main():
    module_args = dict(
        version_string=dict(type='str', required=True)
    )

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

    version = module.params['version_string']

    if module.check_mode:
        module.exit_json(changed=False, is_valid=bool(SEMVER_REGEX.match(version)))

    is_valid = bool(SEMVER_REGEX.match(version))

    if is_valid:
        parts = SEMVER_REGEX.match(version).groups()
        result = {
            'is_valid': True,
            'major': parts[0],
            'minor': parts[1],
            'patch': parts[2]
        }
        module.exit_json(changed=False, **result)
    else:
        module.fail_json(msg=f"A string '{version}' não é uma versão SemVer válida.")

if __name__ == '__main__':
    main()

Para utilizar este módulo, salve-o como validate_semver.py dentro de um diretório library/ adjacente ao seu playbook. Em seguida, invoque-o no playbook:

- name: Validar a versão do artefato
  validate_semver:
    version_string: "1.2.3-alpha.1"
  register: semver_result

- name: Exibir resultado
  debug:
    var: semver_result

Inventários Dinâmicos: Gerenciando Infraestrutura em Escala

Manter arquivos de inventário estáticos é impraticável em ambientes cloud-native ou de grande escala, onde recursos são criados e destruídos programaticamente. Inventários dinâmicos resolvem este problema ao utilizar um script para buscar e formatar a lista de hosts em tempo real a partir de uma fonte de verdade, como uma API de um provedor de nuvem (AWS, Azure, GCP), um CMDB (ServiceNow) ou um sistema interno.

O Contrato do Script de Inventário

Um script de inventário dinâmico em Python deve seguir um contrato simples com o Ansible:

  • Quando executado com o argumento --list, o script deve retornar para a saída padrão (stdout) um objeto JSON descrevendo todos os grupos de hosts e seus respectivos hosts e variáveis.
  • Quando executado com o argumento --host <hostname>, o script deve retornar um objeto JSON contendo apenas as variáveis para o host especificado.

Estrutura do JSON de Saída para --list

O JSON retornado deve ter uma estrutura específica, onde cada chave do objeto principal é um nome de grupo. Um grupo especial, _meta, pode ser usado para definir variáveis de host de forma eficiente.

{
    "webservers": {
        "hosts": ["web1.example.com", "web2.example.com"],
        "vars": {
            "http_port": 8080
        }
    },
    "databases": {
        "hosts": ["db1.example.com"]
    },
    "_meta": {
        "hostvars": {
            "web1.example.com": {
                "ansible_user": "deployer"
            },
            "db1.example.com": {
                "ansible_user": "db_admin"
            }
        }
    }
}

Utilizar Python para gerar este JSON permite a integração com qualquer fonte de dados que possua uma biblioteca ou API acessível, tornando o inventário do Ansible um reflexo fiel e em tempo real da sua infraestrutura.

Filtros e Testes Jinja2 com Python

O Ansible utiliza o motor de templates Jinja2 extensivamente para manipulação de variáveis e geração de arquivos de configuração. É possível estender o Jinja2 com filtros e testes customizados escritos em Python para realizar transformações de dados que não são cobertas nativamente.

Filtros Customizados

Filtros são funções que recebem um valor como entrada e retornam um valor transformado. Um caso de uso comum é a formatação de dados para um formato específico. Por exemplo, um filtro para converter uma string para o formato Base64.

Para criar, crie um arquivo Python no diretório filter_plugins/. O arquivo deve definir uma classe chamada FilterModule que contém um método filters(), o qual retorna um dicionário mapeando nomes de filtros para as funções correspondentes.

# filter_plugins/custom_filters.py
import base64

def to_base64(string):
    return base64.b64encode(string.encode('utf-8')).decode('utf-8')

class FilterModule(object):
    def filters(self):
        return {
            'to_base64': to_base64
        }

No playbook, o filtro pode ser usado da seguinte forma: {{ 'my-secret-string' | to_base64 }}

Testes Customizados

Testes são funções que recebem um valor e retornam True ou False, úteis para validações em condicionais (when:). Por exemplo, um teste para verificar se um dicionário possui uma chave específica.

# test_plugins/custom_tests.py
def has_key(data, key):
    if not isinstance(data, dict):
        return False
    return key in data

class TestModule(object):
    def tests(self):
        return {
            'has_key': has_key
        }

Uso no playbook: when: my_variable is has_key('required_config')

Lookup Plugins: Injetando Dados Externos nos Playbooks

Lookup plugins são executados no controlador Ansible e servem para buscar dados de fontes externas. Eles são diferentes dos módulos, pois seu objetivo primário é retornar dados para serem usados em variáveis, laços ou templates, e não executar ações nos nós gerenciados.

Casos de uso incluem:

  • Buscar segredos de um cofre como HashiCorp Vault ou AWS Secrets Manager.
  • Ler dados de um arquivo CSV ou JSON.
  • Consultar um endpoint de uma API REST para obter informações de configuração.

A criação de um lookup plugin em Python envolve a herança da classe LookupBase e a implementação do método run(). O plugin deve ser colocado no diretório lookup_plugins/.

Callback Plugins para Relatórios e Integrações

Callback plugins permitem reagir a eventos durante a execução de um playbook. Eles são a ferramenta ideal para integração com sistemas de monitoramento, logging e notificação. Ao herdar da classe CallbackBase, é possível sobrescrever métodos que são acionados em eventos específicos, como o sucesso (v2_runner_on_ok), falha (v2_runner_on_failed) ou ao final da execução do playbook (v2_playbook_on_stats).

Com callbacks, você pode:

  • Enviar uma notificação para o Slack ou Microsoft Teams quando um deploy falha.
  • Registrar cada tarefa executada em um sistema de log centralizado como o ELK Stack ou Splunk.
  • Gerar um relatório customizado sobre as alterações realizadas na infraestrutura.
  • Acionar um pipeline de CI/CD subsequente após um provisionamento bem-sucedido.

Estratégias para Playbooks Reutilizáveis e Dinâmicos

A verdadeira força da integração entre Ansible e Python emerge quando combinamos essas técnicas. Um fluxo de trabalho avançado pode se parecer com o seguinte:

  1. Um inventário dinâmico em Python consulta a API da AWS para obter a lista de instâncias EC2 com uma tag específica.
  2. Um lookup plugin em Python busca as credenciais do banco de dados para a aplicação a partir do AWS Secrets Manager.
  3. O playbook itera sobre os hosts do inventário, utilizando um filtro Jinja2 customizado para formatar um nome de host único para os logs.
  4. Um módulo customizado em Python é executado em cada host para configurar um serviço proprietário, interagindo com sua API local para verificar o status e aplicar a configuração.
  5. Finalmente, um callback plugin envia uma notificação para um canal do Slack com o resumo da execução, incluindo quais hosts foram alterados e se ocorreram falhas.

Para gerenciar essa complexidade, é fundamental adotar boas práticas como o uso de Ansible Roles para encapsular a lógica (incluindo módulos, filtros e plugins customizados), o versionamento de todo o código (playbooks e scripts Python) em um repositório Git, e a escrita de testes unitários para o código Python, garantindo a robustez e a manutenibilidade da sua plataforma de automação.