Кластеризация необходима для большинства СУБД уровня Enterprise. Есть много способов создать или развернуть кластер: от бесплатных до дорогих, от простых до сложных. У разных вендоров свои приоритеты: одни делают настройку кластера в пару кликов (как в MS SQL), другие фокусируются на надежности и функциональности (Oracle).

В мире СУБД на базе PostgreSQL на сторону которых всё активнее переходит российский IT, тоже есть свои кластерные инструменты и решения: как бесплатные и открытые Patroni, Stolon, pacemaker/corosync, так и проприетарные. Лидер по популярности среди открытых решений – Patroni. Сегодня я расскажу, почему настраивать кластеризацию на нем руками весело только первые пару раз. Поговорим об особенностях поддержки Patroni в Proxima DB, снижении порога входа для новичков и повторяемости конфигураций. 

Часто, когда речь заходит про развертывание отказоустойчивого кластера СУБД на PostgreSQL, можно услышать: «Да мы сами его за 5 минут поднимем... Ну ладно, за пару дней, зато все протестируем». И это правда! Исследовать Open Source решения полезно и интересно. Но когда нужно регулярно и быстро разворачивать кластеры PostgreSQL на Patroni, ручное развертывание кластеров превращается в унылую рутину с кучей подводных камней. 

Настраиваем высокодоступный кластер на Patroni руками

Будем считать, что вы уже знакомы с основными компонентами и решениями для обеспечения высокой доступности и кластеризации PostgreSQL. И даже читали посты про Patroni и PostgreSQL на Хабре и сохранили их в закладки. Но сами еще не пробовали собрать кластер по инструкциям.

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

Из чего состоит кластер СУБД на базе Patroni

Кластеры на базе PostgreSQL архитектурно относится к типу «shared-nothing» кластеров, в которых не используется ни разделяемое дисковое пространство, ни разделяемая память. Для обеспечения высокой доступности и отказоустойчивости кластер Patroni обычно собирается минимально в трехузловой конфигурации.  

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

Скрытый текст

В статьях в интернете и в разной документации можно встретить разные наименования ролей узлов кластеров: master/slave, ведущий/ведомый, primary/secondary – все названия верные. Ранее в документации встречалась терминология master/slave, используемая для всех типов любых кластеров. Сейчас в кластерах PostgreSQL больше используется терминология ведущий/ведомый и primary/secondary, которая лучше подчеркивает факт направленной репликации в кластере. 

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

Вот из каких компонентов может состоять кластер на Patroni c дополнительными компонентами:

  • сам PostgreSQL;

  • распределенное хранилище конфигураций кластера на одном из решений: etcd, Consul, ZooKeeper или Exhibitor; 

  • HAProxy для балансировки нагрузки, распределения запросов между узлами и прозрачного переключения при отказе основного узла; 

  • Keepalived сервис для управления виртуальным IP-адресом (VIP), который будет «плавать» между узлами кластера и тем самым обеспечивать высокую доступность в случае выхода из строя одного из узлов.

Допустим, вы потратили 4-5 часов на изучение документации Patroni, возможных  ограничений и требований. Посмотрели сравнения компонентов, например,  «etcd vs zookeeper» и разобрались в типовой архитектуре отказоустойчивого кластера, схема которого приведена в одном из проектов в GitHub: 

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

Особенности установки необходимых для кластера компонентов на Astra Linux

Если вы решаете задачу импортозамещения, логично взять один из популярных отечественных дистрибутивов. Например, Astra Linux 1.7.5 из реестра ПО.

Но при установке компонентов из репозиториев Astra Linux вы столкнетесь с некоторыми нюансами: 

  • доступен только PostgreSQL версии 11, при необходимости использования более свежих версий PostgreSQL придется собирать самостоятельно –  «…но это уже совсем другая история»(с);

  • версия Patroni 3.0.2 от 24.03.2023, в ней нет поддержки PostgreSQL 16.x, хотя с версией PostgreSQL из штатных репов Astra Linux это не критично. Текущая версия Patroni – 3.3.2 от 11.07.2024;

  • HAProxy и etcd 3.4.23 тоже не самые новые, но критических ограничений тут нет.

