Распределённые конфигурации: etcd, Consul, Vault и когда они действительно нужны

Когда конфигурация через env и файлы перестаёт работать, какие задачи решают etcd, Consul и Vault, чем они отличаются и как не усложнить систему раньше времени

Пока сервис один и деплоится на одну машину, конфигурация через переменные окружения или файл — простое и надёжное решение. Так и стоит начинать (об этом — конфигурация через env без хаоса и слоистая конфигурация). Но когда сервисов становится больше, появляются общие параметры, секреты с ротацией, feature flags и потребность менять конфигурацию без редеплоя — встаёт вопрос о централизованном хранилище.

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

В статье

Когда env и файлов перестаёт хватать

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

  • общие параметры для нескольких сервисов — менять в десяти местах синхронно неудобно и опасно;
  • секреты, которые нужно ротировать без редеплоя каждого сервиса;
  • feature flags и A/B-тесты — поведение меняется на лету, без пересборки;
  • service discovery — сервисы появляются и исчезают, нужно знать «кто где живёт»;
  • горячее обновление — изменить параметр и чтобы сервис подхватил его без рестарта.

Если ничего из этого нет — distributed config вам не нужен, и env остаётся правильным выбором. Это первый и главный фильтр.

Что решает распределённая конфигурация

Под зонтиком «распределённой конфигурации» прячутся четыре разные задачи:

  • shared config — единый источник параметров для многих сервисов;
  • service discovery — динамический реестр «сервис → адрес»;
  • secrets management — хранение и выдача секретов с контролем доступа и ротацией;
  • coordination — распределённые блокировки, leader election (см. распределённые блокировки и координация).

Ключевой момент: ни один инструмент не закрывает все четыре одинаково хорошо. etcd силён в coordination и строгой консистентности, Consul — в service discovery, Vault — в секретах. Отсюда и выбор.

etcd: строгая консистентность и watch

etcd — распределённое key-value хранилище со строгой консистентностью на основе Raft. Это сердце Kubernetes и фундамент многих систем (CockroachDB, TiKV строятся на тех же идеях).

Сильные стороны:

  • strong consistency — чтение всегда видит последнюю подтверждённую запись;
  • watch API — подписка на изменения ключа/префикса, реакция в реальном времени;
  • lease и TTL — ключ с временем жизни: основа heartbeat, discovery и распределённых блокировок;
  • MVCC и версии — история ревизий, оптимистические обновления;
  • транзакции и CAS — атомарные условные операции (Txn: if-then-else по ревизии/значению): фундамент compare-and-swap, distributed locks и leader election.

Типичные сценарии: хранилище состояния Kubernetes, leader election, distributed locks, конфигурация, требующая немедленной консистентности. Watch делает etcd удобным для горячего обновления:

# записать и прочитать ключ
etcdctl put /config/feature/new-ui true
etcdctl get /config/feature/new-ui

# подписаться на изменения префикса — реакция в реальном времени
etcdctl watch --prefix /config/

Но watch — не единственный инструмент координации. Транзакции дают атомарность с условием: набор операций применяется, только если выполнено сравнение по ревизии или значению. Это и есть compare-and-swap — фундамент distributed locks и leader election и безопасный способ менять конфиг при конкурентных писателях (без транзакции два писателя затрут изменения друг друга — lost update).

# атомарно занять /election/leader, только если ключа ещё нет (version == 0).
# при конфликте сработает ветка failure — и мы увидим текущего лидера.
etcdctl txn <<'EOF'
version("/election/leader") = "0"

put /election/leader "nodeA"

get /election/leader

EOF

Атомарные условные обновления — общий паттерн этого класса систем, а не фишка одного, хотя реализованы по-разному: у etcd и Consul это полноценные multi-op транзакции (Consul — multi-key + CAS через ModifyIndex), у ZooKeeper — multi() (всё-или-ничего), а у Vault KV v2 — именно CAS по версии на одном ключе, а не multi-op транзакция. Подробнее, как на CAS строятся блокировки и fencing-токены, — в статье про распределённые блокировки.

etcd избыточен, если вам нужен просто «общий конфиг» без строгой консистентности и watch — тогда хватит более простого хранилища или даже файла в Git.

ZooKeeper: классика координации

