Instruções: como testar funções ansible e descobrir problemas antes da produção

Olá a todos!

Trabalho como engenheiro DevOps em um serviço de reserva de hotéis. Ostrovok.ru. Neste artigo, quero falar sobre nossa experiência em testar funções ansible.

Em Ostrovok.ru, usamos ansible como gerenciador de configuração. Recentemente, chegamos à necessidade de testar funções, mas descobrimos que não existem tantas ferramentas para isso - a mais popular, talvez, seja a estrutura Molecule, então decidimos usá-la. Mas descobriu-se que sua documentação não menciona muitas armadilhas. Não conseguimos encontrar um manual suficientemente detalhado em russo, por isso decidimos escrever este artigo.

Instruções: como testar funções ansible e descobrir problemas antes da produção

molécula

Molécula - uma estrutura para ajudar a testar funções ansible.

Descrição simplificada: A molécula cria uma instância na plataforma que você especifica (nuvem, máquina virtual, contêiner; para mais detalhes, consulte a seção Dirigir), executa sua função nele, executa testes e exclui a instância. Em caso de falha em uma das etapas, a Molécula irá informá-lo.

Agora mais.

Um pouco de teoria

Considere duas entidades principais da Molécula: Cenário e Driver.

Cenário

O script contém uma descrição do que, onde, como e em que sequência será executado. Uma função pode ter vários scripts e cada um é um diretório ao longo do caminho <role>/molecule/<scenario>, que contém descrições das ações necessárias para o teste. O script deve ser incluído default, que será criado automaticamente se você inicializar a função com uma molécula. Os nomes dos scripts a seguir são com você.

A sequência de ações de teste em um script é chamada matriz, e por padrão é:

(Etapas rotuladas ?, ignorado por padrão se não for especificado pelo usuário)

  • lint - executando linters. Por padrão são usados yamllint и flake8,
  • destroy - exclusão de instâncias do último lançamento da Molécula (se houver),
  • dependency? — instalação da dependência ansible da função testada,
  • syntax - verificar a sintaxe da função usando ansible-playbook --syntax-check,
  • create - criando uma instância,
  • prepare? — preparação da instância; por exemplo, verifique/instale python2
  • converge — lançamento do manual que está sendo testado,
  • idempotence - reiniciar o manual para o teste de idempotência,
  • side_effect? - ações não diretamente relacionadas à função, mas necessárias para testes,
  • verify - executar testes da configuração resultante usando testinfra(padrão) /goss/inspec,
  • cleanup? - (em novas versões) - grosso modo, “limpar” a infraestrutura externa afetada pela Molécula,
  • destroy - Excluindo uma instância.

Esta sequência cobre a maioria dos casos, mas pode ser alterada se necessário.

Cada uma das etapas acima pode ser executada separadamente com molecule <command>. Mas deve ser entendido que para cada comando cli pode haver sua própria sequência de ações, que você pode descobrir executando molecule matrix <command>. Por exemplo, ao executar o comando converge (executando o playbook em teste), as seguintes ações serão executadas:

$ molecule matrix converge
...
└── default         # название сценария
    ├── dependency  # установка зависимостей
    ├── create      # создание инстанса
    ├── prepare     # преднастройка инстанса
    └── converge    # прогон плейбука

A sequência dessas ações pode ser editada. Se algo da lista já tiver sido feito, será ignorado. O estado atual, bem como a configuração das instâncias, o Molecule armazena no diretório $TMPDIR/molecule/<role>/<scenario>.

Adicione etapas com ? você pode descrever as ações desejadas no formato ansible-playbook e definir o nome do arquivo de acordo com a etapa: prepare.yml/side_effect.yml. Espere que esses arquivos da molécula estejam na pasta do script.

Dirigir

Um driver é uma entidade onde as instâncias de teste são criadas.
A lista de drivers padrão para os quais o Molecule possui templates prontos é a seguinte: Azure, Docker, EC2, GCE, LXC, LXD, OpenStack, Vagrant, Delegado.

Na maioria dos casos, os modelos são arquivos create.yml и destroy.yml na pasta de script que descreve a criação e exclusão de uma instância, respectivamente.
As exceções são Docker e Vagrant, pois as interações com seus módulos podem ocorrer sem os arquivos mencionados.