Начнем с etcd. Читаем мануалы на etcd.io, чтобы понять, как оно конфигурируется, работает и управляется. 

Важно: помимо самого etcd нужно будет настроить соответствующие сервисы и службы операционной системы. На то, чтобы настроить все именно под Astra Linux, уйдет примерно 15-30 минут. Возможно, придется провести несколько перезагрузок.

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

В результате получаем конфиг для сервера etcd с ролью «лидера», который будет выглядеть примерно так: 

Скрытый текст

#[Member]

ETCD_DATA_DIR="/var/lib/etcd"

ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"

ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"

ETCD_NAME="core"

ETCD_HEARTBEAT_INTERVAL="1000"

ETCD_ELECTION_TIMEOUT="5000"

#[Clustering]

ETCD_INITIAL_ADVERTISE_PEER_URLS="http://db01:2380"

ETCD_ADVERTISE_CLIENT_URLS="http://db01:2379"

ETCD_INITIAL_CLUSTER="core=http://db01:2380,db02=http://db02:2380,db03=http://db03:2380"

ETCD_INITIAL_CLUSTER_TOKEN="etcd-my-cluster01"

ETCD_INITIAL_CLUSTER_STATE="existing"

Здесь и далее примеры конфигурационных файлов приводятся с целью оценки объема конфигурационных параметров. 

Настраиваем Patroni

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

Один из них — выбор типов ведомых узлов в кластере и вида используемой физической репликации (синхронной/асинхронной). 

В синхронной репликации потери производительности зависят от уровня подтверждения при сохранении реплицируемых данных. Например, при уровне "remote_apply" потери начинаются от 20%, а на мелких транзакциях могут превышать 50%. 

Альтернатива –  асинхронная репликация, но не каждой отрасли она подходит, особенно, если речь, например, о финансовом секторе. Есть вариант с «миксованной» репликацией (синхронная + асинхронная), но она не сильно быстрее синхронной, т.к. как минимум одна синхронная нода в кластере все равно остается. Также желательно выбрать инструменты и стратегии бэкапа и восстановления для узлов внутри кластера Patroni (чтобы Patroni не запускал full backup с primary ноды кластера в тот момент, где посчитает, что это необходимо для восстановления узла кластера), но это требует времени и более глубокой экспертизы.

Если нужно сэкономить время, проще взять несложный конфиг Patroni. В сети много примеров, вот один из них:

Скрытый текст

name: patroni-cl

scope: patr-cl

restapi:

listen: 0.0.0.0:8008

connect_address: db01:8008

authentication:

     username: patroni

     password: '********'

etcd:

hosts: db01:2379,db02:2379,db03:2379

username: 'root'

password: '********'

bootstrap:

dcs:

     ttl: 100

     loop_wait: 10

     retry_timeout: 10

     maximum_lag_on_failover: 2097152

     postgresql:

         use_pg_rewind: true

         use_slots: true

         parameters:

             wal_level: replica

             hot_standby: "on"

             wal_keep_segments: 5120

             max_wal_senders: 5

             max_replication_slots: 5

             checkpoint_timeout: 30

initdb:

- auth-host: md5

- auth-local: peer

- encoding: UTF8

- data-checksums

- locale: ru_RU.UTF-8

pg_hba:

- host replication postgres ::1/128 md5

- host replication postgres 127.0.0.1/8 md5

- host replication postgres 192.xxx.yyy.11/24 md5

- host replication postgres 192.xxx.yyy.12/24 md5

- host replication postgres 192.xxx.yyy.13/24 md5

- host all all 0.0.0.0/0 md5

- host replication replicator samenet md5

- host replication all 127.0.0.1/32 md5

- host replication all ::1/128 md5

