Пока сервис один и деплоится на одну машину, конфигурация через переменные окружения или файл — простое и надёжное решение. Так и стоит начинать (об этом — конфигурация через env без хаоса и слоистая конфигурация). Но когда сервисов становится больше, появляются общие параметры, секреты с ротацией, feature flags и потребность менять конфигурацию без редеплоя — встаёт вопрос о централизованном хранилище.
etcd, Consul и Vault часто упоминают через запятую, будто это взаимозаменяемые вещи. Это не так: у них разные сильные стороны и разные задачи. Эта статья — про то, какую проблему решает каждый, чем они отличаются и как не превратить конфигурацию в самое хрупкое место системы.
В статье
- Когда env и файлов перестаёт хватать
- Что решает распределённая конфигурация
- etcd: строгая консистентность и watch
- ZooKeeper: классика координации
- Consul: KV, service discovery и health checks
- Vault: секреты, динамические credentials и ротация
- Сравнение: что выбрать под задачу
- Практические паттерны интеграции
- Антипаттерны и типичные ошибки
- Практика: рабочий стенд
- Checklist: нужна ли вам распределённая конфигурация
Когда 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/appVault — мощный, но и операционно дорогой компонент. Для одного-двух статичных секретов хватит 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 демо → downChecklist: нужна ли вам распределённая конфигурация
- Есть ли реальная боль (общий конфиг, ротация секретов, discovery, hot reload) — или «может пригодиться»?
- Какую из четырёх задач вы решаете: shared config, discovery, secrets или coordination?
- Не кладёте ли вы секреты в KV вместо Vault?
- Развёрнут ли config store кластером, а не одним узлом?
- Стартует ли сервис, если config store недоступен (fallback)?
- Погашены ли watch-обновления от шторма?
Если на пункт 1 ответ «может пригодиться» — скорее всего, вам пока хватит env и файла в Git.
Комментарии