Vale destacar o driver Delegado, pois caso ele seja utilizado nos arquivos de criação e exclusão de instância, apenas é descrito o trabalho com configuração de instâncias, o restante deverá ser descrito pelo engenheiro.

O driver padrão é Docker.

Agora vamos praticar e considerar outros recursos.

Introdução

Como um "olá mundo", vamos testar uma função simples de instalação do nginx. Vamos escolher o docker como driver - acho que a maioria de vocês o tem instalado (e lembrem-se de que o docker é o driver padrão).

Preparar virtualenv e instale nele molecule:

> pip install virtualenv
> virtualenv -p `which python2` venv
> source venv/bin/activate
> pip install molecule docker  # molecule установит ansible как зависимость; docker для драйвера

A próxima etapa é inicializar a nova função.
A inicialização de uma nova função, bem como de um novo script, é realizada usando o comando molecule init <params>:

> molecule init role -r nginx
--> Initializing new role nginx...
Initialized role in <path>/nginx successfully.
> cd nginx
> tree -L 1
.
├── README.md
├── defaults
├── handlers
├── meta
├── molecule
├── tasks
└── vars

6 directories, 1 file

Acabou sendo um típico papel ansible. Além disso, todas as interações com moléculas CLI são feitas a partir da raiz da função.

Vamos ver o que está no diretório role:

> tree molecule/default/
molecule/default/
├── Dockerfile.j2  # Jinja-шаблон для Dockerfile
├── INSTALL.rst.   # Немного информации об установке зависимостей сценария
├── molecule.yml   # Файл конфигурации
├── playbook.yml   # Плейбук запуска роли
└── tests          # Директория с тестами стадии verify
    └── test_default.py

1 directory, 6 files

Vamos analisar a configuração molecule/default/molecule.yml (substitua apenas a imagem do docker):

---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: centos:7
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8

dependência

Esta seção descreve a origem das dependências.

Opções possíveis: galáxia, dourado, concha.

Shell é apenas um shell de comando usado caso o Galaxy e o Gilt não atendam às suas necessidades.

Não vou ficar aqui por muito tempo, está bastante descrito em documentação.

motorista

O nome do motorista. O nosso é docker.

cotão

O linter é yamllint.

Opções úteis nesta parte da configuração são a capacidade de especificar um arquivo de configuração para o yamllint, encaminhar variáveis ​​de ambiente ou desabilitar o linter:

lint:
  name: yamllint
  options:
    config-file: foo/bar
  env:
    FOO: bar
  enabled: False

plataformas

Descreve a configuração das instâncias.
No caso do docker como driver, a molécula é iterada nesta seção e cada elemento da lista está disponível em Dockerfile.j2 como uma variável item.

No caso de um driver que exija create.yml и destroy.yml, a seção está disponível neles como molecule_yml.platforms, e as iterações sobre ele já estão descritas nesses arquivos.

Como o Molecule fornece controle de instâncias para módulos ansible, a lista de configurações possíveis também deve ser procurada lá. Para docker, por exemplo, o módulo é usado docker_container_module. Quais módulos são usados ​​​​em outros drivers podem ser encontrados em documentação.

Bem como exemplos do uso de vários drivers podem ser encontrados nos testes da própria molécula.

Substitua aqui centos:7 em ubuntu.

provedor

“Fornecedor” – entidade que gerencia instâncias. No caso do Molecule, isso é ansible, o suporte para outros não está planejado, portanto esta seção pode ser chamada de configuração estendida ansible com uma ressalva.
Aqui você pode especificar muitas coisas, vou destacar os pontos principais, na minha opinião:

  • cartilhas: você pode especificar quais playbooks devem ser usados ​​em determinados estágios.

provisioner:
  name: ansible
  playbooks:
    create: create.yml
    destroy: ../default/destroy.yml
    converge: playbook.yml
    side_effect: side_effect.yml
    cleanup: cleanup.yml

provisioner:
  name: ansible
  config_options:
    defaults:
      fact_caching: jsonfile
    ssh_connection:
      scp_if_ssh: True

