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

Olá pessoal!


Trabalho como engenheiro de DevOps no serviço de reservas de hotéis Ostrovok.ru . Neste artigo, quero falar sobre a nossa experiência em testar funções ansíveis.


No Ostrovok.ru, usamos o ansible como gerenciador de configuração. Recentemente, surgiu a necessidade de testar funções, mas, como se viu, não existem muitas ferramentas para isso - a estrutura Molecule é talvez a mais popular, então decidimos usá-la. Mas a documentação dele ficou em silêncio sobre muitas armadilhas. Como não conseguimos encontrar um guia suficientemente detalhado em russo, decidimos escrever este artigo.



Molécula


Molécula - uma estrutura para ajudar a testar papéis ansíveis.


Descrição simplificada: a molécula cria uma instância na plataforma especificada (nuvem, máquina virtual, contêiner; para mais detalhes, consulte a seção Driver ), executa sua função nela, executa os testes e exclui a instância. Em caso de falha em uma das etapas, a molécula informará você sobre isso.


Agora com mais detalhes.


Pouco de teoria


Considere as duas principais entidades da molécula: cenário e driver.


Cenário


O script contém uma descrição do que, onde, como e em qual sequência será executada. Uma função pode ter vários scripts e cada um é um diretório no caminho <role>/molecule/<scenario> , que contém descrições das ações necessárias para o teste. O script default deve estar presente, que será criado automaticamente se você inicializar a função usando a Molécula. Os nomes dos seguintes cenários são escolhidos a seu critério.


A sequĂŞncia de teste no script Ă© chamada de matriz e, por padrĂŁo, Ă© a seguinte:


(As etapas marcadas com ? ignoradas por padrão, se não forem descritas pelo usuário)


  • lint - execute o linter. Por padrĂŁo, yamllint e flake8 ,
  • destroy - remova instâncias do Ăşltimo lançamento da molĂ©cula (se deixado),
  • dependency ? - instalação de dependĂŞncia ansiável da função testada,
  • syntax - verifique a sintaxe de uma função usando ansible-playbook --syntax-check ,
  • create - cria uma instância,
  • prepare ? - preparação da instância; por exemplo, verificação / instalação do python2
  • converge - lançamento do manual testado,
  • idempotence - reinicie o manual para o teste de idempotency,
  • side_effect ? - ações nĂŁo diretamente relacionadas ao papel, mas necessárias para testes,
  • verify - execute testes da configuração resultante usando testinfra (padrĂŁo) / goss / inspec ,
  • cleanup ? - (em novas versões) - grosso modo, a "limpeza" da infraestrutura externa afetada pela molĂ©cula,
  • destroy - excluir uma instância.

Essa sequência abrange a maioria dos casos, mas você pode alterá-la, se necessário.


Cada uma das etapas acima pode ser executada separadamente usando a molecule <command> . Mas vale a pena entender que, para cada um desses comandos cli, pode existir sua própria sequência de ações, que pode ser reconhecida pela execução da molecule matrix <command> . Por exemplo, quando você executa o comando converge (executa o manual testado), 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 alguma coisa da lista já tiver sido concluída, ela será ignorada. O estado atual, bem como a configuração da instância, é armazenado no diretório $TMPDIR/molecule/<role>/<scenario> .


Adicionar etapas com ? Você pode, após descrever as ações desejadas no formato do ansible-playbook, e criar o nome do arquivo de acordo com a etapa: prepare.yml / side_effect.yml . A molécula aguardará esses arquivos na pasta de scripts.


Driver


Um driver é uma entidade na qual as instâncias de teste são criadas.
A lista de drivers padrão para os quais a molécula tem modelos prontos é a seguinte: Azure, Docker, EC2, GCE, LXC, LXD, OpenStack, Vagrant, Delegado.


Na maioria dos casos, os modelos são os destroy.yml e destroy.yml na pasta de script que descrevem a criação e a exclusão da instância, respectivamente.
Exceções são Docker e Vagrant, pois as interações com seus módulos podem ocorrer sem os arquivos acima.


Vale ressaltar o driver Delegado, pois se ele for usado nos arquivos para criar e excluir uma instância, apenas o trabalho com a configuração das instâncias será descrito, o restante deverá ser descrito pelo engenheiro.


O driver padrĂŁo Ă© o Docker.


Agora, voltamos à prática e consideramos outros recursos lá.


Introdução


Como "olá mundo", testamos o simples papel de instalar o nginx. Escolheremos o docker como o driver - acho que ele está instalado na maioria de vocês (e lembre-se de que o docker é o driver padrão).


Prepare o virtualenv e instale a molecule nele:


 > pip install virtualenv > virtualenv -p `which python2` venv > source venv/bin/activate > pip install molecule docker # molecule  ansible  ; docker   

O próximo passo é inicializar uma nova função.
A inicialização de uma nova função, bem como de um novo cenário, é executada usando o comando molecule init <params> da 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 

O resultado foi um papel típico e ansioso. Além disso, todas as interações com as moléculas de CLI são feitas a partir da raiz do papel.


Vamos ver o que está no diretório de funções:


 > 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 da molecule/default/molecule.yml (substituiremos 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 fonte das dependências.


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


Shell é apenas um shell de comando usado se galáxia e dourado não atenderem às suas necessidades.


Não pararei aqui por um longo tempo, está descrito o suficiente na documentação .


motorista


O nome do driver. NĂłs temos esta janela de encaixe.


cotĂŁo


Como um linter, yamllint Ă© usado.


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 desativar o linter:


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

plataformas


Descreve a configuração de instâncias.
No caso da janela de encaixe como driver, a Molécula é iterada nesta seção e cada item da lista está disponível no Dockerfile.j2 como uma variável do item .


No caso do driver, no qual create.yml e destroy.yml , a seção está disponível neles como molecule_yml.platforms , e as iterações já estão descritas nesses arquivos.


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


E também exemplos de uso de vários drivers podem ser encontrados nos testes da própria molécula .


Substitua centos: 7 no ubuntu aqui .


provisionador


"Provedor" é a entidade que controla as instâncias. No caso da molécula, isso é ansível, o suporte a outros não é planejado; portanto, esta seção pode ser chamada com reserva de configuração estendida do ansible.
Aqui vocĂŞ pode especificar muitas coisas, vou destacar os principais, na minha opiniĂŁo, momentos:


  • playbooks : 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 

  • connection_options : parâmetros de conexĂŁo

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

  • options : opções possĂ­veis e variáveis ​​de ambiente

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

cenário


O nome e a descrição das seqüências de scripts.
Você pode alterar a matriz de ação padrão de um <command>_sequence adicionando a chave <command>_sequence e determinando a lista de etapas que precisamos como um valor para ela.
Suponha que desejemos alterar a sequência de ações ao executar o comando playbook run: molecule converge


 # : # - dependency # - create # - prepare # - converge scenario: name: default converge_sequence: - create - converge 

verificador


Configurando a estrutura para testes e o link para ela. Por padrão, testinfra e flake8 são usados testinfra flake8 . As opções possíveis são semelhantes às 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 

Vamos voltar ao nosso papel. Edite o tasks/main.yml para ficar assim:


 --- - name: Install nginx apt: name: nginx state: present - name: Start nginx service: name: nginx state: started 

E adicione os testes Ă  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") 

Feito, resta apenas executar (da raiz do papel, eu lembro):


 > 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, se houve problemas durante a operação do molecule test da molecule test , se você não alterou a sequência padrão, a molécula 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


A adição de um novo script a uma função existente ocorre no 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 script na função, a -s poderá ser omitida, pois o script default será criado.


ConclusĂŁo


Como você pode ver, a Molécula não é muito complicada e, ao usar seus próprios modelos, você pode reduzir a implantação de um novo script para editar variáveis ​​em playbooks para criar e excluir instâncias. A molécula se integra perfeitamente aos sistemas de CI, o que permite aumentar a velocidade de desenvolvimento, reduzindo o tempo necessário para testar manualmente os manuais.


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

Source: https://habr.com/ru/post/pt448136/


All Articles