Orquestrando sua rede com Ansible e Gitlab

De Wiki BPF
Revisão de 19h04min de 9 de janeiro de 2020 por Renato.Oliveira (discussão | contribs)
Ir para navegação Ir para pesquisar

Introdução

Neste artigo será apresentado uma metodologia de automação de redes baseada em GitLab e Ansible, onde, utilizando templates em Jinja2, será possível entregar ao operador uma estrutura de configurações baseada em modelos de dados. As peculiaridades de cada fabricante estarão encapsuladas através de templates que podem abstrair desde a CLI específica o equipamento até modelos YANG. Este artigo será divido em 5 seções, sendo elas:

  • A instalação do ambiente que para simplificação será monolítica, com o Ansible e o GitLab instalado no mesmo servidor;
  • A apresentação da metodologia CI/CD com o GitLab e a criação de um workflow de implantação, com sua respectiva configuração no GitLab-Runner;
  • A descrição do funcionamento do Ansible-playbook, em conjunto com uma proposta de estrutura de diretório.
  • A criação uma abstração de modelos de dados utilizando Jinja2.
  • Integrando a solução e realização de testes.

Instalação

O ambiente que será demostrado neste artigo utiliza como sistema operacional o CentOS 7 , instalado a partir da imagem: CentOS-7-x86_64-DVD-1908, que pode ser encontrada no repositório da distribuição. Neste ambiente, todos os componentes foram instalados em conjunto, porém, para um ambiente de produção, pode ser recomendado separar os componentes GitLab-Runner e Ansible em outro servidor.

             Para realizar a instalação dos componentes pode-se utilizar o script fornecido ou realizar a instalação via repositórios, devendo tomar o cuidado de atualizar a versão do git padrão da distribuição pois ela é incompatível com git-fetch que é utilizado pelo gitlab-runner, realizar a instalação via pip do ncclient e netmiko e criar um domínio em DNS para o gitlab. Caso deseje utilizar o script fornecido, atualize a url “gitlab.local” para o domínio registrado.

             Finalizado o processo de instalação das ferramentas, execute o procedimento de registro do gitlab-runner conforme esta url da referência [1] .

GitLab

O GitLab é um gerenciador de repositório de software baseado em git com suporte à Wiki, gerenciamento de tarefas e CI/CD. GitLab é similar ao GitHub, mas o GitLab permite que os desenvolvedores armazenem o código em seus próprios servidores, ao invés de servidores de terceiros. Ele é um software livre, distribuído pela Licença MIT. (wikipedia)

             E com todas essas funcionalidades pode agregar valor à operação, sendo capaz de prover uma base de conhecimento com sua wiki e, através do gitlab-runner, é possível criar um ciclo onde o técnico escreve a configuração desejada e faz um commit no repositório, que automaticamente executa as tarefas de compilação e validação de sintaxe, podendo inclusive realizar o deploy das configurações em um ambiente de laboratório como o EVE-NG.

Configurando a pipeline

Uma vez que o gitlab-runner foi instalado e configurado, é necessário adicionar no repositório o arquivo .gitlab-ci.yml que irá descrever todo o fluxo da pipeline [2].

             Durante a execução de cada tarefa, o gitlab-runner executa uma operação git fetch dentro da sua pasta home o que, para o contexto deste artigo e outras aplicações, pode ser problemático, já que todo os arquivos gerados pelas tarefas anteriores serão perdidos.

             Para a solução deste problema é necessário realizar um cache dos arquivos de configuração compilados, conforme sintaxe abaixo:

cache:

  key: ${CI_COMMIT_REF_SLUG}

  paths:

  - cfg/tmp

Após a configuração do cache, pode ser configurado os estágios da automação onde o gitlab-runner irá executar todas as suas tarefas em paralelo, porém,  somente irá executar as tarefas do próximo estágio, quando todas as tarefas anteriores forem concluídas com êxito. Para a configuração dos estágios, basta adicionar uma seção os relacionando conforme sintaxe abaixo:

stages:

- build

- deploy

Por fim, são configuradas as tarefas que cada estágio executa, para o cenário apresentado nesse tutorial iremos configurar apenas o script a ser executado, o estágio que a tarefa pertence e quando ela deve ser executada, conforme script abaixo, porém existem diversas opções de configuração e customizações que podem ser feitas e estão documentadas no link da referência [3]

