Files

439 lines
23 KiB
HTML
Raw Permalink Normal View History

<!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>