users: admin:

     password: '********'

     options:

         - superuser

postgresql:

listen: 0.0.0.0:5432

connect_address: db01:5432

conf_dir: /etc/postgresql/11/main

bin_dir: /usr/bin/

data_dir: /data/patroni

pgpass: /tmp/pgpass

authentication:

     superuser:

         username: 'postgres'

         password: '********'

     replication:

         username: 'replicator'

         password: '********'

     rewind:

         username: 'rewind_user'

         password: '*******'

parameters:

     unix_socket_directories: '/var/lib/pgsql/'

tags:

nofailover: false

noloadbalance: false

clonefrom: false

nosync: false

Далее нам придется изучить и разобраться со всеми нужными параметрами конфигурации, это займет еще 1,5-2 часа. 

Запускаем Patroni в виде сервисов

Следующий шаг — организовать запуск Patroni в виде сервисов. На этом этапе могут возникнуть различные проблемы:

  • запускается старый Python вместо нужного Python 3;

  • отсутствуют пути к бинарникам или они неверные;

  • репликация не работает из-за отсутствия роли replicator;

  • несуществующие каталоги и файлы (если конфиг взят из сети);

Все эти ошибки – не страшные, они легко ищутся по логам и исправляются, но некоторое время тоже отнимают. 

Далее нужно скорректировать конфиг и раскатать его на все три сервера.

Проверяем работу кластера

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

Но если оглянуться назад, то понимаем, что на всё ушло минимум 3-5 часов (в зависимости от навыков и компетенций). И на текущий момент у нас есть только:

  • настроенное распределенное хранилище конфигураций;

  • 3 сервера PostgreSQL (в кластерной конфигурации) c физической репликацией под управлением Patroni.

Настраиваем единую точку подключения

А вот единого кластерного IP и единой точки подключения у нас пока нет. Для этого настраиваем Keepalived и HAProxy.

Keepalived – не сложный сервис, но в нем есть свои подводные камни. Вспоминаем, как работает Keepalived и протокол VRRP (что кому рассылает и как реагирует), читаем полезные материалы, узнаем, что будет, если вдруг займем чужой virtual_router_id. 

Далее выделяем IP для VIP, подбираем конфиги для 3-узловой конфигурации (благо примеров для связки Keepalived с HAProxy много):

Скрытый текст

global_defs {

router_id db01

}

vrrp_script haproxy {

script "/usr/bin/killall -0 haproxy"

interval 2

weight 2

}

vrrp_instance admin-1 {

state MASTER

virtual_router_id 50

advert_int 1

priority 150

interface eth0

}

virtual_ipaddress {

     192.xxx.yyy.10/32 dev eth0

}

track_script {

     haproxy

}

}

(Опять) решаем проблемы с пакетами и утилитами

При настройке Keepalived и HAProxy мы наверняка столкнемся с отсутствием некоторых пакетов и утилит, необходимых для работы скриптов из примеров. Например, утилита killall не установлена по умолчанию.

Быстро решаем найденные проблемы, запускаем сервис, запускаем пинги на VIP и проверяем его работу:

  • поочередно отключаем сетевые интерфейсы на узлах;

  • убеждаемся, что VIP «прыгает» между узлами;

  • проверяем, что VIP возвращается на изначальный узел;

Настраиваем маршрутизацию запросов от VIP до ведущей (primary) ноды PostgreSQL в кластере Patroni 

Нам важно полученный на VIP сетевой трафик отправлять именно на ведущую (primary) ноду PosrgreSQL в кластере Patroni. Для этой цели мы используем HAProxy.

HAProxy — наверное, самый популярный сетевой сервис из всех вспомогательных сервисов, используемых в нашем кластере. Примеров конфигов для него много. HAProxy проверяет узлы Patroni и всегда перенаправляет трафик со своего выделенного порта (тот, что слушается) на порт PostgreSQL на том узле, где отвечает Patroni именно как лидер (лидер Patroni  = primary PostgreSQL) . 

