refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -0,0 +1,27 @@
# L7 두 홉과 진입점 이중화 재귀
## Alternative text
브라우저에서 edge nginx와 Traefik을 거쳐 Pod로 가는 두 L7 홉과, edge nginx를 두 대로 늘릴 때 앞단 selector가 새 단일 장애점이 되는 재귀를 함께 보여 주는 흐름도.
## Long description
왼쪽의 정상 경로는 브라우저가 TLS를 edge nginx에서 종료하고 고정 노드 IP로 전달한 뒤, Traefik이 동적으로 Pod를 고르는 흐름이다. 아래쪽 이중화 경로는 nginx-1과 nginx-2 앞에 selector를 두면 어느 nginx로 갈지 정할 수 있지만 selector 자체가 새 SPOF가 되어 같은 질문이 반복된다는 점을 보여 준다. VRRP·DNS·BGP·클라우드 LB 같은 재귀 종료 방식의 상세 비교는 본문 표에 남기고 그림에는 넣지 않는다.
## Elements and evidence
- **Browser** (actor): 외부 요청의 시작점. Evidence: L15860L15864.
- **edge nginx** (service): 클러스터 밖 진입점. TLS와 인증서, forwarded header, 어느 노드로 보낼지를 맡는다. Evidence: L15860L15882.
- **Traefik** (service): 클러스터 안에서 Service/Ingress 상태를 따라 어느 Pod로 보낼지 정한다. Evidence: L15872L15882.
- **Pod** (runtime): Traefik이 선택한 실제 워크로드. Evidence: L15860L15864.
- **앞단 selector** (service): nginx를 여러 머신으로 이중화했을 때 어느 nginx로 갈지 정하는 새 앞단. Evidence: L15941L15945.
- **nginx-1 · nginx-2** (service): 진짜 이중화를 위해 머신을 늘린 edge nginx 쌍. Evidence: L15934L15944.
- **selector가 새 SPOF** (result): selector를 하나 더 두면 같은 단일 장애점 질문이 한 단계 앞으로 이동한다. Evidence: L15941L15952.
## Relationships
- **Browser → edge nginx:** HTTPS. Evidence: L15860L15864.
- **edge nginx → Traefik:** 노드 선택 · HTTP. Evidence: L15872L15882.
- **앞단 selector → nginx-1 · nginx-2:** nginx 선택. Evidence: L15941L15945.
- **앞단 selector → selector가 새 SPOF:** 단일 selector. Evidence: L15941L15952.
- **Traefik → Pod:** Ingress routing. Evidence: L15860L15882.
@@ -0,0 +1,29 @@
# L7 두 홉과 진입점 이중화 재귀
# Question: 요청은 왜 nginx와 Traefik이라는 두 L7 홉을 지나며, nginx를 두 대로 늘리면 왜 다시 selector가 필요한가
direction: right
n0: "Browser" {
shape: person
}
n1: "edge nginx" {
shape: rectangle
}
n2: "Traefik" {
shape: rectangle
}
n3: "Pod" {
shape: rectangle
}
n4: "앞단 selector" {
shape: rectangle
}
n5: "nginx-1 · nginx-2" {
shape: rectangle
}
n6: "selector가 새 SPOF" {
shape: rectangle
}
n0 -> n1: "HTTPS"
n1 -> n2: "노드 선택 · HTTP"
n2 -> n3: "Ingress routing"
n4 -> n5: "nginx 선택"
n4 -> n6: "단일 selector"
@@ -0,0 +1,17 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="Browser", shape=box, style="rounded,dashed,filled"];
n1 [label="edge nginx", shape=box, style="rounded,filled"];
n2 [label="Traefik", shape=box, style="rounded,filled"];
n3 [label="Pod", shape=box, style="rounded,filled"];
n4 [label="앞단 selector", shape=box, style="rounded,filled"];
n5 [label="nginx-1 · nginx-2", shape=box, style="rounded,filled"];
n6 [label="selector가 새 SPOF", shape=box, style="rounded,filled"];
n0 -> n1 [label="HTTPS", style=solid];
n1 -> n2 [label="노드 선택 · HTTP", style=solid];
n2 -> n3 [label="Ingress routing", style=solid];
n4 -> n5 [label="nginx 선택", style=solid];
n4 -> n6 [label="단일 selector", style=solid];
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="two-l7-hops-entry-recursion" name="L7 두 홉과 진입점 이중화 재귀">
<mxGraphModel dx="1198" dy="489" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1198" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_browser" value="Browser" tooltip="외부 요청의 시작점. | Evidence: L15860-L15864" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
<mxGeometry x="70.0" y="138.5" width="150.0" height="84.0" as="geometry"/>
</mxCell>
<mxCell id="n_selector" value="앞단 selector&lt;br/&gt;edge redundancy" tooltip="nginx를 여러 머신으로 이중화했을 때 어느 nginx로 갈지 정하는 새 앞단. | Evidence: L15941-L15945" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="294.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_edge-nginx" value="edge nginx&lt;br/&gt;L7&lt;br/&gt;TLS 종료&lt;br/&gt;고정 노드 선택" tooltip="클러스터 밖 진입점. TLS와 인증서, forwarded header, 어느 노드로 보낼지를 맡는다. | Evidence: L15860-L15882" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="381.5" y="60.0" width="150.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_new-spof" value="selector가 새 SPOF" tooltip="selector를 하나 더 두면 같은 단일 장애점 질문이 한 단계 앞으로 이동한다. | Evidence: L15941-L15952" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="381.5" y="237.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_nginx-pair" value="nginx-1 · nginx-2&lt;br/&gt;서로 다른 머신" tooltip="진짜 이중화를 위해 머신을 늘린 edge nginx 쌍. | Evidence: L15934-L15944" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="380.0" y="373.0" width="153.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_traefik" value="Traefik&lt;br/&gt;L7&lt;br/&gt;Ingress&lt;br/&gt;동적 Pod 선택" tooltip="클러스터 안에서 Service/Ingress 상태를 따라 어느 Pod로 보낼지 정한다. | Evidence: L15872-L15882" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="693.0" y="199.5" width="150.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_pod" value="Pod" tooltip="Traefik이 선택한 실제 워크로드. | Evidence: L15860-L15864" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1003.0" y="220.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="e_browser-edge" value="HTTPS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_browser" target="n_edge-nginx">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.8" y="146.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_edge-traefik" value="노드 선택 · HTTP" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_edge-nginx" target="n_traefik">
<mxGeometry relative="1" as="geometry">
<mxPoint x="636.2" y="182.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_selector-pair" value="nginx 선택" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_selector" target="n_nginx-pair">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="373.8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_selector-spof" value="단일 selector" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_selector" target="n_new-spof">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.8" y="295.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_traefik-pod" value="Ingress routing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_traefik" target="n_pod">
<mxGeometry relative="1" as="geometry">
<mxPoint x="923.0" y="224.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,991 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-browser-edge",
"type": "arrow",
"x": 220.0,
"y": 112.5,
"width": 161.5,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 186905999,
"version": 1,
"versionNonce": 1820419618,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
68.0
],
[
80.75,
68.0
],
[
80.75,
0.0
],
[
161.5,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-browser",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-edge-nginx",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-browser-edge",
"type": "text",
"x": 279.75,
"y": 134.5,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1526221385,
"version": 1,
"versionNonce": 1972303568,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "HTTPS",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "HTTPS",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-edge-traefik",
"type": "arrow",
"x": 531.5,
"y": 112.5,
"width": 161.5,
"height": 139.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 554407219,
"version": 1,
"versionNonce": 216820257,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.75,
0.0
],
[
80.75,
139.5
],
[
161.5,
139.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-edge-nginx",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-traefik",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-edge-traefik",
"type": "text",
"x": 588.25,
"y": 170.25,
"width": 96,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1588681295,
"version": 1,
"versionNonce": 1551632150,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "노드 선택 · HTTP",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "노드 선택 · HTTP",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-selector-pair",
"type": "arrow",
"x": 220.0,
"y": 339.0,
"width": 160.0,
"height": 69.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 2620936,
"version": 1,
"versionNonce": 157891078,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
69.5
],
[
160.0,
69.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-selector",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-nginx-pair",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-selector-pair",
"type": "text",
"x": 279.0,
"y": 361.75,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 960601273,
"version": 1,
"versionNonce": 86143142,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "nginx 선택",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "nginx 선택",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-selector-spof",
"type": "arrow",
"x": 220.0,
"y": 269.0,
"width": 161.5,
"height": 52.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 356089597,
"version": 1,
"versionNonce": 433321811,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
52.0
],
[
80.75,
52.0
],
[
80.75,
0.0
],
[
161.5,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-selector",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-new-spof",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-selector-spof",
"type": "text",
"x": 279.75,
"y": 283.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 423024263,
"version": 1,
"versionNonce": 617422139,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "단일 selector",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "단일 selector",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-traefik-pod",
"type": "arrow",
"x": 843.0,
"y": 252.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1096728329,
"version": 1,
"versionNonce": 1723994398,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-traefik",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-pod",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-traefik-pod",
"type": "text",
"x": 863.0,
"y": 212.0,
"width": 120,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1933132434,
"version": 1,
"versionNonce": 670041335,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "Ingress routing",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Ingress routing",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-browser",
"type": "rectangle",
"x": 70.0,
"y": 138.5,
"width": 150.0,
"height": 84.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1549664096,
"version": 1,
"versionNonce": 397227867,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-browser",
"type": "text",
"x": 80.0,
"y": 148.5,
"width": 130.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 225442297,
"version": 1,
"versionNonce": 1441680053,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Browser",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Browser",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-selector",
"type": "rectangle",
"x": 70.0,
"y": 294.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1184769825,
"version": 1,
"versionNonce": 1029245291,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-selector",
"type": "text",
"x": 80.0,
"y": 304.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 288604214,
"version": 1,
"versionNonce": 460695876,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "앞단 selector\nedge redundancy",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "앞단 selector\nedge redundancy",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-edge-nginx",
"type": "rectangle",
"x": 381.5,
"y": 60.0,
"width": 150.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 248904590,
"version": 1,
"versionNonce": 1382229884,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-edge-nginx",
"type": "text",
"x": 391.5,
"y": 70.0,
"width": 130.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1452529323,
"version": 1,
"versionNonce": 150210873,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "edge nginx\nL7\nTLS 종료\n고정 노드 선택",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "edge nginx\nL7\nTLS 종료\n고정 노드 선택",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-new-spof",
"type": "rectangle",
"x": 381.5,
"y": 237.0,
"width": 150.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 202855345,
"version": 1,
"versionNonce": 974263866,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-new-spof",
"type": "text",
"x": 391.5,
"y": 247.0,
"width": 130.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1946517495,
"version": 1,
"versionNonce": 1945847116,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "selector가 새 SPOF",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "selector가 새 SPOF",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-nginx-pair",
"type": "rectangle",
"x": 380.0,
"y": 373.0,
"width": 153.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1402329794,
"version": 1,
"versionNonce": 1572853951,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-nginx-pair",
"type": "text",
"x": 390.0,
"y": 383.0,
"width": 133.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1201103364,
"version": 1,
"versionNonce": 1167919269,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "nginx-1 · nginx-2\n서로 다른 머신",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "nginx-1 · nginx-2\n서로 다른 머신",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-traefik",
"type": "rectangle",
"x": 693.0,
"y": 199.5,
"width": 150.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1004362376,
"version": 1,
"versionNonce": 1434039951,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-traefik",
"type": "text",
"x": 703.0,
"y": 209.5,
"width": 130.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1067358536,
"version": 1,
"versionNonce": 1366351535,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Traefik\nL7\nIngress\n동적 Pod 선택",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Traefik\nL7\nIngress\n동적 Pod 선택",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-pod",
"type": "rectangle",
"x": 1003.0,
"y": 220.0,
"width": 150.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 339878091,
"version": 1,
"versionNonce": 723591255,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-pod",
"type": "text",
"x": 1013.0,
"y": 230.0,
"width": 130.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1089876355,
"version": 1,
"versionNonce": 1055792127,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Pod",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Pod",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,42 @@
{
"harness_version": "0.2.0",
"spec_id": "two-l7-hops-entry-recursion",
"spec_version": "1.1",
"spec_sha256": "552c4dddd1e282894d019a6969c02211c756842dfc2c9678f3aef4d5a4179b51",
"source_context": {
"document": "docs/virtualization/final/document.md",
"document_sha256": "8c4ecc64c8cea9a4450ed7131fdd9cb2048dc092b66cd969f6346ed77887c210",
"anchor": {
"kind": "heading",
"value": "261. 진입점 자체가 죽으면 — 로드밸런서의 재귀 문제",
"line": 15796
}
},
"outputs": [
"two-l7-hops-entry-recursion.svg",
"two-l7-hops-entry-recursion.mmd",
"two-l7-hops-entry-recursion.d2",
"two-l7-hops-entry-recursion.dot",
"two-l7-hops-entry-recursion.drawio",
"two-l7-hops-entry-recursion.excalidraw",
"two-l7-hops-entry-recursion.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true,
"source_spec_file_sha256": "696b5eacdc38802d1a5292be1f7c55021bd3c613d0672b69fbb1321ccd356170",
"output_sha256": {
"two-l7-hops-entry-recursion.svg": "e9f30d5041e9c29252b8fd92fd8ba6c2304cc7502db29709cbfa11a36c419b2f",
"two-l7-hops-entry-recursion.mmd": "89b13720136391d5ccce8ef1e0fbbece0c31e6b2251fb8e78a79cedd16f4d5cd",
"two-l7-hops-entry-recursion.d2": "89612014c64a9efdc5e5398f2e4d945ce834b157627395b22d746c75c31203e2",
"two-l7-hops-entry-recursion.dot": "df604779925b64355402019d985d4c2e34cb3a25630d9fb1a542ffed9a6f3f65",
"two-l7-hops-entry-recursion.drawio": "c4cb00a19ad30bcf4c2284c1f6937e4d5059546982b7f3c98422963262e0697c",
"two-l7-hops-entry-recursion.excalidraw": "1620c8848c78a0d47e6538dbe117208fd63625fe23782d0f800030230c335bf3",
"two-l7-hops-entry-recursion.alt.md": "8f0ad5d24a038648568192a7b461f1acbe6be87407de91fbfa82253c3b9c3829"
}
}
@@ -0,0 +1,17 @@
%% L7 두 홉과 진입점 이중화 재귀
%% question: 요청은 왜 nginx와 Traefik이라는 두 L7 홉을 지나며, nginx를 두 대로 늘리면 왜 다시 selector가 필요한가
flowchart LR
n0(["Browser"])
n1["edge nginx"]
n2["Traefik"]
n3["Pod"]
n4["앞단 selector"]
n5["nginx-1 · nginx-2"]
n6["selector가 새 SPOF"]
n0 -->|"HTTPS"| n1
n1 -->|"노드 선택 · HTTP"| n2
n2 -->|"Ingress routing"| n3
n4 -->|"nginx 선택"| n5
n4 -->|"단일 selector"| n6
classDef external stroke-dasharray: 6 4
class n0 external
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1198" height="489" viewBox="0 0 1198 489" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">L7 두 홉과 진입점 이중화 재귀</title>
<desc id="diagram-description">왼쪽의 정상 경로는 브라우저가 TLS를 edge nginx에서 종료하고 고정 노드 IP로 전달한 뒤, Traefik이 동적으로 Pod를 고르는 흐름이다. 아래쪽 이중화 경로는 nginx-1과 nginx-2 앞에 selector를 두면 어느 nginx로 갈지 정할 수 있지만 selector 자체가 새 SPOF가 되어 같은 질문이 반복된다는 점을 보여 준다. VRRP·DNS·BGP·클라우드 LB 같은 재귀 종료 방식의 상세 비교는 본문 표에 남기고 그림에는 넣지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;two-l7-hops-entry-recursion&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/virtualization/final/document.md&quot;,&quot;document_sha256&quot;:&quot;8c4ecc64c8cea9a4450ed7131fdd9cb2048dc092b66cd969f6346ed77887c210&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;261. 진입점 자체가 죽으면 — 로드밸런서의 재귀 문제&quot;,&quot;line&quot;:15796}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1198" height="489" />
<polyline class="edge kind-request style-solid emphasis-normal" points="220.0,180.5 300.8,180.5 300.8,112.5 381.5,112.5" data-evidence="15860-15864" />
<rect class="edge-label-bg" x="299.0" y="132.5" width="51.5" height="22" rx="3" />
<text class="edge-label" x="324.8" y="147.5">HTTPS</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="531.5,112.5 612.2,112.5 612.2,252.0 693.0,252.0" data-evidence="15872-15882" />
<rect class="edge-label-bg" x="587.0" y="168.2" width="98.4" height="22" rx="3" />
<text class="edge-label" x="636.2" y="183.2">노드 선택 · HTTP</text>
<polyline class="edge kind-request style-dashed emphasis-normal" points="220.0,339.0 300.0,339.0 300.0,408.5 380.0,408.5" data-evidence="15941-15945" />
<rect class="edge-label-bg" x="288.2" y="359.8" width="71.6" height="22" rx="3" />
<text class="edge-label" x="324.0" y="374.8">nginx 선택</text>
<polyline class="edge kind-result style-dashed emphasis-normal" points="220.0,321.0 300.8,321.0 300.8,269.0 381.5,269.0" data-evidence="15941-15952" />
<rect class="edge-label-bg" x="278.9" y="281.0" width="91.7" height="22" rx="3" />
<text class="edge-label" x="324.8" y="296.0">단일 selector</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="843.0,252.0 923.0,252.0 923.0,252.0 1003.0,252.0" data-evidence="15860-15882" />
<rect class="edge-label-bg" x="863.8" y="210.0" width="118.5" height="22" rx="3" />
<text class="edge-label" x="923.0" y="225.0">Ingress routing</text>
<g id="node-browser">
<g class="actor-symbol emphasis-normal" data-evidence="15860-15864"><circle cx="145.0" cy="158.5" r="11.0" /><line x1="145.0" y1="174.5" x2="145.0" y2="193.5" /><line x1="127.0" y1="184.5" x2="163.0" y2="184.5" /><line x1="145.0" y1="193.5" x2="130.0" y2="210.5" /><line x1="145.0" y1="193.5" x2="160.0" y2="210.5" /></g>
<text class="node-label" x="145.0" y="215.5">Browser</text>
</g>
<g id="node-selector">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="15941-15945" x="70.0" y="294.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="321.5">앞단 selector</text>
<line class="node-detail-divider" x1="84.0" y1="342.5" x2="206.0" y2="342.5" />
<text class="node-detail" x="86.0" y="359.5">edge redundancy</text>
</g>
<g id="node-edge-nginx">
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="15860-15882" x="381.5" y="60.0" width="150.0" height="105.0" rx="7" />
<text class="node-label" x="456.5" y="87.0">edge nginx</text>
<line class="node-detail-divider" x1="395.5" y1="108.0" x2="517.5" y2="108.0" />
<text class="node-detail" x="397.5" y="125.0">L7</text>
<text class="node-detail" x="397.5" y="141.0">TLS 종료</text>
<text class="node-detail" x="397.5" y="157.0">고정 노드 선택</text>
</g>
<g id="node-new-spof">
<rect class="node-shape kind-result emphasis-normal role-sink" data-evidence="15941-15952" x="381.5" y="237.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="456.5" y="267.0">selector가 새 SPOF</text>
</g>
<g id="node-nginx-pair">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="15934-15944" x="380.0" y="373.0" width="153.0" height="71.0" rx="7" />
<text class="node-label" x="456.5" y="400.0">nginx-1 · nginx-2</text>
<line class="node-detail-divider" x1="394.0" y1="421.0" x2="519.0" y2="421.0" />
<text class="node-detail" x="396.0" y="438.0">서로 다른 머신</text>
</g>
<g id="node-traefik">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="15872-15882" x="693.0" y="199.5" width="150.0" height="105.0" rx="7" />
<text class="node-label" x="768.0" y="226.5">Traefik</text>
<line class="node-detail-divider" x1="707.0" y1="247.5" x2="829.0" y2="247.5" />
<text class="node-detail" x="709.0" y="264.5">L7</text>
<text class="node-detail" x="709.0" y="280.5">Ingress</text>
<text class="node-detail" x="709.0" y="296.5">동적 Pod 선택</text>
</g>
<g id="node-pod">
<rect class="node-shape kind-runtime emphasis-normal role-sink" data-evidence="15860-15864" x="1003.0" y="220.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="1078.0" y="250.0">Pod</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB