Настройка и обеспечение безопасности кластера etcd с помощью Ansible в Ubuntu 18.04

By Daniel Li Published on September 11, 2020 7k views

Введение

etcd — это распределенное хранилище типа «ключ-значение», на которое опирается множество платформ и инструментов, включая Kubernetes, Vulcand и Doorman. Внутри Kubernetes etcd используется в качестве глобального хранилища, где хранится состояние кластера. Знание того, как управлять etcd, обязательно для управления кластером Kubernetes. Хотя существует большое количество предложений, использующих Kubernetes, также известных как Kubernetes как услуга, которые избавляют вас от необходимости выполнения административной работы, многие компании все еще предпочитают запускать управляемые кластеры Kubernetes самостоятельно, используя собственные ресурсы, по причине гибкости, которую дает этот подход.

Первая половина этой статьи поможет вам настроить состоящий из 3 узлов кластер etcd на серверах Ubuntu 18.04. Вторая половина будет посвящена обеспечению безопасности кластера с помощью протокола безопасности транспортного уровня или TLS. Для автоматического запуска каждой настройки мы будем на протяжении всего руководства использовать Ansible. Ansible — это инструмент управления конфигурацией, аналогичный Puppet, Chef и SaltStack, который позволяет нам определять каждый шаг настройки в декларативной манере внутри файлов, которые называются плейбуками.

В конце этого руководства у вас будет защищенный кластер etcd из 3 узлов, запущенный на ваших серверах. Также у вас будет плейбук Ansible, который позволяет вам многократно и последовательно воссоздавать одну и ту же настройку на новом наборе серверов.

Предварительные требования

Для прохождения этого обучающего руководства вам потребуется следующее:

  • Python, pip и пакет pyOpenSSL, установленные на вашем локальном компьютере. Чтобы узнать, как установить Python3, pip и пакеты Python, воспользуйтесь руководством по установке Python 3 и настройке локальной среды программирования в Ubuntu 18.04.
  • Три сервера Ubuntu 18.04 в одной локальной сети с минимум 2 ГБ оперативной памяти и доступом root через SSH. Также вам необходимо задать для серверов имена хостов etcd1, etcd2 и etcd3. Шаги, описанные в этой статье, будут работать на любом базовом сервере, а не только на дроплетах DigitalOcean. Однако если вы хотите разместить ваши серверы в DigitalOcean, вы можете воспользоваться руководством по созданию дроплета в панели управления DigitalOcean, чтобы выполнить это требование. Обратите внимание, что вы должны активировать опцию Private Networking (Частная сеть) при создании вашего дроплета. Чтобы активировать частную сеть для существующих дроплетов, воспользуйтесь руководством по активации частной сети в дроплетах.

Предупреждение. Поскольку цель этой статьи состоит в знакомстве с настройкой кластера etcd в частной сети, три сервера Ubuntu 18.04 в рамках данной настройки не были протестированы с брандмауэром и доступны при работе с пользователем root. В производственной среде для любого узла, открытого для публичного Интернета, необходимо придерживаться передовых практик обеспечения безопасности при настройке брандмауэра и пользователя sudo. Дополнительную информацию см. в руководстве по начальной настройке сервера Ubuntu 18.04.

  • Пара ключей SSH, обеспечивающая для локального компьютера доступ к серверам etcd1, etcd2 и etcd3. Если вы не знаете, что такое SSH, или у вас нет пары ключей SSH, вы можете получить необходимую информацию, прочитав статью Основы SSH: работа с серверами, клиентами и ключами SSH.
  • Система Ansible, установленная на локальном компьютере. Например, если вы используете Ubuntu 18.04, вы можете установить Ansible, выполнив указания в шаге 1 статьи Установка и настройка Ansible в Ubuntu 18.04. После этого команды ansible и ansible-playbook будут доступны на вашем компьютере. Также вам может пригодиться статья Использование Ansible: справочное руководство. Команды в этом руководстве должны работать с Ansible версии 2.х; мы протестировали его на Ansible 2.9.7 с Python 3.8.2.

Шаг 1 — Настройка Ansible для узла управления

Ansible — это инструмент, используемый для управления серверами. Серверы, которыми управляет Ansible, называются управляемыми узлами, а компьютер, на котором запущен Ansible, называется узлом управления. Ansible использует ключи SSH на узле управления, чтобы получить доступ к управляемым узлам. После установки сеанса SSH Ansible запускает набор скриптов для предоставления и настройки управляемых узлов. На этом шаге мы протестируем возможность использования Ansible для подключения к управляемым узлам и запустим команду hostname.

Типичный день системного администратора может включать управление различными наборами узлов. Например, вы можете использовать Ansible для предоставления новых серверов, а позже использовать Ansible для изменения конфигурации другого набора серверов. Чтобы позволить администраторам лучше организовать набор управляемых узлов, Ansible предоставляет концепцию inventory хостов (или inventory для краткости). Вы можете определить каждый узел, которым вы хотите управлять с помощью Ansible, внутри inventory-файла и организовать их в группы. Затем при запуске команд ansible и ansible-playbook вы можете указать, к каким хостам или группам применяется эта команда.

По умолчанию Ansible считывает inventory-файл в каталоге /etc/ansible/hosts, однако мы можем указать другой inventory-файл с помощью флага --inventory (или -i для краткости).

Для начала создайте новый каталог на локальном компьютере (узле управления) для размещения всех файлов данного руководства:

mkdir -p $HOME/playground/etcd-ansible

Затем перейдите в только что созданный каталог:

cd $HOME/playground/etcd-ansible

Внутри каталога создайте и откройте пустой inventory-файл с именем hosts с помощью вашего редактора:

nano $HOME/playground/etcd-ansible/hosts

