Como Testar Seu Deployment Ansible com InSpec e Kitchen

O autor escolheu a Diversity in Tech Fund para receber uma doação como parte do programa Write for DOnations.

Introdução

O InSpec é um framework open-source de auditoria e teste automatizado usado para descrever e testar preocupações, recomendações ou requisitos regulatórios. Ele foi projetado para ser inteligível e independente de plataforma. Os desenvolvedores podem trabalhar com o InSpec localmente ou usando SSH, WinRM ou Docker para executar testes, portanto, é desnecessário instalar quaisquer pacotes na infraestrutura que está sendo testada.

Embora com o InSpec você possa executar testes diretamente em seus servidores, existe um potencial de erro humano que poderia causar problemas em sua infraestrutura. Para evitar esse cenário, os desenvolvedores podem usar o Kitchen para criar uma máquina virtual e instalar um sistema operacional de sua escolha nas máquinas em que os testes estão sendo executados. O Kitchen é um executor de testes, ou ferramenta de automação de teste, que permite testar o código de infraestrutura em uma ou mais plataformas isoladas. Ele também suporta muitos frameworks de teste e é flexível com uma arquitetura de plug-in de driver para várias plataformas, como Vagrant, AWS, DigitalOcean, Docker, LXC containers, etc.

Neste tutorial, você escreverá testes para seus playbooks Ansible em execução em um Droplet Ubuntu 18.04 da DigitalOcean. Você usará o Kitchen como executor de teste e o InSpec para escrever os testes. No final deste tutorial, você poderá testar o deploy do seu playbook Ansible.

Pré-requisitos

Antes de começar com este guia, você precisará de uma conta na DigitalOcean além do seguinte:

  • Uma instalação local do Ruby em sua máquina. Você pode instalar o Ruby seguindo o tutorial para sua distribuição na série: How To Install and Set Up a Local Programming Environment for Ruby.
  • O Chef Development Kit (ChefDK) instalado em sua máquina.
  • Chaves SSH configuradas em sua máquina seguindo os passos 1 e 2 de How To Set Up SSH Keys. Para carregar sua chave SSH pública na sua conta DigitalOcean, você pode seguir o nosso tutorial How To Add SSH Keys to a DigitalOcean Account.
  • Um token de acesso pessoal da DigitalOcean com permissões de leitura e gravação. Certifique-se de gravar o token em um local seguro; você o usará posteriormente neste tutorial. Isso permite que você crie um Droplet na DigitalOcean, que é onde este tutorial executará os testes.

Passo 1 — Configurando e Inicializando o Kitchen

Você instalou o ChefDK como parte dos pré-requisitos que vem empacotados com o kitchen. Neste passo, você configurará o Kitchen para se comunicar com a DigitalOcean.

Antes de inicializar o Kitchen, você criará e se moverá para um diretório de projeto. Neste tutorial, o chamaremos de ansible_testing_dir.

Execute o seguinte comando para criar o diretório:

  • mkdir ~/ansible_testing_dir

E então passe para ele:

  • cd ~/ansible_testing_dir

Usando o gem instale o pacote kitchen-digitalocean em sua máquina local. Isso permite que você diga ao kitchen para usar o driver da DigitalOcean ao executar testes:

  • gem install kitchen-digitalocean

No diretório do projeto, você executará o comando kitchen init especificando ansible_playbook como o provisionador e digitalocean como o driver ao inicializar o Kitchen:

  • kitchen init --provisioner=ansible_playbook --driver=digitalocean

Você verá a seguinte saída:

Outputcreate  kitchen.yml create  chefignore create  test/integration/default 

Isso criou o seguinte no diretório do projeto:

  • test/integration/default é o diretório no qual você salvará seus arquivos de teste.

  • chefignore é o arquivo que você usaria para garantir que certos arquivos não sejam carregados para o Chef Infra Server, mas você não o usará neste tutorial.

  • kitchen.yml é o arquivo que descreve sua configuração de teste: o que você deseja testar e as plataformas de destino.

Agora, você precisa exportar suas credenciais da DigitalOcean como variáveis de ambiente para ter acesso para criar Droplets a partir da sua CLI. Primeiro, inicie com seu token de acesso da DigitalOcean executando o seguinte comando:

  • export DIGITALOCEAN_ACCESS_TOKEN="SEU_TOKEN_DE_ACESSO_DIGITALOCEAN"

Você também precisa obter seu número de ID da chave SSH; note que SEU_ID_DE_CHAVE_SSH_DIGITALOCEAN deve ser o ID numérico da sua chave SSH, não o nome simbólico. Usando a API da DigitalOcean, você pode obter o ID numérico de suas chaves com o seguinte comando:

  • curl -X GET https://api.digitalocean.com/v2/account/keys -H "Authorization: Bearer $DIGITALOCEAN_ACCESS_TOKEN"

Após este comando, você verá uma lista de suas chaves SSH e metadados relacionados. Leia a saída para encontrar a chave correta e identificar o número de ID nela:

Output...  {"id":seu-ID-numérico,"fingerprint":"fingerprint","public_key":"ssh-rsa sua-chave-ssh","name":"nome-da-sua-chave-ssh" ... 

Nota: Se você deseja tornar sua saída mais legível para obter seus IDs numéricos, você pode encontrar e baixar o jq com base no seu sistema operacional na página de download do jq. Agora, você pode executar o comando anterior fazendo um pipe para o jq da seguinte maneira:

  • curl -X GET https://api.digitalocean.com/v2/account/keys -H "Authorization: Bearer $DIGITALOCEAN_ACCESS_TOKEN" | jq

Você verá as informações da chave SSH formatadas de forma semelhante a:

Output{   "ssh_keys": [     {       "id": ID_DA_SUA_CHAVE_SSH,       "fingerprint": "2f:d0:16:6b",       "public_key": "ssh-rsa AAAAB3NzaC1yc2 [email protected]",       "name": "sannikay"     }   ], } 

Depois de identificar seus IDs numéricos de SSH, exporte-os com o seguinte comando:

  • export DIGITALOCEAN_SSH_KEY_IDS="SEU_ID_DE_CHAVE_SSH_DIGITALOCEAN"

Você inicializou o kitchen e configurou as variáveis de ambiente para suas credenciais da DigitalOcean. Agora você vai criar e executar testes em seus Droplets diretamente da linha de comando.

Passo 2 — Criando o Playbook Ansible

Neste passo, você criará um playbook e roles (funções) que configurará o Nginx e o Node.js no Droplet criado pelo kitchen no próximo passo. Seus testes serão executados no playbook para garantir que as condições especificadas no playbook sejam atendidas.

Para começar, crie um diretório roles para as roles ou funções Nginx e Node.js:

  • mkdir -p roles/{nginx,nodejs}/tasks

Isso criará uma estrutura de diretórios da seguinte maneira:

roles ├── nginx │   └── tasks └── nodejs     └── tasks 

Agora, crie um arquivo main.yml no diretório roles/nginx/tasks usando o seu editor preferido:

  • nano roles/nginx/tasks/main.yml

Neste arquivo, crie uma tarefa ou task que configura e inicia o Nginx adicionando o seguinte conteúdo:

roles/nginx/tasks/main.yml

--- - name: Update cache repositories and install Nginx   apt:     name: nginx     update_cache: yes  - name: Change nginx directory permission   file:     path: /etc/nginx/nginx.conf     mode: 0750  - name: start nginx   service:     name: nginx     state: started 

Depois de adicionar o conteúdo, salve e saia do arquivo.

Em roles/nginx/tasks/main.yml você define uma tarefa que atualizará o repositório de cache do seu Droplet, o que equivale a executar o comando apt update manualmente em um servidor. Essa tarefa também altera as permissões do arquivo de configuração do Nginx e inicia o serviço Nginx.

Você também criará um arquivo main.yml em roles/nodejs/tasks para definir uma tarefa que configure o Node.js.

  • nano roles/nodejs/tasks/main.yml

Adicione as seguintes tarefas a este arquivo:

roles/nodejs/tasks/main.yml

--- - name: Update caches repository   apt:     update_cache: yes  - name: Add gpg key for NodeJS LTS   apt_key:     url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key"     state: present  - name: Add the NodeJS LTS repo   apt_repository:     repo: "deb https://deb.nodesource.com/node_{{ NODEJS_VERSION }}.x {{ ansible_distribution_release }} main"     state: present     update_cache: yes  - name: Install Node.js   apt:     name: nodejs     state: present  

Salve e saia do arquivo quando terminar.

Em roles/nodejs/tasks/main.yml, você primeiro define uma tarefa que atualizará o repositório de cache do seu Droplet. Em seguida, na próxima tarefa, você adiciona a chave GPG para o Node.js, que serve como um meio de verificar a autenticidade do repositório apt do Node.js. As duas tarefas finais adicionam o repositório apt do Node.js e o instalam.

Agora você definirá suas configurações do Ansible, como variáveis, a ordem em que você deseja que suas roles sejam executadas e configurações de privilégios de superusuário. Para fazer isso, você criará um arquivo chamado playbook.yml, que serve como um entry point para o Kitchen. Quando você executa seus testes, o Kitchen inicia no seu arquivo playbook.yml e procura as roles a serem executadas, que são seus arquivos roles/nginx/tasks/main.yml e roles/nodejs/tasks/main.yml.

Execute o seguinte comando para criar o playbook.yml:

  • nano playbook.yml

Adicione o seguinte conteúdo ao arquivo:

ansible_testing_dir/playbook.yml

---  - hosts: all    become: true    remote_user: ubuntu    vars:     NODEJS_VERSION: 8 

Salve e saia do arquivo.

Você criou as roles do playbook do Ansible com as quais executará seus testes para garantir que as condições especificadas no playbook sejam atendidas.

Passo 3 — Escrevendo Seus Testes InSpec

Neste passo, você escreverá testes para verificar se o Node.js está instalado no seu Droplet. Antes de escrever seu teste, vejamos o formato de um exemplo de teste InSpec. Como em muitos frameworks de teste, o código InSpec se assemelha a uma linguagem natural. O InSpec possui dois componentes principais, o assunto a ser examinado e o estado esperado desse assunto:

block A

describe '<entity>' do   it { <expectation> } end 

Em block A, as palavras-chave do e end definem um bloco ou block. A palavra-chave describe é comumente conhecida como conjuntos ou suites de testes, que contêm casos de teste. A palavra-chave it é usada para definir os casos de teste.

<entity> é o assunto que você deseja examinar, por exemplo, um nome de pacote, serviço, arquivo ou porta de rede. O <expectation> especifica o resultado desejado ou o estado esperado, por exemplo, o Nginx deve ser instalado ou deve ter uma versão específica. Você pode verificar a documentação da InSpec DSL para aprender mais sobre a linguagem InSpec.

Outro exemplo de bloco de teste InSpec:

block B

control 'Pode ser qualquer coisa única' do     impact 0.7                            title 'Um título inteligível'        desc  'Uma descrição opcional'   describe '<entity>' do                  it { <expectation> }   end end 

A diferença entre o bloco A e o bloco B é o bloco control. O bloco control é usado como um meio de controle regulatório, recomendação ou requisito. O bloco control tem um nome; geralmente um ID único, metadados como desc, title, impact e, finalmente, agrupam blocos describe relacionados para implementar as verificações.

desc, title, e impact definem metadados que descrevem completamente a importância do controle, seu objetivo, com uma descrição sucinta e completa. impact define um valor numérico que varia de 0.0 a 1.0 onde 0.0 a <0.01 é classificado como sem impacto, 0.01 a <0.4 é classificado como baixo impacto, 0.4 a <0.7 é classificado como médio impacto, 0,7 a <0,9 é classificado como alto impacto, 0,9 a 1,0 é classificado como controle crítico.

Agora, vamos implementar um teste. Usando a sintaxe do bloco A, você usará o recurso package do InSpec para testar se o Node.js está instalado no sistema. Você irá criar um arquivo chamado sample.rb em seu diretório test/integration/default para seus testes.

Crie o sample.rb:

  • nano test/integration/default/sample.rb

Adicione o seguinte ao seu arquivo:

test/integration/default/sample.rb

describe package('nodejs') do   it { should be_installed } end 

Aqui seu teste está usando o recurso package para verificar se o node.js está instalado.

Salve e saia do arquivo quando terminar.

Para executar este teste, você precisa editar kitchen.yml para especificar o playbook que você criou anteriormente e para adicionar às suas configurações.

Abra seu arquivo kitchen.yml:

  • nano ansible_testing_dir/kitchen.yml

Substitua o conteúdo de kitchen.yml com o seguinte:

ansible_testing_dir/kitchen.yml

--- driver:   name: digitalocean  provisioner:   name: ansible_playbook   hosts: test-kitchen   playbook: ./playbook.yml  verifier:   name: inspec  platforms:   - name: ubuntu-18     driver_config:       ssh_key: CAMINHO_PARA_SUA_CHAVE_PRIVADA_SSH       tags:         - inspec-testing       region: fra1       size: 1gb       private_networking: false     verifier:       inspec_tests:         - test/integration/default suites:   - name: default  

As opções de platform incluem o seguinte:

  • name: A imagem que você está usando.
  • driver_config: A configuração do seu Droplet da DigitalOcean. Você está especificando as seguintes opções para driver_config:

    • ssh_key: Caminho para SUA_CHAVE_SSH_PRIVADA. Sua SUA_CHAVE_SSH_PRIVADA está localizada no diretório que você especificou ao criar sua chave ssh.
    • tags: As tags associadas ao seu Droplet.
    • region: A region ou região onde você deseja que seu Droplet seja hospedado.
    • size: A memória que você deseja que seu Droplet tenha.
  • verifier: Isso define que o projeto contém testes InSpec.

    • A parte do inspec_tests especifica que os testes existem no diretório test/integration/default do projeto.

Observe que name e region usam abreviações. Você pode verificar na documentação do test-kitchen as abreviações que você pode usar.

Depois de adicionar sua configuração, salve e saia do arquivo.

Execute o comando kitchen test para executar o teste. Isso verificará se o Node.js está instalado — ele falhará propositalmente, porque você atualmente não possui a role Node.js no seu arquivo playbook.yml:

  • kitchen test

Você verá uma saída semelhante à seguinte:

Output: failing test results-----> Starting Kitchen (v1.24.0) -----> Cleaning up any prior instances of <default-ubuntu-18> -----> Destroying <default-ubuntu-18>...        DigitalOcean instance <145268853> destroyed.        Finished destroying <default-ubuntu-18> (0m2.63s). -----> Testing <default-ubuntu-18> -----> Creating <default-ubuntu-18>...        DigitalOcean instance <145273424> created.        Waiting for SSH service on 138.68.97.146:22, retrying in 3 seconds        [SSH] Established        (ssh ready)         Finished creating <default-ubuntu-18> (0m51.74s). -----> Converging <default-ubuntu-18>... $$$$$$ Running legacy converge for 'Digitalocean' Driver -----> Installing Chef Omnibus to install busser to run tests        PLAY [all] *********************************************************************         TASK [Gathering Facts] *********************************************************        ok: [localhost]         PLAY RECAP *********************************************************************        localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0         Downloading files from <default-ubuntu-18>        Finished converging <default-ubuntu-18> (0m55.05s). -----> Setting up <default-ubuntu-18>... $$$$$$ Running legacy setup for 'Digitalocean' Driver        Finished setting up <default-ubuntu-18> (0m0.00s). -----> Verifying <default-ubuntu-18>...        Loaded tests from {:path=>". ansible_testing_dir.test.integration.default"}  Profile: tests from {:path=>"ansible_testing_dir/test/integration/default"} (tests from {:path=>"ansible_testing_dir.test.integration.default"}) Version: (not specified) Target:  ssh://[email protected]:22    System Package nodejs      ×  should be installed       expected that System Package nodejs is installed  Test Summary: 0 successful, 1 failure, 0 skipped >>>>>> ------Exception------- >>>>>> Class: Kitchen::ActionFailed >>>>>> Message: 1 actions failed. >>>>>>     Verify failed on instance <default-ubuntu-18>.  Please see .kitchen/logs/default-ubuntu-18.log for more details >>>>>> ---------------------- >>>>>> Please see .kitchen/logs/kitchen.log for more details >>>>>> Also try running `kitchen diagnose --all` for configuration    4.54s user 1.77s system 5% cpu 2:02.33 total 

A saída informa que seu teste está falhando porque você não possui o Node.js instalado no Droplet que você provisionou com o kitchen. Você corrigirá seu teste adicionando a role nodejs ao seu arquivo playbook.yml e executará o teste novamente.

Edite o arquivo playbook.yml para incluir a role nodejs:

  • nano playbook.yml

Adicione as seguintes linhas destacadas ao seu arquivo:

ansible_testing_dir/playbook.yml

---  - hosts: all    become: true    remote_user: ubuntu    vars:     NODEJS_VERSION: 8     roles:     - nodejs 

Salve e feche o arquivo.

Agora, você executará novamente o teste usando o comando kitchen test:

  • kitchen test

Você verá a seguinte saída:

Output...... Target:  ssh://[email protected]:22    System Package nodejs      ✔  should be installed  Test Summary: 1 successful, 0 failures, 0 skipped        Finished verifying <default-ubuntu-18> (0m4.89s). -----> Destroying <default-ubuntu-18>...        DigitalOcean instance <145512952> destroyed.        Finished destroying <default-ubuntu-18> (0m2.23s).        Finished testing <default-ubuntu-18> (2m49.78s). -----> Kitchen is finished. (2m55.14s)   4.86s user 1.77s system 3% cpu 2:56.58 total 

Seu teste agora passa porque você tem o Node.js instalado usando a role nodejs.

Aqui está um resumo do que o Kitchen está fazendo em Test Action:

  • Destrói o Droplet se ele existir
  • Cria o Droplet
  • Converge o Droplet
  • Verifica o Droplet com o InSpec
  • Destrói o Droplet

O Kitchen interromperá a execução em seu Droplet se encontrar algum problema. Isso significa que, se o seu playbook do Ansible falhar, o InSpec não será executado e o seu Droplet não será destruído. Isso permite que você inspecione o estado da instância e corrija quaisquer problemas. O comportamento da ação final de destruição pode ser substituído, se desejado. Verifique a ajuda da CLI para a flag --destroy executando o comando kitchen help test.

Você escreveu seus primeiros testes e os executou no seu playbook com uma instância falhando antes de corrigir o problema. Em seguida, você estenderá seu arquivo de teste.

Passo 4 — Adicionando Casos de Teste

Neste passo, você adicionará mais casos de teste ao seu arquivo de teste para verificar se os módulos do Nginx estão instalados no seu Droplet e se o arquivo de configuração tem as permissões corretas.

Edite seu arquivo sample.rb para adicionar mais casos de teste:

  • nano test/integration/default/sample.rb

Adicione os seguintes casos de teste ao final do arquivo:

test/integration/default/sample.rb

. . . control 'nginx-modules' do   impact 1.0   title 'NGINX modules'   desc 'The required NGINX modules should be installed.'   describe nginx do     its('modules') { should include 'http_ssl' }     its('modules') { should include 'stream_ssl' }     its('modules') { should include 'mail_ssl' }   end end  control 'nginx-conf' do   impact 1.0   title 'NGINX configuration'   desc 'The NGINX config file should owned by root, be writable only by owner, and not writeable or and readable by others.'   describe file('/etc/nginx/nginx.conf') do     it { should be_owned_by 'root' }     it { should be_grouped_into 'root' }     it { should_not be_readable.by('others') }     it { should_not be_writable.by('others') }     it { should_not be_executable.by('others') }   end end 

Esses casos de teste verificam se os módulos nginx-modules no seu Droplet incluem http_ssl, stream_ssl e mail_ssl. Você também está verificando as permissões do arquivo /etc/nginx/nginx.conf.

Você está usando as palavras-chave it e its para definir seu teste. A palavra-chave its é usada apenas para acessar propriedades de resources. Por exemplo, modules é uma propriedade de nginx.

Salve e saia do arquivo depois de adicionar os casos de teste.

Agora execute o comando kitchen test para testar novamente:

  • kitchen test

Você verá a seguinte saída:

Output... Target:  ssh://[email protected]:22    ↺  nginx-modules: NGINX modules      ↺  The `nginx` binary not found in the path provided.   ×  nginx-conf: NGINX configuration (2 failed)      ×  File /etc/nginx/nginx.conf should be owned by "root"      expected `File /etc/nginx/nginx.conf.owned_by?("root")` to return true, got false      ×  File /etc/nginx/nginx.conf should be grouped into "root"      expected `File /etc/nginx/nginx.conf.grouped_into?("root")` to return true, got false      ✔  File /etc/nginx/nginx.conf should not be readable by others      ✔  File /etc/nginx/nginx.conf should not be writable by others      ✔  File /etc/nginx/nginx.conf should not be executable by others    System Package nodejs      ✔  should be installed Profile Summary: 0 successful controls, 1 control failure, 1 control skipped Test Summary: 4 successful, 2 failures, 1 skipped 

Você verá que alguns dos testes estão falhando. Você irá corrigi-los adicionando a role nginx ao seu arquivo playbook e executando novamente o teste. No teste que falhou, você está verificando módulos nginx e permissões de arquivo que não estão presentes atualmente no seu servidor.

Abra seu arquivo playbook.yml:

  • nano ansible_testing_dir/playbook.yml

Adicione a seguinte linha destacada às suas roles:

ansible_testing_dir/playbook.yml

--- - hosts: all   become: true   remote_user: ubuntu   vars:   NODEJS_VERSION: 8    roles:   - nodejs   - nginx 

Salve e feche o arquivo quando terminar.

Em seguida, execute seus testes novamente:

  • kitchen test

Você verá a seguinte saída:

Output... Target:  ssh://[email protected]:22 ✔  nginx-modules: NGINX version      ✔  Nginx Environment modules should include "http_ssl"      ✔  Nginx Environment modules should include "stream_ssl"      ✔  Nginx Environment modules should include "mail_ssl"   ✔  nginx-conf: NGINX configuration      ✔  File /etc/nginx/nginx.conf should be owned by "root"      ✔  File /etc/nginx/nginx.conf should be grouped into "root"      ✔  File /etc/nginx/nginx.conf should not be readable by others      ✔  File /etc/nginx/nginx.conf should not be writable by others      ✔  File /etc/nginx/nginx.conf should not be executable by others    System Package nodejs      ✔  should be installed  Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped Test Summary: 9 successful, 0 failures, 0 skipped 

Depois de adicionar a role nginx ao playbook, todos os seus testes agora passam. A saída mostra que os módulos http_ssl, stream_ssl e mail_ssl estão instalados em seu Droplet e as permissões corretas estão definidas para o arquivo de configuração.

Quando terminar, ou não precisar mais do seu Droplet, você poderá destruí-lo executando o comando kitchen destroy para excluí-lo após executar seus testes:

  • kitchen destroy

Após este comando, você verá uma saída semelhante a:

Output-----> Starting Kitchen (v1.24.0) -----> Destroying <default-ubuntu-18>...        Finished destroying <default-ubuntu-18> (0m0.00s). -----> Kitchen is finished. (0m5.07s)   3.79s user 1.50s system 82% cpu 6.432 total 

Você escreveu testes para o seu playbook, executou os testes e corrigiu os testes com falha para garantir que todos os testes sejam aprovados. Agora você está pronto para criar um ambiente virtual, escrever testes para o seu Playbook Ansible e executar seu teste no ambiente virtual usando o Kitchen.

Conclusão

Agora você tem uma base flexível para testar seu deployment Ansible, que lhe permite testar seus playbooks antes de executar em um servidor ativo. Você também pode empacotar seu teste em um perfil. Você pode usar perfis para compartilhar seu teste através do Github ou do Chef Supermarket e executá-lo facilmente em um servidor ativo.

Para detalhes mais abrangentes sobre o InSpec e o Kitchen, consulte a documentação oficial do InSpec e a documentação oficial do Kitchen.