O autor selecionou a Wikimedia Foundation para receber uma doação como parte do programa Write for DOnations.
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.
Antes de iniciar este guia, será necessário o seguinte:
O Python , pip
e o pacote pyOpenSSL
instalados em sua máquina local. Para aprender como instalar os pacotes Python3, pip e Python, consulte Como instalar o Python 3 e configurar um ambiente de programação local no Ubuntu 18.04.
Três servidores Ubuntu 18.04 na mesma rede local, com pelo menos 2 GB de RAM e acesso root ao SSH. Você também deve configurar os servidores para que tenham os nomes de host etcd1, etcd2 e etcd3. Os passos delineados neste artigo funcionariam em qualquer servidor genérico, não necessariamente os Droplets da DigitalOcean. No entanto, se quiser hospedar seus servidores na DigitalOcean, siga o guia Como criar um Droplet a partir do painel de controle da DigitalOcean para atender a essa exigência. Observe que é necessário habilitar a opção Private Networking (rede privada) ao criar seu Droplet. Para habilitar a rede privada em Droplets já existentes, consulte Como habilitar a rede privada em Droplets.
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.
Um par de chaves SSH permitindo que sua máquina local tenha acesso aos servidores etcd1, etcd2 e etcd3. Se você não sabe o que é o SSH ou não tiver um par de chaves SSH, aprenda sobre isso lendo Fundamentos do SSH: trabalhando com servidores, clientes e chaves SSH.
O Ansible instalado em sua máquina local. Por exemplo, se estiver executando o Ubuntu 18.04, é possível instalar o Ansible seguindo o Passo 1 do artigo Como instalar e configurar o Ansible no Ubuntu 18.04. Isso tornará os comandos ansible
e ansible-playbook
disponíveis em sua máquina. Também pode ser interessante salvar o artigo Como usar o Ansible: um guia de referência. Os comandos neste tutorial devem funcionar com o Ansible v2.x. Testamos o processo no Ansible v2.9.7 executando o Python v3.8.2.
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:
[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:
Outputetcd2 | 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.
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:
- 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:
OutputPLAY [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.
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:
- 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:
Outputetcd 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:
Outputetcdctl 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.
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
:
[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:
- 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.
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:
- 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:
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:
- 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:
[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.
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:
- 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).
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:
OutputOK
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:
Outputfoo
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:
Output1
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.
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:
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:
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:
127.0.0.1
178.128.169.51
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
:
- 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:
- 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:
Outputhttp://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:
Outputfoo
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.
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:
Output3: 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:
Output10.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:
...
- 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).
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/
:
...
- 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:
...
- 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
:
...
- 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:
Outputetcd1.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).
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:
- 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:
- 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:
- 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
):
Outputca.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.
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"
:
- 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:
Outputca.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.
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/
:
- 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:
- 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.
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:
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:
Outputhttps://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:
Outputfoo
bar
Você pode fazer o mesmo no terceiro nó para garantir que todos os três membros estejam operacionais.
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:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!