Внутри файла hosts перечислите все ваши управляемые узлы в следующем формате, заменив выделенные публичные IP-адреса на реальные IP-адреса ваших серверов:

~/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

Строка [etcd] определяет группу с именем etcd. Под определением группы мы перечисляем все наши управляемые узлы. Каждая строка начинается с псевдонима (например, etcd1), который позволяет нам обращаться к каждому хосту, используя простое для запоминания имя вместо длинного IP-адреса. ansible_host и ansible_user — это переменные Ansible. В этом случае они используются для предоставления Ansible публичных IP-адресов и пользовательских имен SSH, которые используются при подключении через SSH.

Чтобы гарантировать, что Ansible сможет подключаться к нашим управляемым узлам, мы можем протестировать подключение с помощью Ansible и запустить команду hostname на каждом хосте в группе etcd:

ansible etcd -i hosts -m command -a hostname

Давайте подробно разберем эту команду, чтобы узнать, что означает каждая часть:

  • etcd: указывает шаблон хоста, который используется для определения того, какие хосты из inventory управляются с помощью этой команды. Здесь мы используем имя группы в качестве шаблона хоста.
  • -i hosts: указывает inventory-файл, который нужно использовать.
  • -m command: функциональность Ansible обеспечивается модулями. Модуль command принимает передаваемый в него аргумент и выполняет его как команду на каждом из управляемых узлов. В этом руководстве мы будем внедрять несколько дополнительных модулей Ansible по мере нашего прогресса.
  • -a hostname: аргумент, который необходимо передать в модуль. Количество и типы аргументов зависят от модуля.

После запуска команды вы получите следующий вывод, который означает, что Ansible настроен корректно:

Outputetcd2 | CHANGED | rc=0 >>
etcd2

etcd3 | CHANGED | rc=0 >>
etcd3

etcd1 | CHANGED | rc=0 >>
etcd1

Каждая команда, которую запускает Ansible, называется задачей. Использование ansible в командной строке для запуска задач называется запуском ситуативных команд. Преимущество ситуативных команд состоит в том, что они быстрые и требуют минимальной настройки, а недостаток состоит в том, что они запускаются вручную, а значит не могут быть добавлены в систему контроля версий, например Git.

Небольшим улучшением может быть запись скрипта оболочки и запуск команд с помощью модуля script Ansible. Это позволит нам записать этапы конфигурации, которые мы передали в систему контроля версий. Однако скрипты оболочки имеют императивный характер, что означает, что нам нужно определить команды для запуска («как») для приведения системы в желаемое состояние. Ansible, с другой стороны, выступает за декларативный подход, где мы определяем «какое» состояние сервера нам нужно внутри файлов конфигурации, а Ansible отвечает за приведение сервера в это желаемое состояние.

Декларативный метод является предпочтительным, поскольку назначение файла конфигурации передается немедленно, что означает, что его легче понять и поддерживать. Также подобный подход возлагает ответственность за обработку пограничных случаев на Ansible, а не на администратора, избавляя от большого объема работы.

Теперь, когда вы настроили узел управления Ansible для связи с управляемыми узлами, в следующем шаге мы познакомим вас с плейбуками Ansible, которые позволяют определять задачи декларативным образом.

Шаг 2 — Получение имен хостов управляемых узлов с помощью плейбуков Ansible

На этом шаге мы воспроизведем то, что было сделано в шаге 1, т. е. выведем имена хостов управляемых узлов, но вместо запуска ситуативных задач мы определим каждую задачу декларативно в виде плейбука Ansible и запустим ее. Цель этого шага — продемонстрировать, как работают плейбуки Ansible. В последующих шагах мы будем выполнять гораздо более серьезные задачи с помощью плейбуков.

Внутри каталога проекта создайте новый файл с именем playbook.yaml с помощью вашего редактора:

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

Внутри playbook.yaml добавьте следующие строки:

~/playground/etcd-ansible/playbook.yaml

- hosts: etcd
  tasks:
    - name: "Retrieve hostname"
      command: hostname
      register: output
    - name: "Print hostname"
      debug: var=output.stdout_lines

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y.

Плейбук содержит список инструкций; каждая инструкция содержит список задач, которые следует запускать на всех хостах, соответствующих шаблону хоста, указанному ключом hosts. В этом плейбуке у нас есть одна инструкция, содержащая две задачи. Первая задача запускает команду hostname, используя модуль command, и записывает вывод в переменную с именем output. Во второй задаче мы используем модуль debug для вывода свойства stdout_lines переменной output.

Теперь мы можем запустить этот плейбук с помощью команды ansible-playbook:

ansible-playbook -i hosts playbook.yaml

Вы получите следующий вывод, что означает, что ваш плейбук работает корректно:

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   

Примечание: ansible-playbook иногда использует cowsay для нестандартного вывода заголовков. Если вы обнаружите в своем терминале много нарисованных с помощью ASCII-графики коров, то впредь будете знать, почему это происходит. Чтобы отключить эту функцию, задайте для переменной среды ANSIBLE_NOCOWS значение 1 перед запуском ansible-playbook, запустив export ANSIBLE_NOCOWS=1 в оболочке.

На этом шаге мы перешли от запуска императивных ситуативных задач к декларативным плейбукам. В следующем шаге мы заменим эти две демонстрационные задачи на задачи, которые будут настраивать наш кластер etcd.

Шаг 3 — Установка etcd на управляемые узлы

На этом шаге мы покажем вам команды для ручной установки etcd и продемонстрируем, как перевести эти самые команды в задачи внутри нашего плейбука Ansible.

etcd и соответствующий клиент etcdctl доступны в качестве бинарных файлов, которые мы загрузим, извлечем и переместим в каталог, являющийся частью переменной среды PATH. При ручной настройке эти шаги необходимо выполнять для каждого управляемого узла:

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