Setup:

  stage: build

script:

  - ansible-playbook parse.yaml

when: always

              Ao término da configuração do .gitlab-ci.yml, a cada commit realizado pelo técnico será executada a pipeline que, para este tutorial, terá a forma da Figura 1:

Figura 1. Ciclo de execução

Ansible

O Ansible é uma ferramenta open-source para gerenciamento de configuração e provisionamento, que utiliza uma comunicação com os nós baseadas em SSH sem a necessidade da instalação de agentes.

             Para a utilização da ferramenta é necessário criar um arquivo ansible.cfg, que contém as configurações básicas do ansible, criar um arquivo contendo a relação dos dispositivos, e criar um arquivo contendo as instruções a serem executadas.

              Com os três arquivos citados anteriormente já é possível realizar alguns testes de acesso com o Ansible, porém para aplicações mais complexas é conveniente utilizar uma estrutura de diretórios para dois propósitos, o primeiro é melhorar a usabilidade e semântica da automação, e a segunda é criar um ambiente flexível e que possa ser reutilizado para diversos propósitos.

             Com este propósito a partir da versão 1.2 o Ansible introduziu o conceito de Roles, que através de uma estrutura de diretórios possibilita o carregamento automático de diversos arquivos e o agrupamento de configurações conforme sua semelhança e necessidade específica, a documentação a respeito de roles pode ser encontrada no link da referência [4], outra vantagem da utilização de roles reside no seu compartilhamento, onde pelo link da referência [5] podem ser encontrados diversos roles prontos para uso.

             Neste tutorial iremos criar roles de acordo com o software que roda nos equipamentos, por exemplo RouterOS, IOS, IOS-XR, Junos e etc, pois cada software possui uma sintaxe própria que ao final da implementação devem ser encapsuladas, outra vantagem desta escolha reside na possibilidade de separação de equipes de modo que a equipe com experiencia em RouteOS, não precisa se preocupar com particularidades do IOS, ou seja a única preocupação dessa equipe é considerando o modelo de dados de entrada que é padrão para todas as implementações gerar a configuração correspondente, conforme Figura 2.

Figura 2. Metologia de utilização dos Roles

Com a estrutura de diretórios pronta é necessário criar as abstrações das configurações o que é dependente de cada modelo e dos módulos disponíveis, para esse tutorial como configuraremos um mikrotik (RouterOS), e um cisco (IOS) iremos utilizar os seguintes módulos routeros_command [6] ios_config [7], e a linguagem de template Jinja2 para criar as abstrações, e devido as diferenças entre os módulos do ios_config e do routeros_command as etapas para a configuração serão diferentes conforme Figuras 3 e 4

Figura 3. Implementação de um Role para IOS
Figura 4. Implementação de um Role para RouterOS

Criando templates com Jinja2

Neste tutorial irei demonstrar a criação dos templates apenas para o IOS, mas para o RouterOS a implementação é análoga.

             O primeiro passo é conceber a estrutura de dados do modelo, será utilizado uma interface como base. Abstraindo toda a sintaxe os dados que são inseridos em uma interface em geral são nome da interface, endereço IP, máscara, estado ( shut ou no shut), velocidade, encapsulamento e etc, um exemplo de modelo seria o apresentado abaixo:

Interfaces:

- interface:

name: Loopback0

address: 10.0.0.1

description: LDP Loopback

mask: 255.255.255.255

enabled: True

- interface:

name: GigabitEthernet0/0

address: 10.0.1.1

mask: 255.255.255.252

enabled: True

Os arquivos contendo estes modelos seriam armazenados dentro da pasta roles/models/files/interfaces/<hostname>.yml. Percebe que neste momento estamos fazendo duas associações implícitas, a primeira é utilizar a palavra interfaces no caminho que será utilizada posteriormente para acessar todos os arquivos de configuração relativos a configuração das interfaces, e a segunda associação está no nome do arquivo <hostname>.yml, que será utilizado em tempo de execução para associar dada configuração ao ativo.

             Com o modelo pronto precisamos escrever o template, que no caso do IOS ficará em roles/ios/templates/interfaces.j2, e iremos criar um template para cada funcionalidade que desejarmos automatizar.

             Para a criação do template é necessário conhecer algumas estruturas de controle que nos permitirão realizar testes, e executar laços dentro da execução. A primeira estrutura que será apresentada é o laço {% for iterador in variável %} ... {% endfor %}. Ao observar o modelo que criamos acima pode se perceber que ele forma uma lista de interfaces, e será carregado dentro do Ansible como uma variável de nome interfaces, então para montar o laço iremos utilizar:

