Deployment guide
This guide is in Russian; an English version is pending.
Для оператора, который ставит Postwind в свою инфраструктуру: один сервер (Docker Compose) или кластер Kubernetes (Helm chart). Нормативная часть — docs/spec/13-deployment.md: артефакты, сетевые требования, полный справочник конфига, команды CLI. Здесь — порядок действий; где правило уже записано в ТЗ, стоит ссылка, а не копия. Настройка DNS и репутации отправки — чеклист доставляемости.
Что разворачивается
Один статический бинарь postwind в образе distroless (UID 65532). Внутри — подсервисы, каждый включается флагом конфига (docs/spec/03-architecture.md, «Подсервисы и роли»):
| Подсервис | Флаг | Порт | Кто к нему ходит |
|---|---|---|---|
API, /healthz, /readyz, /metrics |
api.enabled |
8080 | Ваши сервисы и агенты; Prometheus. Внутренний |
MCP (/mcp на слушателе API) |
mcp.enabled |
8080 | Агенты. Внутренний |
| Публичный веб: отписка, подтверждение, пиксель, редиректы | web.enabled |
8081 (HTTP), 443 (HTTPS при web.tls.mode ≠ off) |
Получатели писем, интернет |
| Ядро доставки: рассылка кампаний, перепроверка доменов | delivery.enabled |
— (исходящий 25 к MX получателей) | — |
| Входящий SMTP: DSN, жалобы ARF, mailto-отписки | smtp.inbound.enabled |
25 | Почтовые серверы интернета |
Хранилище — только ClickHouse (docs/spec/05-storage-clickhouse.md). В Kubernetes он внешний: оператор ClickHouse или managed-сервис; chart его не ставит. Требования:
- HTTP-интерфейс (
8123),storage.clickhouse.url = "http://...".https://пока не поддерживается:serveс ним не стартует (вопрос 13 вdocs/spec/15-out-of-scope.md); держите ClickHouse в частной сети или за TLS-туннелем. - Пользователь с правом создавать базу и таблицы в
storage.clickhouse.database(миграции на старте создают базу, если её нет). - Для нескольких узлов (
coordination.mode = keepermap): ClickHouse Keeper (<zookeeper>) иkeeper_map_path_prefix; минимальный конфиг —deploy/compose/clickhouse/keeper.xml. С реплицированными таблицами —storage.clickhouse.sequential_consistency = true. - Версия, на которой проверяется Postwind: 26.8 LTS (
clickhouse/clickhouse-server:26.8.36.3).
Образ
Реестра образов у Postwind пока нет: образ собирают из корня репозитория и кладут в свой реестр.
docker build --build-arg POSTWIND_GIT_COMMIT="$(git rev-parse --short HEAD)" -t registry.example.com/postwind:0.1.0 .
docker push registry.example.com/postwind:0.1.0
docker run --rm registry.example.com/postwind:0.1.0 --version
Процессор узлов должен уметь инструкции, нужные graviola (x86_64 примерно с 2014 года, aarch64 с AES/PMULL/SHA-2): без них serve стартует без транспорта direct, без DKIM-подписи и без встроенного TLS Web (docs/spec/13-deployment.md, «Артефакты»).
Секреты
| Секрет | Что делает | Как сделать | Если потерян |
|---|---|---|---|
Мастер-ключ (security.master_key_file) |
Шифрует хранимые секреты: DKIM-ключи доменов, ключи TLS-сертификатов и аккаунта ACME | head -c 32 /dev/urandom > master_key (ровно 32 байта; hex или base64 этих байт тоже годится) |
Секреты в ClickHouse не открыть: DKIM-ключи всех доменов придётся сделать заново (новые записи DNS). Храните копию вне кластера |
Ключи токенов (security.token_keys) |
Подписывают ссылки писем (отписка, подтверждение, трекинг) и адреса bounce+<token> |
head -c 32 /dev/urandom > token_k1 (не короче 32 байт) |
Ссылки и bounce-адреса уже отправленных писем перестают работать |
Пароль ClickHouse (storage.clickhouse.password_file) |
Доступ к хранилищу | Ваш ClickHouse | — |
Bootstrap admin-ключ (security.bootstrap_admin_key_file, необязателен) |
Первый admin-ключ без команды в контейнере | Рецепт ниже | Создать новый ключ postwind keys create-admin |
| Пароли relay, ключи Porkbun (необязательны) | Транспорт relay:<name>, DNS-провайдер Porkbun |
У провайдера | — |
Ключевой материал — высокоэнтропийные байты, не пароль: строку, которая не раскодируется как hex/base64 хотя бы в 32 байта, config check отвергает (docs/spec/13-deployment.md, «Секреты»; docs/spec/11-security.md, «Секреты at rest»).
Bootstrap admin-ключ — строка pw_adm_<prefix>_<secret>, которую оператор делает сам (рецепт из docs/spec/13-deployment.md, «Bootstrap admin-ключ»; тест проверяет рецепт из ТЗ):
printf 'pw_adm_%s_%s\n' "$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')" "$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n')" > admin_key
serve регистрирует его при старте, только если admin-ключей в хранилище ещё нет; отозванный bootstrap-ключ перезапуск не возвращает. Этой строкой вызывают API (Authorization: Bearer pw_adm_...) — храните её как пароль администратора.
Один сервер: Docker Compose
deploy/compose/: Postwind и ClickHouse со встроенным Keeper на одном хосте (docs/spec/13-deployment.md, «Артефакты»).
-
Хост: Linux с Docker и compose; открыты входящие 25 и 443 (или 8081 за вашим прокси) и исходящий 25 («Сетевые требования» ниже).
-
Отредактируйте
deploy/compose/postwind.toml:web.public_url— адрес, по которому получатели открывают ссылки; пулdefault—public_ipхоста и егоhostname(PTR). За NAT Docker пул состоит из одного адреса:ip = "",public_ip— внешний адрес. Остальные ключи —docs/spec/13-deployment.md, «Конфиг: справочник». -
Запуск:
cd deploy/compose docker compose up -d --build curl -s localhost:8080/readyz # 200, когда ClickHouse ответил и миграции примененыОдноразовый сервис
secretsпри первом запуске кладёт в томsecretsпароль ClickHouse, мастер-ключ и первый ключ токенов; существующие файлы не трогает. -
Первый admin-ключ (строка печатается один раз):
docker compose exec postwind postwind keys create-admin -
Домен отправки создаётся через API или MCP; требуемые DNS-записи —
GET /api/v1/projects/{project_id}/domains/{domain_id}/recordsилиdocker compose exec postwind postwind domain records <домен>(чеклист). -
HTTPS публичного веба: либо ваш прокси перед 8081 (
web.tls.mode = "off",web.trusted_proxies— адрес прокси), либо встроенный ACME:web.tls.mode = "acme",web.acme.contact_email,web.acme.accept_tos = trueи порт443:443вdocker-compose.yml. -
Резервные копии: том
clickhouse(данные) и томsecrets(мастер-ключ!).docker compose down -vудаляет оба.
Kubernetes: Helm chart
deploy/helm/postwind: chart для вашего кластера. Значения по умолчанию и комментарии — values.yaml; готовые наборы — examples/single-node.yaml и examples/multi-node.yaml.
Как устроен chart
<release> ниже — базовое имя объектов chart: имя release, если в нём есть postwind (release postwind → postwind, postwind-api, postwind-secrets), иначе <имя release>-postwind. Примеры команд — для release postwind в namespace mail.
-
Конфиг. Ключ
configв values — тот же конфиг, что TOML изdocs/spec/13-deployment.md, «Конфиг: справочник», записанный в YAML: те же секции и ключи. Chart рендерит его вpostwind.toml(ConfigMap<release>-config, путь/etc/postwind/postwind.toml) — один конфиг для всех ролей: узлам API тоже нужны пулы иweb.public_url(по ним строятся требуемые DNS-записи). Значение"off"пишите в кавычках: YAML читаетoffкакfalse(chart это переживает, но лучше явно). -
Что chart держит сам и не принимает в
config. Флаги подсервисов (api/mcp/web/delivery/smtp.inbound.enabled) — их задают значения ролей (api.enabled,web.enabled, …), а chart выставляет каждому Deployment переменнымиPOSTWIND__API__ENABLEDи т. д. (docs/spec/13-deployment.md, «Переопределение через окружение»). Все секреты и их*_file— из Kubernetes Secret (ниже). Такой ключ вconfig— ошибка рендера с подсказкой. -
Режим координации (
config.coordination.mode) выбирает форму установки:local(по умолчанию) — вся установка один процесс: один Deployment<release>со всеми включёнными подсервисами,replicas: 1, обновлениеRecreate. Арендыlocalживут в памяти процесса; второй под на той же базе отправил бы тот же шард кампании ещё раз (docs/spec/06-delivery.md, «Координация узлов»). Значенияreplicasиpodролей в этом режиме не действуют.keepermap— аренды в ClickHouse KeeperMap; Deployment на роль:<release>-api(API+MCP),<release>-web,<release>-delivery(доставка + входящий SMTP — рольdelivery+smtp.inboundизdocs/spec/03-architecture.md). Роли масштабируются и размещаются независимо. Нужен ClickHouse с Keeper иkeeper_map_path_prefix: без них узел на старте не создаёт таблицу аренд и выходит с кодом 1 (docs/spec/13-deployment.md, «Запуск»). Релизы Postwind до кластерного координатора (тикет P8-1)keepermapне принимают (error: coordination.mode: ...).
-
Пробы и метрики.
/healthz,/readyzи/metricsесть только на слушателе API (docs/spec/03-architecture.md, «Наблюдаемость»). Поэтому подыwebиdeliveryпо умолчанию тоже запускают задачуapi(web.apiListener,delivery.apiListener; MCP у них выключен) — для readiness по/readyzи для метрик движка доставки. Это полноценный API на 8080 пода (ключи API его защищают), но в Service<release>-apiтакие поды не входят. Без него у подаwebпроба —/healthzслушателя Web, у пода доставки — TCP 25 или никакой, и метрик нет (вопрос 14 вdocs/spec/15-out-of-scope.md). -
Services (
docs/spec/13-deployment.md, «Артефакты»):Service Тип Порты Когда <release>-apiClusterIP 8080 api.enabled<release>-webClusterIP + Ingress <release>-web80 → 8081 web.tls.mode = off: TLS на вашем Ingress<release>-webLoadBalancer 443 → 443, 80 → 8081, при входящем SMTP ещё 25 → 25 web.tls.modefilesилиacme: TLS в Postwind, проверка TLS-ALPN-01 должна дойти до самого пода<release>-smtpLoadBalancer 25 входящий SMTP при web.tls.mode = off(илиsmtp.inbound.service.shareWebLoadBalancer: false)<release>-metricsheadless 8080 serviceMonitor.enabled -
Безопасность пода. Pod Security Standard
restricted: UID/GID 65532,allowPrivilegeEscalation: false,capabilities: drop [ALL], add [NET_BIND_SERVICE]. Подробно — «Pod Security и порты ниже 1024».
Kubernetes: секреты
Chart монтирует один Secret (по умолчанию <release>-secrets, secrets.name) в /run/secrets/postwind и сам вписывает в конфиг пути *_file. Имена записей Secret:
| Запись | Когда нужна | Ключ конфига |
|---|---|---|
master_key |
всегда | security.master_key_file |
previous_master_key |
secrets.previousMasterKey: true (ротация) |
security.previous_master_key_file |
clickhouse_password |
secrets.clickhousePassword: true (по умолчанию) |
storage.clickhouse.password_file |
token_<id> |
на каждый id из secrets.tokenKeys (по умолчанию [k1]), первый подписывает |
security.token_keys |
relay_<name>_password |
на каждое имя из secrets.relayPasswords (relay из config.relays) |
relays.<name>.password_file |
porkbun_api_key, porkbun_secret_api_key |
secrets.porkbun: true |
dns.porkbun.*_file |
Рекомендуемый путь — Secret делаете вы, chart его только читает (secrets.create: false, по умолчанию). Ключи не попадают ни в values, ни в историю релиза Helm:
head -c 32 /dev/urandom > master_key
head -c 32 /dev/urandom > token_k1
printf '%s' 'пароль-clickhouse' > clickhouse_password
kubectl -n mail create secret generic postwind-secrets \
--from-file=master_key --from-file=token_k1 --from-file=clickhouse_password
Bootstrap admin-ключ — отдельный Secret с записью admin_key (рецепт — «Секреты» выше):
kubectl -n mail create secret generic postwind-admin-key --from-file=admin_key
и в values bootstrapAdminKey.secretName: postwind-admin-key; chart монтирует его в /run/secrets/postwind-admin/admin_key и задаёт security.bootstrap_admin_key_file. Ключ регистрируется при первом старте (docs/spec/13-deployment.md, «Bootstrap admin-ключ»), в логе пода — bootstrap admin key registered. Без bootstrap-ключа первый ключ делают командой в работающем поде (образ distroless без shell, но бинарь вызывается напрямую):
kubectl -n mail exec deploy/postwind -- postwind keys create-admin # local: Deployment <release>
kubectl -n mail exec deploy/postwind-api -- postwind keys create-admin # keepermap
secrets.create: true — Secret делает chart из secrets.values (ключевой материал — текстом hex или base64: head -c 32 /dev/urandom | xxd -p -c 64). Тогда ключи лежат в вашем файле values и в Secret релиза Helm; изменение values перекатывает поды (аннотация checksum/secret). Свой Secret chart не отслеживает: после его изменения — kubectl rollout restart deployment -l app.kubernetes.io/instance=<release>.
Установка: один узел
examples/single-node.yaml: coordination.mode = local, встроенный ACME, входящий SMTP, пул из одного адреса за NAT узла.
-
Образ в вашем реестре (раздел «Образ»), внешний ClickHouse, Secret’ы (выше).
-
Скопируйте пример, замените
example.com, адреса документации (198.51.100.0/24),image,storage.clickhouse.url. -
Установка:
helm install postwind deploy/helm/postwind -n mail -f my-values.yaml kubectl -n mail rollout status deploy/postwind kubectl -n mail get svc postwind-web # внешний адрес LoadBalancer -
DNS: A-запись хоста
web.public_url— на внешний адресpostwind-web(на нём 443, 80 и 25). Сертификат ACME выпускается сам, когда имя начнёт указывать на этот адрес (проверка раз вweb.acme.check_interval_hours). -
kubectl -n mail port-forward svc/postwind-api 8080:8080иcurl -s localhost:8080/readyz→ 200;helm test postwind -n mailделает тот же запрос из кластера.
Установка: несколько узлов
examples/multi-node.yaml: coordination.mode = keepermap; API ×2, Web ×2 за Ingress (TLS — cert-manager), доставка ×2 на hostNetwork с пулами адресов, входящий SMTP, relay ses как запасной транспорт, ServiceMonitor, PodDisruptionBudget.
- ClickHouse с Keeper и
keeper_map_path_prefix; таблицу арендleasesPostwind создаёт сам на старте. Часы узлов синхронизированы (NTP) (docs/spec/05-storage-clickhouse.md, «KeeperMap»)./readyzв этом режиме проверяет и Keeper (полеkeeper). - Узел доставки берёт кампании только тех пулов, у которых есть адрес на его интерфейсах (
docs/spec/06-delivery.md, «Конвейер кампании», шаг 3 «Обнаружение работы»), поэтому узлы с разными адресами делят работу по пулам сами. - Web с
web.tls.mode = acmeв v1 — один под: ответ на TLS-ALPN-01 знает только под, оформивший заказ (вопрос 10 вdocs/spec/15-out-of-scope.md); chart не рендеритacmeсweb.replicas > 1. Несколько подов Web —web.tls.mode = offза Ingress илиfilesс сертификатом из Secret (web.tlsFilesSecret, типkubernetes.io/tls, например от cert-manager; смена файлов — перезапуск подов). - За Ingress укажите
config.web.trusted_proxies— сеть подов ingress-контроллера: лимитweb.rate_limit_per_ip_per_minuteсчитает адрес изX-Forwarded-Forтолько от них. Каждыйtracking_hostдоменов отправки — вweb.ingress.extraHostsи в TLS Ingress. mcp.allowed_hostsпо умолчанию —localhostи имена Service<release>-api(<release>-api.<namespace>.svc…): MCP отвергает запрос с другимHost(docs/spec/13-deployment.md, «postwind config check»). Ходят к MCP по другому имени — задайтеconfig.mcp.allowed_hosts.
Узлы доставки и адреса пулов
Транспорт direct шлёт с адресов пула, поднятых на интерфейсе узла; адрес, которого на узле нет, этот узел не использует (docs/spec/06-delivery.md, «Адаптер direct»; docs/spec/13-deployment.md, «Сетевые требования для direct-доставки»).
- Один адрес за NAT (обычный под, без
hostNetwork): пул из одного элемента{ ip: "", public_ip: "<внешний адрес egress>", hostname: "..." }. Так устроен пример single-node. Egress узлов должен выходить ровно с этого адреса (NAT-шлюз со статическим адресом). - Несколько адресов: выделенные узлы доставки с дополнительными адресами на интерфейсе и
delivery.pod.hostNetwork: true(плюсnodeSelector/tolerationsна эти узлы), либо отдельные VM вне кластера с тем же конфигом. Chart для подов наhostNetworkставитdnsPolicy: ClusterFirstWithHostNet(имя ClickHouse в кластере резолвится), обязательную anti-affinity по узлу и обновлениеmaxSurge: 0(два пода на узле заняли бы одни порты). Под наhostNetworkслушает 8080 (и 25 со входящим SMTP) на адресе узла: закройте 8080 снаружи или выключитеdelivery.apiListener. postwind config check(и логserve) предупреждает, если ни один адрес пулов не поднят на интерфейсах узла;smtp.outbound.local_ipsсужает набор адресов узла.
Входящий SMTP
MX bounce-поддомена каждого домена отправки — хост web.public_url (docs/spec/10-dns-adapters.md, «Требуемые записи»), поэтому TCP 25 должен отвечать на том же адресе, куда резолвится web.public_url.
web.tls.modefiles/acme: chart добавляет 25 в LoadBalancer<release>-web(smtp.inbound.service.shareWebLoadBalancer: true, по умолчанию) — один адрес на всё.web.tls.mode = off(Web за Ingress):<release>-smtp— отдельный LoadBalancer. Дайте ему адрес Ingress (аннотация общего адреса вашего балансировщика, напримерmetallb.universe.tf/allow-shared-ip, иloadBalancerIP) или пробросьте TCP 25 ingress-контроллера на<release>-smtp(у ingress-nginx —tcp-services).externalTrafficPolicy: Local(по умолчанию) сохраняет адрес клиента: по нему считаютсяsmtp.inbound.max_connections_per_ipиmax_messages_per_minute_per_ip.
Pod Security и порты ниже 1024
Под по умолчанию проходит Pod Security Standard restricted: UID/GID 65532, runAsNonRoot, allowPrivilegeEscalation: false, seccompProfile: RuntimeDefault, capabilities: drop [ALL], add [NET_BIND_SERVICE], readOnlyRootFilesystem. NET_BIND_SERVICE нужна, чтобы слушать 25 (входящий SMTP) и 443 (TLS Web в Postwind) не от root (docs/spec/13-deployment.md, «Артефакты»).
- Без
add: [NET_BIND_SERVICE]приdrop: [ALL]под не стартует:exec /usr/local/bin/postwind: operation not permitted. Бинарь образа несёт файловую capabilitycap_net_bind_service, а ядро не запускает такой бинарь, если bounding set её не допускает. - Добавленную capability процессу UID 65532 выдаёт сам container runtime (containerd; проверено в kind: у процесса
CapEff=NET_BIND_SERVICEприNoNewPrivs: 1), поэтомуallowPrivilegeEscalation: falseне мешает слушать 25 и 443. Runtime, который не выдаёт добавленные capability процессу не от root, оставляет только файловую capability, а еёno_new_privsотключает: тогда либоsecurityContext.allowPrivilegeEscalation: true, либо слушатели на портах ≥ 1024 —config.web.tls.listen: "0.0.0.0:8443",config.smtp.inbound.listen: "0.0.0.0:2525"(Services по-прежнему публикуют 443 и 25: ихtargetPort— именованные порты пода, chart берёт номера изlisten). hostNetwork(узлы доставки с пулами адресов) Pod Securityrestrictedиbaselineне допускают: такие поды — в отдельном пространстве имён сprivileged, либо доставка на VM вне кластера.
Наблюдаемость
- Логи — JSON в stdout (
docs/spec/03-architecture.md, «Наблюдаемость»);logging.hash_emails = trueпо умолчанию. readinessProbe—/readyz(ClickHouse отвечает, миграции применены, подсервисы запущены, вkeepermap— ещё доступность Keeper),livenessProbeиstartupProbe—/healthz.startupProbeпо умолчанию ждёт до 5 минут (миграции на старте).terminationGracePeriodSeconds=server.shutdown_timeout_secs+ 10: после SIGTERM узел дописывает письма в полёте и отпускает аренды.serviceMonitor.enabled: true— ServiceMonitor Prometheus Operator над headless<release>-metrics(все поды со слушателем API,/metrics).
Значения chart
| Ключ | По умолчанию | Что |
|---|---|---|
image.repository, image.tag |
postwind, appVersion |
Образ из вашего реестра |
config |
coordination.mode = local, ClickHouse http://clickhouse:8123, web.tls.mode = off, логи JSON |
Конфиг Postwind (YAML-форма TOML) |
api.enabled, api.mcp, api.replicas |
true, true, 2 |
Роль API+MCP |
web.enabled, web.replicas, web.apiListener |
true, 2, true |
Роль Web |
web.ingress.* |
включён, без класса | Ingress при web.tls.mode = off; хост web.public_url всегда в правилах, extraHosts — tracking hosts |
web.loadBalancer.* |
externalTrafficPolicy: Local, httpPort: 80 |
LoadBalancer при files/acme |
web.tlsFilesSecret |
"" |
Secret kubernetes.io/tls для web.tls.mode = files |
delivery.enabled, delivery.replicas, delivery.apiListener |
true, 1, true |
Роль доставки (+ входящий SMTP) |
smtp.inbound.enabled |
false |
Входящий SMTP |
smtp.inbound.service.* |
LoadBalancer, 25, shareWebLoadBalancer: true |
Service входящего SMTP |
secrets.* |
create: false, tokenKeys: [k1], clickhousePassword: true |
Secret ключей (таблица выше) |
bootstrapAdminKey.secretName |
"" |
Secret с admin_key |
pod.*, <role>.pod.* |
— | resources, nodeSelector, tolerations, affinity, topologySpreadConstraints, hostNetwork, extraEnv, extraVolumes, аннотации; блок роли поверх общего (только keepermap) |
podSecurityContext, securityContext |
restricted: UID 65532, без повышения привилегий, drop ALL + NET_BIND_SERVICE |
См. «Pod Security и порты ниже 1024» |
probes.* |
startup 5 s × 60 | Периоды и пороги проб |
podDisruptionBudget.enabled |
false |
PDB для ролей с replicas > 1 (keepermap) |
serviceMonitor.enabled |
false |
ServiceMonitor |
clusterDomain |
cluster.local |
Для имён по умолчанию в mcp.allowed_hosts |
Сетевые требования
Нормативно — docs/spec/13-deployment.md, «Сетевые требования для direct-доставки». Перед первой рассылкой:
- Исходящий TCP 25 с узлов доставки к MX получателей открыт. Большинство облаков (AWS, GCP, Azure, многие VPS) блокируют его по умолчанию — заявка провайдеру на снятие блокировки. Только relay (
delivery.default_transport = "relay:<name>", без пулов) — нужен исходящий порт relay (587/465). - Каждый адрес каждого пула: PTR на
hostnameадреса (ставит владелец адреса — хостер), A/AAAAhostnameна этот адрес, адрес поднят на интерфейсе хотя бы одного узла доставки (hostNetworkили VM). Egress через NAT — только пул из одного адреса сpublic_ip. - 443 узлов Web доступен из интернета по имени
web.public_urlи по каждомуtracking_hostприweb.tls.mode = acme(проверка TLS-ALPN-01 приходит на 443; порт 80 ACME не нужен). Перед Postwind не должно быть ничего, что завершает TLS. - Входящий TCP 25 на адресе хоста
web.public_url— для DSN и жалоб (MX bounce-поддоменов). - ClickHouse доступен узлам по HTTP; Keeper — серверу ClickHouse (
keepermap). - DNS-резолверы
dns.resolvers(по умолчанию1.1.1.1,8.8.8.8) доступны по UDP/TCP 53: через них идут MX получателей и проверка доменов. Свои резолверы — задайте их. - Исходящий HTTPS к ACME (
web.acme.directory_url) и к API DNS-провайдера, если используется.
Обновления
Нормативно — docs/spec/13-deployment.md, «Обновления».
- Миграции схемы forward-only и совместимы с предыдущей версией бинаря (expand–contract): старый бинарь работает на новой схеме, поэтому узлы обновляются по одному, без остановки. Миграции применяет первый стартующий узел (
storage.clickhouse.auto_migrate = true, по умолчанию; вkeepermap— под арендойmigrate). - Аренды делают rolling update безопасным для идущих кампаний: узел на SIGTERM дописывает письма в полёте, пишет результаты и отпускает аренды шардов; другой узел подхватывает шард сразу, а аренду упавшего узла — после
coordination.lease_ttl_secs(docs/spec/06-delivery.md, «Координация узлов»). keepermap:helm upgrade— обычный RollingUpdate (maxSurge: 1, maxUnavailable: 0; наhostNetwork—maxSurge: 0, maxUnavailable: 1).local: обновлениеRecreate— старый под останавливается до старта нового, короткий простой. Кампании продолжаются после старта (аренды и статусы в ClickHouse).- Применять миграции отдельно от старта:
config.storage.clickhouse.auto_migrate: falseи передhelm upgrade—postwind migrateновым образом с теми же ConfigMap и Secret (Job или под с томами, как у Deployment, аргументmigrate). Безauto_migrateузел с неприменёнными миграциями стартует, но не готов (/readyz503,migrations_pending). - Docker Compose:
git pull && docker compose up -d --build(миграции на старте). - Откат: схема вперёд-совместима, поэтому откат бинаря на предыдущую версию допустим; удаление колонок приходит отдельной миграцией в следующем релизе.
Ротация мастер-ключа
Порядок и команда — docs/spec/13-deployment.md, «Ротация мастер-ключа» (ADR-0029). В Kubernetes с Secret postwind-secrets (secrets.create: false) и release postwind в namespace mail:
-
Новый ключ и копия старого:
head -c 32 /dev/urandom > master_key.new kubectl -n mail get secret postwind-secrets -o jsonpath='{.data.master_key}' | base64 -d > master_key.old -
В Secret:
master_key— новый ключ,previous_master_key— старый; затемsecrets.previousMasterKey: true. Конфиг подов меняется (появляетсяsecurity.previous_master_key_file), поэтомуhelm upgradeперекатывает все поды:kubectl -n mail patch secret postwind-secrets --type merge -p "{\"data\":{\ \"master_key\":\"$(base64 < master_key.new | tr -d '\n')\",\ \"previous_master_key\":\"$(base64 < master_key.old | tr -d '\n')\"}}" helm upgrade postwind deploy/helm/postwind -n mail -f my-values.yaml --set secrets.previousMasterKey=true kubectl -n mail rollout status deployment -l app.kubernetes.io/instance=postwindШаг 3 — только когда перезапущены все поды: узел со старым конфигом не открывает секреты, которые уже записал узел с новым ключом.
-
Ротация в одном поде. Новый ключ смонтирован как
master_key, поэтому--new-key-fileуказывает на него:kubectl -n mail exec deploy/postwind-api -- postwind secrets rotate --new-key-file /run/secrets/postwind/master_key(в режиме
local—deploy/postwind). Код 0 и строкаrotated secrets to key <key_id>: ...— ротация завершена; иначе исправить причину из строкerror:и запустить снова с тем же ключом. -
Убрать старый ключ:
helm upgrade ... --set secrets.previousMasterKey=false(поды перекатываются), послеrollout status— запись из Secret:kubectl -n mail patch secret postwind-secrets --type json -p '[{"op":"remove","path":"/data/previous_master_key"}]'master_key.oldхраните, пока есть резервные копии ClickHouse, сделанные до ротации;master_key.new— туда же, где хранится копия действующего ключа.
С secrets.create: true то же делается правкой secrets.values (master_key, previous_master_key — hex или base64) и helm upgrade.
Docker Compose: те же шаги с файлами тома secrets (/run/postwind в контейнере) и строкой previous_master_key_file в postwind.toml, перезапуск — docker compose up -d, команда — docker compose exec postwind postwind secrets rotate --new-key-file /run/postwind/master_key.
Ротация ключа токенов
Первый ключ security.token_keys подписывает, остальные только проверяют (docs/spec/09-web-public.md, «Токены»). Новый ключ: запись token_<новый id> в Secret, secrets.tokenKeys: [<новый id>, <старый id>], helm upgrade (поды перекатятся). Старый ключ держат около года: им подписаны ссылки уже отправленных писем.
Резервное копирование
Нормативно — docs/spec/13-deployment.md, «Обновления»: резервное копирование ClickHouse — ответственность оператора.
- Критичные таблицы — сущности (проекты, ключи API, списки, подписчики, шаблоны, кампании, домены отправки, подавления, сертификаты TLS),
deliveriesиevents. Схема —docs/spec/05-storage-clickhouse.md, «Схема». - Агрегаты статистики восстанавливаются из
eventsпересозданием материализованных представлений; их можно не копировать. - Таблица аренд
leases(KeeperMap) не копируется: аренды живут секунды, адаптер создаёт таблицу при старте. - Инструменты:
BACKUP DATABASE postwind TO ...ClickHouse, clickhouse-backup или снимки managed-сервиса. - Вместе с копией базы храните мастер-ключ (и прошлые, пока есть копии под ними) и ключи токенов: без мастер-ключа DKIM-ключи и ключи TLS в копии не открыть.
- Проверка восстановления: поднять ClickHouse из копии, узел Postwind с тем же мастер-ключом,
/readyz200,GET /api/v1/projectsadmin-ключом.
Проверка chart
scripts/check-helm.sh (docs/spec/12-testing.md, «Проверка Helm chart»):
scripts/check-helm.sh # helm lint --strict + helm template всех наборов values
scripts/check-helm.sh --image postwind:local # + postwind config check каждой роли в образе
scripts/check-helm.sh --kind --image postwind:local # + установка в kind с одноразовым ClickHouse
Copied from Postwind docs/ops/deployment-guide.md at commit a19b2e4.