Первые четыре команды загружают и извлекают бинарный файл в каталог /opt/etcd/bin/. По умолчанию клиент etcdctl использует API версии 2 для связи с сервером etcd. Поскольку мы запускаем etcd версии 3.x, последняя команда устанавливает для переменной среды ETCDCTL_API значение 3.

Примечание. Здесь мы используем etcd версии 3.3.13 для компьютеров с процессорами, использующими набор инструкций AMD64. Вы можете найти бинарные файлы для других систем и других версий на официальной странице выпусков на GitHub.

Чтобы воспроизвести аналогичные шаги в стандартизированном формате, мы можем добавить задачи в наш плейбук. Откройте файл playbook.yaml в вашем редакторе:

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

Замените все содержимое файла playbook.yaml на следующее содержимое:

~/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

Каждая задача использует модуль; для этого набора задач мы используем следующие модули:

  • file: для создания каталога /opt/etcd/bin и последующей настройки разрешений файлов для бинарных файлов etcd и etcdctl.
  • get_url: для загрузки тарбола в формате GZIP на управляемые узлы.
  • unarchive: для извлечения и распаковки бинарных файлов etcd и etcdctl из тарбола в формате GZIP.
  • lineinfile: для добавления записи в файл .profile.

Чтобы применить эти изменения, закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y. После этого в терминале снова запустите ту же команду ansible-playbook:

ansible-playbook -i hosts playbook.yaml

Раздел PLAY RECAP в выводе будет отображать только ok и changed:

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   

Чтобы подтвердить правильную установку etcd, выполните ручное подключение через SSH к одному из управляемых узлов и запустите etcd и etcdctl:

ssh root@etcd1_public_ip

etcd1_public_ip — это публичные IP-адреса сервера с именем etcd1. После получения доступа через SSH запустите etcd --version для вывода версии установленного etcd:

etcd --version

Вы получите вывод, соответствующий представленному ниже, что означает, что бинарный файл etcd успешно установлен:

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

Чтобы подтвердить успешную установку etcdctl, запустите etcdctl version:

etcdctl version

Вы увидите примерно следующий результат:

Outputetcdctl version: 3.3.13
API version: 3.3

Обратите внимание, что в выводе указано API version: 3.3, что также подтверждает, что наша переменная среды ETCDCTL_API была настроена корректно.

Выйдите из сервера etcd1, чтобы вернуться в локальную среду.

Мы успешно установили etcd и etcdctl на все наши управляемые узлы. В следующем шаге мы добавим дополнительные задачи в нашу инструкцию для запуска etcd в качестве фоновой службы.

Шаг 4 — Создание юнит-файла для etcd

Может показаться, что самым быстрым способом запуска etcd с помощью Ansible может быть использование модуля command для запуска /opt/etcd/bin/etcd. Однако этот способ не сработает, поскольку он будет запускать etcd в качестве активного процесса. Использование модуля command будет приводить к зависанию Ansible в ожидании результата, возвращаемого командой etcd, чего никогда не произойдет. Поэтому в этом шаге мы обновим наш плейбук для запуска нашего бинарного файла etcd в качестве фоновой службы.

Ubuntu 18.04 использует systemd в качестве инит-системы, что означает, что мы можем создавать новые службы, записывая юнит-файлы и размещая их внутри каталога /etc/systemd/system/.

Во-первых, внутри каталога нашего проекта создайте новый каталог с именем files/:

mkdir files

Затем с помощью вашего редактора создайте в этом каталоге новый файл с именем etcd.service:

nano files/etcd.service

Скопируйте следующий блок кода в файл 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

Этот юнит-файл определяет службу, которая запускает исполняемый файл в /opt/etcd/bin/etcd, уведомляет systemd о завершении инициализации и перезапускается при каждом случае сбоя.

Примечание. Если вы хотите узнать больше о systemd и юнит-файлах или хотите настроить юнит-файл согласно вашим нуждам, ознакомьтесь с руководством Знакомство с юнитами systemd и юнит-файлами.

Закройте и сохраните файл files/etcd.service, нажав CTRL+X, а затем Y.

Далее нам нужно добавить в наш плейбук задачу, которая будет копировать локальный файл files/etcd.service в каталог /etc/systemd/system/etcd.service для каждого управляемого узла. Мы можем сделать это с помощью модуля copy.

Откройте ваш плейбук:

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

Добавьте следующую выделенную задачу после существующих задач:

~/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

После копирования юнит-файла в /etc/systemd/system/etcd.service служба будет определена.

Сохраните и закройте плейбук.

Запустите ту же команду ansible-playbook снова для применения изменений:

ansible-playbook -i hosts playbook.yaml

Чтобы убедиться, что изменения вступили в силу, выполните подключение через SSH к одному из управляемых узлов:

ssh root@etcd1_public_ip

Затем запустите systemctl status etcd для отправки systemd запроса о состоянии службы etcd:

systemctl status etcd

Вы получите следующий вывод, который подтверждает, что служба загружена:

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

Примечание. Последняя строка (Active: inactive (dead)) вывода указывает на неактивный статус службы, что означает, что она не будет запускаться автоматически при запуске системы. Это ожидаемое поведение, которое не является ошибкой.

Нажмите q для возврата в оболочку, а затем запустите команду exit для выхода из управляемого узла и возврата в локальную оболочку:

exit

В этом шаге мы обновили наш плейбук для запуска бинарного файла etcd в качестве службы systemd. В следующем шаге мы продолжим настройку etcd, предоставив службе пространство для хранения данных.

Шаг 5 — Настройка каталога данных

etcd — это хранилище данных типа «ключ-значение», а это значит, что мы должны предоставить ему пространство для хранения данных. В этом шаге мы обновим наш плейбук для определения специального каталога хранения данных, который будет использовать etcd.