ZooKeeper важен для понимания ландшафта: до etcd и Consul именно он был стандартным ответом на coordination — leader election, distributed locks, service registry. Его до сих пор встречают в старых инсталляциях Kafka, Hadoop/HBase, SolrCloud, в Keeper-сценариях ClickHouse и во внутренних платформах.

Но для нового проекта ZooKeeper редко выбирают как основное хранилище конфигурации — без конкретной причины или экосистемной зависимости берут etcd/Consul/Vault под задачу. Зато ZooKeeper хорошо подсвечивает важную мысль всей статьи: distributed config часто путают с distributed coordination, а ZooKeeper — именно про координацию, а не про конфигурацию.

Итог по нему честный: ZooKeeper стоит знать и уметь сопровождать (вы встретите его в наследованных системах), но для нового config store чаще выбирают etcd/Consul/Vault. Пощупать znode-дерево и эфемерные узлы можно в том же стенде.

Consul: KV, service discovery и health checks

Consul — это KV-хранилище, service discovery и health checks в одном продукте. Если у etcd акцент на консистентность и coordination, то у Consul — на «кто где живёт и жив ли он».

Сильные стороны:

  • service discovery — сервисы регистрируются, клиенты находят их по имени;
  • health checks — Consul сам проверяет живость и убирает мёртвые инстансы из выдачи;
  • DNS-интерфейс — сервис доступен как service.consul, без изменения кода;
  • Consul Template — генерация конфиг-файлов из KV с автоперезагрузкой;
  • service mesh (Consul Connect) — mTLS между сервисами.
# KV-хранилище
consul kv put config/db/host db.internal
consul kv get config/db/host

# зарегистрировать сервис и найти живые инстансы
consul services register -name=api -port=8080
consul catalog services
consul catalog nodes -service=api   # живые инстансы конкретного сервиса

Consul удобен там, где главная боль — динамический реестр сервисов. Для строгой координации (locks, leader election) обычно берут etcd.

Vault: секреты, динамические credentials и ротация

Vault решает отдельную задачу — управление секретами. Класть пароли в etcd или Consul небезопасно: они не для этого. Vault шифрует секреты, контролирует доступ политиками и, главное, умеет выдавать динамические credentials.

Сильные стороны:

  • static secrets — хранилище ключей/паролей с политиками доступа и аудитом;
  • dynamic secrets — Vault сам создаёт временные учётки в PostgreSQL/AWS/и др. по запросу и отзывает их по TTL;
  • PKI — Vault как внутренний удостоверяющий центр (см. Vault как собственный CA);
  • transit engine — шифрование «как сервис», не отдавая ключ наружу;
  • auth methods — AppRole, Kubernetes, Token: как сервис доказывает, что он — это он.

Главная идея динамических секретов: вместо «вечного» пароля в env сервис при старте просит у Vault временные credentials к базе. У них короткий TTL, они привязаны к конкретному запросу и отзываются автоматически.

# настроить database secrets engine (однократно)
vault secrets enable database
vault write database/config/app-db plugin_name=postgresql-database-plugin \
  connection_url="postgresql://{{username}}:{{password}}@postgres:5432/app?sslmode=disable" \
  allowed_roles="app" username="vault-admin" password="..."

# роль с TTL
vault write database/roles/app db_name=app-db default_ttl=1h max_ttl=24h \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"

# сервис получает временные креды по запросу
vault read database/creds/app

Vault — мощный, но и операционно дорогой компонент. Для одного-двух статичных секретов хватит Sealed Secrets или SOPS; Vault окупается, когда нужны ротация, динамические креды или PKI.

Сравнение: что выбрать под задачу

etcd Consul Vault
Главная задача coordination, строгий конфиг service discovery секреты
Консистентность strong (Raft) strong по умолчанию, но есть stale-чтения strong (Raft в integrated storage)
Watch / реакция да (watch) да (blocking queries)
Service discovery вручную встроенный + health checks
Секреты не предназначен не предназначен да, включая динамические
Типичный дом Kubernetes service mesh / discovery управление секретами

На практике их часто используют вместе: etcd под Kubernetes, Consul для discovery, Vault для секретов — каждый по своей задаче, а не «один вместо всех».