{% for interface in interfaces %}

{% endfor %}

              Outra estrutura que será usada extensivamente é condicional IF que possui a seguinte estrutura:

{% if confição %}

..

{% elif condição %}

...

{% else %}

...

{% endif %}

             E para acessar as variáveis basta utilizar a seguinte sintaxe {{ nome da variável }}.  O próximo passo é escrever a configuração que se deseja aplicar tomando muito cuidado com espaços, e sem a necessidade de se utilizar end ao final, pois o módulo executa uma comparação textual com a configuração em produção antes de configurar, no final a configuração do template será semelhando a abaixo:

{% for interface in interfaces %}

interface {{ interface.name }}

{% if interface.description is defined %}

description {{ interface.description}}

{% endif %}

{% if (interface.address is defined ) and (interface.mask is defined ) %}

ip address {{ interface.address }} {{ interface.mask }}

{% endif %}

{% if interface.enabled == True %}

no shutdown

{% endif %}

{% if interface.enabled == False %}

shutdown

{% endif %}

exit

{% endfor %}

Integrando a solução

Com os modelos prontos e todos os templates está na hora de criar os playbooks, que serão utilizados pelo Ansible para guiar a automação. O primeiro playbook a ser criado fica na raiz do diretório e será responsável por “chamar” os playbooks de cada role, o que é entregue automaticamente, bastando se incluir o role no playbook, para esse primeiro playbook que chamarei de principal, será necessário criar um “Play” para cada grupo de ativos e neste play será incluído a role desejada, conforme exemplo abaixo:

- name: Cisco

hosts: cisco

roles:

  - cisco

- name: MikroTik

hosts: routeros

roles:

  - routeros

A relação de hosts para cada grupo é definida dentro do arquivo hosts, que tem a seguinte sintaxe:

[cisco]

CPE7 ansible_host=192.168.237.206

PE4 ansible_host=192.168.237.202

P2  ansible_host=192.168.237.203

P1  ansible_host=192.168.237.205

PE5 ansible_host=192.168.237.207

CPE6 ansible_host=192.168.237.204

[routeros]

P3  ansible_host=192.168.237.183

              Neste arquivo os grupos são definidos dentro dos colchetes, [cisco] e [routeros], enquanto os membros do grupo são definidos individualmente abaixo com a seguinte sintaxe: HOSTNAME  ansible_host=<endereço IP>.              Adicionalmente ao endereço é necessário prover as credenciais de acesso ao equipamento e o sistema que roda em cada um conforme [3], neste tutorial será utilizado a mesma credencial de acesso a todos os equipamentos e tal configuração ficará dentro do arquivo group_vars/all.yml que tem a seguinte forma:

ansible_connection: network_cli

ansible_become: yes

ansible_become_method: enable

ansible_user: 'renato'

ansible_ssh_pass: '123456'

Note que nesse tutorial estou usando uma senha em claro dentro de um arquivo de configuração o que não é uma boa prática, para encriptar esta senha utilize os procedimentos descritos em [8].

E para as configurações específicas de cada plataforma será criado um arquivo com o mesmo nome do groupo na pasta group_vars, estas configurações podem ser visualizadas em [9].

             Finalizado estas configurações, criação do playbook principal, do inventário e das variáveis, vamos aos arquivos específicos de cada modelo. Para isso deverá ser criado um arquivo com o nome main.yml na pasta roles/<role>/tasks. Para a configuração no cisco, conforme Figura 3 precisamos primeiro ler o arquivo com o modelo e transformar ele em variável, porém é importante notar que cada tarefa é executada para todos os equipamentos do grupo, assim para evitar erros de execução iremos pesquisar pelo modelo na sua pasta respectiva:

- name: List interface directory

find:

  paths: roles/models/files/interfaces

  patterns: "*{{ inventory_hostname }}.yml"

  recurse: yes

  file_type: file

register: Files

delegate_to: localhost

             

