Перейти к основному содержимому

Эффективное управление парком qf

Документ описывает рекомендованную модель управления крупным парком: что держать в IaC, как таргетировать политики, как заводить хосты zero-touch.

TL;DR

Terraform (стационар) Runtime (динамика)
├─ qf_object_group ─────┐ ├─ bulk-токен (label_template = role/env)
└─ qf_policy │ └─ хост энроллится → сам штампует лейблы
(selector по лейблам) │ → label-selector политики применяются авто

ничего на хост вручную не вешаем

Главное: хост таргетируется лейблами, а лейблы приезжают из enrollment-токена. Политики селектят по лейблам напрямую — host-group для этого не обязателен. Это и есть zero-touch: ни одного per-host ручного шага.


Что реально делает каждый слой (по коду)

ОбъектГде живётЧем управляетсяНазначение
qf_object_groupTerraformпровайдерпереиспользуемые IP/port/host-сеты
qf_policy (+ selector)Terraformпровайдерправила + кого они касаются (по лейблам)
bootstrap-токенREST/UIне в Terraformнесёт label_template → штампует лейблы на хост при энролле
host-groupREST/UIне в Terraformбандл общих лейблов + удобство каскада
членство в группеREST/UI/APIhost_group_members, явноекакие хосты входят в группу

Два факта, которые меняют схему:

  1. Токен → лейблы всегда; в группу — опционально. label_template токена применяется к хосту при первом подключении. По умолчанию токен не трогает host-group. Но bulk-токен с полем target_group_id авто-добавляет хост в указанную группу при энролле (feature 383, см. group-first флоу ниже).
  2. Членство в группе — явное. Хост попадает в группу либо руками через POST /host-groups/{id}/members (UI/API), либо авто через target_group_id токена. Селектора группы по лейблам нет: сменишь лейбл хоста — он не будет автоматически добавлен в группу.

Матчинг политики идёт по эффективным лейблам = лейблы групп (по приоритету, меньше число — выше) ⊕ собственные лейблы хоста (хостовые перетирают групповые).


Рекомендованный поток (label-first)

Шаг 1. Стационар в Terraform

Object-group'ы и политики — как код, версионируются, ревьюятся в PR.

resource "qf_object_group" "mgmt_net" {
name = "mgmt-net"
type = "ipset"
spec = jsonencode({ cidrs = ["10.0.0.0/16"] })
}

resource "qf_policy" "web" {
name = "web-ingress"
priority = 100
# таргет по ЛЕЙБЛАМ — никакой привязки к IP/хостам
selector = jsonencode({ matchLabels = { role = "web", env = "prod" } })

rule {
priority = 10
action = "allow"
match = jsonencode({ protocol = "tcp", dst_ports = ["443"] })
}
}

selector — это AND по всем matchLabels. Хост получит политику, только если у него (эффективно) есть оба лейбла role=web И env=prod.

Шаг 2. Bulk-токены под когорты

Один токен на роль/окружение, label_template = те же лейблы, по которым селектят политики. Через UI (Tokens) или REST:

POST /tokens
{ "type": "bulk",
"label_template": { "role": "web", "env": "prod" },
"max_uses": 500, "ttl_seconds": 2592000 }

Шаг 3. Энролл хостов

agent.conf из двух полей (QF_ENDPOINT + QF_ENROLL_TOKEN), раскатывается Ansible-ролью. При первом подключении хост:

  • штампует на себя role=web, env=prod из токена,
  • авто-детектит интерфейс,
  • получает client-cert,
  • CP резолвит label-селекторы → политика web применяется сразу, без участия оператора.

Добавили 300 web-хостов — раздали тот же токен, политики применяются автоматически.


Альтернативный поток (group-first)

Когда удобнее рассуждать группами, а не отдельными лейблами: политики привязаны к host-group, а токен добавляет хост в группу при энролле (target_group_id, feature 383). Хост наследует лейблы группы → политики группы применяются сразу. Тоже zero-touch, но точка таргетинга — группа, не label_template.