provisioner:
  name: ansible  
  connection_options:
    ansible_ssh_common_args: "-o 'UserKnownHostsFile=/dev/null' -o 'ForwardAgent=yes'"

  • opções: Opções Ansible e variáveis ​​de ambiente

provisioner:
  name: ansible  
  options:
    vvv: true
    diff: true
  env:
    FOO: BAR

cenário

Nome e descrição das sequências de script.
Você pode alterar a matriz de ação padrão de qualquer comando adicionando a chave <command>_sequence e como um valor para isso, definindo a lista de etapas que precisamos.
Digamos que queremos alterar a sequência de ações ao executar o comando run do playbook: molecule converge

# изначально:
# - dependency
# - create
# - prepare
# - converge
scenario:
  name: default
  converge_sequence:
    - create
    - converge

verificador

Configurando uma estrutura para testes e um linter para ela. O linter padrão é testinfra и flake8. As opções possíveis são as mesmas acima:

verifier:
  name: testinfra
  additional_files_or_dirs:
    - ../path/to/test_1.py
    - ../path/to/test_2.py
    - ../path/to/directory/*
  options:
    n: 1
  enabled: False
  env:
    FOO: bar
  lint:
    name: flake8
    options:
      benchmark: True
    enabled: False
    env:
      FOO: bar

Voltemos ao nosso papel. Vamos editar o arquivo tasks/main.yml para este tipo:

---
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Start nginx
  service:
    name: nginx
    state: started

E adicione testes a molecule/default/tests/test_default.py

def test_nginx_is_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

def test_nginx_running_and_enabled(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

def test_nginx_config(host):
    host.run("nginx -t")

Pronto, resta apenas executar (da raiz do papel, deixe-me lembrá-lo):

> molecule test

Escape longo sob o spoiler:

--> Validating schema <path>/nginx/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in <path>/nginx/...
Lint completed successfully.
--> Executing Flake8 on files found in <path>/nginx/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on <path>/nginx/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: <path>/nginx/molecule/default/playbook.yml

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Create docker network(s)] ************************************************

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=4    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    TASK [nginx : Install nginx] ***************************************************
    changed: [instance]

    TASK [nginx : Start nginx] *****************************************************
    changed: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=3    changed=2    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in <path>/nginx/molecule/default/tests/...
    ============================= test session starts ==============================
    platform darwin -- Python 2.7.15, pytest-4.3.0, py-1.8.0, pluggy-0.9.0
    rootdir: <path>/nginx/molecule/default, inifile:
    plugins: testinfra-1.16.0
collected 4 items

    tests/test_default.py ....                                               [100%]

    ========================== 4 passed in 27.23 seconds ===========================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

Nosso papel simples foi testado sem problemas.
Vale lembrar que caso haja problemas durante a obra molecule test, então, se você não alterou a sequência padrão, o Molecule excluirá a instância.

Os seguintes comandos são úteis para depuração:

> molecule --debug <command> # debug info. При обычном запуске Молекула скрывает логи.
> molecule converge          # Оставляет инстанс после прогона тестируемой роли.
> molecule login             # Зайти в созданный инстанс.
> molecule --help            # Полный список команд.

Função existente

Adicionar um novo script a uma função existente é do diretório de funções com os seguintes comandos:

# полный список доступных параметров
> molecule init scenarion --help
# создание нового сценария
> molecule init scenario -r <role_name> -s <scenario_name>

Caso este seja o primeiro cenário da função, então o parâmetro -s pode ser omitido pois criará um script default.

Conclusão

Como você pode ver, o Molecule não é muito complexo e, usando seus próprios modelos, a implantação de um novo script pode ser reduzida à edição de variáveis ​​nos manuais de criação e exclusão de instâncias. A molécula se integra perfeitamente aos sistemas de CI, o que permite aumentar a velocidade de desenvolvimento, reduzindo o tempo de teste manual de playbooks.

Obrigado pela sua atenção. Se você tem experiência em testar papéis ansible, e isso não está relacionado à Molécula, conte-nos nos comentários!

Fonte: habr.com

Adicionar um comentário