Пример конфигурационного файла HAProxy:

Скрытый текст

global

    log 127.0.0.1 local2

    chroot /var/lib/haproxy

    stats socket /run/haproxy/admin.sock mode 660 level admin

    stats timeout 30s

    user _haproxy

    group _haproxy

    daemon

    maxconn 300

defaults

    log global

    mode tcp

    retries 2

    mode http

    option http-server-close

    timeout client 50s

    timeout server 50s

    timeout connect 8s

    timeout check 5s

listen stats

    mode http

    bind *:7000

    stats enable

    stats uri /

frontend patroni_front

    bind *:5433

    default_backend patroni_backend

backend patroni_backend

    option httpchk

    http-check expect status 200

    default-server inter 3s fall 3 rise 2

    server db01 db01:5432 maxconn 100 check port 8008

    server db02 db02:5432 maxconn 100 check port 8008

    server db03 db03:5432 maxconn 100 check port 8008

Настраиваем HAProxy, проверяем его — работает! 

В общем-то, на этом всё. Кластер работает, трафик с VIP-а правильно маршрутизируется (хотя может оказаться, что после тестового или аварийного выключения одного из узлов кластера «мастер» Keepalived, на котором находится выделенный Virtual IP, и новый primary PostgreSQL окажутся на разных узлах (из-за различий в алгоритмах и протоколах выбора “мастеров”), появится некий «диагональный» трафик между узлами кластера. Не критично, но с этим тоже придется повозиться.

Стоит ли это потраченного времени?

Если вашей целью было потренироваться и освежить знания по куче сетевых технологий, сервисов и протоколов, а заодно с этим – самостоятельно собрать рабочий кластер PostgreSQL из доступных компонентов, то можно считать, что всё прошло отлично. Если выполнять развертывание кластера осознанно и без спешки, потребуется примерно 1–1,5 дня.

Но если оценивать результат как реальную бизнес-задачу, картинка получается не такая оптимистичная. В нашем примере на базе Astra Linux мы получили: 

  • старые версии PostgreSQL и Patroni; 

  • открытые вопросы безопасности внутри etcd и Patroni; 

  • не очень оптимальные дефолтные настройки бэкапа и восстановления узлов кластера (по умолчанию восстановление узла кластера требует full backup на primary PostgreSQL, что в общем-то может быть достаточно ресурсозатратно); 

  • нет балансировки запросов на чтение (хотя это несложно сделать через HAProxy);

  • нет пулера подключений, который желателен, а иногда просто необходим при более чем 300-400 сетевых подключениях. Можно “прикрутить” почти дефолтный PgBouncer, но он однопоточный и сам может “уронить” узел PostgreSQL с ролью primary только своей нагрузкой, если рассматривать другие альтернативные и высокопроизводительные, многопоточные пулеры, например Odyssey, то готового билда Odyssey под PG16 нет, опять же нужно собирать.

Кластер работает, но еще много мест, которые нужно оптимизировать. Изучение вариантов реализации, сама реализация и тесты займут еще от 2-3 дней до 2 недель в зависимости от объема дополнений. При этом не забываем, что нам нужно задокументировать каждый шаг, чтобы обеспечить повторяемость конфигурации при переносе, например, из тестовой среды в прод.

Proxima DB: разворачиваем кластер за 4 шага

Можно сказать что все, написанное выше — это очень большая вводная :) Но без краткого знакомства с путем самостоятельной «сборки» кластера, описание развертывания в Proxima DB было бы не таким увлекательным! Итак, следим за руками и замечаем отличия.

Весь процесс развертывания в Proxima DB можно кратко описать в одном предложении. Вам потребуется лишь ввести минимально необходимый набор данных на 4 шагах мастера настройки и подождать 10–15 минут, пока кластер будет развернут.

Шаг 1: Задаем базовые параметры кластера. 