Perceba que a tarefa acima pesquisa na pasta /roles/models/files/interfaces todos os arquivos com o seguinte padrão "*{{ inventory_hostname }}.yml", note a sintaxe {{ inventory_hostname }}, vista anteriormente nos templates em jinja2, que  faz referência ao equipamento que está sendo processado, registra o resultado de sua busca na variável Files e por fim executa esse procedimento localmente, sem a necessidade de se conectar via ssh no host.

Com os arquivos identificados o próximo passo é importar as variáveis que será feito utilizando o módulo template, conforme descrito abaixo:

- name: Include interface vars

  include_vars:

  file: roles/models/files/interfaces/{{ inventory_hostname }}.yml

when: Files.files != []

delegate_to: localhost

             Com o modelo importado o próximo passo é a criação dos arquivos de configuração, nesta etapa é fundamental que na sua estrutura de diretórios já exista uma pasta destinada a estes arquivos, e para a sua configuração basta seguir o modelo abaixo:

- name: Make Config

template:

  src: interface.j2

  dest: cfg/interfaces/{{ inventory_hostname }}.cfg

  mode: 0777

  when: Files.files != []

delegate_to: localhost

             Finalmente com todos os arquivos de configuração prontos, está na hora de aplicar as configurações nos equipamentos que no caso do ios é feito utilizando o módulo ios_config, conforme sintaxe abaixo:

  - name: Configure

  ios_config:

    src:  “cfg/interfaces/{{ inventory_hostname }}.cfg”

  when: configFiles.files != []

                          Finalizadas estas configurações basta repetir o processo para cada funcionalidade que se deseja implementar em sua automação, e caso seja de interesse é possível simplificar ainda mais a criação dos playbooks, realizando a separação da configuração das tarefas por escopos, ou seja dentro da pasta roles/<role>/tasks existirá um arquivo main.yml que é responsável por importar as tarefas de cada uma das funcionalidades e cada funcionalidade terá um arquivo próprio como por exemplor interfaces.yml, ospf.yml , mpls.yml e etc, desta forma as configurações apresentadas acima residirão dentro de cada um dos playbooks de funcionalidades e o playbook main.yml irá apenas instanciar usando a seguinte sintaxe:

- import_tasks: interfaces.yml

vars:

  scope: interfaces

            

             Observe no código acima que foi introduzido uma variável durante a inclusão, e ao observar as configurações realizadas anteriormente é possível perceber que em nenhum momento foi utilizado alguma especificidade de alguma funcionalidade, portanto com é possível apenas com a alteração da variável scope, recém introduzida, replicar todo o código que criamos sem a necessidade de escreve-lo novamente.

             Finalizado as configurações dos outros roles, para executar a automação basta executar o comando ansible-playbook <playbook).yaml na pasta raiz do projeto, comando este que deve ser inserido no .gitlab-ci.yml para que o gitlab execute a configuração sempre que o usuário realizar um commit.

Conclusão

             Neste tutorial foi apresentado uma metodologia de automação de redes baseada na integração de ferramentas open source, Ansible e gitlab. Onde através da utilização da abstração de modelos de dados e roles, é possível entregar ao administrador de redes uma interface de configuração agnóstica a fabricantes e com um mecanismo de aprovação característico de metodologias de desenvolvimento de software, com commits e processos de aprovação.

Por fim toda a configuração demonstrada neste artigo pode ser acessada no seguinte repositório do github [10], e a demonstração deste cenário poderá ser assistido em um vídeo no YouTube que será publicado em breve.

Muito obrigado pela atenção, espero que tenham gostado do conteúdo, e assim que possível estarei disponibilizando um vídeo no YouTube demonstrando e dando maiores explicações sobre a metodologia

Referências

[1] (https://docs.gitlab.com/runner/register/)

[2]     (https://docs.gitlab.com/ee/ci/quick_start/README.html#creating-a-gitlab-ciyml-file)

[3]     (https://docs.gitlab.com/ee/ci/yaml/)

[4]     (https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)

[5]     (https://galaxy.ansible.com/)

[6]     (https://docs.ansible.com/ansible/latest/modules/routeros_command_module.html#routeros-command-module)

[7]     (https://docs.ansible.com/ansible/latest/modules/ios_config_module.html#ios-config-module)

[8]     (https://docs.ansible.com/ansible/latest/user_guide/vault.html)

[9]   (https://docs.ansible.com/ansible/latest/network/user_guide/platform_index.html)

AUTOR: Usuário:Renato.Oliveira