Tutorial

Como configurar e proteger um cluster etcd com o Ansible no Ubuntu 18.04

AnsibleLet's EncryptOpen SourceUbuntu 18.04

O autor selecionou a Wikimedia Foundation para receber uma doação como parte do programa Write for DOnations.

Introdução

O etcd é um armazenamento distribuído de chaves-valores utilizado em muitas plataformas e ferramentas, incluindo o Kubernetes, Vulcand e o Doorman. Dentro do Kubernetes, o etcd é usado como um armazenamento de configuração global que armazena o estado do cluster. Saber administrar o etcd é essencial para administrar um cluster Kubernetes. Embora existam muitas opções gerenciadas do Kubernetes, também conhecidas como Kubernetes-as-a-Service, que removem esse peso administrativo de você, muitas empresas ainda escolhem utilizar clusters Kubernetes locais autogeridos devido à flexibilidade que eles trazem.

A primeira metade deste artigo irá guiá-lo através da configuração de um cluster etcd de 3 nós em servidores Ubuntu 18.04. A segunda metade irá focar na proteção do cluster usando o Transport Layer Security, ou TLS. Para executar cada configuração de forma automatizada, usaremos o Ansible durante todo o processo. O Ansible é uma ferramenta de gerenciamento de configuração semelhante ao Puppet, Chef e ao SaltStack. Ele nos permite definir cada passo da configuração de maneira declarativa, dentro de arquivos chamados playbooks.

No final deste tutorial, você terá um cluster etcd de 3 nós protegido em execução nos seus servidores. Você também terá um playbook do Ansible que lhe permite recriar repetidamente e consistentemente a mesma configuração em um conjunto de servidores novos.

Pré-requisitos

Antes de iniciar este guia, será necessário o seguinte:

Aviso: como o objetivo deste artigo é fornecer uma introdução para configurar de um cluster etcd em uma rede privada, os três servidores Ubuntu 18.04 nesta configuração não foram testados com um firewall e são acessados com o usuário root. Em uma configuração de produção, qualquer nó exposto à internet pública exigiria um firewall e um usuário sudo para que ele se adequasse às práticas recomendadas de segurança. Para mais informações, confira o tutorial Configuração inicial de servidor com o Ubuntu 18.04.

Passo 1 — Configurando o Ansible para o nó de controle

O Ansible é uma ferramenta usada para gerenciar servidores. Os servidores que o Ansible está gerenciando são chamados de nós gerenciados, e a máquina que está executando o Ansible é chamada de nó de controle. O Ansible funciona usando as chaves SSH no nó de controle para obter acesso aos nós gerenciados. Depois que uma sessão SSH for estabelecida, o Ansible irá executar um conjunto de scripts para provisionar e configurar os nós gerenciados. Neste passo, vamos testar se somos capazes de usar o Ansible para nos conectar aos nós gerenciados e executar o comando hostname.

Um dia típico para um administrador de sistemas pode envolver o gerenciamento de conjuntos diferentes de nós. Por exemplo, é possível usar o Ansible para provisionar alguns novos servidores, mas mais tarde usá-lo para reconfigurar outro conjunto de servidores. Para permitir que os administradores organizem melhor o conjunto de nós gerenciados, o Ansible oferece o conceito de inventário de host (ou somente inventário). Você pode definir todos os nós que deseja gerenciar com o Ansible dentro de um arquivo de inventário, e organizá-los em grupos. Dessa forma, ao executar os comandos ansible e ansible-playbook, é possível especificar a quais hosts ou grupos o comando se aplica.

Por padrão, o Ansible lê o arquivo de inventário a partir de /etc/ansible/hosts. No entanto, podemos especificar um arquivo de inventário diferente usando o sinalizador --inventory (ou somente -i).

Para iniciar, crie um novo diretório em sua máquina local (o nó de controle) para abrigar todos os arquivos para este tutorial:

  • mkdir -p $HOME/playground/etcd-ansible

Em seguida, entre no diretório que acabou de criar:

  • cd $HOME/playground/etcd-ansible

Dentro do diretório, crie e abra um arquivo de inventário em branco chamado hosts usando seu editor:

  • nano $HOME/playground/etcd-ansible/hosts

Dentro do arquivo hosts, liste todos os seus seus nós gerenciados no seguinte formato, substituindo os endereços IP públicos destacados pelos endereços IP públicos reais dos seus servidores:

~/playground/etcd-ansible/hosts
[etcd]
etcd1 ansible_host=etcd1_public_ip  ansible_user=root
etcd2 ansible_host=etcd2_public_ip  ansible_user=root
etcd3 ansible_host=etcd3_public_ip  ansible_user=root

A linha [etcd] define um grupo chamado etcd. Sob a definição de grupo, listamos todos os nossos nós gerenciados. Cada linha começa com um alias (por exemplo, etcd1), que nos permite fazer referência a cada host usando um nome fácil de lembrar, em vez de um endereço IP longo. O ansible_host e o ansible_user são variáveis do Ansible. Neste caso, são usados para fornecer ao Ansible os endereços IP públicos e os nomes de usuários SSH para usar ao conectar-se via SSH.

Para garantir que o Ansible seja capaz de se conectar com nossos nós gerenciados, podemos testar a conectividade usando o Ansible para executar o comando hostname em cada um dos hosts dentro do grupo etcd:

  • ansible etcd -i hosts -m command -a hostname

Vamos dividir esse comando para aprender o que cada parte significa:

  • etcd: especifica o padrão de host a ser usado para determinar quais hosts do inventário estão sendo gerenciados com este comando. Aqui, usamos o nome do grupo como padrão de host.
  • -i hosts: especifica o arquivo de inventário a ser usado.
  • -m command: as funcionalidades por trás do Ansible são fornecidas através de módulos. O módulo command recebe o argumento passado e executa-o como um comando em cada um dos nós gerenciados. Este tutorial irá introduzir alguns outros módulos do Ansible à medida que progredimos.
  • -a hostname: o argumento a ser passado para o módulo. A quantidade e os tipos de argumentos dependem do módulo.

Depois de executar o comando, você verá o seguinte resultado, o que significa que o Ansible está configurado corretamente:

Output
etcd2 | CHANGED | rc=0 >> etcd2 etcd3 | CHANGED | rc=0 >> etcd3 etcd1 | CHANGED | rc=0 >> etcd1

Cada comando que o Ansible executa é chamado de tarefa. Usar o ansible na linha de comando para executar tarefas é um processo conhecido como executar comandos ad-hoc. A vantagem dos comandos ad-hoc é que eles são rápidos e exigem pouca configuração. O lado negativo é que eles são executados manualmente, e assim não podem ser consignados a um sistema de controle de versão como o Git.