Откройте ваш плейбук:

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

Добавьте следующую задачу в конец списка задач:

~/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

Здесь мы используем /var/lib/etcd/hostname.etcd в качестве каталога данных, где hostname — это имя хоста текущего управляемого узла. inventory_hostname — это переменная, представляющая имя хоста текущего управляемого узла; Ansible подставляет ее значение автоматически. Конструкция с фигурными скобками (например, {{ inventory_hostname }}) применяется для подстановки переменной, поддерживаемой в механизме шаблонов Jinja2, используемом по умолчанию в Ansible.

Закройте текстовый редактор и сохраните файл.

Далее нам нужно будет указать etcd на необходимость использования этого каталога данных. Мы сделаем это, передав параметр data-dir в etcd. Чтобы задать параметры etcd, мы можем использовать сочетание переменных среды, флагов командной строки и файлов конфигурации. В этом руководстве мы будем использовать файл конфигурации, поскольку гораздо удобнее изолировать все конфигурации внутри файла вместо их размещения по всему плейбуку.

В каталоге вашего проекта создайте новый каталог с именем templates/:

mkdir templates

Затем с помощью вашего редактора создайте в этом каталоге новый файл с именем etcd.conf.yaml.j2:

nano templates/etcd.conf.yaml.j2

Затем скопируйте следующую строку и вставьте ее в файл:

~/playground/etcd-ansible/templates/etcd.conf.yaml.j2

data-dir: /var/lib/etcd/{{ inventory_hostname }}.etcd

Этот файл использует тот же синтаксис для подстановки переменной в Jinja2, что и наш плейбук. Чтобы подставить переменные и загрузить результат в каждый управляемый хост, мы можем использовать модуль template. Он работает схожим с модулем copy образом, но выполняет подстановку переменных перед загрузкой.

Выйдите из etcd.conf.yaml.j2, а затем откройте ваш плейбук:

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

Добавьте в список задач следующие задачи по созданию каталога и загрузке в него шаблонного файла конфигурации:

~/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

Сохраните и закройте файл.

Поскольку мы внесли это изменение, нам нужно обновить юнит-файл нашей службы, чтобы передать ему расположение нашего файла конфигурации (например, /etc/etcd/etcd.conf.yaml).

Откройте файл службы etcd на локальном компьютере:

nano files/etcd.service

Обновите файл files/etcd.service, добавив флаг --config-file, как показано в следующем выделенном фрагменте:

~/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

Сохраните и закройте файл.

В этом шаге мы использовали наш плейбук для предоставления etcd каталога для хранения данных. В следующем шаге мы добавим еще несколько задач для перезапуска службы etcd и ее автоматической загрузки при запуске.

Шаг 6 — Включение и запуск службы etcd

При внесении изменений в юнит-файл службы мы должны перезапустить службу для вступления этих изменений в силу. Мы можем сделать это, запустив команду systemctl restart etcd. Кроме того, для автоматического запуска службы etcd при запуске системы нам нужно воспользоваться командой systemctl enable etcd. В этом шаге мы запустим эти две команды с помощью плейбука.

Чтобы запустить команды, воспользуемся модулем command:

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

Добавьте следующие задачи в конец списка задач:

~/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

Сохраните и закройте файл.

Запустите ansible-playbook -i hosts playbook.yaml еще раз:

ansible-playbook -i hosts playbook.yaml

Чтобы убедиться, что служба etcd теперь перезапущена и активирована, подключитесь через SSH к одному из управляемых узлов:

ssh root@etcd1_public_ip

Затем запустите systemctl status etcd для проверки состоянии службы etcd:

systemctl status etcd

Вы получите значения enabled и active (running) в выделенных ниже местах; это означает, что изменения, которые мы внесли в наш плейбук, вступили в силу:

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)

В этом шаге мы использовали модуль command для запуска команд systemctl, которые перезапускают и активируют службу etcd на наших управляемых узлах. Теперь, когда мы выполнили установку etcd, в следующем шаге мы протестируем ее функциональность, выполнив несколько базовых операций создания, чтения, обновления и удаления (CRUD).

Шаг 7 — Тестирование etcd

Хотя у нас есть работающая установка etcd, она небезопасна и еще не готова к производственному использованию. Но прежде чем мы сможем обеспечить безопасность нашей установки etcd в последующих шагах, давайте сначала поймем, что может делать etcd с точки зрения функциональности. В этом шаге мы будем вручную отправлять запросы в etcd для добавления, получения, обновления и удаления хранимых данных.

По умолчанию etcd предоставляет API, который прослушивает порт 2379 для связи с клиентом. Это означает, что мы можем отправлять etcd запросы API в чистом виде с помощью клиента HTTP. Однако быстрее будет использовать официальный клиент etcd etcdctl, который позволяет вам создавать/обновлять, получать и удалять пары «ключ-значение» с помощью подкоманд put, get и del соответственно.

Убедитесь, что вы все еще находитесь внутри управляемого узла etcd1, и запустите следующие команды etcdctl, чтобы убедиться, что ваша установка etcd работает.

Во-первых, создайте новую запись с помощью подкоманды put.

Подкоманда put имеет следующий синтаксис:

etcdctl put key value

В etcd1 запустите следующую команду:

etcdctl put foo "bar"

Команда, которую мы только что запустили, указывает etcd записать значение "bar" для ключа foo в хранилище.

После этого вы получите сообщение OK в выводе, что сигнализирует о сохранении данных:

OutputOK

После этого вы можете получить эту запись с помощью подкоманды get, которая имеет синтаксис etcdctl get key:

etcdctl get foo

Вы получите данный вывод, который показывает ключ в первой строке и значение, которое вы вставили ранее, во второй строке:

Outputfoo
bar