Terraform (стационар) Runtime (динамика)
├─ qf_object_group └─ bulk-токен { target_group_id = <grp> }
└─ qf_policy (selector = anyOf └─ хост энроллится
[ _groupId блок группы ]) → авто-join в группу
▲ → наследует лейблы группы
│ руками: создать группу, → политики группы применяются авто
└─ слинковать политику на неё

Шаг 1. Стационар в Terraform

Object-group'ы и политики — как раньше. Но selector политики таргетит группу (anyOf-блок с _groupId), а не голые лейблы:

resource "qf_policy" "web" {
name = "web-ingress"
priority = 100
selector = jsonencode({
anyOf = [{ matchLabels = { role = "web" }, _groupId = "<group-uuid>" }]
})
rule {
priority = 10
action = "allow"
match = jsonencode({ protocol = "tcp", dst_ports = ["443"] })
}
}

Шаг 2. Руками создать группы, слинковать политики

Группа = бандл лейблов ({role: web}). В UI: Policy → Assign host group (даёт тот самый anyOf-блок с _groupId), либо через Terraform-selector выше. Группа с пустым набором лейблов к политике не привязывается — задай лейблы.

Шаг 3. Bulk-токен с target_group_id

POST /tokens
{ "type": "bulk",
"target_group_id": "<group-uuid>",
"max_uses": 500, "ttl_seconds": 2592000 }

label_template можно оставить пустым — лейблы приедут из группы через эффективные лейблы. (FK ON DELETE SET NULL: при удалении группы токен сохраняется, поле обнуляется, авто-join выключается.)

Шаг 4. Энролл

Хост подключается → CP делает best-effort AddHostToGroup → живое членство (наследует лейблы группы, каскад при смене лейблов группы) → политики группы применяются сразу. agent.conf всё те же 2 поля.

Live-validated (2026-06-30): re-enroll qt1 токеном с target_group_id и пустым label_template → qt1 авто-вошёл в k8s-servers, effective_labels={role:k8s-server} при собственных {}, log-политика группы применилась.

Label-first vs group-first — что брать

label-firstgroup-first
Таргет политикиголые лейблы (matchLabels)группа (anyOf + _groupId)
Токен несётlabel_templatetarget_group_id
Смена таргета когортыправишь лейблы токенадвигаешь хосты между группами / лейблы группы
Общий бандл лейблов в 1 местенетда (лейблы группы)
Кол-во ручных шагов0создать группу + линк (1 раз на когорту)

Дефолт — label-first (меньше сущностей). group-first — когда нужен общий управляемый-в-одном-месте бандл лейблов или иерархия приоритетов.


Трейдофф: лейбл токена иммутабелен, bulk-relabel нет

Ключевое ограничение label-first: лейбл из токена задаётся один раз при энролле и после не двигается массово. Примитива «добавить лейбл X на N уже-заенролленных хостов» в CP нет. Что реально есть:

МеханизмМассовый релейбл?
PATCH /hosts/{id}нет — один хост, full-replace map (read-merge-write вручную) + каскад
bulk-label endpointотсутствует
re-enroll с новым label_templateтяжёлый: роль скипает при наличии сертов, нужен wipe pki/ + повторный энролл

Три выхода, по убыванию:

  1. Держать мутируемое измерение в host-group, не в токене. Меняешь лейбл группы один раз → каскад пересчитывает всех членов. Это и есть смысл группы. label-first не имеет bulk-relabel намеренно — для этого группа.
  2. Скриптовый PATCH-loop (одноразовый сдвиг): GET /hosts → на каждый read-merge-write PATCH /hosts/{id}. Гонок нет (хосты независимы), но N запросов.
    for id in $(curl -s .../hosts | jq -r '.[].id'); do
    cur=$(curl -s .../hosts/$id | jq '.labels')
    new=$(jq -n --argjson c "$cur" '$c + {tier:"gold"}')
    curl -X PATCH .../hosts/$id -d "{\"labels\":$new}"
    done
  3. Re-enroll — наихудший вариант: роль не приспособлена (нужен wipe certs + повторный энролл). Применять ради одного лейбла не стоит.