Вводим название и описание юнита (в терминологии Proxima DB юнит — это единица развернутой СУБД, причем не важно, кластерной или не кластерной) — в нашем случае это наименование и назначение будущей СУБД. 

Выбираем тип «Распределенный» — это указывает Proxima DB, что мы будем разворачивать именно кластерную конфигурацию. 

Шаг 2. Тот самый шаг с вводом минимально обязательных данных. 

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

  • тип выбранной кластерной конфигурации;  

  • Virtual IP;

  • сетевые интерфейсы, которые мы будем использовать на всех серверах; 

  • логины и пароли;

  • FQDN узлов для развертывания нод кластера с указанием конкретного узла, который после развертывания станет лидером;

        

Шаг 3. Дополнительные параметры PostgreSQL (опционально). 

На этом шаге можно сразу указать дополнительные параметры для PostgreSQL, например, задать work_mem и shared_buffers в зависимости от ресурсов, выделенных для физических или виртуальных машин, на которые выполняется развертывание. Однако все эти параметры можно задать позже и пропустить шаг 3.   

Шаг 4. Просмотр и верификация всех параметров будущего кластера.

Ввод каких-либо данных тут не требуется. 

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

На выходе мы получаем полностью готовый и сконфигурированный отказоустойчивый кластер на базе PostgreSQL 16.2 с поддержкой 1С. При этом кластер включает свежие сборки компонентов: 

  • Patroni (конечно, с поддержкой 16.х PostgreSQL);

  • etcd;  

  • HAProxy; 

  • Keepalived;  

  • Odyssey. 

И всё это полностью совместимо с Astra Linux 1.7.5. 

Для кластера сразу же настроены 4 порта подключения в следующих вариациях:

  • напрямую без пулера и без балансировки запросов на чтение;

  • напрямую без пулера и с балансировкой запросов на чтение;

  • через пулер и без балансировки запросов на чтение;

  • через пулер и с балансировкой запросов на чтение.

Для БД кластера тут же можно настроить и включить план резервного копирования (полного и инкрементного), либо выполнять РК вручную по необходимости. Если понадобится повторно развернуть новый кластер с точно такой же конфигурацией, можно сделать копию существующей. Это позволит развернуть новый узел или кластер и сократить и без того небольшие затраты на конфигурирование.   

   

Делаем выводы

Еще раз подчеркну: этим постом я не отрицаю, а наоборот — еще раз подтверждаю возможность собрать велосипед кластер СУБД на базе PostgreSQL и Patroni своими руками. Эта задача действительно посильна, для прохождения квеста есть все инструкции, сам квест не такой уж и сложный и не имеет каких-то непонятных ветвлений. Потратив от одного дня до… наверное, семи дней, такой кластер сможет собрать даже специалист  не с очень глубокими знаниями Linux.

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

Факт остается фактом: готовый инструмент позволяет развернуть кластер за 15 минут. А возможность сохранять и переиспользовать конфигурации избавляет от необходимости каждый раз все документировать и гарантирует наличие «точно такой же среды» во всех контурах. 

Если для вас важны скорость и сокращение Time-to-Market, экономия ресурсов и снижение требований к скиллам администраторов СУБД, решения вроде Proxima DB могут оказаться очень кстати.