Мы можем удалить запись с помощью подкоманды del, которая имеет синтаксис etcdctl del key:

etcdctl del foo

Вы получите следующий вывод, который указывает количество удаленных записей:

Output1

Теперь давайте запустим подкоманду get еще раз, чтобы попытаться получить удаленную пару «ключ-значение».

etcdctl get foo

Вы не получите вывод, что означает, что etcdctl не может получить пару «ключ-значение». Это подтверждает, что запись удалена и не может быть найдена.

Теперь, когда вы протестировали основные операции etcd и etcdctl, давайте выйдем из нашего управляемого узла и вернемся в локальную среду:

exit

В этом шаге мы использовали клиент etcdctl для отправки запросов в etcd. На этом этапе мы используем три отдельных экземпляра etcd, каждый из которых действует независимо друг от друга. Однако etcd — это распределенное хранилище данных типа «ключ-значение», что означает, что несколько экземпляров etcd можно сгруппировать для формирования одного кластера. Каждый экземпляр в этом случае становится членом кластера. После формирования кластера вы сможете получить пару «ключ-значение», которая была вставлена из другого члена кластера. В следующем шаге мы используем наш плейбук для преобразования трех кластеров с одним узлом в один кластер с 3 узлами.

Шаг 8 — Создание кластера с помощью статического обнаружения

Чтобы создать один кластер из 3 узлов вместо трех кластеров с 1 узлом, нам нужно сконфигурировать эти установки etcd, чтобы они могли общаться друг с другом. Это означает, что каждая из них должна знать IP-адреса остальных. Этот процесс называется обнаружением. Обнаружение можно реализовать в форме статической конфигурации или динамического обнаружения службы. В этом шаге мы будем обсуждать разницу между этими двумя подходами, а также обновим наш плейбук для настройки кластера etcd с помощью статического обнаружения.

Обнаружение с помощью статической конфигурации — это метод, который требует наименьшей настройки. Именно в этом случае конечные точки каждого члена передаются в команду etcd перед ее выполнением. Для использования статической конфигурации необходимо соблюсти следующие условия перед инициализацией кластера:

  • известное количество членов
  • известные конечные точки каждого члена
  • статические IP-адреса для всех конечных точек

Если соблюсти эти условия невозможно, вы можете использовать службу динамического обнаружения. При динамическом обнаружении службы все экземпляры будут регистрироваться в службе обнаружения, что позволяет каждому члену получать информацию о расположении других членов.

Поскольку мы знаем, что нам нужен кластер etcd с 3 узлами, а все наши серверы имеют статические IP-адреса, мы будем использовать статическое обнаружение. Чтобы инициировать создание кластера с помощью статического обнаружения, нам нужно добавить несколько параметров в наш файл конфигурации. Воспользуйтесь редактором, чтобы открыть файл шаблона templates/etcd.conf.yaml.j2:

nano templates/etcd.conf.yaml.j2

Затем добавьте следующие выделенные строки:

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

Закройте и сохраните файл templates/etcd.conf.yaml.j2, нажав CTRL+X, а затем Y.

Ниже представлено краткое разъяснение каждого параметра:

  • name — это человекочитаемое имя члена. По умолчанию etcd использует уникальный, генерируемый случайным образом идентификатор для каждого члена, а человекочитаемое имя позволяет легче ссылаться на них внутри файлов конфигурации и в командной строке. Здесь мы будем использовать имена хостов в качестве имен членов (т. е. etcd1, etcd2 и etcd3).
  • initial-advertise-peer-urls — это список комбинаций IP-адреса/порта, которые могут использовать другие члены для связи с этим членом. Помимо порта API (2379) etcd также предоставляет порт 2380 для коммуникации между членами etcd, что позволяет им отправлять сообщения друг другу и обмениваться данными. Обратите внимание, что эти URL-адреса должны быть доступны для других членов (и не быть локальными IP-адресами).
  • listen-peer-urls — это список комбинаций IP-адреса/порта, с помощью которых текущий член будет прослушивать данные, поступающие от других членов. Он должен включать все URL-адреса, переданные с флагом --initial-advertise-peer-urls, а также локальные URL-адреса, такие как 127.0.0.1:2380. Комбинации IP-адреса/порта назначения входящих сообщений других членов должны соответствовать одному из перечисленных здесь URL-адресов.
  • advertise-client-urls — это список комбинаций IP-адреса/порта, которые клиенты должны использовать для коммуникации с этим членом. Эти URL-адреса должны быть доступны клиенту (и не быть локальными адресами). Если клиент получает доступ к кластеру через общедоступную часть Интернета, это должен быть публичный IP-адрес.
  • listen-client-urls — это список комбинаций IP-адреса/порта, с помощью которых текущий член будет прослушивать данные, поступающие от клиентов. Он должен включать все URL-адреса, переданные с флагом --advertise-client-urls, а также локальные URL-адреса, такие как 127.0.0.1:2380. Комбинации IP-адреса/порта назначения входящих сообщений других клиентов должны соответствовать одному из перечисленных здесь URL-адресов.
  • initial-cluster — это список конечных точек для каждого члена кластера. Каждая конечная точка должна соответствовать одному из URL-адресов списка initial-advertise-peer-urls соответствующего члена.
  • initial-cluster-state — либо значение new, либо existing.

Чтобы гарантировать последовательность, etcd может принимать решения только в том случае, когда большинство узлов являются рабочими. Подобная практика известна как достижение кворума. Другими словами, в кластере из трех членов кворум достигается, если два или более членов являются рабочими.

