Создание кластера etcd с TLS с помощью Ansible в Ubuntu 22
Развернем кластер etcd из трех нод.
Общение между нодами будет по TLS протоколу.
На управляющем сервере (локальном компьютере) имеем следующую структуру файлов-директорий:
1 2 3 4 5 6 7 8 9 |
etcd-ansible/ ├── artifacts/ ├── files/ ├── inventory/ │ └── hosts └── templates/ └── etcd.conf.yaml.j2 ansible.cfg playbook.yaml |
ansible.cfg:
Указываем путь к файлу инвентаря, повышаем привилегии.
1 2 3 4 5 6 7 |
[defaults] inventory = inventory/hosts host_key_checking = false [privilege_escalation] become = true |
inventory/hosts:
1 2 3 4 5 |
[etcd] etcd1 ansible_host=89.169.174.1 ansible_user=root ansible_ssh_private_key_file="/home/dmitry/.ssh/id_ed25519" etcd2 ansible_host=89.169.174.2 ansible_user=root ansible_ssh_private_key_file="/home/dmitry/.ssh/id_ed25519" etcd3 ansible_host=89.169.174.3 ansible_user=root ansible_ssh_private_key_file="/home/dmitry/.ssh/id_ed25519" |
Список нод, на которых будут развернуты экземпляры etcd.
Предварительно должен быть настроен доступ по ssh с помощью ключей.
playbook.yaml — тут будут все задачи по развертыванию кластера.
Ниже представлен полный playbook.yaml. Дальше разберем его содержимое посекционно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
- hosts: localhost gather_facts: False become: False tasks: - name: "Create ./artifacts directory to house keys and certificates" file: path: ./artifacts state: directory - 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'] }}" - 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 - clientAuth subject_alt_name: - IP:{{ hostvars[item].ansible_host}} - IP:127.0.0.1 force: True with_items: "{{ groups['etcd'] }}" - 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 - 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" 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://github.com/etcd-io/etcd/releases/download/v3.3.25/etcd-v3.3.25-linux-amd64.tar.gz # url: https://storage.googleapis.com/etcd/v3.3.13/etcd-v3.3.13-linux-amd64.tar.gz url: https://github.com/etcd-io/etcd/releases/download/v3.5.16/etcd-v3.5.16-linux-amd64.tar.gz dest: /tmp/etcd.tar.gz owner: root group: root mode: 0600 force: True validate_certs: no - 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 - 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: "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: path: "{{ item }}" state: directory owner: root group: root mode: 0755 with_items: - /etc/etcd - /etc/etcd/ssl - 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: "Copy .pem certs" copy: src: ./artifacts/{{inventory_hostname}}_crt_key.pem remote_src: False dest: /etc/etcd/ssl/cert_crt_key.pem owner: root group: root mode: 0600 - 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 - name: "Enable the etcd service" command: systemctl enable etcd - name: "Start the etcd service" command: systemctl restart etcd |
Сначала на текущем сервере создаем сертификаты для TLS обмена данными между нодами кластера.
Создаем директорию для сертификатов:
1 2 3 4 5 |
- name: "Create ./artifacts directory to house keys and certificates" file: path: ./artifacts state: directory |
Генерируем приватные ключи для каждой ноды:
1 2 3 4 5 6 7 8 9 |
- 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'] }}" |
Здесь через переменную {{ groups[‘etcd’] }} — получаем все элементы группы ‘etcd’ из инвентарного файла.
Создание приватных ключей и запросов на подпись сертификата выполняется с помощью модулей openssl_privatekey
и openssl_csr
соответственно.
Атрибут force: True
гарантирует, что приватный ключ генерируется заново, даже если он уже существует.
Генерация запроса на подпись сертификата:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- 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 - clientAuth subject_alt_name: - IP:{{ hostvars[item].ansible_host}} - IP:127.0.0.1 force: True with_items: "{{ groups['etcd'] }}" |
Мы указываем, что данный сертификат может быть использован в механизме цифровой подписи для аутентификации сервера. Этот сертификат ассоциируется с именем хоста (берем IP хоста из инвентаря через переменную hostvars[item].ansible_host), но при проверке необходимо также рассматривать приватные и локальные кольцевые IP-адреса каждого узла в качестве альтернативных имен.
В кластере etcd узлы шифруют сообщения с помощью публичного ключа получателя. Чтобы убедиться в подлинности публичного ключа, получатель упаковывает публичный ключ в запрос на подпись сертификата (CSR) и просит доверенный объект (например, ЦС) подписать CSR. Поскольку мы контролируем все узлы и ЦС, которым они доверяют, нам не нужно использовать внешний ЦС, и мы можем выступать в качестве собственного ЦС. В этом шаге мы будем действовать в качестве собственного ЦС, что означает, что нам нужно будет генерировать приватный ключ и самоподписанный сертификат, который будет функционировать в качестве ЦС.
1 2 3 4 5 6 7 8 |
- name: "Generate private key for CA" openssl_privatekey: path: ./artifacts/ca.key type: RSA size: 4096 state: present force: True |
Затем воспользуйтесь модулем openssl_csr
для генерирования нового запроса на подпись сертификата. Это похоже на предыдущий шаг, но в этом запросе на подпись сертификата мы добавляем базовое ограничение и расширение использования ключа, чтобы показать, что данный сертификат можно использовать в качестве сертификата ЦС:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- 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 |
Воспользуемся модулем openssl_certificate
для самостоятельной подписи CSR:
1 2 3 4 5 6 7 8 9 |
- 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 |
Далее мы будем подписывать CSR каждого узла. Это процесс аналогичен тому, как мы использовали модуль openssl_certificate
для самостоятельной подписи сертификата ЦС, но вместо использования поставщика selfsigned
мы будем использовать поставщика ownca
, который позволяет добавлять подпись с помощью нашего собственного сертификата ЦС.
1 2 3 4 5 6 7 8 9 10 |
- 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'] }}" |
В этом шаге мы подписали CSR каждого узла с помощью ключа ЦС. В следующих шагах мы настроим собственно etcd кластер и скопируем соответствующие файлы сертификатов на каждый управляемый узел, чтобы etcd смог получить доступ к соответствующим ключам и сертификатам для настройки подключений TLS.
Далее сгенерируем .pem сертификаты (понадобятся в дальнейшем для HAProxy) — это всего навсего объединение двух файлов сертификата и приватного ключа:
1 2 3 4 |
- name: "Make .pem file (certicate and key files concatenate)" shell: cat ./artifacts/{{item}}.crt ./artifacts/{{item}}.key > ./artifacts/{{item}}_crt_key.pem with_items: "{{ groups['etcd'] }}" |
А пока создадим на узлах директории для бинарников etcd, скачаем нужную версию, настроим права доступа к директириям:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
- 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://github.com/etcd-io/etcd/releases/download/v3.5.16/etcd-v3.5.16-linux-amd64.tar.gz dest: /tmp/etcd.tar.gz owner: root group: root mode: 0600 force: True validate_certs: no - 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
.
Поскольку мы запускаем etcd версии 3.x, последняя команда lineinfile устанавливает для переменной среды ETCDCTL_API
значение 3
.
Может показаться, что самым быстрым способом запуска etcd с помощью Ansible может быть использование модуля command
для запуска /opt/etcd/bin/etcd
. Однако этот способ не сработает, поскольку он будет запускать etcd
в качестве активного процесса. Использование модуля command
будет приводить к зависанию Ansible в ожидании результата, возвращаемого командой etcd
, чего никогда не произойдет. Поэтому в этом шаге мы обновим наш плейбук для запуска нашего бинарного файла etcd
в качестве фоновой службы.
Ubuntu 22 использует systemd в качестве инит-системы, что означает, что мы можем создавать новые службы, записывая юнит-файлы и размещая их внутри каталога /etc/systemd/system/
.
Создадим в каталоге files файл службы etcd.service
со следующим содержимым:
1 2 3 4 5 6 7 |
[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 |
Этот юнит-файл определяет службу, которая запускает исполняемый файл в /opt/etcd/bin/etcd
, уведомляет systemd о завершении инициализации и перезапускается при каждом случае сбоя.
Далее нам нужно добавить в наш плейбук задачу, которая будет копировать локальный файл files/etcd.service
в каталог /etc/systemd/system/etcd.service
для каждого управляемого узла. Мы можем сделать это с помощью модуля copy
.
1 2 3 4 5 6 7 8 9 |
- 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 |
Далее останавливаем службу перед внесением изменений.
1 2 3 |
- name: "Stop the etcd service" command: systemctl stop etcd |
Создаем директорию для хранения данных:
1 2 3 4 5 6 7 8 9 10 11 |
- 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 |
Создаем директорию для хранения конфигурации etcd:
1 2 3 4 5 6 7 8 9 10 11 |
- name: "Create directory for etcd configuration" file: path: "{{ item }}" state: directory owner: root group: root mode: 0755 with_items: - /etc/etcd - /etc/etcd/ssl |
Копирование сертификатов с локальной машины на ноды etcd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- 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 |
Копируем .pem сертификаты
1 2 3 4 5 6 7 8 9 |
- name: "Copy .pem certs" copy: src: ./artifacts/{{inventory_hostname}}_crt_key.pem remote_src: False dest: /etc/etcd/ssl/cert_crt_key.pem owner: root group: root mode: 0600 |
Генерируем из jinja2 шаблона файл конфига для каждой ноды:
1 2 3 4 5 6 7 8 |
- 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 |
Файл конфига содержит настройки кластера и параметры активации TLS.
Шаблон конфига etcd.conf.yaml.j2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
data-dir: /var/lib/etcd/{{ inventory_hostname }}.etcd name: {{ inventory_hostname }} initial-advertise-peer-urls: https://{{ ansible_host }}:2380 listen-peer-urls: https://0.0.0.0:2380 advertise-client-urls: https://{{ ansible_host }}:2379 listen-client-urls: https://0.0.0.0:2379 initial-cluster-state: new initial-cluster: etcd1=https://{{ hostvars['etcd1'].ansible_host }}:2380,etcd2=https://{{ hostvars['etcd2'].ansible_host }}:2380,etcd3=https://{{ hostvars['etcd3'].ansible_host }}:2380 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 |
Активируем автозапуск службы после рестарта ноды:
1 2 3 |
- name: "Enable the etcd service" command: systemctl enable etcd |
И наконец запускаем службу etcd:
1 2 3 |
- name: "Start the etcd service" command: systemctl restart etcd |
Проверим работу кластера. Для этого на любой ноде выполним команду в консоли:
1 |
etcdctl endpoint health --cluster --cacert=/etc/etcd/ssl/ca.crt --cert=/etc/etcd/ssl/server.crt --key=/etc/etcd/ssl/server.key |
должно вывести:
https://89.169.167.221:2379 is healthy: successfully committed proposal: took = 26.505599ms
https://130.193.52.254:2379 is healthy: successfully committed proposal: took = 27.199004ms
https://89.169.174.35:2379 is healthy: successfully committed proposal: took = 39.359524ms
кол-во строк — равно количеству нод в кластере.
Чтобы убедиться, что кластер etcd
действительно работает, мы можем снова создать запись на узле и получить ее из другого узла:
1 |
etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/etcd/ssl/ca.crt --cert=/etc/etcd/ssl/server.crt --key=/etc/etcd/ssl/server.key put foo "bar" |
Затем откроем терминал любого другого узла и выполним:
1 |
etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/etcd/ssl/ca.crt --cert=/etc/etcd/ssl/server.crt --key=/etc/etcd/ssl/server.key get foo |
Должно вывести:
1 2 3 |
Output foo bar |
А следовательно — кластер успешно работает!
Recommended Posts
Отслеживание изменений в etcd
14.01.2024