Generated from Postwind commit a19b2e4 OpenAPI 0.1.0 synced

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, «Артефакты»).

  1. Хост: Linux с Docker и compose; открыты входящие 25 и 443 (или 8081 за вашим прокси) и исходящий 25 («Сетевые требования» ниже).

  2. Отредактируйте deploy/compose/postwind.toml: web.public_url — адрес, по которому получатели открывают ссылки; пул default — public_ip хоста и его hostname (PTR). За NAT Docker пул состоит из одного адреса: ip = "", public_ip — внешний адрес. Остальные ключи — docs/spec/13-deployment.md, «Конфиг: справочник».

  3. Запуск:

    cd deploy/compose
    docker compose up -d --build
    curl -s localhost:8080/readyz      # 200, когда ClickHouse ответил и миграции применены
    

    Одноразовый сервис secrets при первом запуске кладёт в том secrets пароль ClickHouse, мастер-ключ и первый ключ токенов; существующие файлы не трогает.

  4. Первый admin-ключ (строка печатается один раз):

    docker compose exec postwind postwind keys create-admin
    
  5. Домен отправки создаётся через API или MCP; требуемые DNS-записи — GET /api/v1/projects/{project_id}/domains/{domain_id}/records или docker compose exec postwind postwind domain records <домен> (чеклист).

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

  7. Резервные копии: том 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>-api ClusterIP 8080 api.enabled
    <release>-web ClusterIP + Ingress <release>-web 80 → 8081 web.tls.mode = off: TLS на вашем Ingress
    <release>-web LoadBalancer 443 → 443, 80 → 8081, при входящем SMTP ещё 25 → 25 web.tls.mode files или acme: TLS в Postwind, проверка TLS-ALPN-01 должна дойти до самого пода
    <release>-smtp LoadBalancer 25 входящий SMTP при web.tls.mode = off (или smtp.inbound.service.shareWebLoadBalancer: false)
    <release>-metrics headless 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 узла.

  1. Образ в вашем реестре (раздел «Образ»), внешний ClickHouse, Secret’ы (выше).

  2. Скопируйте пример, замените example.com, адреса документации (198.51.100.0/24), image, storage.clickhouse.url.

  3. Установка:

    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
    
  4. DNS: A-запись хоста web.public_url — на внешний адрес postwind-web (на нём 443, 80 и 25). Сертификат ACME выпускается сам, когда имя начнёт указывать на этот адрес (проверка раз в web.acme.check_interval_hours).

  5. 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; таблицу аренд leases Postwind создаёт сам на старте. Часы узлов синхронизированы (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.mode files/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. Бинарь образа несёт файловую capability cap_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 Security restricted и 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/AAAA hostname на этот адрес, адрес поднят на интерфейсе хотя бы одного узла доставки (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 узел с неприменёнными миграциями стартует, но не готов (/readyz 503, 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:

  1. Новый ключ и копия старого:

    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
    
  2. В 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 — только когда перезапущены все поды: узел со старым конфигом не открывает секреты, которые уже записал узел с новым ключом.

  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: и запустить снова с тем же ключом.

  4. Убрать старый ключ: 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 с тем же мастер-ключом, /readyz 200, GET /api/v1/projects admin-ключом.

Проверка 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.