Files
cloud-ip-validator/docs/CONTROL_DATA_PLANE.html
ayurishchevandClaude Sonnet 5 2246369b64 Add control/data plane diagrams for microservices deployment to docs
Standalone HTML page (docs/CONTROL_DATA_PLANE.html) showing the
docker-compose deployment topology (hosts, COMPOSE_PROFILES) and the
egress/inbound check traffic, refined through several presentation
review passes. Linked from README.md's docs table and docs/DIAGRAMS.md
alongside the existing Mermaid diagrams.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeVbMVEiE7XQAkBd7HQgj6
2026-09-13 21:38:51 +03:00

440 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Control Plane и Data Plane — Cloud IP Validator</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
<style>
:root {
--bg: #F1F4F6;
--surface: #FFFFFF;
--surface-2: #E6ECEF;
--ink: #142027;
--muted: #55666F;
--line: #C7D2D8;
--control: #2F6FA3;
--egress: #B8681A;
--inbound: #0E7F76;
--shadow: 0 1px 2px rgba(20,32,39,0.07), 0 8px 24px rgba(20,32,39,0.05);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #10171C;
--surface: #172129;
--surface-2: #1E2932;
--ink: #E8EEF1;
--muted: #93A5AF;
--line: #2B3B44;
--control: #74B3E3;
--egress: #E3A75D;
--inbound: #54CBBF;
--shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 24px rgba(0,0,0,0.25);
}
}
:root[data-theme="dark"] {
--bg: #10171C;
--surface: #172129;
--surface-2: #1E2932;
--ink: #E8EEF1;
--muted: #93A5AF;
--line: #2B3B44;
--control: #74B3E3;
--egress: #E3A75D;
--inbound: #54CBBF;
--shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 24px rgba(0,0,0,0.25);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--ink);
font-family: 'IBM Plex Sans', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
.sheet {
max-width: 1100px;
margin-inline: auto;
padding-inline: clamp(16px, 4vw, 40px);
padding-block: clamp(28px, 5vw, 56px) 40px;
}
header.title-block { margin-bottom: clamp(30px, 5vw, 46px); }
.eyebrow {
display: inline-block;
font-family: 'IBM Plex Mono', monospace;
font-size: 12.5px;
font-weight: 500;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--control);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 4px;
padding: 4px 10px;
margin-bottom: 16px;
}
h1 {
font-size: clamp(30px, 4.6vw, 42px);
line-height: 1.12;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 14px;
text-wrap: balance;
}
.lead {
font-size: clamp(16px, 1.8vw, 18px);
line-height: 1.62;
color: var(--muted);
margin: 0;
}
.lead strong { color: var(--ink); font-weight: 600; }
section.plane { margin-bottom: clamp(20px, 4vw, 32px); }
h2 {
font-size: clamp(21px, 2.6vw, 25px);
font-weight: 600;
letter-spacing: -0.005em;
margin: 0 0 12px;
display: flex;
align-items: baseline;
gap: 11px;
}
h2 .num {
font-family: 'IBM Plex Mono', monospace;
font-size: 15px;
font-weight: 600;
color: var(--control);
border: 1px solid var(--line);
border-radius: 50%;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--surface);
flex: none;
}
.plane > p.intro {
font-size: 15.5px;
line-height: 1.65;
color: var(--muted);
margin: 0 0 18px;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: var(--shadow);
padding: clamp(16px, 3vw, 30px);
}
figure { margin: 0; }
.panel svg { display: block; width: 100%; height: auto; }
figcaption {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 14.5px;
line-height: 1.68;
color: var(--muted);
}
figcaption strong { color: var(--ink); font-weight: 600; }
.legend { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 18px; }
.legend .item {
display: flex; align-items: center; gap: 9px;
font-size: 13.5px; color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
}
.legend .swatch { width: 28px; height: 0; border-top-width: 3.5px; border-top-style: solid; flex: none; }
.legend .swatch.dashed { border-top-style: dashed; }
.legend .c-control { border-color: var(--control); }
.legend .c-egress { border-color: var(--egress); }
.legend .c-inbound { border-color: var(--inbound); }
hr.fold { border: 0; border-top: 1.5px dashed var(--line); margin: clamp(30px, 5vw, 46px) 0; }
footer {
margin-top: 8px; padding-top: 20px; border-top: 1px solid var(--line);
font-size: 13px; line-height: 1.7; color: var(--muted);
}
footer code {
font-family: 'IBM Plex Mono', monospace; background: var(--surface-2);
border-radius: 3px; padding: 1px 5px; font-size: 12.5px; color: var(--ink);
}
/* diagram text */
.diagram text { font-family: 'IBM Plex Sans', system-ui, sans-serif; fill: var(--ink); }
.diagram .lbl-title { font-weight: 600; font-size: 19px; }
.diagram .lbl-sub { font-size: 14px; fill: var(--muted); }
.diagram .lbl-mono { font-family: 'IBM Plex Mono', monospace; font-size: 13px; fill: var(--muted); }
.diagram .lbl-arrow { font-family: 'IBM Plex Mono', monospace; font-size: 13px; fill: var(--ink); }
.diagram .lbl-group { font-weight: 600; font-size: 18px; fill: var(--ink); }
.diagram .box { fill: var(--surface); stroke: var(--line); stroke-width: 1.5; }
.diagram .box-stack { fill: var(--surface-2); stroke: var(--line); stroke-width: 1; }
.diagram .box-outer { fill: none; stroke: var(--muted); stroke-width: 1.5; stroke-dasharray: 7 5; }
.diagram .box-accent { fill: none; stroke: var(--control); stroke-width: 3; }
.diagram .box-tag { fill: var(--surface); stroke: var(--control); stroke-width: 1.5; }
.diagram .box-tag-egress { fill: var(--surface); stroke: var(--egress); stroke-width: 1.5; }
.diagram .box-tag-inbound { fill: var(--surface); stroke: var(--inbound); stroke-width: 1.5; }
.diagram .flow-control { stroke: var(--control); stroke-width: 2.5; fill: none; }
.diagram .flow-control-dashed { stroke: var(--control); stroke-width: 2; stroke-dasharray: 6 5; fill: none; }
.diagram .flow-egress { stroke: var(--egress); stroke-width: 4; fill: none; }
.diagram .flow-inbound { stroke: var(--inbound); stroke-width: 4; fill: none; }
.diagram .flow-inbound-dashed { stroke: var(--inbound); stroke-width: 2; stroke-dasharray: 5 5; fill: none; }
.diagram .fip-circle { fill: var(--surface-2); stroke: var(--ink); stroke-width: 2.25; }
</style>
</head>
<body>
<div class="sheet">
<header class="title-block">
<span class="eyebrow">Cloud IP Validator · микросервисный деплой</span>
<h1>Control Plane и Data Plane</h1>
<p class="lead">
Система ревалидации освобождённых публичных IPv4-адресов в облаке на
базе SDN VK Cloud. С переходом на <strong>docker-compose</strong>
каждый компонент — <strong>control-api</strong>,
<strong>admin-dashboard</strong>, <strong>prober</strong>,
<strong>validator-agent</strong> — разворачивается отдельным
контейнером по своему профилю (<strong>COMPOSE_PROFILES</strong>), на
своём хосте. Ниже показано, как они связаны между собой (control
plane) и что именно проверяется в реальном сетевом трафике (data
plane).
</p>
</header>
<section class="plane">
<h2><span class="num">1</span> Control plane: кто кем управляет</h2>
<p class="intro">
Оператор — через веб-панель или напрямую curl'ом — обращается к
единственному источнику истины, control-api. <code>validator-agent</code>
и <code>prober</code> сами инициируют все вызовы (pull-модель):
регистрируются, шлют heartbeat, забирают назначение, отчитываются о
результатах. control-api сам к ним не обращается.
</p>
<div class="panel">
<figure>
<svg class="diagram" viewBox="0 0 1000 940" role="img" aria-label="Control plane: оператор и admin-dashboard обращаются к control-api; validator-agent и prober сами инициируют вызовы к control-api по HTTP; control-api опционально обращается к OpenStack (Keystone, Neutron / Sprut) для управления floating IP. Компоненты развёрнуты по docker-compose profiles: control-plane, dashboard, prober, validator.">
<defs>
<marker id="arrowControl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8.5" markerHeight="8.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" style="fill:var(--control)"></path>
</marker>
</defs>
<!-- Operator -->
<rect class="box" x="400" y="30" width="200" height="56" rx="8"></rect>
<text class="lbl-title" x="500" y="55" text-anchor="middle">Оператор</text>
<text class="lbl-sub" x="500" y="73" text-anchor="middle">инженер эксплуатации</text>
<!-- Host: управляющая машина -->
<rect class="box-outer" x="100" y="120" width="800" height="300" rx="12"></rect>
<text class="lbl-group" x="140" y="150">Управляющая машина</text>
<text class="lbl-mono" x="140" y="170">profiles: control-plane, dashboard</text>
<rect class="box" x="140" y="188" width="720" height="78" rx="8"></rect>
<text class="lbl-title" x="500" y="220" text-anchor="middle">admin-dashboard</text>
<text class="lbl-sub" x="500" y="243" text-anchor="middle">браузерная панель оператора · :8090</text>
<rect class="box" x="140" y="282" width="720" height="126" rx="8"></rect>
<rect class="box-accent" x="140" y="282" width="720" height="6" rx="3"></rect>
<text class="lbl-title" x="500" y="320" text-anchor="middle">control-api</text>
<text class="lbl-sub" x="500" y="345" text-anchor="middle">HTTP API + оркестратор + SQLite · единственный stateful-сервис</text>
<text class="lbl-mono" x="500" y="368" text-anchor="middle">:8080</text>
<!-- Operator -> admin-dashboard -->
<path class="flow-control" d="M500,86 L500,188" marker-end="url(#arrowControl)"></path>
<!-- admin-dashboard -> control-api -->
<path class="flow-control" d="M500,266 L500,282" marker-end="url(#arrowControl)"></path>
<!-- Operator -> control-api (routed around the left, direct curl) -->
<path class="flow-control" d="M420,86 L420,105 L40,105 L40,345 L140,345" marker-end="url(#arrowControl)"></path>
<text class="lbl-arrow" transform="rotate(-90 24 220)" x="24" y="220" text-anchor="middle">curl /api/v1/admin/* · :8080</text>
<!-- OpenStack -->
<path class="flow-control-dashed" d="M500,408 L500,460" marker-end="url(#arrowControl)"></path>
<text class="lbl-arrow" x="516" y="438" text-anchor="start">OpenStack API · mode: real</text>
<rect class="box" x="300" y="460" width="400" height="112" rx="10"></rect>
<text class="lbl-title" x="500" y="494" text-anchor="middle">OpenStack</text>
<text class="lbl-sub" x="500" y="516" text-anchor="middle">Keystone · Neutron / Sprut</text>
<text class="lbl-mono" x="500" y="538" text-anchor="middle">associate / disassociate floating ip</text>
<!-- Host 2: внешние площадки (prober) -->
<rect class="box-outer" x="40" y="630" width="430" height="250" rx="12"></rect>
<text class="lbl-group" x="70" y="660">Внешние площадки ×N</text>
<text class="lbl-mono" x="70" y="680">profiles: prober</text>
<rect class="box-stack" x="90" y="692" width="370" height="130" rx="8"></rect>
<rect class="box-stack" x="80" y="701" width="370" height="130" rx="8"></rect>
<rect class="box" x="70" y="710" width="370" height="130" rx="8"></rect>
<text class="lbl-title" x="255" y="748" text-anchor="middle">prober</text>
<text class="lbl-sub" x="255" y="772" text-anchor="middle">TCP + ICMP пробы наружу</text>
<text class="lbl-mono" x="255" y="793" text-anchor="middle">по 1 на площадку</text>
<!-- Host 3: ВМ-валидаторы -->
<rect class="box-outer" x="530" y="630" width="430" height="250" rx="12"></rect>
<text class="lbl-group" x="560" y="660">ВМ-валидаторы ×N</text>
<text class="lbl-mono" x="560" y="680">profiles: validator</text>
<rect class="box-stack" x="580" y="692" width="370" height="130" rx="8"></rect>
<rect class="box-stack" x="570" y="701" width="370" height="130" rx="8"></rect>
<rect class="box" x="560" y="710" width="370" height="130" rx="8"></rect>
<text class="lbl-title" x="745" y="748" text-anchor="middle">validator-agent</text>
<text class="lbl-sub" x="745" y="772" text-anchor="middle">egress-проверки + self-check</text>
<text class="lbl-mono" x="745" y="793" text-anchor="middle">по 1 на ВМ-валидатор</text>
<!-- prober -> control-api -->
<path class="flow-control" d="M255,710 L255,408" marker-end="url(#arrowControl)"></path>
<rect class="box-tag" x="60" y="507" width="180" height="104" rx="10"></rect>
<text class="lbl-arrow" x="150" y="537" text-anchor="middle">register · heartbeat</text>
<text class="lbl-arrow" x="150" y="559" text-anchor="middle">assignments · results</text>
<text class="lbl-mono" x="150" y="581" text-anchor="middle">→ /api/v1/probers/*</text>
<!-- validator-agent -> control-api -->
<path class="flow-control" d="M745,710 L745,408" marker-end="url(#arrowControl)"></path>
<rect class="box-tag" x="760" y="507" width="180" height="104" rx="10"></rect>
<text class="lbl-arrow" x="850" y="528" text-anchor="middle">register · heartbeat</text>
<text class="lbl-arrow" x="850" y="548" text-anchor="middle">self-check · events</text>
<text class="lbl-mono" x="850" y="568" text-anchor="middle">results · complete</text>
<text class="lbl-mono" x="850" y="588" text-anchor="middle">→ /api/v1/agents/*</text>
<!-- legend -->
<line class="flow-control" x1="70" y1="905" x2="108" y2="905"></line>
<text class="lbl-mono" x="116" y="909">управляющий вызов (control-plane API)</text>
<line class="flow-control-dashed" x1="560" y1="905" x2="598" y2="905"></line>
<text class="lbl-mono" x="606" y="909">только при openstack.mode: real</text>
</svg>
</figure>
<figcaption>
<strong>control-api</strong> — единственный компонент с состоянием
(SQLite) и единственная точка принятия решений: какой IP кому
назначить и когда считать проверку завершённой. Остальные сервисы
развёрнуты по одному контейнеру на профиль docker-compose — на
управляющей машине, на каждой внешней площадке и на каждой
ВМ-валидаторе; все стрелки к control-api идут <em>от</em> них — это
они опрашивают control-api, а не наоборот. Связь с облаком идёт
через сеть OpenStack — Neutron и его альтернативная реализация в
SDN VK Cloud, Sprut, — поэтому на схеме указаны оба.
</figcaption>
</div>
</section>
<hr class="fold">
<section class="plane">
<h2><span class="num">2</span> Data plane: что реально проверяется</h2>
<p class="intro">
Один и тот же публичный адрес проверяется одновременно с двух
независимых сторон. Этот сетевой трафик control-api не видит
напрямую — он получает только заявленный агентами результат по
отдельному управляющему каналу (см. схему control plane выше).
</p>
<div class="panel">
<figure>
<svg class="diagram" viewBox="0 0 1000 660" role="img" aria-label="Data plane: validator-agent проверяет исходящий трафик через floating IP — self-check во внешнем IP-echo сервисе, затем HTTPS/ICMP/SSH до целевых серверов. Независимо N внешних площадок-проберов проверяют входящий трафик на тот же floating IP — TCP-коннект на портах 22, 80, 443, 8080 и ICMP echo.">
<defs>
<marker id="arrowEgress" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" style="fill:var(--egress)"></path>
</marker>
<marker id="arrowInbound" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" style="fill:var(--inbound)"></path>
</marker>
</defs>
<!-- FIP center -->
<circle class="fip-circle" cx="500" cy="280" r="80"></circle>
<text class="lbl-title" x="500" y="274" text-anchor="middle">Floating IP</text>
<text class="lbl-mono" x="500" y="294" text-anchor="middle">203.0.113.10</text>
<!-- VM (left) -->
<rect class="box" x="40" y="210" width="240" height="140" rx="8"></rect>
<text class="lbl-title" x="160" y="255" text-anchor="middle">ВМ-валидатор</text>
<text class="lbl-sub" x="160" y="278" text-anchor="middle">validator-agent</text>
<text class="lbl-mono" x="160" y="300" text-anchor="middle">слушает 22/80/443/8080</text>
<path class="flow-egress" d="M280,265 L420,265" marker-end="url(#arrowEgress)"></path>
<rect class="box-tag-egress" x="170" y="150" width="220" height="36" rx="9"></rect>
<text class="lbl-arrow" x="280" y="173" text-anchor="middle">1. self-check + проверки</text>
<path class="flow-inbound-dashed" d="M420,300 L280,300" marker-end="url(#arrowInbound)"></path>
<!-- IP-echo (top) -->
<rect class="box" x="340" y="30" width="320" height="90" rx="8"></rect>
<text class="lbl-title" x="500" y="68" text-anchor="middle">IP-echo сервис</text>
<text class="lbl-sub" x="500" y="90" text-anchor="middle">api.ipify.org и т.п., вне облака</text>
<path class="flow-egress" d="M500,200 L500,120" marker-end="url(#arrowEgress)"></path>
<rect class="box-tag-egress" x="405" y="147" width="190" height="32" rx="9"></rect>
<text class="lbl-arrow" x="500" y="168" text-anchor="middle">GET, сравнение адреса</text>
<!-- Targets (bottom) -->
<rect class="box" x="340" y="470" width="320" height="100" rx="8"></rect>
<text class="lbl-title" x="500" y="508" text-anchor="middle">Целевые серверы (targets)</text>
<text class="lbl-sub" x="500" y="530" text-anchor="middle">HTTPS · ICMP · SSH (опционально)</text>
<path class="flow-egress" d="M500,360 L500,470" marker-end="url(#arrowEgress)"></path>
<rect class="box-tag-egress" x="405" y="402" width="190" height="32" rx="9"></rect>
<text class="lbl-arrow" x="500" y="423" text-anchor="middle">2. HTTPS / ICMP / SSH</text>
<!-- Probers (right) -->
<rect class="box-stack" x="760" y="222" width="240" height="140" rx="8"></rect>
<rect class="box-stack" x="750" y="216" width="240" height="140" rx="8"></rect>
<rect class="box" x="740" y="210" width="240" height="140" rx="8"></rect>
<text class="lbl-title" x="860" y="255" text-anchor="middle">prober ×N площадок</text>
<text class="lbl-sub" x="860" y="278" text-anchor="middle">внешние тестовые точки,</text>
<text class="lbl-mono" x="860" y="300" text-anchor="middle">независимо друг от друга</text>
<path class="flow-inbound" d="M740,265 L580,265" marker-end="url(#arrowInbound)"></path>
<rect class="box-tag-inbound" x="685" y="150" width="240" height="36" rx="9"></rect>
<text class="lbl-arrow" x="805" y="173" text-anchor="middle">TCP 22/80/443/8080 + ICMP</text>
</svg>
</figure>
<figcaption>
<strong>Egress</strong> (слева): <code>validator-agent</code> сам
всегда обращается наружу через назначенный Floating IP — сначала
self-check во внешнем IP-echo сервисе (адрес обязан быть вне
облака — иначе SNAT не сработает), затем проверки из конфига до
целей. <strong>Inbound</strong> (справа): N внешних площадок
независимо стучатся в тот же адрес снаружи. Только сочетание
обоих направлений даёт полную картину — адрес может нормально
работать «наружу», но быть заблокирован для конкретной внешней
сети, и наоборот.
</figcaption>
<div class="legend">
<span class="item"><span class="swatch c-egress"></span>egress-проверка (от validator-agent, через FIP)</span>
<span class="item"><span class="swatch c-inbound"></span>inbound-проверка (от внешних площадок, в FIP)</span>
</div>
</div>
</section>
<footer>
Полная конфигурация деплоя — <code>deploy/docker/docker-compose.yml</code>
(+ <code>.override.yml</code> для dev, <code>.prod.yml</code> для прод,
профили <code>control-plane / dashboard / prober / validator</code>).
Логика проверок, агрегации и телеметрии подробнее — в
<code>docs/DIAGRAMS.md</code> и <code>docs/API.md</code>. Сетевой слой
облака — OpenStack Neutron / Sprut (SDN VK Cloud).
</footer>
</div>
</body>
</html>