Mudanças entre as edições de "Orquestrando sua rede com Ansible e Gitlab"

De Wiki BPF
Ir para: navegação, pesquisa
(Ajustes na formatação dos códigos fonte)
(Criando templates com Jinja2: Correção na identação)
Linha 33: Linha 33:
 
key: ${CI_COMMIT_REF_SLUG}
 
key: ${CI_COMMIT_REF_SLUG}
 
paths:
 
paths:
- cfg/tmp
+
- cfg/tmp
 
</pre>
 
</pre>
  
Linha 88: Linha 88:
 
interface {{ interface.name }}
 
interface {{ interface.name }}
 
{% if interface.description is defined %}
 
{% if interface.description is defined %}
description {{ interface.description}}
+
description {{ interface.description}}
 
{% endif %}
 
{% endif %}
 
{% if (interface.address is defined ) and (interface.mask is defined ) %}
 
{% if (interface.address is defined ) and (interface.mask is defined ) %}
ip address {{ interface.address }} {{ interface.mask }}
+
ip address {{ interface.address }} {{ interface.mask }}
 
{% endif %}
 
{% endif %}
 
{% if interface.enabled == True %}
 
{% if interface.enabled == True %}
no shutdown
+
no shutdown
 
{% endif %}
 
{% endif %}
 
{% if interface.enabled == False %}
 
{% if interface.enabled == False %}
shutdown
+
shutdown
 
{% endif %}
 
{% endif %}
 
exit
 
exit
Linha 107: Linha 107:
 
<pre>
 
<pre>
 
- name: Cisco
 
- name: Cisco
hosts: cisco
+
  hosts: cisco
roles:
+
  roles:
- cisco
+
  - cisco
 
- name: MikroTik
 
- name: MikroTik
hosts: routeros
+
  hosts: routeros
roles:
+
  roles:
- routeros
+
  - routeros
 
</pre>
 
</pre>
 
A relação de hosts para cada grupo é definida dentro do arquivo hosts, que tem a seguinte sintaxe:
 
A relação de hosts para cada grupo é definida dentro do arquivo hosts, que tem a seguinte sintaxe:
Linha 146: Linha 146:
 
<pre>
 
<pre>
 
- name: List interface directory
 
- name: List interface directory
find:
+
  find:
paths: roles/models/files/interfaces
+
    paths: roles/models/files/interfaces
patterns: "*{{ inventory_hostname }}.yml"
+
    patterns: "*{{ inventory_hostname }}.yml"
recurse: yes
+
    recurse: yes
file_type: file
+
    file_type: file
register: Files
+
  register: Files
delegate_to: localhost
+
  delegate_to: localhost
 
</pre>               
 
</pre>               
 
Perceba que a tarefa acima pesquisa na pasta /roles/models/files/interfaces todos os arquivos com o seguinte padrão ''"*<nowiki>{{ inventory_hostname }}</nowiki>.yml"'', note a sintaxe ''<nowiki>{{ inventory_hostname }}</nowiki>'', 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.
 
Perceba que a tarefa acima pesquisa na pasta /roles/models/files/interfaces todos os arquivos com o seguinte padrão ''"*<nowiki>{{ inventory_hostname }}</nowiki>.yml"'', note a sintaxe ''<nowiki>{{ inventory_hostname }}</nowiki>'', 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.
Linha 159: Linha 159:
 
<pre>
 
<pre>
 
- name: Include interface vars
 
- name: Include interface vars
include_vars:
+
  include_vars:
file: roles/models/files/interfaces/{{ inventory_hostname }}.yml
+
    file: roles/models/files/interfaces/{{ inventory_hostname }}.yml
when: Files.files != []
+
  when: Files.files != []
delegate_to: localhost
+
  delegate_to: localhost
 
</pre>
 
</pre>
 
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:
 
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:
 
<pre>
 
<pre>
 
- name: Make Config
 
- name: Make Config
template:
+
  template:
src: interface.j2
+
    src: interface.j2
dest: cfg/interfaces/{{ inventory_hostname }}.cfg
+
    dest: cfg/interfaces/{{ inventory_hostname }}.cfg
mode: 0777
+
    mode: 0777
when: Files.files != []
+
  when: Files.files != []
delegate_to: localhost
+
  delegate_to: localhost
 
</pre>
 
</pre>
 
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:
 
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:
 
<pre>
 
<pre>
 
- name: Configure
 
- name: Configure
ios_config:
+
  ios_config:
src:  “cfg/interfaces/{{ inventory_hostname }}.cfg”
+
    src:  “cfg/interfaces/{{ inventory_hostname }}.cfg”
when: configFiles.files != []
+
  when: configFiles.files != []
 
</pre>
 
</pre>
 
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:
 
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:
 
<pre>
 
<pre>
 
- import_tasks: interfaces.yml
 
- import_tasks: interfaces.yml
vars:
+
  vars:
scope: interfaces
+
    scope: interfaces
 
</pre>
 
</pre>
 
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.
 
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.

Edição das 09h09min de 10 de janeiro de 2020

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

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 %}

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)

[10] (https://github.com/renatoalmeidaoliveira/TutorialAnsible)

AUTOR: Renato.Oliveira