Если для параметра initial-cluster-state установлено значение new, etcd будет знать, что это новый кластер, который будет запущен, и позволит членам начинать работу параллельно, не ожидая достижения кворума. Если говорить конкретнее, после запуска первого члена у него не будет кворума, поскольку одна треть (33,33%) меньше или равна 50%. Обычно etcd будет приостанавливать работу и отказывать в совершении любых действий, а кластер не будет сформирован. Однако, если для initial-cluster-state установлено значение new, отсутствие кворума будет игнорироваться.

Если установлено значение existing, член будет пытаться присоединиться к существующему кластеру и ожидать, что кворум будет достигнут.

Примечание. Дополнительную информацию обо всех поддерживаемых флагах конфигурации вы можете найти в разделе конфигурации документации etcd.

В обновленном файле шаблонов templates/etcd.conf.yaml.j2 существует несколько экземпляров hostvars. Во время работы Ansible собирает переменные из разных источников. Мы уже использовали переменную inventory_hostname ранее, но существует большое количество других переменных. Эти переменные доступны в виде hostvars[inventory_hostname]['ansible_facts']. Здесь мы извлекаем частные IP-адреса каждого узла и используем их для построения значения нашего параметра.

Примечание. Поскольку мы включили опцию Private Networking (Частная сеть) при создании наших серверов, каждый сервер будет иметь три связанных с ним IP-адреса. Кольцевой IP-адрес — адрес, который действителен внутри одного компьютера. Он используется для ссылки компьютера на себя самого, например, 127.0.0.1. Публичный IP-адрес — адрес, который размещается в общедоступной части Интернета, например, 178.128.169.51. Частный IP-адрес — адрес, который маршрутизируется только в частной сети; в случае с дроплетами DigitalOcean внутри каждого набора данных существует частная сеть, например, 10.131.82.225. Каждый из этих IP-адресов ассоциируется с другим сетевым интерфейсом — кольцевой адрес ассоциируется с интерфейсом lo, публичный — с интерфейсом eth0, а частный — с интерфейсом eth1. Мы используем интерфейс eth1, чтобы весь трафик оставался внутри частной сети, не попадая в Интернет. Понимание сетевых интерфейсов в рамках этой статьи не обязательно, но если вы хотите узнать больше по этой теме, рекомендуем вам начать со статьи Знакомство с сетевой терминологией, интерфейсами и протоколами.

Синтаксис {% %} Jinja2 определяет структуру цикла for для итерации по каждому узлу в группе etcd и получения строки initial-cluster в требуемом etcd формате.

Чтобы сформировать новый кластер из трех членов, необходимо сначала остановить работу службы etcd и очистить каталог данных перед запуском кластера. Для этого откройте в редакторе файл playbook.yaml на локальном компьютере:

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

Затем перед задачей "Create a data directory" (Создать каталог данных) добавьте задачу для остановки службы 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:
    ...

Затем обновите задачу "Create a data directory" (Создать каталог данных) таким образом, чтобы каталог данных сначала удалялся, а потом создавался снова:

~/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:
    ...

Свойство with_items определяет список строк, по которым эта задача будет итерироваться. Это эквивалентно повторению одной и той же задачи дважды, но с разными значениями свойства state. Здесь мы итерируемся по списку с элементами absent и directory, что гарантирует, что каталог данных сначала удаляется, а потом создается повторно.

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y. Затем запустите ansible-playbook повторно. Ansible теперь создаст отдельный кластер etcd с 3 членами:

ansible-playbook -i hosts playbook.yaml

Вы можете проверить это, выполнив подключение через SSH к любому узлу etcd:

ssh root@etcd1_public_ip

После установления подключения запустите команду etcdctl endpoint health --cluster:

etcdctl endpoint health --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

Мы успешно создали кластер etcd из 3 узлов. Мы можем подтвердить это, добавив запись в etcd на одном узле и получив ее на другом узле. На одном из узлов запустите etcdctl put:

etcdctl put foo "bar"

Затем используйте новый терминал для подключения через SSH к другому узлу:

ssh root@etcd2_public_ip

Далее попытайтесь получить ту же запись с помощью ключа:

etcdctl get foo

Вы сможете получить запись, что доказывает, что кластер работает:

Outputfoo
bar

В заключение выйдите из каждого управляемого узла и вернитесь на локальный компьютер:

exit

В этом шаге мы предоставили новый кластер с 3 узлами. На данный момент связь между членами etcd и другими узлами и клиентами осуществляется через HTTP. Это означает, что коммуникации не зашифрованы, и любая сторона, которая может перехватить трафик, сможет прочитать сообщения. Это не является большой проблемой, если кластер etcd и клиенты размещены внутри частной сети или виртуальной частной сети (VPN), которую вы полностью контролируете. Однако, если какой-либо трафик должен проходить через общую сеть (частную или публичную), вам нужно гарантировать, что этот трафик будет зашифрован. Кроме того, необходимо создать механизм, который позволяет клиенту или другому узлу проверить аутентичность сервера.

В следующем шаге мы рассмотрим то, как обеспечить безопасность коммуникации между клиентом и серверами, а также другими узлами, используя TLS.

Шаг 9 — Получение частных IP-адресов управляемых узлов

Для шифрования сообщений, пересылаемых между узлами, etcd использует протокол защищенного переноса гипертекста, или HTTPS, который представляет собой слой поверх протокола безопасности транспортного уровня, или TLS. TLS использует систему приватных ключей, сертификатов и доверенных объектов, называемых центрами сертификации (ЦС), для аутентификации и отправки зашифрованных сообщений друг другу.

В этом руководстве каждый узел должен генерировать сертификат для собственной идентификации и получать подпись ЦС для этого сертификата. Мы настроим все узлы членов так, чтобы они доверяли этому ЦС, а значит доверяли любым сертификатам, которые он подписал. Это позволяет узлам взаимно аутентифицировать друг друга.