ZooKeeper намеренно не вынесен в таблицу как равноправный четвёртый. Это предок ландшафта по части coordination (leader election, locks, registry), который чаще наследуют в старых платформах, чем выбирают для нового config store — см. раздел выше. По строке «где силён» он стоял бы как: legacy coordination и платформенные зависимости.

Практические паттерны интеграции

Как сервис на Go (или любой другой) работает с config store:

  • Sidecar / agent — рядом с сервисом крутится агент (Consul agent, Vault agent), сервис ходит к localhost, а агент берёт на себя кеш, аутентификацию и обновление.
  • Init-контейнер — конфигурация/секреты загружаются при старте пода до запуска приложения.
  • Watch + reload — сервис подписан на изменения и горячо перечитывает конфиг.
  • Fallback — если config store недоступен, сервис должен деградировать осознанно. Для tunables и feature flags разумно стартовать на последней известной/дефолтной конфигурации (fail open). Но для секретов и критичных security-политик правильнее fail closed — не стартовать на устаревших кредах и не ослаблять политику, а честно отказать.

Эскиз watch+reload на Go с etcd:

cli, _ := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"}})
// стартовое чтение с fallback на дефолт
cfg := loadOrDefault(cli, "/config/app")

// горячее обновление: подписка на префикс
go func() {
    for resp := range cli.Watch(ctx, "/config/app", clientv3.WithPrefix()) {
        for range resp.Events {
            cfg.Store(loadOrDefault(cli, "/config/app")) // атомарная замена
        }
    }
}()

Ключевой принцип: config store — это зависимость, а зависимости падают. loadOrDefault и атомарная замена конфигурации важнее, чем кажется.

Антипаттерны и типичные ошибки

  • Слишком ранний переход — distributed config для одного сервиса на одной машине: вся сложность, ноль пользы.
  • Конфигурация как замена деплоя — менять поведение через флаги вместо нормального релиза до тех пор, пока система не превращается в неотлаживаемый клубок.
  • Секреты в etcd/Consul без шифрования — KV не предназначен для паролей; для секретов есть Vault.
  • Single point of failure — один экземпляр etcd/Consul/Vault. Это кворумные системы, их разворачивают кластером (3/5 узлов).
  • Watch без debounce — шторм обновлений при частых изменениях кладёт сервис; обновления нужно гасить.
  • Неатомарное обновление связанных ключей — несколько отдельных put вместо одной транзакции: watch увидит промежуточное состояние (полуобновлённый конфиг), а конкурентные писатели затрут изменения друг друга. Связанные ключи меняют одной транзакцией — если backend её поддерживает; иначе связанную конфигурацию держат одним документом/ключом с версией (CAS).
  • Нет fallback — config store недоступен, и все сервисы не стартуют. Зависимость от конфигурации стала зависимостью доступности.
  • Хранение бинарей в KV — KV для небольших значений, а не для артефактов.

Практика: рабочий стенд

Чтобы пощупать всё это руками, есть готовый стенд в digital-cookbook: distributed-config. Одной командой поднимаются etcd, ZooKeeper, Consul, Vault и PostgreSQL, а demo-скрипты показывают:

  • etcd: put/get/watch, реакцию на изменение и транзакцию (CAS / leader election);
  • ZooKeeper: persistent znode (config) и ephemeral znode (liveness);
  • Consul: KV и регистрацию/поиск сервиса;
  • Vault: выдачу динамических credentials к PostgreSQL с TTL.
bash scripts/smoke.sh   # up → etcd / zookeeper / consul / vault демо → down

Checklist: нужна ли вам распределённая конфигурация

  1. Есть ли реальная боль (общий конфиг, ротация секретов, discovery, hot reload) — или «может пригодиться»?
  2. Какую из четырёх задач вы решаете: shared config, discovery, secrets или coordination?
  3. Не кладёте ли вы секреты в KV вместо Vault?
  4. Развёрнут ли config store кластером, а не одним узлом?
  5. Стартует ли сервис, если config store недоступен (fallback)?
  6. Погашены ли watch-обновления от шторма?

Если на пункт 1 ответ «может пригодиться» — скорее всего, вам пока хватит env и файла в Git.

Обсуждение в Telegram

Присоединиться →

Комментарии