Комментарии (21)


  1. MixaSg
    26.08.2024 11:56
    +1

    Я что-то не понял. Если мы кластер прячем за haproxy, то зачем на кластере keepalived? Мы же с помощью haproxy проверяем, кто является master, зачем им в этом случае общий IP? Я понимаю, если у нас два хоста с haproxy, тут VIP нам позволит обеспечить один и тот же адрес подключения.


    1. Kliffoth
      26.08.2024 11:56

      Мы поднимаем ванильные кластера PosrtgreSQL+Patroni+etcd+haproxy в полном комплекте, сразу готовые к работе за 15 минут. Дело проработки сценариев развертывания.


      1. MixaSg
        26.08.2024 11:56
        +3

        Это не ответ на мой вопрос.


        1. Kliffoth
          26.08.2024 11:56

          Промазал с местом размещения. Это к статье.


    1. SGontzov Автор
      26.08.2024 11:56

      Скажу больше, у нас даже 3 хоста с HAProxy. И, конечно же, нам в кластере нужна единая точка подключения. Patroni умеет переключать роль лидера, контролирует ее доступность, сам может «восстанавливать» узлы PostgreSQL, которые, например, возвращены из обслуживания, но не умеет управлять входящим трафиком. HAProxy, подсматривая за тем, кто лидер в Patroni, умеет направлять трафик на лидера PostgreSQL, но HAProxy по-прежнему не дает нам единой точки подключения. Как раз Keepalived и дает нам VIP, поддерживая его в нужном активном состоянии на одном из узлов кластера.


      1. MixaSg
        26.08.2024 11:56

        Бога ради, haproxy делайте сколько необходимо. Но повторю, не понимаю, зачем нужен keepalived на серверах СУБД, тем более в статье выше написано об опасности расхождения VIP адреса и master PG. Почему haproxy не проверяет статус серверов через простой health check script, в этом случае keepalived лишний.


        1. Kotofeus
          26.08.2024 11:56

          keepalived обеспечивает VIP для балансировки, в т.ч. прозрачно через dns.

          Не то чтобы это самый прогрессивный метод, но он работает.


  1. shurutov
    26.08.2024 11:56
    +2

    начиная с 10-й версии постгрес умеет в несколько хостов в строке подключения. И умеет в определение к мастеру подключение идёт, или как попало. А сейчас это определение умеет ещё больше всяких вкусностей. Соответственно, у меня тот же непраздный вопрос: а зачем общий IP и лишние сущности в виде haproxy&keepalived?

    И второе у вас гвоздями etcd в качестве DCS прибит? зачем? Патроны не только в etcd умеют.

    И да, у меня есть ролей для ансибла. Поэтому разворачивание кластера (причём не только патрони+етцд) - меньше 15 минут с учётом определения всех нужных данных.

    Во! У меня одиссея нет, зато есть пгбаунсер. ВотЪ.

    ЗЫ. Если есть патроны, то haproxy совсем не надо дёргать внешний скрипт, вполне себе достаточно http-check expect status 200 на нужный урл, который отдают патроны.


    1. Kotofeus
      26.08.2024 11:56

      В строке подключения какого клиента? CLI, Java, PHP, python, rust, javascript, ruby, c++, go, perl?


      1. shurutov
        26.08.2024 11:56

        Любой клиент, использующий libpq + Java (у неё своя собственная библиотека, но данная возможность присутствует, проверено).


        1. PrinceKorwin
          26.08.2024 11:56

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


          1. RekGRpth
            26.08.2024 11:56
            +1

            смотрите опцию target_session_attrs


            1. PrinceKorwin
              26.08.2024 11:56

              Спасибо!


          1. shurutov
            26.08.2024 11:56

            Я прошу прощения, но хабр (именно хабр! на других ресурсах такого ограничения нет, увы мне) не позволяет Ctrl+V/Shift+Ins (ссылки были бы в предыдущем посте). Раздел "libpq - библиотека для языка C" в документации.

            А ручками набирать - извините...


  1. vitaly_il1
    26.08.2024 11:56

    ИМХО, Proxima не отменяет необходимость понимать все упомянутые компоненте. То есть поднять демо за 10 минут - отлично, но для для production-ready все равно надо учиться и учиться.


  1. vitaly_il1
    26.08.2024 11:56

    ИМХО, Proxima не отменяет необходимость понимать все упомянутые компоненты. То есть поднять демо за 10 минут - отлично, но для для production-ready все равно надо учиться и учиться.


    1. ildarz
      26.08.2024 11:56

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


  1. SargeTDK
    26.08.2024 11:56

    Если автоматизировать всё через terraform | ansible, то так же будет деплой за 15 минут :-)