Сертификат, который генерирует узел, должен позволить другим узлам идентифицировать себя. Все сертификаты включают стандартное имя (CN) объекта, с которым они ассоциируются. Часто оно используется как идентификатор объекта. Однако при проверке сертификата клиентские реализации могут сравнить собранную информацию об объекте с информацией, представленной в сертификате. Например, когда клиент загрузил сертификат TLS с субъектом CN=foo.bar.com, но клиент фактически подключился к серверу с помощью IP-адреса (например, 167.71.129.110), возникает противоречие, и клиент может не доверять сертификату. Указанное в сертификате дополнительное имя субъекта (SAN) во время верификации сообщает, что оба имени принадлежат одному и тому же объекту.

Поскольку наши члены etcd обмениваются данными, используя свои частные IP-адреса, когда мы определяем наши сертификаты, нам нужно будет предоставить эти частные IP-адреса в качестве дополнительных имен субъекта.

Чтобы узнать частный IP-адрес управляемого узла, выполните подключение через SSH к этому узлу:

ssh root@etcd1_public_ip

Затем запустите следующую команду:

ip -f inet addr show eth1

Вы увидите вывод примерно следующего содержания:

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

В нашем примере вывод 10.131.255.176 — это частный IP-адрес управляемого узла, который является единственной интересующей нас информацией. Чтобы отфильтровать все остальное содержание помимо частного IP-адреса, мы можем передать вывод команды ip в утилиту sed, которая используется для фильтрации и преобразования текста.

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

Теперь единственной информацией в выводе является частный IP-адрес:

Output10.131.255.176

Когда вы убедитесь, что предыдущая команда работает, выйдите из управляемого узла:

exit

Чтобы включить предшествующие команды в наш плейбук, откройте файл playbook.yaml:

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

Затем добавьте новую инструкцию с одной задачей перед существующей инструкцией:

~/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:
...

Задача использует модуль shell для запуска команд ip и sed, которые получают частный IP-адрес управляемого узла. Затем он регистрирует возвращаемое значение команды shell внутри переменной с именем privateIP, которую мы будем использовать позже.

В этом шаге мы добавили в плейбук задачу получения частного IP-адреса управляемых узлов. В следующем шаге мы будем использовать эту информацию для генерирования сертификатов для каждого узла и получим подпись для этих сертификатов в центре сертификации (ЦС).

Шаг 10 — Генерация приватных ключей и запросов на подпись сертификата для членов etcd

Чтобы узел мог принимать шифрованный трафик, отправитель должен использовать публичный ключ узла для шифрования данных, а узел должен использовать свой приватный ключ для расшифровки зашифрованного сообщения и получения оригинальных данных. Публичный ключ упаковывается в сертификат и подписывается ЦС для гарантии его подлинности.

Следовательно, нам нужно будет создать приватный ключ и запрос на подпись сертификата (CSR) для каждого узла etcd. Чтобы облегчить эту задачу, мы сгенерируем все пары ключей и подпишем все сертификаты локально, на узле управления, а затем скопируем соответствующие файлы на управляемые хосты.

Сначала создайте каталог с именем artifacts/, куда мы поместим файлы (ключи и сертификаты), сгенерированные в ходе этого процесса. Откройте файл playbook.yaml в редакторе:

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

Используйте модуль file для создания каталога 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:
...

Затем добавьте еще одну задачу в конец инструкции для генерирования приватного ключа:

~/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:
...

Создание приватных ключей и запросов на подпись сертификата выполняется с помощью модулей openssl_privatekey и openssl_csr соответственно.

Атрибут force: True гарантирует, что приватный ключ генерируется заново, даже если он уже существует.

Аналогичным образом добавьте следующую новую задачу в ту же инструкцию для генерирования запроса на подпись сертификата для каждого члена с помощью модуля 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'] }}"

Мы указываем, что данный сертификат может быть использован в механизме цифровой подписи для аутентификации сервера. Этот сертификат ассоциируется с именем хоста (например, etcd1), но при проверке необходимо также рассматривать приватные и локальные кольцевые IP-адреса каждого узла в качестве альтернативных имен. Обратите внимание на использование переменной privateIP, которую мы зарегистрировали в предыдущей инструкции.

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y. Затем запустите наш плейбук повторно:

ansible-playbook -i hosts playbook.yaml

Теперь мы найдем новый каталог с именем artifacts внутри каталога проекта; используйте ls для вывода его содержимого:

ls artifacts

Вы получите приватные ключи и запросы на подпись сертификата для каждого члена etcd:

Outputetcd1.csr  etcd1.key  etcd2.csr  etcd2.key  etcd3.csr  etcd3.key

В этом шаге мы использовали несколько модулей Ansible для генерирования приватных ключей и сертификатов публичного ключа для каждого узла. В следующем шаге мы узнаем, как подписать запрос на подпись сертификата (CSR).

Шаг 11 — Генерирование сертификатов ЦС

В кластере etcd узлы шифруют сообщения с помощью публичного ключа получателя. Чтобы убедиться в подлинности публичного ключа, получатель упаковывает публичный ключ в запрос на подпись сертификата (CSR) и просит доверенный объект (например, ЦС) подписать CSR. Поскольку мы контролируем все узлы и ЦС, которым они доверяют, нам не нужно использовать внешний ЦС, и мы можем выступать в качестве собственного ЦС. В этом шаге мы будем действовать в качестве собственного ЦС, что означает, что нам нужно будет генерировать приватный ключ и самоподписанный сертификат, который будет функционировать в качестве ЦС.

Во-первых, откройте файл playbook.yaml в редакторе:

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

Затем, как и в предыдущем шаге, добавьте задачу в инструкцию localhost для генерирования приватного ключа для ЦС:

~/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"
...

