One place for hosting & domains

      обеспечение

      Обеспечение безопасности MongoDB в Ubuntu 20.04


      Предыдущая версия данного руководства была написана Мелиссой Андерсон.

      Введение

      MongoDB, или просто Mongo, — это документоориентированная база данных с открытым исходным кодом, используемая во многих современных веб-приложениях. Она относится к базам данных NoSQL, поскольку не опирается на традиционную табличную структуру реляционных баз данных. Вместо этого она хранит документы типа JSON с динамическими схемами.

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

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

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

      • Сервер на базе Ubuntu 20.04. На сервере должен быть пользователь без привилегий root с правами администратора и брандмауэр, настроенный с помощью UFW. Вы можете выполнить настройку, следуя указаниям руководства по начальной настройке сервера для Ubuntu 20.04.
      • MongoDB, установленная на сервере. Данное руководство было протестировано с помощью MongoDB версии 4.4, но оно также должно быть актуально для более старых версий MongoDB. Чтобы установить Mongo на ваш сервер, воспользуйтесь руководством по установке MongoDB на Ubuntu 20.04.

      Шаг 1 — Добавление пользователя с правами администратора

      С момента выхода версии 3.0 демон MongoDB настроен только на прием соединений с локального сокета Unix и автоматически не открывается для широкого Интернета. Однако аутентификация по умолчанию все еще отключена. Это означает, что любой пользователь, который имеет доступ к серверу, на котором установлена MongoDB, также имеет полный доступ к базам данных.

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

      Чтобы добавить пользователя с правами администратора, вам нужно сначала подключиться к оболочке Mongo. Поскольку аутентификация отключена, вы можете сделать это с помощью команды mongo без каких-либо опций:

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

      Output

      MongoDB shell version v4.4.0 . . . 2020-06-09T13:26:51.391+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database. 2020-06-09T13:26:51.391+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted. . . . >

      Эти предупреждения исчезнут после активации аутентификации, но сейчас они означают, что любой человек, который может получить доступ к вашему серверу Ubuntu, также может получить контроль над вашей базой данных.

      Чтобы продемонстрировать это, запустите команду Mongo show dbs:

      Эта команда возвращает список всех баз данных на сервере. Однако после активации аутентификации список отображается на основе роли пользователя Mongo или уровня доступа к конкретной базе данных, который имеет пользователь. Поскольку аутентификация отключена, команда будет возвращать каждую базу данных, присутствующую в системе, без ограничений:

      Output

      admin 0.000GB config 0.000GB local 0.000GB

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

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

      Output

      switched to db admin

      MongoDB устанавливается с рядом методов оболочки на базе JavaScript, которые вы можете использовать для управления вашей базой данных. Например, метод db.createUser используется для создания новых пользователей в базе данных, для которой запускается метод.

      Воспользуйтесь методом db.createUser:

      Этот метод требует указать имя и пароль пользователя, а также любые роли, которые будет иметь пользователь. Напомним, что MongoDB хранит свои данные в виде документов JSON. Поэтому при создании нового пользователя все, что вы делаете, — это создаете документ для хранения соответствующих данных пользователя в форме отдельных полей.

      Как и в случае объектов в JSON, документы в MongoDB начинаются и заканчиваются фигурными скобками ({ и }). Чтобы начать добавление пользователя, введите открывающую фигурную скобку:

      Примечание. Mongo не зарегистрирует метод db.createUser как законченный, пока вы не добавите закрывающую скобку. Когда вы сделаете этого, указание поменяется со знака больше (>) на многоточие (...).

      Затем добавьте поле user: с желаемым именем пользователя в качестве значения в двойных кавычках и поставьте запятую. В следующем примере указано имя пользователя AdminSammy, но вы можете ввести любое имя пользователя, которое вам нравится:

      Далее введите поле pwd с методом passwordPrompt() в качестве значения. Когда вы выполните метод db.createUser, метод passwordPrompt() будет отображать запрос на ввод пароля. Это более безопасное решение по сравнению с вводом вашего пароля в виде открытого текста, как вы делали это при вводе имени пользователя.

      Примечание. Метод passwordPrompt() совместим только с MongoDB версии 4.2 и выше. Если вы используете более старую версию Mongo, вам нужно будет вводить пароль в виде открытого текста, так же как вы вводили ваше имя пользователя:

      Обязательно добавьте запятую после этого поля:

      Затем укажите роли, которые вы хотите добавить для вашего пользователя с правами администратора. Поскольку вы создаете пользователя с правами администратора, необходимо как минимум предоставить ему роль userAdminAnyDatabase для базы данных admin. Это позволит пользователю с правами администратора создавать и изменять новых пользователей и роли. Поскольку у пользователя с правами администратора есть эта роль в базе данных admin, это также предоставит ему доступ с правами суперпользователя ко всему кластеру.

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

      • roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]

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

      Поставьте закрывающую круглую скобку, чтобы завершить и выполнить метод db.createUser:

      В целом ваш метод db.createUser должен выглядеть следующим образом:

      > db.createUser(
      ... {
      ... user: "AdminSammy",
      ... pwd: passwordPrompt(),
      ... roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
      ... }
      ... )
      

      Если синтаксис каждой строки не имеет ошибок, метод будет выполняться корректно и вам будет предложено ввести пароль:

      Output

      Enter password:

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

      Output

      Successfully added user: { "user" : "AdminSammy", "roles" : [ { "role" : "userAdminAnyDatabase", "db" : "admin" }, "readWriteAnyDatabase" ] }

      После этого вы можете выйти из клиента MongoDB:

      На этом этапе вашему пользователю будет разрешено вводить учетные данные. Однако делать это будет необязательно, пока вы не активируете аутентификацию и не перезапустите демон MongoDB.

      Шаг 2 — Активация аутентификации

      Чтобы активировать аутентификацию, необходимо изменить файл конфигурации MongoDB mongod.conf. После активации и перезапуска службы Mongo пользователи все равно смогут подключаться к базе данных без аутентификации. Однако они не смогут читать или изменять какие-либо данные до тех пор, пока не представят корректное имя пользователя и пароль.

      Откройте файл конфигурации в предпочитаемом текстовом редакторе. Мы будем использовать nano:

      • sudo nano /etc/mongod.conf

      Прокрутите страницу вниз и найдите закомментированный раздел security:

      /etc/mongod.conf

      . . .
      #security:
      
      #operationProfiling:
      
      . . .
      

      Раскомментируйте эту строку, удалив знак решетки (#):

      /etc/mongod.conf

      . . .
      security:
      
      #operationProfiling:
      
      . . .
      

      Затем добавьте параметр authorization и установите для него значение "enabled". Когда вы закончите, строки должны выглядеть следующим образом:

      /etc/mongod.conf

      . . .
      security:
        authorization: "enabled"
      . . .
      

      Обратите внимание, что у строки security: нет пробелов в начале, а строка authorization: выделена двумя пробелами в начале.

      После добавления этих строк сохраните и закройте файл. Если вы использовали nano для редактирования файла, нажмите CTRL + X, Y, а затем ENTER.

      Затем перезапустите демон, чтобы изменения вступили в силу:

      • sudo systemctl restart mongod

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

      • sudo systemctl status mongod

      Если команда restart была выполнена успешно, вы получите вывод, указывающий, что служба mongod активна и была недавно запущена:

      Output

      ● mongod.service - MongoDB Database Server Loaded: loaded (/lib/systemd/system/mongod.service; enabled; vendor preset: enabled) Active: active (running) since Tue 2020-06-09 22:06:20 UTC; 7s ago Docs: https://docs.mongodb.org/manual Main PID: 15370 (mongod) Memory: 170.1M CGroup: /system.slice/mongod.service └─15370 /usr/bin/mongod --config /etc/mongod.conf Jun 09 22:06:20 your_host systemd[1]: Started MongoDB Database Server.

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

      Шаг 3 — Тестирование настроек аутентификации

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

      Теперь, когда вы активировали аутентификацию, ни одно из отображавшихся ранее предупреждений не появится:

      Output

      MongoDB shell version v4.4.0 connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb Implicit session: session { "id" : UUID("5d50ed96-f7e1-493a-b4da-076067b2d898") } MongoDB server version: 4.4.0 >

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

      Как было упомянуто в шаге 1, на вашем сервере есть как минимум несколько баз данных по умолчанию. Однако в этом случае команда не будет выводить данные, поскольку вы не выполнили аутентификацию в качестве пользователя с соответствующими правами.

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

      Теперь мы можем двигаться дальше и выйти из оболочки MongoDB:

      Примечание. Вместо запуска команды exit, которую вы использовали ранее в шаге 1, в качестве альтернативы вы можете закрыть оболочку, просто нажав CTRL + C.

      Далее необходимо убедиться, что ваш пользователь с правами администратора может корректно выполнять аутентификацию, выполнив следующую команду mongo для подключения с помощью этого пользователя. Эта команда имеет флаг -u, который предшествует имени пользователя, от имени которого вы хотите выполнить подключение. Обязательно замените AdminSammy на имя вашего пользователя с правами администратора. Команда также имеет флаг -p, который будет запрашивать пароль пользователя, и указывает admin в качестве базы данных аутентификации, где было создано соответствующее имя пользователя:

      • mongo -u AdminSammy -p --authenticationDatabase admin

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

      Так как вы успешно выполнили аутентификацию, на этот раз команда успешно вернет список всех баз данных на сервере:

      Output

      admin 0.000GB config 0.000GB local 0.000GB

      Это служит подтверждением, что аутентификация была успешно активирована.

      Заключение

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

      Дополнительную информацию об управлении пользователями MongoDB см. в официальной документации по этой теме. Также вам, возможно, будет интересно узнать больше о том, как работает аутентификация в MongoDB.

      Если вы планируете взаимодействовать с вашим экземпляром MongoDB удаленно, воспользуйтесь нашим руководством по настройке удаленного доступа для MongoDB в Ubuntu 20.04.



      Source link

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


      Автор выбрал фонд Wikimedia Foundation для получения пожертвования в рамках программы Write for DOnations.

      Введение

      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 настроен корректно:

      Output

      etcd2 | 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

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

      Output

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

      Примечание: 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:

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

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

      Output

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

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

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

      Output

      etcdctl 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/:

      Затем с помощью вашего редактора создайте в этом каталоге новый файл с именем 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 к одному из управляемых узлов:

      Затем запустите systemctl status etcd для отправки systemd запроса о состоянии службы 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 для выхода из управляемого узла и возврата в локальную оболочку:

      В этом шаге мы обновили наш плейбук для запуска бинарного файла 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/:

      Затем с помощью вашего редактора создайте в этом каталоге новый файл с именем 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 на локальном компьютере:

      Обновите файл 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 к одному из управляемых узлов:

      Затем запустите systemctl status etcd для проверки состоянии службы 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 запустите следующую команду:

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

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

      Output

      OK

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

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

      Output

      foo bar

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

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

      Output

      1

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

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

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

      В этом шаге мы использовали клиент 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:

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

      • etcdctl endpoint health --cluster

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

      Output

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

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

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

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

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

      Output

      foo bar

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

      В этом шаге мы предоставили новый кластер с 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 к этому узлу:

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

      • ip -f inet addr show eth1

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

      Output

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

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

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

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

      Output

      10.131.255.176

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

      Чтобы включить предшествующие команды в наш плейбук, откройте файл 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 для вывода его содержимого:

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

      Output

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

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

      Output

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

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

      Output

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

      В этом шаге мы подписали 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 к одному из управляемых узлов:

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

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

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

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

      Output

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

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

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

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

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

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

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

      Output

      foo 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.

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



      Source link

      Масштабирование и обеспечение безопасности приложения Django с помощью Docker, Nginx и Let’s Encrypt


      Введение

      В облачных средах существует множество способов масштабирования и защиты приложения Django. Используя горизонтальное масштабирование и запустив несколько экземпляров приложения, вы можете создать более отказоустойчивую систему с высоким уровнем доступности, а также увеличить ее пропускную способность для одновременной обработки запросов. Одним из способов горизонтального масштабирования приложения Django является предоставление дополнительных серверов приложения, запускающих ваше приложение Django и его HTTP-сервер WSGI (например, Gunicorn или uWSGI). Чтобы направить и распределить входящие запросы через эту группу серверов приложения, вы можете использовать балансировщик нагрузки и обратный прокси-сервер, например Nginx. Nginx также может кэшировать статический контент и прерывать протокол безопасности Transport Layer Security (TLS), который используется для активации HTTPS и безопасных подключений к вашему приложению.

      Запуск приложения Django и прокси-сервера Nginx в контейнерах Docker гарантирует одинаковое поведение этих компонентов, независимо от среды развертывания. Кроме того, контейнеры предоставляют много возможностей, облегчающих упаковку и настройку вашего приложения.

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

      Также вы активируете HTTPS, предоставив и настроив третий прокси-сервер, который будет выполнять запуск контейнера обратного прокси-сервера Nginx и контейнера клиента Certbot. Certbot будет предоставлять сертификаты TLS для Nginx от имени органа сертификации Let’s Encrypt. Это позволит вашему сайту получить высокий рейтинг безопасности от SSL Labs. Этот прокси-сервер будет получать все внешние запросы приложения и находиться перед двумя исходящими серверами приложения Django. И наконец, вы усилите данную распределенную систему, оставив внешний доступ только к прокси-серверу.

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

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

      • Три сервера Ubuntu 18.04:

        • Два сервера будут выполнять функции серверов приложения, используемых для запуска приложения Django и Gunicorn.
        • Один сервер будет выполнять роль прокси-сервера, используемого для запуска Nginx и Certbot.
        • Все должны иметь пользователя non-root user с привилегиями sudo и активный брандмауэр. Дополнительную информацию о настройке этих параметров см. в руководстве по первоначальной настройке сервера.
      • Установленный на всех трех серверах Docker. Инструкции по установке Docker см. в шагах 1 и 2 руководства Установка и использование Docker в Ubuntu 18.04.

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

      • Запись DNS A, где your_domain.com​​​ указывает на публичный IP-адрес вашего прокси-сервера. Вы можете воспользоваться введением в работу с DigitalOcean DNS, чтобы получить подробную информацию о добавлении доменов в учетную запись DigitalOcean, если вы используете этот способ.

      • Хранилище объектов S3, например пространство DigitalOcean, для хранения статических файлов проекта Django и набор ключей доступа к этому пространству. Чтобы узнать, как создать пространство, ознакомьтесь с документом Как создавать пространства. Чтобы узнать, как создать ключи доступа, ознакомьтесь со статьей Предоставление доступа к пространству с помощью ключей доступа. Внеся незначительные изменения, вы можете использовать любой сервис хранения, который поддерживает плагин django-storages.

      • Экземпляр сервера PostgreSQL, база данных и пользователь для приложения Django. Внеся незначительные изменения, вы можете использовать любую базу данных, которую поддерживает Django.

      Шаг 1 — Настройка первого сервера приложения Django

      Для начала мы клонируем репозиторий приложения Django на первый сервер приложения. Затем настроим и создадим образ приложения Docker, после чего протестируем приложение, запустив контейнер Django.

      Примечание. Если вы уже выполнили инструкции руководства Создание приложения Django и Gunicorn с помощью Docker, то вы уже выполнили шаг 1 и можете переходить к шагу 2, чтобы настроить второй сервер приложения.

      Начните с входа в первый из двух серверов приложения Django и используйте git для клонирования ветки polls-docker репозитория GitHub для приложения опросов Django Tutorial Polls App. В этом репозитории содержится код документации Django для образца приложения «Опросы». Ветка polls-docker содержит контейнеризированную версию приложения «Опросы». Чтобы узнать об изменениях, внесенных в приложение «Опросы» для эффективной работы в контейнеризированной среде, см. руководство Создание приложения Django и Gunicorn с помощью Docker.

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Перейдите в каталог django-polls:

      cd django-polls
      

      В этом каталоге содержится код Python приложения Django, Dockerfile, который Docker будет использовать для построения образа контейнера, а также файл env, содержащий список переменных среды для прохождения в рабочую среду контейнера. Проверьте файл Dockerfile с помощью cat:

      cat Dockerfile
      

      Output

      FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex && apk add --no-cache --virtual .build-deps postgresql-dev build-base && python -m venv /env && /env/bin/pip install --upgrade pip && /env/bin/pip install --no-cache-dir -r /app/requirements.txt && runDeps="$(scanelf --needed --nobanner --recursive /env | awk '{ gsub(/,/, "nso:", $2); print "so:" $2 }' | sort -u | xargs -r apk info --installed | sort -u)" && apk add --virtual rundeps $runDeps && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

      Dockerfile использует официальный образ Docker Python 3.7.4 в качестве базы и устанавливает пакетные требования Python для Django и Gunicorn в соответствии с файлом django-polls/requirements.txt. Затем он удаляет некоторые ненужные файлы сборки, копирует код приложения в образ и устанавливает параметр выполнения PATH. И в заключение заявляет, что порт 8000 будет использоваться для принятия входящих подключений контейнера, и запускает gunicorn с тремя рабочими элементами, прослушивающими порт 8000.

      Дополнительную информацию о каждом этапе в Dockerfile см. в шаге 6 руководства Создание приложения Django и Gunicorn с помощью Docker.

      Теперь создайте образ с помощью docker build:

      Назовем образ polls, используя флаг -t, и передадим в текущий каталог как контекст сборки набор файлов для справки при построении образа.

      После того как Docker создаст и отметит образ, перечислите доступные образы, используя docker images:

      docker images
      

      Вы должны увидеть образ polls:

      Output

      REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

      Перед запуском контейнера Django нам нужно настроить его рабочую среду с помощью файла env, присутствующего в текущем каталоге. Этот файл будет передан в команду docker run, которая используется для запуска контейнера, а Docker будет вводить настроенные переменные среды в рабочую среду контейнера.

      Откройте файл env с помощью nano или своего любимого редактора:

      nano env
      

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

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Заполните недостающие значения для следующих ключей:

      • DJANGO_SECRET_KEY: задает уникальное, непрогнозируемое значение, как описано в документации Django. Один метод генерирования этого ключа представлен в разделе Изменение настроек приложения руководства Масштабируемое приложение Django.
      • DJANGO_ALLOWED_HOSTS: эта переменная защищает приложение и предотвращает атаки через заголовки хоста HTTP. С целью тестирования установите * как подстановочный символ, подходящий для всех хостов. В производственной среде вы должны задать значение your_domain.com. Дополнительную информацию об этой настройке Django см. в разделе Базовые настройки документации Django.
      • DATABASE_USERNAME: задает пользователя базы данных PostgreSQL, созданного на предварительных этапах.
      • DATABASE_NAME: задает polls или имя базы данных, созданной на предварительных этапах.
      • DATABASE_PASSWORD: задает пароль пользователя базы данных PostgreSQL, созданного на предварительных этапах.
      • DATABASE_HOST: задает имя хоста базы данных.
      • DATABASE_PORT: задает порт базы данных.
      • STATIC_ACCESS_KEY_ID: задает хранилище S3 или ключ доступа к пространству.
      • STATIC_SECRET_KEY: задает хранилище S3 или секретный ключ доступа к пространству.
      • STATIC_BUCKET_NAME: задает хранилище S3 или имя пространства.
      • STATIC_ENDPOINT_URL: задает соответствующее хранилище S3 или URL-адрес пространства, например https://space-name.nyc3.digitaloceanspaces.com, если ваше пространство находится в регионе nyc3.

      После завершения редактирования сохраните и закройте файл.

      Теперь мы будем использовать docker run, чтобы переопределить CMD, установленный в Dockerfile, и создать схему базы данных с помощью команд manage.py makemigrations и manage.py migrate:

      docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"
      

      Мы запускаем образ контейнера polls:latest, передаем в только что измененный файл переменной среды и переопределяем команду Dockerfile с помощью sh -c "python manage.py makemigrations && python manage.py migrate", которая создаст схему базы данных, определяемую кодом приложения. Если вы проводите запуск впервые, вы должны увидеть следующее:

      Output

      No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

      Это означает, что схема базы данных успешно создана.

      Если в следующий раз вы запускаете migrate, Django будет выполнять холостую команду, если не изменить схему базы данных.

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

      docker run -i -t --env-file env polls sh
      

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

      python manage.py createsuperuser
      

      Введите имя пользователя, адрес электронной почты и пароль для пользователя, а после создания пользователя нажмите CTRL+D для выхода из контейнера и его удаления.

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

      docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"
      

      После создания и загрузки файлов вы получите следующий вывод.

      Output

      121 static files copied.

      Теперь мы можем запустить приложение:

      docker run --env-file env -p 80:8000 polls
      

      Output

      [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

      Мы запустим команду по умолчанию, определенную в Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application и раскроем порт контейнера 8000, чтобы сопоставить порт 80 на сервере Ubuntu с портом 8000 контейнера polls.

      Теперь вы сможете перейти к приложению polls с использованием веб-браузера, набрав в адресной-строке URL http://APP_SERVER_1_IP. Поскольку маршрут для пути / не определен, скорее всего, вы получите ошибку 404 Страница не найдена.

      Предупреждение. При использовании с Docker брандмауэра UFW, Docker обходит любые настроенные правила брандмауэра UFW. Об этой ситуации можно прочитать в GitHub. Это объясняет, почему у вас есть доступ к порту 80 сервера, хотя вы не создавали правило доступа UFW на предварительном этапе. На шаге 5 мы рассмотрим эту брешь в системе безопасности и скорректируем настройки UFW. Если вы не используете UFW, а используете облачные брандмауэры DigitalOcean, вы можете спокойно игнорировать это предупреждение.

      Перейдите в адрес http://APP_SERVER_1_IP/polls, чтобы увидеть интерфейс приложения «Опросы»:

      Интерфейс приложения «Опросы»

      Чтобы посмотреть административный интерфейс, перейдите по адресу http://APP_SERVER_1_IP/admin. Вы должны увидеть окно аутентификации администратора приложения «Опросы»:

      Страница аутентификации администратора приложения

      Введите имя администратора и пароль, которые вы создали с помощью команды createsuperuser.

      После аутентификации вы сможете получить доступ к административному интерфейсу приложения «Опросы»:

      Основной интерфейс администратора приложения

      Обратите внимание, что статические активы приложений admin и polls поступают напрямую из хранилища объекта. Чтобы убедиться в этом, ознакомьтесь с материалами Тестирование доставки статических файлов пространства.

      После завершения изучения данных нажмите CTRL+C в окне терминала, где запущен контейнер Docker, чтобы удалить контейнер.

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

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Флаг -d предписывает Docker запустить контейнер в раздельном режиме, флаг -rm очищает файловую систему контейнера после выхода контейнера, а мы называем контейнер polls.

      Выйдите из первого сервера приложения Django и перейдите к адресу http://APP_SERVER_1_IP/polls для подтверждения того, что контейнер работает корректно.

      После запуска и активации первого сервера приложения Django вы можете настроить второй сервер приложения Django.

      Шаг 2 — Настройка второго сервера приложения Django

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

      Начните с входа на второй сервер приложения Django.

      Клонируйте ветку polls-docker репозитория GitHub django-polls:

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Перейдите в каталог django-polls:

      cd django-polls
      

      Создайте образ с помощью docker build:

      Откройте файл env с помощью nano или своего любимого редактора:

      nano env
      

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Заполните недостающие значения, как показано в шаге 1. После завершения редактирования сохраните и закройте файл.

      Наконец, запустите контейнер приложения в раздельном режиме:

      docker run -d --rm --name polls --env-file env -p 80:8000 polls
      

      Перейдите на http://APP_SERVER_2_IP/polls для подтверждения того, что контейнер работает корректно. Вы можете безопасно выйти из второго сервера приложения без завершения работы контейнера.

      Когда вы настроили и запустили оба контейнера приложения Django, можно переходить к настройке обратного прокси-сервера Nginx.

      Шаг 3 — Настройка контейнера Docker Nginx

      Nginx — это универсальный веб-сервер, предлагающий ряд функций, включая обратное проксирование, балансирование нагрузки и кэширование. В этом обучающем руководстве мы вызгрузили статические активы Django в хранилище объектов, поэтому мы не будем использовать возможности кэширования Nginx. Однако мы будем использовать Nginx как обратный прокси-сервер для двух серверов приложения Django и распределим входящие запросы между ними. Кроме того, Nginx будет осуществлять прекращение TLS и перенаправление с помощью сертификата TLS, предоставленного Certbot. Это означает, что он заставит клиентов использовать HTTPS, перенаправляя входящие запросы HTTP в порт 443. Затем он расшифрует запросы HTTPS и передаст их на серверы Django.

      В этом обучающем руководстве мы приняли проектное решение отделить контейнеры Nginx от серверов. В зависимости от своих потребностей вы можете запускать контейнер Nginx на одном из серверов приложения Django, обрабатывая запросы с помощью прокси-сервера локально или на другом сервере Django. Еще один вариант архитектуры: запуск двух контейнеров Nginx, по одному на каждом сервере, и облачного балансировщика нагрузки перед ними. Каждая архитектура имеет свои преимущества безопасности и производительности, и вам потребуется провести нагрузочное тестирование, чтобы определить узкие места. Гибкая архитектура, описанная в данном руководстве, позволяет масштабировать как серверный слой приложения Django, так и прокси-сервер Nginx. Если узким местом станет единственный контейнер Nginx, вы сможете масштабировать несколько прокси-серверов Nginx и добавить облачный балансировщик нагрузок или быстрый балансировщик нагрузок L4, например HAProxy.

      Когда вы создадите и запустите оба сервера приложения Django, можно начинать настройку прокси-сервера Nginx. Войдите на ваш прокси-сервер и создайте каталог с именем conf:

      mkdir conf
      

      Создайте файл конфигурации nginx.conf с помощью nano или своего любимого редактора:

      nano conf/nginx.conf
      

      Вставьте следующую конфигурацию Nginx:

      conf/nginx.conf

      
      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      
      server {
          listen 80 default_server;
          return 444;
      }
      
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      
      server {
          listen 443 ssl http2;
          listen [::]:443 ssl http2;
          server_name your_domain.com;
      
          # SSL
          ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      
          client_max_body_size 4G;
          keepalive_timeout 5;
      
              location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Host $http_host;
                proxy_redirect off;
                proxy_pass http://django;
              }
      
          location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
          }
      
      }
      

      Блоки upstream, server и location настраивают Nginx таким образом, чтобы перенаправлять HTTP-запросы на HTTPS и распределять нагрузку на два сервера приложения Django, настроенных в шагах 1 и 2. Дополнительную информацию о структуре файла конфигурации Nginx см. в статье Понимание структуры файла конфигурации и контекста конфигурации. Также может быть полезна статья Понимание работы сервера Nginx и алгоритмов выбора блоков расположения.

      Эта конфигурация была собрана из примеров файлов конфигурации из Gunicorn, Certbot и Nginx и является минимальной конфигурацией Nginx, необходимой для построения и запуска данной архитектуры. Настройка данной конфигурации Nginx выходит за рамки этой статьи, но вы можете использовать такой инструмент, как NGINXConfig, для генерирования исполнительных и безопасных файлов конфигурации для вашей архитектуры.

      Блок upstream определяет группу серверов, которые используются для передачи запросов с использованием директивы proxy_pass:

      conf/nginx.conf

      upstream django {
          server APP_SERVER_1_IP;
          server APP_SERVER_2_IP;
      }
      . . .
      

      В этом блоке мы используем имя django и включаем IP-адреса обоих серверов приложения Django. Если серверы приложения работают на DigitalOcean с активированной сетью VPC, вам следует использовать их частные IP-адреса. Чтобы узнать, как активировать сеть VPC на DigitalOcean, см. Как активировать сеть VPC на существующих дроплетах.

      Первый блок server захватывает запросы, которые не соответствуют вашему домену, и прерывает соединение. Например, в этом блоке будет обрабатываться прямой запрос HTTP на IP-адрес вашего сервера:

      conf/nginx.conf

      . . .
      server {
          listen 80 default_server;
          return 444;
      }
      . . .
      

      Следующий блок server перенаправляет запросы HTTP на ваш домен на HTTPS, используя перенаправление HTTP 301. Эти запросы рассматриваются в последнем блоке server:

      conf/nginx.conf

      . . .
      server {
          listen 80;
          listen [::]:80;
          server_name your_domain.com;
          return 301 https://$server_name$request_uri;
      }
      . . .
      

      Эти две директивы определяют путь к сертификату TLS и секретному ключу. Они предоставляются с помощью Certbot и монтируются в контейнер Nginx на следующем шаге.

      conf/nginx.conf

      . . .
      ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
      . . .
      

      Это по умолчанию параметры безопасности SSL, рекомендованные Certbot. Дополнительную информацию о них см. в разделе Модуль ngx_http_ssl_module в документации Nginx. Еще одним полезным руководством, которое вы можете использовать для настройки конфигурации SSL, является инструкция по безопасности от Mozilla Security/Server Side TLS.

      conf/nginx.conf

      . . .
          ssl_session_cache shared:le_nginx_SSL:10m;
          ssl_session_timeout 1440m;
          ssl_session_tickets off;
      
          ssl_protocols TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers off;
      
          ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
      . . .
      

      Эти две директивы из примерной конфигурации Nginx от Gunicorn определяют максимальный разрешенный размер тела запроса клиента и назначают временной интервал поддержки соединения с клиентом. Nginx закроет соединения с клиентом после keepalive_timeout.

      conf/nginx.conf

      . . .
      client_max_body_size 4G;
      keepalive_timeout 5;
      . . .
      

      Первый блок location предписывает Nginx направлять запросы на серверы upstream django через HTTP. Также он сохраняет клиентские заголовки HTTP, захватывающие исходный IP-адрес, протокол, используемый для соединения, и целевой хост:

      conf/nginx.conf

      . . .
      location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;
          proxy_pass http://django;
      }
      . . .
      

      Дополнительную информацию об этих директивах см. в разделах развертывание Gunicorn и Модуль ngx_http_proxy_module из документации Nginx.

      Последний блок location захватывает запросы на путь /well-known/acme-challenge/, используемый Certbot для задач HTTP-01 по проверке вашего домена с помощью Let’s Encrypt и предоставлению или обновлению сертификатов TLS. Дополнительную информацию о задаче HTTP-01, используемой Certbot, см. в разделе Виды задач из документации Let’s Encrypt.

      conf/nginx.conf

      . . .
      location ^~ /.well-known/acme-challenge/ {
              root /var/www/html;
      }
      

      После завершения редактирования сохраните и закройте файл.

      Теперь вы можете использовать этот файл конфигурации для запуска контейнера Nginx Docker. В этом обучающем руководстве мы будем использовать образ nginx:1.19.0, версия 1.19.0 официального образа Docker, обслуживаемого Nginx.

      При первом запуске контейнера Nginx будет выдавать ошибку и отказывать в работе, так как мы еще не предоставили сертификаты, определенные в файле конфигурации. Однако мы все равно будем выполнять команду для локальной загрузки образа Nginx и протестируем корректность работы остальных элементов.

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Давайте назовем контейнер nginx и свяжем порты хоста 80 и 443 с соответствующими портами контейнера. Флаг -v монтирует файл конфигурации в контейнер Nginx на /etc/nginx/conf.d/nginx.conf, который образ Nginx предварительно настраивает для загрузки. Он монтируется в режиме ro или «только для чтения», поэтому контейнер не может изменять файл. Также в контейнер монтируется корневой веб-каталог /var/www/html. Наконец, nginx:1.19.0 предписывает Docker вытащить и запустить образ nginx:1.19.0 из Dockerhub.

      Docker вытащит и запустит образ, затем Nginx выдаст ошибку, т.к. не найдет настроенный сертификат TLS и секретный ключ. На следующем шаге мы предоставим их с помощью клиента Dockerized Certbot и органа сертификации Let’s Encrypt.

      Шаг 4 — Настройка Certbot и обновление сертификата Let’s Encrypt

      Certbot — это клиент Let’s Encrypt, разработанный фондом Electronic Frontier Foundation. Он предоставляет бесплатные сертификаты TLS от органа сертификации Let’s Encrypt, которые позволяют браузерам идентифицировать ваши веб-серверы. Поскольку мы установили Docker на нашем прокси-сервере Nginx, мы будем использовать образ Docker Certbot для предоставления и обновления сертификатов

      Для начала проверьте наличие записи А DNS, соединенной с публичным IP-адресом прокси-сервера. Затем на вашем прокси-сервере представьте тестовую версию сертификатов с помощью образа Docker certbot:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone --staging -d your_domain.com
      

      Эта команда запускает образ Docker certbot в интерактивном режиме и направляет порт 80 на хосте на порт контейнера 80. Она создает и монтирует два каталога хоста в контейнер: /etc/letsencrypt/ и /var/lib/letsencrypt/. certbot работает в режиме standalone без Nginx и будет использовать серверы staging Let’s Encrypt для валидации домена.

      Когда появится строка ввода, укажите свой электронный адрес и примите Условия обслуживания. Если валидация домена прошла успешно, вы увидете следующий вывод:

      Output

      Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

      Вы можете проверить сертификат с помощью cat:

      sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem
      

      После предоставления сертификата TLS мы можем протестировать конфигурацию Nginx, собранную на предыдущем шаге:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      Задаем те же команды, что и на шаге 3, добавив оба только что созданных каталога Let’s Encrypt.

      После установки и активации Nginx перейдите на http://your_domain.com. Вы можете получить предупреждение в браузере о том, что орган сертификации недействителен. Это ожидаемо, так как мы предоставили тестовые, а не рабочие сертификаты Let’s Encrypt. Проверьте строку URL-адреса вашего браузера, чтобы убедиться, что ваш запрос HTTP был перенаправлен на HTTPS.

      Нажмите CTRL+C на вашем терминале, чтобы выйти из Nginx, и снова запустите клиент certbot, на этот раз опустив флаг --staging:

      docker run -it --rm -p 80:80 --name certbot 
               -v "/etc/letsencrypt:/etc/letsencrypt" 
               -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
               certbot/certbot certonly --standalone -d your_domain.com
      

      Когда появится окно выбора: сохранить действующий сертификат или обновить и заменить, нажмите 2, чтобы обновить, а затем ENTER, чтобы подтвердить свой выбор.

      После предоставления рабочего сертификата TLS запустите сервер Nginx снова:

      docker run --rm --name nginx -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
          -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      В вашем браузере перейдите на http://your_domain.com. В строке URL-адреса подтвердите, что запрос HTTP был перенаправлен на HTTPS. Поскольку приложение «Опросы» не имеет настроенного по умолчанию маршрута, вы увидите ошибку Django Страница не найдена. Перейдите на https://your_domain.com/polls и вы увидите стандартный интерфейс приложения «Опросы»:

      Интерфейс приложения «Опросы»

      На этом этапе вы предоставили рабочий сертификат TLS с помощью клиента Docker Certbot, направляете внешние запросы с помощью обратного прокси-сервера и распределяете нагрузку между двумя серверами приложения Django.

      Срок действия сертификатов Let’s Encrypt истекает каждые 90 дней. Чтобы удостовериться в действительности вашего сертификата, необходимо регулярно обновлять его до истечения срока действия. После запуска Nginx вы должны использовать клиент Certbot в режиме webroot вместо режима standalone. Это означает, что Certbot будет выполнять проверку путем создания файла в каталоге /var/www/html/.well-known/acme-challenge/, и запросы Let’s Encrypt о валидации на этом пути будут захватываться правилом location, определенном в конфигурации Nginx на шаге 3. Certbot будет менять сертификаты, и вы сможете перезагрузить Nginx, чтобы он использовал уже новый предоставленный сертификат.

      Существует множество способов автоматизации этой процедуры, но автоматическое обновление сертификатов TLS выходит за рамки данного обучающего руководства. Для аналогичного процесса с помощью утилиты планирования cron см. шаг 6 руководства Обеспечение безопасности контейнеризированного приложения Node.js с помощью Nginx, Let’s Encrypt и Docker Compose.

      На вашем терминале нажмите CTRL+C, чтобы удалить контейнер Nginx. Запустите его снова в раздельном режиме, добавив флаг -d:

      docker run --rm --name nginx -d -p 80:80 -p 443:443 
          -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro 
          -v /etc/letsencrypt:/etc/letsencrypt 
          -v /var/lib/letsencrypt:/var/lib/letsencrypt 
        -v /var/www/html:/var/www/html 
          nginx:1.19.0
      

      При запущенном в фоновом режиме Nginx используйте следующую команду для выполнения сухого запуска процедуры обновления сертификата:

      docker run -it --rm --name certbot 
          -v "/etc/letsencrypt:/etc/letsencrypt" 
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" 
        -v "/var/www/html:/var/www/html" 
        certbot/certbot renew --webroot -w /var/www/html --dry-run
      

      Мы используем плагин --webroot, указываем корневой путь и используем флаг –--dry-run, чтобы убедиться, что все работает корректно без обновления сертификата.

      Если моделирование обновления завершится успешно, вы увидите следующий вывод:

      Output

      Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      В рабочей настройке после обновления сертификата вы должны перезагрузить Nginx, чтобы изменения вступили в силу. Чтобы перезагрузить Nginx, запустите следующую команду:

      docker kill -s HUP nginx
      

      Эта команда будет отправлять сигнал Unix HUP для процесса Nginx, работающего внутри контейнера Docker nginx. После получения этого сигнала Nginx перезагрузит конфигурацию и обновленные сертификаты.

      После активации HTTPS и всех компонентов данной архитектуры итоговым шагом является блокировка настройки от внешнего доступа к двум серверам приложения. Все запросы HTTP должны проходить через прокси-сервер Nginx.

      Шаг 5 — Предотвращение внешнего доступа к серверу приложения Django

      В описанной в этом руководстве архитектуре прекращение SSL происходит на прокси-сервере Nginx. Это означает, что Nginx расшифровывает соединение SSL, а пакеты передаются на серверы приложения Django без шифрования. Для многих случаев использования этого уровня безопасности достаточно. Для приложений, содержащих финансовые данные или данные о состоянии здоровья, вам может понадобится внедрение сквозного шифрования. Вы можете сделать это, передавая зашифрованные пакеты через балансировщик нагрузки и расшифровывая их на серверах приложения, или зашифровывая на прокси-сервере и снова расшифровывая на серверах приложения Django. Эти методы выходят за рамки данного руководства, но для получения более подробной информации можно ознакомиться со статьей Сквозное шифрование.

      Прокси-сервер Nginx действует как шлюз между внешним трафиком и внутренней сетью. Теоретически ни один внешний клиент не должен иметь прямого доступа к внутренним серверам приложения, и все запросы должны проходить через сервер Nginx. В примечании на шаге 1 кратко описывается проблема с Docker, где Docker обходит настройки брандмауэра по умолчанию ufw и открывает порты внешним способом, что может быть небезопасным. Чтобы решить эту проблему безопасности, рекомендуется использовать облачные брандмауэры при работе с серверами на базе Docker. Дополнительную информацию о создании облачного брандмауэра с помощью DigitalOcean можно найти в руководстве Создание брандмауэра. Также вы можете использовать непосредственно iptables вместо ufw. Дополнительную информацию об использовании iptables с Docker можно найти в статье Docker and iptables.

      На этом шаге мы изменим конфигурацию UFW, чтобы заблокировать внешний доступ к портам хоста, которые открывает Docker. При запуске Django на серверах приложения мы передаем флаг -p 80:8000 на docker, который направляет порт 80 на хосте в порт контейнера 8000. Это также открывает порт 80 для внешних клиентов, которых вы можете верифицировать, посетив http://your_app_server_1_IP. Чтобы предотвратить прямой доступ, мы изменим конфигурацию UFW с помощью метода, описанного в репозитории GitHub ufw-docker.

      Начните с входа на первый сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

      sudo nano /etc/ufw/after.rules
      

      Введите свой пароль при запросе и нажмите ENTER для подтверждения.

      Вы должны увидеть следующие правила ufw:

      /etc/ufw/after.rules

      #
      # rules.input-after
      #
      # Rules that should be run after the ufw command line added rules. Custom
      # rules should be added to one of these chains:
      #   ufw-after-input
      #   ufw-after-output
      #   ufw-after-forward
      #
      
      # Don't delete these required lines, otherwise there will be errors
      *filter
      :ufw-after-input - [0:0]
      :ufw-after-output - [0:0]
      :ufw-after-forward - [0:0]
      # End required lines
      
      # don't log noisy services by default
      -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
      -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
      -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
      
      # don't log noisy broadcast
      -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
      
      # don't delete the 'COMMIT' line or these rules won't be processed
      COMMIT
      

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

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      Эти правила ограничивают публичный доступ к портам, которые открывает Docker, и позволяют получить доступ от частных диапазонов IP-диапазонов 10.0.0.0/8, 172.16.0.0/12 и 192.168.0.0/16. Если вы используете VPC с DigitalOcean, тогда дроплеты в вашей сети VPC получат доступ к открытому порту через закрытый интерфейс, а внешние клиенты не получат. Дополнительную информацию о VPC см. в официальной документации VPC. Дополнительную информацию о правилах, которые использованы в этом фрагменте, ищите в разделе Как это работает? инструкции ufw-docker README.

      Если вы не используете VPC с Digitalocean и ввели публичные IP-адреса серверов приложений в блоке upstream вашей конфигурации Nginx, вам придется изменить брандмауэр UFW, чтобы разрешить трафик с сервера Nginx через порт 80 на серверы приложения Django. Инструкции по созданию правил allow с брандмауэром UFW см. в руководстве Основы UFW: общие правила и команды брандмауэра.

      После завершения редактирования сохраните и закройте файл.

      Перезапустите ufw для выбора новой конфигурации:

      sudo systemctl restart ufw
      

      Перейдите на http://APP_SERVER_1_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

      Повторите этот процесс на втором сервере приложения Django.

      Выйдите из первого сервера приложения или откройте другое окно терминала и войдите на второй сервер приложения Django. Затем откройте файл /etc/ufw/after.rules с привилегиями суперпользователя, используя nano или свой любимый редактор:

      sudo nano /etc/ufw/after.rules
      

      Введите свой пароль при запросе и нажмите ENTER для подтверждения.

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

      /etc/ufw/after.rules

      . . .
      
      # BEGIN UFW AND DOCKER
      *filter
      :ufw-user-forward - [0:0]
      :DOCKER-USER - [0:0]
      -A DOCKER-USER -j RETURN -s 10.0.0.0/8
      -A DOCKER-USER -j RETURN -s 172.16.0.0/12
      -A DOCKER-USER -j RETURN -s 192.168.0.0/16
      
      -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
      
      -A DOCKER-USER -j ufw-user-forward
      
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
      -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
      
      -A DOCKER-USER -j RETURN
      COMMIT
      # END UFW AND DOCKER
      

      После завершения редактирования сохраните и закройте файл.

      Перезапустите ufw для выбора новой конфигурации:

      sudo systemctl restart ufw
      

      Перейдите на http://APP_SERVER_2_IP в вашем браузере для подтверждения того, что вы больше не можете получить доступ к серверу приложения через порт 80.

      В заключение перейдите на https://your_domain_here/polls, чтобы подтвердить, что прокси-сервер Nginx все еще имеет доступ к серверам Django. Вы должны увидеть интерфейс приложения «Опросы» по умолчанию.

      Заключение

      В этом обучающем руководстве мы настроили масштабируемое приложение Django «Опросы» при помощи контейнеров Docker. По мере увеличения трафика и загрузки системы вы можете масштабировать каждый слой отдельно: слой прокси-сервера Nginx, слой серверного приложения Django и слой базы данных PostgreSQL.

      При построении распределенной системы вам часто приходится выбирать из нескольких проектных решений и архитектур. Архитектура, описанная в этом руководстве, является гибким планом для разработки масштабируемых приложений с Django и Docker.

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

      При работе в масштабе с несколькими хостами на одном и том же образе Docker более эффективной может стать автоматизация шагов с использованием инструмента управления конфигурацией, например Ansible или Chef. Дополнительную информацию об управлении конфигурацией см. в руководствах Введение в управление конфигурацией и Автоматизированная настройка сервера с помощью Ansible: набор материалов для тренинга DigitalOcean.

      Вместо создания такого же образа на каждом хосте вы можете также упорядочить развертывание с помощью реестра образов, такого как Docker Hub, который централизованно строит, хранит и распределяет образы Docker на несколько серверов. Наряду с реестром образов непрерывный конвейер интеграции и развертывания может помочь вам строить, тестировать и развертывать образы на ваших серверах приложения. Дополнительную информацию о CI/CD см. в руководстве Введение в передовой опыт CI/CD.



      Source link