Правило раскладки измерений:

ИзмерениеКуда
иммутабельное на всю жизнь хоста (role, env)лейбл токена (label_template)
будешь массово двигать (tier, maintenance, canary)host-group (меняешь в одном месте)

Т.е. штатная схема — гибрид: неизменное через токен, мутируемое через группу. Не клади в токен то, что придётся потом массово релейблить.


Когда нужны host-group'ы

Группы не нужны для таргетинга политик (это делают лейблы). Бери группу, когда:

  • Общий бандл лейблов на много хостов, который хочешь менять в одном месте. Сменил лейбл группы → каскад пересчитал политики у всех членов разом.
  • Иерархия приоритетов лейблов (группа задаёт дефолт, хост перетирает).
  • OR в селекторе политики (см. ниже) — каждая группа = отдельный OR-блок.

Цена: членство явное — либо руками (POST /host-groups/{id}/members), либо авто через target_group_id токена (group-first флоу). Без target_group_id группы нарушают zero-touch. Поэтому: по умолчанию label-first, группы — точечно под перечисленные случаи (а если group-first — то токеном с target_group_id).


AND vs OR в селекторе

  • AND (дефолт): все лейблы в matchLabels одного блока. {role=web, env=prod} = «web И prod».
  • OR: только через anyOf — несколько блоков, матч по любому.
    { "anyOf": [ {"matchLabels":{"role":"web"}}, {"matchLabels":{"role":"api"}} ] }
    В UI anyOf собирается неявно: добавляешь ≥2 блоков (несколько групп через «Add group», или хосты через «Assign host») → получаешь OR. Свободного OR по голым лейблам в одном поле UI нет — поле Host selector всегда AND-блок.
  • В Terraform пишешь anyOf руками через jsonencode.

Антипаттерны

  • Таргетить политику на конкретные host-id / группы вместо лейблов. Теряешь декларативность: новый хост требует ручного действия. Лейбл из токена — авто.
  • Считать, что обычный токен заведёт хост в group. Не заведёт — проставляет только лейблы. В группу заводит только bulk-токен с target_group_id (group-first флоу); иначе членство — отдельный явный шаг.
  • Грузить всё в один host-group и таргетить политики на группу. Работает, но возвращает ручное членство в каждый деплой. Лейблы из токена этого не требуют.
  • default-deny / match-all deny. Модель fail-open: сегментация — это явные deny на сервис-порты при default-allow, не глобальный запрет.

Object-group'ы: типы и семантика

Переиспользуемые сеты, которые CP резолвит в бандл на этапе компиляции:

ТипspecВо что резолвится
ipset{ cidrs = [...] }CIDR/IP; при >8 инлайн-записях становятся LPM-trie BPF-map
portset{ ports = [...] }порты + диапазоны
hostset{ selector = {...} }динамически — в IP хостов, матчащихся селектором (реальный IP attached-интерфейса каждого хоста)

Поведение:

  • Правка spec object-group'ы → каскад на все хосты, где эта OG используется.
  • Удаление референсной object-group → HTTP 409 (FK ON DELETE RESTRICT).
  • Orphan (никем не референсится) — чистится GC; референсные защищены.
  • Валидация spec на create/update: битый CIDR / порт / оператор → HTTP 400.

Разделение ответственности

СлойКто ведётКаналЧастота изменений
object-groups, политики, селекторыплатформа/сетьTerraform (PR-ревью)редко, стационар
токены (label_template когорт)платформаREST/UI, один раз на рольредко
энролл хостовавтоматизация (Ansible)agent.conf 2 полячасто, zero-touch
host-group'ы (если нужны)платформаREST/UI/APIредко

Стационар — в git через Terraform. Динамика парка — через лейблы токенов. Оператор не трогает отдельные хосты в штатном потоке.