Затем воспользуйтесь модулем openssl_csr для генерирования нового запроса на подпись сертификата. Это похоже на предыдущий шаг, но в этом запросе на подпись сертификата мы добавляем базовое ограничение и расширение использования ключа, чтобы показать, что данный сертификат можно использовать в качестве сертификата ЦС:

~/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"
...

Наконец, воспользуйтесь модулем openssl_certificate для самостоятельной подписи 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"
...

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y. Затем запустите наш плейбук повторно для применения изменений:

ansible-playbook -i hosts playbook.yaml

Также вы можете запустить команду ls для проверки содержимого каталога artifacts/:

ls artifacts/

Теперь вы получите заново созданный сертификат ЦС (ca.crt):

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

В этом шаге мы сгенерировали приватный ключ и самоподписанный сертификат для ЦС. В следующем шаге мы будем использовать сертификат ЦС для подписи CSR каждого члена.

Шаг 12 — Подписание запросов на подпись сертификата для членов etcd

В этом шаге мы будем подписывать CSR каждого узла. Это процесс аналогичен тому, как мы использовали модуль openssl_certificate для самостоятельной подписи сертификата ЦС, но вместо использования поставщика selfsigned мы будем использовать поставщика ownca, который позволяет добавлять подпись с помощью нашего собственного сертификата ЦС.

Откройте ваш плейбук:

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

Добавьте следующую выделенную задачу в задачу "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"
...

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y. Затем запустите плейбук повторно для применения изменений:

ansible-playbook -i hosts playbook.yaml

Теперь выведите содержимое каталога artifacts/:

ls artifacts/

Вы получите приватный ключ, CSR и сертификат для каждого члена etcd и ЦС:

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

В этом шаге мы подписали CSR каждого узла с помощью ключа ЦС. В следующем шаге мы скопируем соответствующие файлы на каждый управляемый узел, чтобы etcd смог получить доступ к соответствующим ключам и сертификатам для настройки подключений TLS.

Шаг 13 — Копирование приватных ключей и сертификатов

Каждый узел должен иметь копию самоподписанного сертификата ЦС (ca.crt). Каждый узел etcd также должен иметь свой собственный приватный ключ и сертификат. В этом шаге мы загрузим эти файлы и поместим их в новый каталог /etc/etcd/ssl/.

Для начала откройте файл playbook.yaml в редакторе:

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

Чтобы внести эти изменения в наш плейбук Ansible, сначала обновите свойство path задачи Create directory for etcd configuration (Создать каталог для конфигурации ectd), чтобы создать каталог /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:
...

Затем сразу после измененной задачи добавьте еще три задачи для копирования файлов:

~/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:
...

Закройте и сохраните файл playbook.yaml, нажав CTRL+X, а затем Y.

Запустите ansible-playbook снова для внесения этих изменений:

ansible-playbook -i hosts playbook.yaml

В этом шаге мы успешно загрузили приватные ключи и сертификаты на управляемые узлы. После копирования файлов нам нужно обновить наш файл конфигурации etcd, чтобы мы могли использовать эти файлы.

Шаг 14 — Активация TLS на etcd

В последнем шаге данного руководства мы обновим ряд конфигураций Ansible для активации TLS в кластере etcd.

Сначала откройте файл templates/etcd.conf.yaml.j2 с помощью редактора:

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

Внутри файла измените все URL-адреса для использования https в качестве протокола вместо http. Кроме того, добавьте раздел в конце шаблона для указания расположения сертификата ЦС, сертификата сервера и ключа сервера:

~/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

Закройте и сохраните файл templates/etcd.conf.yaml.j2.

Затем запустите ваш плейбук Ansible:

ansible-playbook -i hosts playbook.yaml

Подключитесь по SSH к одному из управляемых узлов:

ssh root@etcd1_public_ip

Выполнив подключение, запустите команду etcdctl endpoint health для проверки использования HTTPS конечными точками и состояния всех членов:

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

Поскольку наш сертификат ЦС по умолчанию не является доверенным корневым сертификатом ЦС, установленным в каталоге /etc/ssl/certs/, нам нужно передать его etcdctl с помощью флага --cacert.

Результат будет выглядеть следующим образом:

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

Чтобы убедиться, что кластер etcd действительно работает, мы можем снова создать запись на узле и получить ее из другого узла:

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

Затем используйте новый терминал для подключения через SSH к другому узлу:

ssh root@etcd2_public_ip

Теперь попробуйте получить ту же запись с помощью ключа foo:

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

Это позволит получить запись, показанную в выводе ниже:

Outputfoo
bar

Вы можете сделать то же самое на третьем узле, чтобы убедиться, что все три члена работоспособны.

Заключение

Вы успешно создали кластер etcd, состоящий из 3 узлов, обеспечили его безопасность с помощью TLS и подтвердили его работоспособность.

etcd — это инструмент, первоначально созданный для CoreOS. Чтобы понять, как etcd используется вместе с CoreOS, прочитайте статью Использование Etcdctl и Etcd, распределенного хранилища типа «ключ-значение» для CoreOS. В этой статье вы также можете ознакомиться с настройкой модели динамического обнаружения, которая была описана, но не продемонстрирована в данном руководстве.

Как отмечалось в начале данного руководства, etcd является важной частью экосистемы Kubernetes. Дополнительную информацию о Kubernetes и роли etcd в рамках этой системы вы можете найти в статье Знакомство с Kubernetes. Если вы развертываете etcd в рамках кластера Kubernetes, вам могут пригодиться другие доступные инструменты, такие как kubespray и kubeadm. Дополнительную информацию о последних можно найти в статье Создание кластера Kubernetes с помощью kubeadm в Ubuntu 18.04.

Наконец, в этом руководстве мы использовали множество инструментов, но не могли достаточно подробно описать каждый из них. Ниже вы найдете ссылки, которые позволят вам более детально познакомиться с каждым из этих инструментов:

Источник