Uma ligeira melhoria seria escrever o script do shell e executar nossos comandos usando o módulo script do Ansible. Isso permitiria que gravássemos os passos de configuração que fizemos no controle de versão. No entanto, os scripts do shell são imperativos, ou seja, somos responsáveis por arranjar os comandos a serem executados (os “como"s) para configurar o sistema no estado desejado. Por outro lado, o Ansible defende uma abordagem declarativa, onde definimos "qual” o estado desejado em que o nosso servidor deve estar dentro dos arquivos de configuração, e o Ansible é responsável por levar o servidor até esse estado desejado.

A abordagem declarativa é mais interessante porque a intenção do arquivo de configuração é imediatamente transmitida, sendo assim mais fácil entendê-lo e mantê-lo. Ela também coloca a responsabilidade de casos extremos nas costas do Ansible ao invés do administrador, poupando-nos muito trabalho.

Agora que o nó de controle do Ansible foi configurado para se comunicar com os nós gerenciados, no próximo passo, vamos apresentar os playbooks do Ansible, que permitem especificar tarefas de forma declarativa.

Passo 2 — Obtendo os nomes de host dos nós gerenciados com os playbooks do Ansible

Neste passo, vamos replicar o que foi feito no Passo 1 — impressão dos nomes de host dos nós gerenciados— mas em vez de executar tarefas ad-hoc, iremos definir cada tarefa declarativamente como um playbook do Ansible para então executá-lo. O objetivo deste passo é demonstrar como os playbooks do Ansible funcionam. Vamos realizar tarefas muito mais robustas com os playbooks em etapas posteriores.

Dentro do seu diretório de projeto, crie um novo arquivo chamado playbook.yaml usando seu editor:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Dentro do playbook.yaml, adicione as seguintes linhas:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  tasks:
    - name: "Retrieve hostname"
      command: hostname
      register: output
    - name: "Print hostname"
      debug: var=output.stdout_lines

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y.

O playbook contém uma lista de peças; cada peça contém uma lista de tarefas que devem ser executadas em todos os hosts que correspondem ao padrão de host especificado pela chave hosts. Neste playbook, temos uma peça que contém duas tarefas. A primeira tarefa executa o comando hostname usando o módulo command e registra o resultado em uma variável chamada output. Na segunda tarefa, usamos o módulo debug para imprimir a propriedade stdout_lines da variável output.

Agora, podemos executar este playbook usando o comando ansible-playbook:

  • ansible-playbook -i hosts playbook.yaml

Você verá o seguinte resultado, o que significa que seu playbook está funcionando corretamente:

Output
PLAY [etcd] *********************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************ ok: [etcd2] ok: [etcd3] ok: [etcd1] TASK [Retrieve hostname] ********************************************************************************************************** changed: [etcd2] changed: [etcd3] changed: [etcd1] TASK [Print hostname] ************************************************************************************************************* ok: [etcd1] => { "output.stdout_lines": [ "etcd1" ] } ok: [etcd2] => { "output.stdout_lines": [ "etcd2" ] } ok: [etcd3] => { "output.stdout_lines": [ "etcd3" ] } PLAY RECAP ************************************************************************************************************************ etcd1 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 etcd2 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 etcd3 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Nota: o ansible-playbook às vezes usa o cowsay como uma maneira brincalhona de imprimir os cabeçalhos. Caso encontre muitas vacas em arte ASCII impressas em seu terminal, agora sabe porquê. Para desativar esse recurso, defina a variável de ambiente ANSIBLE_NOCOWS como 1 antes de executar o ansible-playbook. Faça isso executando export ANSIBLE_NOCOWS=1 no seu shell

Neste passo, passamos a executar playbooks declarativos ao invés de tarefas ad-hoc imperativas. No próximo passo, vamos substituir essas duas tarefas demonstrativas por tarefas que irão configurar nosso cluster do etcd.

Passo 3 — Instalando o etcd nos nós gerenciados

Neste passo, vamos mostrar os comandos para instalar o etcd manualmente e demonstrar como traduzir esses mesmos comandos para tarefas dentro do nosso playbook do Ansible.

O etcd e seu cliente etcdctl estão disponíveis como binários, os quais vamos baixar, extrair e mover para um diretório que faz parte da variável de ambiente PATH. Quando configurados manualmente, esses são os passos que vamos seguir em cada um dos nós gerenciados:

  • mkdir -p /opt/etcd/bin
  • cd /opt/etcd/bin
  • wget -qO- https://storage.googleapis.com/etcd/v3.3.13/etcd-v3.3.13-linux-amd64.tar.gz | tar --extract --gzip --strip-components=1
  • echo 'export PATH="$PATH:/opt/etcd/bin"' >> ~/.profile
  • echo 'export ETCDCTL_API=3" >> ~/.profile

Os primeiros quatro comandos baixam e extraem os binários para o diretório /opt/etcd/bin/. Por padrão, o cliente etcdctl usará a API v2 para se comunicar com o servidor do etcd. Como estamos executando etcd v3.x, o último comando define a variável de ambiente ETCDCTL_API como 3.

Nota: aqui, estamos usando o etcd v3.3.13 desenvolvido para uma máquina com processadores que usam o conjunto de instruções AMD64. Você pode encontrar binários para outros sistemas e outras versões na página oficial de Lançamentos no GitHub.

Para replicar os mesmos passos em um formato padronizado, podemos adicionar tarefas ao nosso playbook. Abra o arquivo de playbook playbook.yaml no seu editor:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Substitua tudo no arquivo playbook.yaml pelo seguinte conteúdo:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    - name: "Create directory for etcd binaries"
      file:
        path: /opt/etcd/bin
        state: directory
        owner: root
        group: root
        mode: 0700
    - name: "Download the tarball into the /tmp directory"
      get_url:
        url: https://storage.googleapis.com/etcd/v3.3.13/etcd-v3.3.13-linux-amd64.tar.gz
        dest: /tmp/etcd.tar.gz
        owner: root
        group: root
        mode: 0600
        force: True
    - name: "Extract the contents of the tarball"
      unarchive:
        src: /tmp/etcd.tar.gz
        dest: /opt/etcd/bin/
        owner: root
        group: root
        mode: 0600
        extra_opts:
          - --strip-components=1
        decrypt: True
        remote_src: True
    - name: "Set permissions for etcd"
      file:
        path: /opt/etcd/bin/etcd
        state: file
        owner: root
        group: root
        mode: 0700
    - name: "Set permissions for etcdctl"
      file:
        path: /opt/etcd/bin/etcdctl
        state: file
        owner: root
        group: root
        mode: 0700
    - name: "Add /opt/etcd/bin/ to the $PATH environment variable"
      lineinfile:
        path: /etc/profile
        line: export PATH="$PATH:/opt/etcd/bin"
        state: present
        create: True
        insertafter: EOF
    - name: "Set the ETCDCTL_API environment variable to 3"
      lineinfile:
        path: /etc/profile
        line: export ETCDCTL_API=3
        state: present
        create: True
        insertafter: EOF

Cada tarefa usa um módulo e para este conjunto de tarefas, estamos usando os seguintes módulos:

  • file: usado para criar o diretório /opt/etcd/bin, e para definir mais tarde as permissões de arquivo para os binários etcd e etcdctl.
  • get_url: usado para baixar o tarball gzipped nos nós gerenciados.
  • unarchive: usado para extrair e descompactar os binários etcd e etcdctl do tarball gzipped.
  • lineinfile: usado para adicionar uma entrada no arquivo .profile.

Para aplicar essas alterações, feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y. Então, no terminal, execute o mesmo comando ansible-playbook novamente:

  • ansible-playbook -i hosts playbook.yaml

A seção PLAY RECAP da saída mostrará apenas ok e changed (alterado):

Output
... PLAY RECAP ************************************************************************************************************************ etcd1 : ok=8 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 etcd2 : ok=8 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 etcd3 : ok=8 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Para confirmar se houve uma instalação correta do etcd, entre manualmente via SSH em um dos nós gerenciados e execute o etcd e o etcdctl:

  • ssh root@etcd1_public_ip

etcd1_public_ip é o endereço IP público do servidor chamado etcd1. Depois que tiver obtido acesso ao SSH, execute etcd --version para imprimir a versão instalada do etcd:

  • etcd --version

Você verá um resultado semelhante ao que será mostrado a seguir, que significa que o binário etcd foi instalado com sucesso:

Output
etcd Version: 3.3.13 Git SHA: 98d3084 Go Version: go1.10.8 Go OS/Arch: linux/amd64

Para confirmar se o etcdctl foi instalado com sucesso, execute version etcdctl:

  • etcdctl version

Você irá obter um resultado similar ao seguinte:

Output
etcdctl version: 3.3.13 API version: 3.3

Observe que o resultado diz API version: 3.3, o que também confirma que nossa variável de ambiente ETCDCTL_API foi definida corretamente.

Saia do servidor etcd1 para voltar ao seu ambiente local.

Agora, instalamos com sucesso o etcd e o etcdctl em todos os nossos nós gerenciados. No próximo passo, vamos adicionar mais tarefas à nossa peça para executar o etcd como um serviço em segundo plano.

Passo 4 — Criando um arquivo de unidade para o etcd

Aparentemente, a maneira mais rápida de executar o etcd com o Ansible é usando o módulo command para executar /opt/etcd/bin/etcd. No entanto, isso não funcionará porque fará com que o etcd seja executado como um processo em primeiro plano. Usar o módulo command fará com que o Ansible pare enquanto espera que o comando etcd gere um retorno, o que nunca acontecerá. Sendo assim, neste passo, vamos atualizar nosso playbook para que execute nosso binário etcd como um serviço em segundo plano.

O Ubuntu 18.04 usa o systemd como seu sistema init, o que significa que podemos criar novos serviços criando arquivos de unidade e colocando-os dentro do diretório /etc/systemd/system/.

Primeiro, dentro do nosso diretório de projeto, crie um novo diretório chamado files/:

  • mkdir files

Em seguida, usando seu editor, crie um novo arquivo chamado etcd.service dentro desse diretório:

  • nano files/etcd.service

Depois disso, copie o seguinte bloco de código no arquivo files/etcd.service:

~/playground/etcd-ansible/files/etcd.service
[Unit]
Description=etcd distributed reliable key-value store

[Service]
Type=notify
ExecStart=/opt/etcd/bin/etcd
Restart=always

Este arquivo de unidade define um serviço que executa o arquivo executável /opt/etcd/bin/etcd, notifica o systemd assim que tiver terminado a inicialização e sempre reinicializa-se caso seja fechado.

Nota: se quiser entender mais sobre o systemd e os arquivos de unidade, ou quiser adaptar o arquivo de unidade às suas necessidades, leia o guia Entendendo as unidades e arquivos de unidade do Systemd.

Feche e salve o arquivo files/etcd.service pressionando CTRL+X e depois Y.

Em seguida, é necessário adicionar uma tarefa dentro do nosso playbook que irá criar uma cópia do arquivo local files/etcd.service no diretório /etc/systemd/system/etcd.service para cada nó gerenciado. Podemos fazer isso usando o módulo copy.

Abra seu playbook:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Adicione a seguinte tarefa destacada no final das tarefas já existentes:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
    - name: "Set the ETCDCTL_API environment variable to 3"
      lineinfile:
        path: /etc/profile
        line: export ETCDCTL_API=3
        state: present
        create: True
        insertafter: EOF
    - name: "Create a etcd service"
      copy:
        src: files/etcd.service
        remote_src: False
        dest: /etc/systemd/system/etcd.service
        owner: root
        group: root
        mode: 0644

Ao copiar o arquivo de unidade em /etc/systemd/system/etcd.service, um serviço agora está definido.

Salve e saia do playbook.

Execute novamente o mesmo comando ansible-playbook para aplicar as novas alterações:

  • ansible-playbook -i hosts playbook.yaml

Para confirmar se as alterações foram aplicadas, primeiro entre via SSH em um dos nós gerenciados:

  • ssh root@etcd1_public_ip

Em seguida, execute systemctl status etcd para consultar o systemd sobre o status do serviço etcd:

  • systemctl status etcd

Você receberá o seguinte resultado, que afirma que o serviço está carregado:

Output
● etcd.service - etcd distributed reliable key-value store Loaded: loaded (/etc/systemd/system/etcd.service; static; vendor preset: enabled) Active: inactive (dead) ...

Nota: a última linha (Active: inactive (dead)) do resultado afirma que o serviço está inativo, o que significa que ele não seria executado automaticamente quando o sistema iniciasse. Isso é esperado e não representa um erro.

Pressione q para voltar ao shell e então execute exit para sair do nó gerenciado e voltar para seu shell local:

  • exit

Neste passo, atualizamos nosso playbook para executar o binário etcd como um serviço do systemd. No próximo passo, vamos continuar a configuração do etcd fornecendo espaço para armazenar seus dados.

Passo 5 — Configurando o diretório de dados

O etcd é um armazenamento de dados de chaves-valores, o que significa que devemos fornecer-lhe espaço para armazenar seus dados. Neste passo, vamos atualizar nosso playbook para definir um diretório de dados dedicado para o etcd usar.

Abra seu playbook:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Acrescente a seguinte tarefa no final da lista de tarefas:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
    - name: "Create a etcd service"
      copy:
        src: files/etcd.service
        remote_src: False
        dest: /etc/systemd/system/etcd.service
        owner: root
        group: root
        mode: 0644
    - name: "Create a data directory"
      file:
        path: /var/lib/etcd/{{ inventory_hostname }}.etcd
        state: directory
        owner: root
        group: root
        mode: 0755

Aqui, estamos usando o /var/lib/etcd/hostname.etcd como o diretório de dados, onde hostname é o nome de host do nó gerenciado em questão e inventory_hostname é uma variável que representa o nome de host do nó gerenciado em questão; seu valor é preenchido pelo Ansible automaticamente. A sintaxe com chaves (ou seja, {{ inventory_hostname }}) é usada para a substituição de variável, suportada pelo mecanismo de modelo padrão para o Ansible, Jinja2.

Feche o editor de texto e salve o arquivo.

Em seguida, é necessário instruir o etcd a usar este diretório de dados. Fazemos isso passando o parâmetro data-dir para o etcd. Para definir parâmetros do etcd, podemos usar uma combinação de variáveis de ambiente, sinalizadores de linha de comando e arquivos de configuração. Para este tutorial, usaremos um único arquivo de configuração, pois é muito mais simples isolar todas as configurações em apenas um arquivo, do que ter partes da configuração espalhadas em todo o nosso playbook.

Em seu diretório de projeto, crie um novo diretório chamado templates/:

  • mkdir templates

Em seguida, usando seu editor, crie um novo arquivo chamado etcd.conf.yaml.j2 dentro do diretório:

  • nano templates/etcd.conf.yaml.j2

Depois disso, copie a linha a seguir e cole no arquivo:

~/playground/etcd-ansible/templates/etcd.conf.yaml.j2
data-dir: /var/lib/etcd/{{ inventory_hostname }}.etcd

Esse arquivo usa a mesma sintaxe de substituição de variável do Jinja2 que nosso playbook. Para substituir as variáveis e fazer upload do resultado para cada host gerenciado, podemos usar módulo template. Ele funciona de maneira semelhante ao copy, exceto que ele irá realizar a substituição de variáveis antes do upload.

Saia do etcd.conf.yaml.j2 e abra seu playbook:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Acrescente as seguintes tarefas na lista de tarefas para criar um diretório e fazer upload do arquivo de configuração modelado nele:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
    - name: "Create a data directory"
      file:
        ...
        mode: 0755
    - name: "Create directory for etcd configuration"
      file:
        path: /etc/etcd
        state: directory
        owner: root
        group: root
        mode: 0755
    - name: "Create configuration file for etcd"
      template:
        src: templates/etcd.conf.yaml.j2
        dest: /etc/etcd/etcd.conf.yaml
        owner: root
        group: root
        mode: 0600

Salve e feche esse arquivo.

Como fizemos essa alteração, precisamos atualizar o arquivo de unidade do nosso serviço para passar-lhe a localização do nosso arquivo de configuração (ou seja, /etc/etcd/etcd.conf.yaml).

Abra o arquivo do serviço etcd em sua máquina local:

  • nano files/etcd.service

Atualize o arquivo files/etcd.service adicionando o sinalizador --config-file destacado a seguir:

~/playground/etcd-ansible/files/etcd.service
[Unit]
Description=etcd distributed reliable key-value store

[Service]
Type=notify
ExecStart=/opt/etcd/bin/etcd --config-file /etc/etcd/etcd.conf.yaml
Restart=always

Salve e feche esse arquivo.

Neste passo, usamos nosso playbook para fornecer um diretório de dados para o etcd armazenar seus dados. No próximo passo, iremos adicionar mais algumas tarefas para reiniciar o serviço etcd e fazê-lo ser executado na inicialização.

Passo 6 — Habilitando e iniciando o serviço etcd

Sempre que fazemos alterações no arquivo de unidade de um serviço, precisamos reiniciar o serviço para elas entrem em vigor. Podemos fazer isso executando o comando systemctl restart etcd. Além disso, para fazer com que o serviço etcd seja iniciado automaticamente na inicialização do sistema, precisamos executar systemctl enable etcd. Neste passo, vamos executar esses dois comandos usando o playbook.

Para executar comandos, podemos usar o módulo command:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Acrescente as seguintes tarefas no final da lista de tarefas:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
    - name: "Create configuration file for etcd"
      template:
        ...
        mode: 0600
    - name: "Enable the etcd service"
      command: systemctl enable etcd
    - name: "Start the etcd service"
      command: systemctl restart etcd

Salve e feche o arquivo.

Execute ansible-playbook -i hosts playbook.yaml mais uma vez:

  • ansible-playbook -i hosts playbook.yaml

Para verificar se o serviço etcd foi reiniciado e está habilitado, entre via SSH em um dos nós gerenciados:

  • ssh root@etcd1_public_ip

Em seguida, execute systemctl status etcd para verificar o status do serviço etcd:

  • systemctl status etcd

Você verá enabled e active (running) como destacado a seguir; isso significa que as alterações que fizemos em nosso playbook tiveram efeito:

Output
● etcd.service - etcd distributed reliable key-value store Loaded: loaded (/etc/systemd/system/etcd.service; static; vendor preset: enabled) Active: active (running) Main PID: 19085 (etcd) Tasks: 11 (limit: 2362)

Neste passo, usamos o módulo command para executar comandos systemctl que reiniciam e habilitam o serviço etcd em nossos nós gerenciados. Agora que configuramos uma instalação etcd, vamos testar, no próximo passo, sua funcionalidade realizando algumas operações básicas de criação, leitura, atualização e exclusão (CRUD).

Passo 7 — Testando o etcd

Embora tenhamos uma instalação funcional do etcd, ela é insegura e ainda não está pronta para o uso na produção. Mas antes de adicionarmos segurança à nossa configuração do etcd em etapas posteriores, vamos primeiro entender o que o etcd pode fazer em termos de funcionalidade. Neste passo, vamos enviar manualmente solicitações ao etcd para adicionar, coletar, atualizar e excluir dados dele.

Por padrão, o etcd expõe uma API que escuta na porta 2379 para a comunicação com o cliente. Isso significa que podemos enviar solicitações de API brutas para o etcd usando um cliente HTTP. No entanto, é mais rápido usar o cliente oficial do etcd, o etcdctl, que permite criar/atualizar, recuperar e excluir pares de chave-valor usando os subcomandos put, get e del, respectivamente.

Certifique-se de ainda estar dentro do nó gerenciado etcd1 e execute os seguintes comandos do etcdctl para confirmar que sua instalação do etcd está funcionando.

Primeiro, crie uma nova entrada usando o subcomando put.

O subcomando put possui a seguinte sintaxe:

etcdctl put key value

No etcd1, execute o seguinte comando:

  • etcdctl put foo "bar"

O comando que acabamos de executar instrui o etcd a escrever o valor "bar" na chave foo no armazenamento.

Em seguida, você verá um OK impresso no resultado, que indica que os dados persistiram:

Output
OK

Em seguida, podemos recuperar essa entrada usando o subcomando get, que possui a sintaxe etcdctl get key:

  • etcdctl get foo

Você verá este resultado, que mostra a chave na primeira linha e o valor que você inseriu mais cedo na segunda linha:

Output
foo bar

Podemos excluir a entrada usando o subcomando del, que possui a sintaxe etcdctl del key:

  • etcdctl del foo

Você verá o seguinte resultado, que indica o número de entradas excluídas:

Output
1

Agora, vamos executar o subcomando get mais uma vez para tentar recuperar um par chave-valor excluído:

  • etcdctl get foo

Você não receberá um resultado, o que significa que o etcdctl não é capaz de recuperar o par chave-valor. Isso confirma que depois que a entrada é excluída, ela não pode mais ser recuperada.

Agora que você testou as operações básicas do etcd e do etcdctl, vamos sair do nosso nó gerenciado e voltar para o ambiente local:

  • exit

Neste passo, usamos o cliente etcdctl para enviar solicitações ao etcd. Neste ponto, estamos executando três instâncias separadas do etcd sendo que cada uma age independentemente uma da outra. No entanto, o etcd é projetado como um armazenamento de chaves-valores distribuído, o que significa que várias instâncias do etcd podem se agrupar para formar um único cluster e cada instância torna-se então um membro do cluster. Depois de formar um cluster, seria possível recuperar um par chave-valor que foi inserido a partir de um membro diferente do cluster. No próximo passo, usaremos nosso playbook para transformar nossos 3 clusters de nó único em um único cluster de três nós.

Passo 8 — Formando um cluster usando a descoberta estática

Para criar um cluster de três nós ao invés de três clusters de somente 1 nó, devemos configurar essas instalações do etcd para se comunicar entre si. Isso significa que cada uma deve conhecer os endereços IP umas das outras. Este processo é chamado de descoberta. A descoberta pode ser feita usando a configuração estática ou a descoberta de serviço dinâmica. Neste passo, vamos discutir a diferença entre as duas, bem como atualizar nosso playbook para configurar um cluster do etcd usando a descoberta estática.

A descoberta por configuração estática é o método cuja configuração é mais curta. É aqui que os pontos de extremidade de cada membro são passados no comando etcd antes que ele seja executado. Para usar a configuração estática, as seguintes condições devem ser atendidas antes da inicialização do cluster:

  • o número de membros é conhecido
  • os pontos de extremidade de cada membro são conhecidos
  • os endereços IP para todos os pontos de extremidade são estáticos

Se essas condições não puderem ser atendidas, então será preciso usar um serviço de descoberta dinâmico. Com a descoberta de serviço dinâmica, todas as instâncias seriam registradas com o serviço de descoberta. Isso permite que cada membro obtenha informações sobre a localização de outros membros.

Como sabemos que queremos um cluster do etcd de 3 nós e todos os nossos servidores têm endereços IP estáticos, usaremos a descoberta estática. Para iniciar nosso cluster usando a descoberta estática, devemos adicionar diversos parâmetros ao nosso arquivo de configuração. Use um editor para abrir o arquivo modelo templates/etcd.conf.yaml.j2:

  • nano templates/etcd.conf.yaml.j2

Então, adicione as linhas destacadas a seguir:

~/playground/etcd-ansible/templates/etcd.conf.yaml.j2
data-dir: /var/lib/etcd/{{ inventory_hostname }}.etcd
name: {{ inventory_hostname }}
initial-advertise-peer-urls: http://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2380
listen-peer-urls: http://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2380,http://127.0.0.1:2380
advertise-client-urls: http://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2379
listen-client-urls: http://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2379,http://127.0.0.1:2379
initial-cluster-state: new
initial-cluster: {% for host in groups['etcd'] %}{{ hostvars[host]['ansible_facts']['hostname'] }}=http://{{ hostvars[host]['ansible_facts']['eth1']['ipv4']['address'] }}:2380{% if not loop.last %},{% endif %}{% endfor %}

Feche e salve o arquivo templates/etcd.conf.yaml.j2 pressionando CTRL+X e depois Y.

Aqui está uma breve explicação de cada parâmetro:

  • name - um nome humanamente legível para o membro. Por padrão, o etcd usa uma ID único gerado aleatoriamente para identificar cada membro. No entanto, um nome humanamente legível nos permite fazer referência a ele com maior facilidade dentro dos arquivos de configuração e na linha de comando. Aqui, usaremos os nomes de host como os nomes de membro (ou seja, etcd1, etcd2 e etcd3).
  • initial-advertise-peer-urls - uma lista de combinações de endereço IP/porta que outros membros podem usar para se comunicar com este membro. Além da porta da API (2379), o etcd também expõe a porta 2380 para a comunicação ponto a ponto entre membros do etcd. Isso permite que eles enviem mensagens entre si e troquem dados. Observe que essas URLs devem ser acessíveis pelos seus pares (e não ser um endereço IP local).
  • listen-peer-urls - uma lista de combinações de endereço IP/porta onde o membro atual irá escutar a comunicação de outros membros. Isso deve incluir todas as URLs do sinalizador --initial-advertise-peer-urls, mas também as URLs locais como 127.0.0.1:2380. O endereço IP/ porta de destino das mensagens de pares recebidas deve corresponder a uma das URLs listadas aqui.
  • advertise-client-urls - uma lista de combinações de endereço IP/porta que os clientes devem usar para se comunicar com este membro. Essas URLs devem ser acessíveis ao cliente (e não ser um endereço local). Se o cliente estiver acessando o cluster através da internet pública, este deve ser um endereço IP público.
  • listen-peer-urls - uma lista de combinações de endereço IP/porta onde o membro atual irá escutar a comunicação de outros clientes. Isso deve incluir todas as URLs do sinalizador --advertise-client-urls, mas também as URLs locais como 127.0.0.1:2379. O endereço IP/ porta de destino das mensagens de clientes recebidas deve corresponder a uma das URLs listadas aqui.
  • initial-cluster - uma lista de pontos de extremidade para cada membro do cluster. Cada ponto de extremidade deve corresponder a uma das URLs do initial-advertise-peer-urls do membro correspondente.
  • initial-cluster-state - ou new (novo) ou existing (existente).

Para garantir a consistência, o etcd só pode tomar decisões quando a maioria dos nós estiver em boas condições. Isso é conhecido como estabelecer o quorum. Em outras palavras, em um cluster de três membros, o quorum é alcançado se dois ou mais membros estiverem em boas condições.

Se o parâmetro initial-cluster-state estiver definido como new, o etcd irá saber que este é um novo cluster sendo inicializado e permitirá que os membros sejam inicializados em paralelo, sem esperar que o quorum seja alcançado. De forma mais concreta, depois que o primeiro membro é iniciado, o quorum não será atingido porque um terço (33,33%) é menor ou igual a 50%. Normalmente, o etcd irá parar e recusar-se a realizar mais ações e o cluster nunca será formado. No entanto, com o initial-cluster-state definido para new, ele irá ignorar a falta inicial de quorum.

Se for definido como existente, o membro irá tentar se juntar a um cluster existente e espera-se que o quorum já esteja estabelecido.

Nota: é possível encontrar mais detalhes sobre todos os sinalizadoras de configuração suportados na seção Configuração da documentação do etcd.

No arquivo modelo templates/etcd.conf.yaml.j2 atualizado, existem algumas instâncias de hostvars. Quando o Ansible for executado, ele irá coletar variáveis vindas de diversas de fontes. Já usamos a variável inventory_hostname anteriormente, mas há muitas outras disponíveis. Essas variáveis estão disponíveis em hostvars[inventory_hostname]['ansible_facts']. Aqui, estamos extraindo os endereços IP privados de cada nó e usamos isso para construir nosso valor de parâmetro.

Nota: como ativamos a opção de Rede privada quando criamos nossos servidores, cada servidor têm três endereços IP associados a eles:

  • Um endereço IP de loopback - um endereço que é válido apenas dentro da mesma máquina. É usado para que a máquina faça referência a si mesma, por exemplo, 127.0.0.1
  • Um endereço endereço IP público - um endereço que é compartilhado através da internet pública, por exemplo, 178.128.169.51
  • Um endereço IP privado - um endereço que é compartilhável apenas dentro da rede privada; no caso das Droplets da DigitalOcean, existe uma rede privada dentro de cada datacenter, por exemplo, 10.131.82.225

Cada um desses endereços endereço IP está associado a uma interface de rede diferente. O endereço loopback está associado à interface lo, o endereço IP público à interface eth0, e o endereço IP privado à interface eth1. Estamos usando a interface eth1 para que todo o tráfego fique dentro da rede privada, sem nunca chegar à internet.

Não é necessário entender as interfaces de rede para este artigo, mas se você quiser aprender mais, Uma introdução à terminologia, interfaces e protocolos de rede é um ótimo lugar para começar.

A sintaxe {% %} do Jinja2 define a estrutura de loop for que itera através de todos os nós no grupo do etcd para compilar a string initial-cluster em um formato exigido pelo etcd.

Para formar o novo cluster de três membros, é necessário primeiro interromper o serviço etcd e limpar o diretório de dados antes de inicializar o cluster. Para fazer isso, use um editor para abrir o arquivo playbook.yaml em sua máquina local:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Em seguida, antes da tarefa "Create a data directory", adicione uma tarefa para interromper o serviço etcd:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
        group: root
        mode: 0644
    - name: "Stop the etcd service"
      command: systemctl stop etcd
    - name: "Create a data directory"
      file:
    ...

Em seguida, atualize a tarefa "Create a data directory" para primeiro excluir o diretório de dados e depois recriá-lo:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  become: True
  tasks:
    ...
    - name: "Stop the etcd service"
      command: systemctl stop etcd
    - name: "Create a data directory"
      file:
        path: /var/lib/etcd/{{ inventory_hostname }}.etcd
        state: "{{ item }}"
        owner: root
        group: root
        mode: 0755
      with_items:
        - absent
        - directory
    - name: "Create directory for etcd configuration"
      file:
    ...

A propriedade with_items define uma lista de strings através das quais esta tarefa irá iterar. Isso é o equivalente a repetir a mesma tarefa duas vezes, mas com valores diferentes para a propriedade state. Aqui, estamos iterando através da lista com os itens absent e directory, garantindo que o diretório de dados seja excluído primeiro e recriado em seguida.

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y. Em seguida, execute o ansible-playbook novamente. Agora, o Ansible irá criar um cluster do etcd único com 3 membros:

  • ansible-playbook -i hosts playbook.yaml

Você pode verificar isso entrando via SSH em qualquer nó que seja membro do etcd:

  • ssh root@etcd1_public_ip

Em seguida, execute etcdctl endpoint health --cluster:

  • etcdctl endpoint health --cluster

Isso irá listar a integridade de cada membro do cluster:

Output
http://etcd2_private_ip:2379 is healthy: successfully committed proposal: took = 2.517267ms http://etcd1_private_ip:2379 is healthy: successfully committed proposal: took = 2.153612ms http://etcd3_private_ip:2379 is healthy: successfully committed proposal: took = 2.639277ms

Agora, criamos com sucesso um cluster do etcd de 3 nós. Podemos verificar isso adicionando uma entrada ao etcd em um dos nós membros, e recuperando-a em outro nó membro. Em um dos nós membros, execute o etcdctl put:

  • etcdctl put foo "bar"

Em seguida, use um novo terminal para entrar via SSH em um nó membro diferente:

  • ssh root@etcd2_public_ip

Depois disso, tente recuperar a mesma entrada usando a chave:

  • etcdctl get foo

Você será capaz de recuperar essa entrada, o que prova que o cluster está funcionando:

Output
foo bar

Por fim, saia de cada um dos nós gerenciados e volte para a sua máquina local:

  • exit
  • exit

Neste passo, criamos um novo cluster de 3 nós. Neste momento, a comunicação entre os membros do etcd e seus pares e clientes é realizada através do HTTP. Isso significa que a comunicação é descriptografada e qualquer pessoa capaz de interceptar o tráfego pode ler as mensagens. Isso não representa um problema grande se o cluster e os clientes do etcd estiverem todos implantados dentro de uma rede privada, ou uma rede privada virtual (VPN) que você tem pleno controle. No entanto, se qualquer um dos sistemas de tráfego precisar viajar através de uma rede compartilhada (privada ou pública), então deve-se garantir que este tráfego seja criptografado. Além disso, um mecanismo precisa ser implantado para que um cliente ou um par verifique a autenticidade do servidor.

No próximo passo, vamos analisar como proteger a comunicação cliente para servidor, bem como a comunicação por pares usando o TLS.

Passo 9 — Obtendo os endereços IP privados dos nós gerenciados

Para criptografar mensagens entre nós membros, o etcd usa o Hypertext Transfer Protocol Secure, ou HTTPS, que é uma camada além do protocolo Transport Layer Security, TLS. O TLS usa um sistema de chaves privadas, certificados e entidades confiáveis chamadas Autoridades de certificação (CAs) para autenticar-se e enviar mensagens criptografadas.

Neste tutorial, cada nó membro precisa gerar um certificado para se identificar e ter este certificado assinado por uma CA. Vamos configurar todos os nós membros para confiar nesta CA e, dessa forma, também confiar em todos os certificados assinados por ela. Isso permite que os nós membros se autentiquem mutuamente entre si.

O certificado gerado por um nó membro deve permitir que outros nós membros se identifiquem. Todos os certificados incluem um Nome comum (CN) da entidade com a qual estão associados. Isso é usado com frequência como a identidade da entidade. No entanto, ao verificar um certificado, pode ser que as implementações do cliente comparem se as informações coletadas sobre a entidade correspondem às fornecidas no certificado. Por exemplo, quando um cliente baixa o certificado TLS com a entidade CN=foo.bar.com, mas o cliente está na verdade se conectando ao servidor usando endereço IP (por exemplo, 167.71.129.110), então existe uma incompatibilidade e o cliente pode não confiar no certificado. Ao especificar um nome alternativo de entidade (SAN) no certificado, ele informa ao verificador que ambos os nomes pertencem à mesma entidade.

Como nossos membros do etcd estão emparelhados uns com os outros usando seus endereços IP privados, quando definirmos nossos certificados, vamos precisar fornecer esses endereços IP privados como os nomes alternativos de entidade.

Para descobrir o endereço IP privado de um nó gerenciado, entre via SSH nele:

  • ssh root@etcd1_public_ip

Em seguida, execute o seguinte comando:

  • ip -f inet addr show eth1

Você verá um resultado parecido com as seguintes linhas:

Output
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 inet 10.131.255.176/16 brd 10.131.255.255 scope global eth1 valid_lft forever preferred_lft forever

Em nosso exemplo de resultado, 10.131.255.176 é o endereço IP privado do nó gerenciado, e é a única informação na qual estamos interessados. Para filtrar tudo exceto o IP privado, podemos canalizar a saída do comando ip para o utilitário sed, que é usado para filtrar e transformar textos.

  • ip -f inet addr show eth1 | sed -En -e 's/.*inet ([0-9.]+).*/\1/p'

Agora, a única saída é o próprio endereço IP privado:

Output
10.131.255.176

Após verificar que o comando anterior funciona, saia do nó gerenciado:

  • exit

Para incorporar os comandos anteriores em nosso playbook, abra primeiro o arquivo playbook.yaml:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Em seguida, adicione uma nova peça com uma única tarefa antes da nossa peça existente:

~/playground/etcd-ansible/playbook.yaml
...
- hosts: etcd
  tasks:
    - shell: ip -f inet addr show eth1 | sed -En -e 's/.*inet ([0-9.]+).*/\1/p'
      register: privateIP
- hosts: etcd
  tasks:
...

A tarefa usa o módulo o shell para executar os comandos ip e sed, os quais buscam o endereço IP privado do nó gerenciado. Em seguida, ela registra o valor de retorno do comando shell dentro de uma variável chamada privateIP, que usaremos mais tarde.

Neste passo, adicionamos uma tarefa ao playbook para obter o endereço IP privado dos nós gerenciados. No próximo passo, vamos usar essas informações para gerar certificados para cada nó membro. Além disso, faremos eles serem assinados por uma Autoridade de Certificação (CA).

Passo 10 — Gerando as chaves privadas e CSRs dos membros do etcd

Para que um nó membro receba tráfego criptografado, o remetente deve usar a chave pública do nó membro criptografar os dados. Além disso, o nó membro deve usar sua chave privada para descriptografar o texto cifrado e recuperar os dados originais. A chave pública é empacotada em um certificado e assinada por uma CA para garantir que seja genuína.

Portanto, vamos precisar gerar uma chave privada e uma solicitação de assinatura de certificado (CSR) para cada nó membro do etcd. Para facilitar o nosso trabalho, vamos gerar todos os pares de chaves e assinar todos os certificados localmente no nó de controle e então copiar os arquivos relevantes para os hosts gerenciados.

Primeiro, crie um diretório chamado artifacts/, onde vamos colocar os arquivos (chaves e certificados) gerados durante o processo. Abra o arquivo playbook.yaml com um editor:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Nele, use o módulo file para criar o diretório artifacts/:

~/playground/etcd-ansible/playbook.yaml
...
    - shell: ip -f inet addr show eth1 | sed -En -e 's/.*inet ([0-9.]+).*/\1/p'
      register: privateIP
- hosts: localhost
  gather_facts: False
  become: False
  tasks:
    - name: "Create ./artifacts directory to house keys and certificates"
      file:
        path: ./artifacts
        state: directory
- hosts: etcd
  tasks:
...

Em seguida, adicione uma outra tarefa no final da peça para gerar a chave privada:

~/playground/etcd-ansible/playbook.yaml
...
- hosts: localhost
  gather_facts: False
  become: False
  tasks:
        ...
    - name: "Generate private key for each member"
      openssl_privatekey:
        path: ./artifacts/{{item}}.key
        type: RSA
        size: 4096
        state: present
        force: True
      with_items: "{{ groups['etcd'] }}"
- hosts: etcd
  tasks:
...

Criar chaves privadas e CSRs pode ser feito usando os módulos openssl_privatekey e openssl_csr respectivamente.

O atributo force: True garante que a chave privada seja regenerada toda vez, mesmo que já exista.

De maneira similar, adicione a nova tarefa a seguir na mesma peça gerar as CSRs para cada membro, usando o módulo openssl_csr:

~/playground/etcd-ansible/playbook.yaml
...
- hosts: localhost
  gather_facts: False
  become: False
  tasks:
    ...
    - name: "Generate private key for each member"
      openssl_privatekey:
        ...
      with_items: "{{ groups['etcd'] }}"
    - name: "Generate CSR for each member"
      openssl_csr:
        path: ./artifacts/{{item}}.csr
        privatekey_path: ./artifacts/{{item}}.key
        common_name: "{{item}}"
        key_usage:
          - digitalSignature
        extended_key_usage:
          - serverAuth
        subject_alt_name:
          - IP:{{ hostvars[item]['privateIP']['stdout']}}
          - IP:127.0.0.1
        force: True
      with_items: "{{ groups['etcd'] }}"

Estamos especificando que este certificado pode estar envolvido em um mecanismo de assinatura digital para fins da autenticação de servidor. Este certificado é associado ao nome do host (por exemplo, etcd1), mas o verificador também deve tratar os endereços IP privados, locais e de loopback de cada nó como nomes alternativos. Observe que estamos usando a variável privateIP que registramos na peça anterior.

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y. Em seguida, execute o playbook novamente:

  • ansible-playbook -i hosts playbook.yaml

Agora, vamos encontrar um novo diretório chamado artifacts dentro do nosso diretório de projeto. Use ls para listar seu conteúdo:

  • ls artifacts

Você verá as chaves privadas e as CSRs para cada um dos membros do etcd:

Output
etcd1.csr etcd1.key etcd2.csr etcd2.key etcd3.csr etcd3.key

Neste passo, usamos vários módulos do Ansible para gerar chaves privadas e certificados de chave pública para cada um dos nós membros. No próximo passo, vamos analisar como assinar uma solicitação de assinatura de certificado (CSR).

Passo 11 — Gerando os certificados CA

Dentro de um cluster do etcd, os nós membros criptografam mensagens usando a chave pública do receptor. Para garantir que a chave pública seja genuína, o receptor empacota a chave pública em uma solicitação de assinatura de certificado (CSR) e tem uma entidade confiável (ou seja, a CA) para assinar a CSR. Como controlamos todos os nós membros e as CA em que eles confiam, não precisamos usar uma CA externa e podemos atuar como nossa própria CA. Neste passo, atuaremos como nossa própria CA. Isso significa que vamos precisar gerar uma chave privada e um certificado auto-assinado que funcionarão como a CA.

Primeiro, abra o arquivo playbook.yaml com seu editor:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Em seguida, de maneira similar ao passo anterior, adicione uma tarefa à peça localhost para gerar uma chave privada para a CA:

~/playground/etcd-ansible/playbook.yaml
- hosts: localhost
  ...
  tasks:
    ...
  - name: "Generate CSR for each member"
    ...
    with_items: "{{ groups['etcd'] }}"
    - name: "Generate private key for CA"
      openssl_privatekey:
        path: ./artifacts/ca.key
        type: RSA
        size: 4096
        state: present
        force: True
- hosts: etcd
  become: True
  tasks:
    - name: "Create directory for etcd binaries"
...

Em seguida, use o módulo openssl_csr para gerar uma nova CSR. O processo é semelhante ao passo anterior, mas neste CSR, estamos adicionando a restrição básica e a extensão de uso de chave para indicar que este certificado pode ser usado como um certificado CA:

~/playground/etcd-ansible/playbook.yaml
- hosts: localhost
  ...
  tasks:
    ...
    - name: "Generate private key for CA"
      openssl_privatekey:
        path: ./artifacts/ca.key
        type: RSA
        size: 4096
        state: present
        force: True
    - name: "Generate CSR for CA"
      openssl_csr:
        path: ./artifacts/ca.csr
        privatekey_path: ./artifacts/ca.key
        common_name: ca
        organization_name: "Etcd CA"
        basic_constraints:
          - CA:TRUE
          - pathlen:1
        basic_constraints_critical: True
        key_usage:
          - keyCertSign
          - digitalSignature
        force: True
- hosts: etcd
  become: True
  tasks:
    - name: "Create directory for etcd binaries"
...

Por fim, use o módulo openssl_certificate para auto-assinar a CSR:

~/playground/etcd-ansible/playbook.yaml
- hosts: localhost
  ...
  tasks:
    ...
    - name: "Generate CSR for CA"
      openssl_csr:
        path: ./artifacts/ca.csr
        privatekey_path: ./artifacts/ca.key
        common_name: ca
        organization_name: "Etcd CA"
        basic_constraints:
          - CA:TRUE
          - pathlen:1
        basic_constraints_critical: True
        key_usage:
          - keyCertSign
          - digitalSignature
        force: True
    - name: "Generate self-signed CA certificate"
      openssl_certificate:
        path: ./artifacts/ca.crt
        privatekey_path: ./artifacts/ca.key
        csr_path: ./artifacts/ca.csr
        provider: selfsigned
        force: True
- hosts: etcd
  become: True
  tasks:
    - name: "Create directory for etcd binaries"
...

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y. Em seguida, execute o playbook para aplicar as alterações:

  • ansible-playbook -i hosts playbook.yaml

Você também pode executar o ls para verificar o conteúdo do diretório artifacts/:

  • ls artifacts/

Agora, você verá o certificado CA recém-gerado (ca.crt):

Output
ca.crt ca.csr ca.key etcd1.csr etcd1.key etcd2.csr etcd2.key etcd3.csr etcd3.key

Neste passo, geramos uma chave privada e um certificado auto-assinado para a CA. No próximo passo, usaremos o certificado CA para assinar a CSR de cada membro.

Passo 12 — Assinando as CSRs dos membros do etcd

Neste passo, vamos assinar a CSR de cada nó membro. Isso será feito de maneira semelhante a como usamos o módulo openssl_certificate para auto-assinar o certificado CA, mas em vez de usar o provedor selfsigned, usaremos o provedor ownca. Isso nos permite assinar usando nosso próprio certificado CA.

Abra seu playbook:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

Acrescente a tarefa destacada a seguir na tarefa "Generate self-signed CA certificate":

~/playground/etcd-ansible/playbook.yaml
- hosts: localhost
  ...
  tasks:
    ...
    - name: "Generate self-signed CA certificate"
      openssl_certificate:
        path: ./artifacts/ca.crt
        privatekey_path: ./artifacts/ca.key
        csr_path: ./artifacts/ca.csr
        provider: selfsigned
        force: True
    - name: "Generate an `etcd` member certificate signed with our own CA certificate"
      openssl_certificate:
        path: ./artifacts/{{item}}.crt
        csr_path: ./artifacts/{{item}}.csr
        ownca_path: ./artifacts/ca.crt
        ownca_privatekey_path: ./artifacts/ca.key
        provider: ownca
        force: True
      with_items: "{{ groups['etcd'] }}"
- hosts: etcd
  become: True
  tasks:
    - name: "Create directory for etcd binaries"
...

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y. Em seguida, execute o playbook novamente para aplicar as alterações:

  • ansible-playbook -i hosts playbook.yaml

Agora, liste o conteúdo do diretório artifacts/:

  • ls artifacts/

Você verá a chave privada, a CSR e o certificado para cada membro do etcd, além da CA:

Output
ca.crt ca.csr ca.key etcd1.crt etcd1.csr etcd1.key etcd2.crt etcd2.csr etcd2.key etcd3.crt etcd3.csr etcd3.key

Neste passo, assinamos as CSRs de cada nó membro usando a chave da CA. No próximo passo, vamos copiar os arquivos relevantes em cada nó gerenciado, para que o etcd tenha acesso às chaves e certificados relevantes para configurar as conexões TLS.

Passo 13 — Copiando chaves privadas e certificados

Todos os nós precisam ter uma cópia do certificado auto-assinado da CA (ca.crt). Cada nó membro do etcd também precisa ter sua própria chave privada e certificado. Neste passo, vamos fazer upload desses arquivos e colocá-los em um novo diretório /etc/etcd/ssl/.

Para começar, abra o arquivo playbook.yaml com seu editor:

  • nano $HOME/playground/etcd-ansible/playbook.yaml

A fim de fazer essas alterações em nosso playbook do Ansible, atualize primeiro a propriedade path da tarefa Create directory for etcd configuration para criar o diretório /etc/etcd/ssl/:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  ...
  tasks:
    ...
      with_items:
        - absent
        - directory
    - name: "Create directory for etcd configuration"
      file:
        path: "{{ item }}"
        state: directory
        owner: root
        group: root
        mode: 0755
      with_items:
        - /etc/etcd
        - /etc/etcd/ssl
    - name: "Create configuration file for etcd"
      template:
...

Em seguida, adicione mais três tarefas após a tarefa modificada para copiar os arquivos:

~/playground/etcd-ansible/playbook.yaml
- hosts: etcd
  ...
  tasks:
    ...
    - name: "Copy over the CA certificate"
      copy:
        src: ./artifacts/ca.crt
        remote_src: False
        dest: /etc/etcd/ssl/ca.crt
        owner: root
        group: root
        mode: 0644
    - name: "Copy over the `etcd` member certificate"
      copy:
        src: ./artifacts/{{inventory_hostname}}.crt
        remote_src: False
        dest: /etc/etcd/ssl/server.crt
        owner: root
        group: root
        mode: 0644
    - name: "Copy over the `etcd` member key"
      copy:
        src: ./artifacts/{{inventory_hostname}}.key
        remote_src: False
        dest: /etc/etcd/ssl/server.key
        owner: root
        group: root
        mode: 0600
    - name: "Create configuration file for etcd"
      template:
...

Feche e salve o arquivo playbook.yaml pressionando CTRL+X e depois Y.

Execute o ansible-playbook novamente para fazer essas alterações:

  • ansible-playbook -i hosts playbook.yaml

Neste passo, fizemos upload das chaves privadas e certificados para os nós gerenciados. Após termos copiado os arquivos, precisamos agora atualizar nosso arquivo de configuração do etcd para fazer uso deles.

Passo 14 — Habilitando o TLS no etcd

No último passo deste tutorial, vamos atualizar algumas configurações do Ansible para habilitar o TLS em um cluster do etcd.

Primeiro, abra o arquivo modelo templates/etcd.conf.yaml.j2 usando seu editor:

  • nano $HOME/playground/etcd-ansible/templates/etcd.conf.yaml.j2

Uma vez dentro, mude todas as URLs para que usem o https como protocolo em vez de http. Além disso, adicione uma seção no final do modelo para especificar a localização do certificado CA, certificado do servidor e chave do servidor:

~/playground/etcd-ansible/templates/etcd.conf.yaml.j2
data-dir: /var/lib/etcd/{{ inventory_hostname }}.etcd
name: {{ inventory_hostname }}
initial-advertise-peer-urls: https://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2380
listen-peer-urls: https://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2380,https://127.0.0.1:2380
advertise-client-urls: https://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2379
listen-client-urls: https://{{ hostvars[inventory_hostname]['ansible_facts']['eth1']['ipv4']['address'] }}:2379,https://127.0.0.1:2379
initial-cluster-state: new
initial-cluster: {% for host in groups['etcd'] %}{{ hostvars[host]['ansible_facts']['hostname'] }}=https://{{ hostvars[host]['ansible_facts']['eth1']['ipv4']['address'] }}:2380{% if not loop.last %},{% endif %}{% endfor %}

client-transport-security:
  cert-file: /etc/etcd/ssl/server.crt
  key-file: /etc/etcd/ssl/server.key
  trusted-ca-file: /etc/etcd/ssl/ca.crt
peer-transport-security:
  cert-file: /etc/etcd/ssl/server.crt
  key-file: /etc/etcd/ssl/server.key
  trusted-ca-file: /etc/etcd/ssl/ca.crt

Feche e salve o arquivo templates/etcd.conf.yaml.j2.

Em seguida, execute seu playbook do Ansible:

  • ansible-playbook -i hosts playbook.yaml

Depois disso, entre via SSH em um dos nós gerenciados:

  • ssh root@etcd1_public_ip

Uma vez dentro, execute o comando etcdctl endpoint health para verificar se os pontos de extremidade estão usando o HTTPS e se todos os membros estão íntegros:

  • etcdctl --cacert /etc/etcd/ssl/ca.crt endpoint health --cluster

Como nosso certificado CA não é, por padrão, um certificado CA root confiável instalado no diretório /etc/ssl/certs/, precisamos passá-lo para o etcdctl usando o sinalizador --cacert.

Isso dará o seguinte resultado:

Output
https://etcd3_private_ip:2379 is healthy: successfully committed proposal: took = 19.237262ms https://etcd1_private_ip:2379 is healthy: successfully committed proposal: took = 4.769088ms https://etcd2_private_ip:2379 is healthy: successfully committed proposal: took = 5.953599ms

Para confirmar se o cluster do etcd está realmente funcionando, podemos mais uma vez criar uma entrada em um nó membro, e recuperá-la em outro nó membro:

  • etcdctl --cacert /etc/etcd/ssl/ca.crt put foo "bar"

Use um novo terminal para entrar via SSH em um nó membro diferente:

  • ssh root@etcd2_public_ip

Agora, recupere a mesma entrada usando a chave foo:

  • etcdctl --cacert /etc/etcd/ssl/ca.crt get foo

Isso irá retornar a entrada, exibindo o resultado abaixo:

Output
foo bar

Você pode fazer o mesmo no terceiro nó para garantir que todos os três membros estejam operacionais.

Conclusão

Agora, você provisionou com sucesso um cluster de 3 nós do etcd, protegeu ele com o TLS e confirmou que está funcionando.

O etcd é uma ferramenta originalmente criada pelo CoreOS. Para entender o uso do etcd em relação ao CoreOS, consulte Como usar o etcdctl e o etcd, o armazenamento de chaves-valores distribuído do CoreOS. O artigo também dá orientação na configuração de um modelo de descoberta dinâmico, algo que foi discutido, mas não demonstrado neste tutorial.

Como mencionado no início deste tutorial, o etcd é uma parte importante do ecossistema do Kubernetes. Para aprender mais sobre o papel do Kubernetes e do etcd dentro dele, leia Uma introdução ao Kubernetes. Se estiver implantando o etcd como parte de um cluster do Kubernetes, saiba que existem outras ferramentas disponíveis, como o kubespray e o kubeadm. Para mais detalhes sobre o kubeadm, leia Como criar um cluster do Kubernetes usando o Kubeadm no Ubuntu 18.04.

Por fim, deve-se destacar que este tutorial fez uso de muitas ferramentas, mas não conseguiu se aprofundar em todos eles com muitos detalhes. A seguir, estão disponíveis links que irão fornecer uma análise mais detalhada sobre cada ferramenta:

Creative Commons License