docs: add SVG diagrams, explicit concept sections and the diagram convention
Twelve SVG architecture diagrams cover the experiments whose documents had little or no structure drawing, embedded under a 구조 heading with a shared convention file. Seven documents carried their concepts under narrative headings and now have an explicit 개념 section so they can be found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
98a74e90a5
commit
78b270559c
@@ -12,3 +12,10 @@
|
|||||||
[ 90768ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
[ 90768ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
[ 105475ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
[ 105475ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
[ 114995ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
[ 114995ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 125443ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 137321ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 147252ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 167227ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 179100ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 187598ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
[ 207362ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
|
||||||
|
|||||||
@@ -137,6 +137,32 @@ readiness 프로브가 이 전환을 정확히 맞춰준다 — A-2 에서 본
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### StatefulSet 롤링 재시작의 무중단 조건
|
||||||
|
|
||||||
|
```
|
||||||
|
한 번에 하나씩 내린다 + readiness 로 전환 시점을 맞춘다
|
||||||
|
└─ 항상 최소 하나는 Ready 다
|
||||||
|
```
|
||||||
|
|
||||||
|
**두 가지가 다 있어야 성립한다.** replica 1 이면 반드시 끊기고,
|
||||||
|
readiness 프로브가 없으면 아직 기동 중인 파드로 트래픽이 간다.
|
||||||
|
|
||||||
|
### 룩어사이드 캐시가 재시작을 견디는 이유
|
||||||
|
|
||||||
|
| | 재시작 후 |
|
||||||
|
|---|---|
|
||||||
|
| 캐시 (프로세스 메모리) | **사라진다** |
|
||||||
|
| DB (진실의 원천) | 남는다 |
|
||||||
|
| 정확성 | **유지된다** — 첫 접근만 느려진다 |
|
||||||
|
|
||||||
|
A-0 에서 세운 모델이 여기서 그대로 확인된다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 재현 절차 (명령어)
|
## 6. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -245,6 +245,44 @@ kubectl -n keycloak-lab scale deployment/bff --replicas=1
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### `AuthenticatedPrincipalOAuth2AuthorizedClientRepository`
|
||||||
|
|
||||||
|
이름이 곧 설명이다 — **인증된 주체(principal) 기준**으로 authorized client 를 찾는다.
|
||||||
|
|
||||||
|
```
|
||||||
|
인증되어 있으면 → OAuth2AuthorizedClientService 에 위임
|
||||||
|
키: (clientRegistrationId, principalName)
|
||||||
|
└─ session ID 가 없다 ★
|
||||||
|
인증되지 않았으면 → HttpSession 에 임시 보관
|
||||||
|
```
|
||||||
|
|
||||||
|
**같은 사용자의 두 브라우저가 같은 항목을 본다.** Q1 미지수 3 과 Q3 제약의 기제다.
|
||||||
|
|
||||||
|
### 인가 코드 흐름은 왕복이 두 번이다
|
||||||
|
|
||||||
|
```
|
||||||
|
① 브라우저 → 앱 → IdP 로 리다이렉트 (state·PKCE verifier 를 저장)
|
||||||
|
② IdP → 브라우저 → 앱의 콜백 (저장한 것을 꺼내 검증)
|
||||||
|
```
|
||||||
|
|
||||||
|
**②가 ①과 같은 인스턴스로 가야 한다.** 저장 위치가 인스턴스 메모리면
|
||||||
|
replica 를 늘리는 순간 로그인 자체가 실패한다.
|
||||||
|
|
||||||
|
### 자동구성은 조용히 고른다
|
||||||
|
|
||||||
|
빈을 직접 만들지 않으면 Spring Boot 가 조건에 따라 고른다.
|
||||||
|
**무엇을 골랐는지는 실행 중인 인스턴스를 봐야 안다.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl exec <pod> -- wget -qO- http://localhost:8083/actuator/beans
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 재현 절차 (명령어)
|
## 6. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -271,6 +271,42 @@ access_token 헤더 : {"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-..."}
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### 조회 키는 저장소와 독립이다
|
||||||
|
|
||||||
|
```sql
|
||||||
|
PRIMARY KEY (client_registration_id, principal_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**저장소를 메모리에서 DB 로 옮겨도 이 키는 그대로다.**
|
||||||
|
"공유 저장소로 바꾼다" 와 "세션별로 구분한다" 는 다른 문제이며,
|
||||||
|
전자만 하면 인스턴스 간 공유는 되고 브라우저 간 격리는 안 된다.
|
||||||
|
|
||||||
|
### Liquibase 스키마의 방언 차이
|
||||||
|
|
||||||
|
Spring Security 는 DDL 을 두 벌 제공한다.
|
||||||
|
|
||||||
|
| 파일 | 타입 |
|
||||||
|
|---|---|
|
||||||
|
| `oauth2-client-schema.sql` | `blob` — PostgreSQL 에 **없는 타입** |
|
||||||
|
| `oauth2-client-schema-postgres.sql` | `bytea` |
|
||||||
|
|
||||||
|
`spring.sql.init.continue-on-error: true` 는 이 실패를 삼킨다.
|
||||||
|
**"없어도 되는 초기화" 에만 써야 하는 이유다.**
|
||||||
|
|
||||||
|
### 로그아웃이 지워야 하는 것은 셋이다
|
||||||
|
|
||||||
|
```
|
||||||
|
① HttpSession (Spring Security 가 지운다)
|
||||||
|
② OAuth2AuthorizedClient ★ 아무도 안 지운다
|
||||||
|
③ IdP SSO 세션 ★ RP-initiated logout 을 보내야 한다
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 8. 재현 절차 (명령어)
|
## 8. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -240,6 +240,44 @@ select VERSION from OFFLINE_USER_SESSION ... for no key update skip locked
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### user session 과 client session
|
||||||
|
|
||||||
|
```
|
||||||
|
user session "이 브라우저는 labuser 로 로그인함"
|
||||||
|
├─ client session : bff-confidential
|
||||||
|
└─ client session : oauth2-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
**재사용 탐지는 client session 만 제거한다.** user session 은 껍데기로 남아
|
||||||
|
`Session doesn't have required client` 가 된다.
|
||||||
|
|
||||||
|
### `revokeRefreshToken` 과 `refreshTokenMaxReuse`
|
||||||
|
|
||||||
|
| 설정 | 뜻 |
|
||||||
|
|---|---|
|
||||||
|
| `revokeRefreshToken` | **회전 스위치.** 켜면 새 토큰 발급 시 옛 토큰을 무효화 |
|
||||||
|
| `refreshTokenMaxReuse` | 그 위에서 **몇 번까지 봐줄 것인가** |
|
||||||
|
|
||||||
|
**이름이 "회전" 이 아니라 "취소" 라서 헷갈린다.**
|
||||||
|
그리고 `maxReuse` 를 올리는 것은 해법이 아니다 — 동시 요청이 N개면
|
||||||
|
`N-1` 이 필요하고, 그러면 회전의 보안 목적이 사라진다.
|
||||||
|
|
||||||
|
### lock 의 수명은 어디에 묶이는가
|
||||||
|
|
||||||
|
| 방식 | 프로세스가 죽으면 |
|
||||||
|
|---|---|
|
||||||
|
| **DB 행 잠금** | **연결이 끊기면 자동 해제** |
|
||||||
|
| Redis lock + TTL | TTL 만료까지 막힌다 |
|
||||||
|
|
||||||
|
**잠금 수명이 연결 수명과 묶이는 것이 DB 잠금의 이점**이며,
|
||||||
|
A-0 에서 Keycloak 자신이 `for no key update skip locked` 를 쓰는 이유다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7. 재현 절차 (명령어)
|
## 7. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -221,6 +221,45 @@ Keycloak 의 role 이름은 임의 문자열이므로 **막을 수 있는 것이
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### nginx 의 헤더 처리는 조건부다
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_set_header X-Forwarded-Proto https; # 설정한 것 → 덮어쓴다
|
||||||
|
# X-Auth-Request-Roles 설정 없음 # 안 한 것 → 통과시킨다
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP 는 **같은 이름의 헤더가 여러 번 오는 것을 허용**하므로,
|
||||||
|
edge 가 붙인 것과 클라이언트가 보낸 것이 **함께 도착**한다.
|
||||||
|
Spring 의 `request.getHeader()` 는 **첫 번째**를 돌려주고,
|
||||||
|
그 순서는 프록시가 정한다.
|
||||||
|
|
||||||
|
### 헤더 크기 한계는 계층마다 다르다
|
||||||
|
|
||||||
|
| 크기 | 누가 거부하나 | 클라이언트가 보는 것 |
|
||||||
|
|---|---|---|
|
||||||
|
| ~8KB | **Tomcat** (`maxHttpHeaderSize`) | `400` + HTML |
|
||||||
|
| ~16KB | **nginx** (`large_client_header_buffers`) | 응답 없음 |
|
||||||
|
|
||||||
|
**같은 원인이 두 가지로 보인다.** 그리고 점진적이 아니라 절벽이며,
|
||||||
|
**role 이 많은 사용자만** 깨진다.
|
||||||
|
|
||||||
|
### 세 곳이 독립적으로 필요하다
|
||||||
|
|
||||||
|
```
|
||||||
|
① 외부 → upstream 직접 경로 차단 (NetworkPolicy)
|
||||||
|
② edge 에서 동명 헤더 덮어쓰기 (proxy_set_header)
|
||||||
|
③ upstream 에서 내부 credential 검증 (공통 경계)
|
||||||
|
```
|
||||||
|
|
||||||
|
**하나라도 빠지면 나머지 둘이 무의미하다.** 2홉 실험의 결론이 그대로 적용되며,
|
||||||
|
거기서는 쿠키 속성이었지만 **여기서는 신원 자체**다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7. 재현 절차 (명령어)
|
## 7. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -218,6 +218,43 @@ _oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a ← ★ 옛 세션. 남아 있
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### 상태를 어디에 두는가가 공유 문제의 성격을 정한다
|
||||||
|
|
||||||
|
| | 상태 위치 | replica 간 공유 |
|
||||||
|
|---|---|---|
|
||||||
|
| BFF | **서버 메모리 / Redis** | **저장소를 공유해야** 한다 |
|
||||||
|
| oauth2-proxy | **쿠키 (서명·암호화)** | **secret 만 같으면** 된다 |
|
||||||
|
|
||||||
|
**공유할 상태가 없으면 공유 문제도 없다.** 대신 secret 이 단일 지점이 된다.
|
||||||
|
|
||||||
|
### 세션 티켓
|
||||||
|
|
||||||
|
`--session-store-type=redis` 를 쓰면 쿠키에는 **티켓**만 담긴다.
|
||||||
|
|
||||||
|
```
|
||||||
|
_oauth2_proxy=<ticket>|<timestamp>|<mac>
|
||||||
|
└─ Redis 키를 여기서 계산한다
|
||||||
|
```
|
||||||
|
|
||||||
|
**secret 이 바뀌면 티켓을 못 푼다 → Redis 키를 계산할 수 없다 →
|
||||||
|
정리도 못 한다.** 고아 세션이 남는 이유다.
|
||||||
|
|
||||||
|
### key 식별자가 없으면 회전에 겹침이 없다
|
||||||
|
|
||||||
|
B-6 에서 Keycloak 은 `kid` 로 여러 키를 구분해 무중단 회전을 했다.
|
||||||
|
**oauth2-proxy 의 쿠키에는 그런 식별자가 없고, `--cookie-secret` 도 단수다.**
|
||||||
|
|
||||||
|
```
|
||||||
|
식별자 있음 → 읽기는 여러 key, 쓰기는 하나 → 겹침 가능
|
||||||
|
식별자 없음 → 전부 한 번에 바뀐다 → 겹침 불가
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 재현 절차 (명령어)
|
## 6. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -215,6 +215,45 @@ volatile 이었다면 세션은 애초에 DB 에 없으므로 **복구해도 전
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개념
|
||||||
|
|
||||||
|
### `pg_dump` 의 두 옵션
|
||||||
|
|
||||||
|
| 옵션 | 없으면 |
|
||||||
|
|---|---|
|
||||||
|
| `--clean` | 복구 시 기존 객체와 충돌 |
|
||||||
|
| `--if-exists` | 없는 객체 DROP 에서 오류가 쏟아진다 |
|
||||||
|
|
||||||
|
### "DB 가 살아 있다" 와 "데이터가 있다" 는 다르다
|
||||||
|
|
||||||
|
```
|
||||||
|
A-2 DB 프로세스 정지 → 커넥션 실패 → readiness DOWN → 파드가 Service 에서 빠짐
|
||||||
|
D-1 스키마만 삭제 → 커넥션 정상 → readiness UP → ★ 파드가 그대로 트래픽을 받는다
|
||||||
|
```
|
||||||
|
|
||||||
|
**헬스체크는 커넥션만 본다.** 그래서 빈 데이터베이스를 통과시킨다.
|
||||||
|
그리고 Keycloak 이 realm 캐시로 일부를 계속 서빙해 **부분적으로만 깨진다.**
|
||||||
|
|
||||||
|
### RPO 는 두 겹이다
|
||||||
|
|
||||||
|
```
|
||||||
|
① 마지막 덤프 이후의 변경 ← 백업 주기가 정한다
|
||||||
|
② synchronous_commit OFF 손실 ← A-3 에서 측정한 수백 ms
|
||||||
|
실제 RPO = ① + ②
|
||||||
|
```
|
||||||
|
|
||||||
|
**백업 주기만 보고 RPO 를 말하면 ②를 빠뜨린다.**
|
||||||
|
|
||||||
|
### 백업의 장애 도메인
|
||||||
|
|
||||||
|
이번 덤프는 `test-server:/tmp` 에 있었다. **호스트가 죽으면 같이 사라진다.**
|
||||||
|
A-4 에서 PVC 가 노드에 묶인 것과 같은 문제이며,
|
||||||
|
**같은 장애 도메인에 있는 백업은 백업이 아니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 재현 절차 (명령어)
|
## 6. 재현 절차 (명령어)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -26,7 +26,21 @@
|
|||||||
| **D-1** | 백업·복구 | `...d1-backup-restore` | **빈 데이터베이스가 `200` 을 냈다** |
|
| **D-1** | 백업·복구 | `...d1-backup-restore` | **빈 데이터베이스가 `200` 을 냈다** |
|
||||||
| **D-2** | 버전 업그레이드 | `...d2-version-upgrade` | **이미지를 되돌려도 스키마는 안 돌아온다** |
|
| **D-2** | 버전 업그레이드 | `...d2-version-upgrade` | **이미지를 되돌려도 스키마는 안 돌아온다** |
|
||||||
| **D-3** | 비밀 관리 | `...d3-secret-management` | **RBAC 만 실제로 감춘다** |
|
| **D-3** | 비밀 관리 | `...d3-secret-management` | **RBAC 만 실제로 감춘다** |
|
||||||
| **D-4** | 인증서 갱신 | `...d4-certificate-renewal` | 구성은 정상. **강제 갱신은 못 했다** |
|
| **D-4** | 인증서 갱신 | `...d4-certificate-renewal` | 구성은 정상. **강제 갱신은 사람 손이 필요** |
|
||||||
|
| **후속** | 미측정 항목 채우기 | `...followup-untested-items` | 정방향 업그레이드 무중단 · **롤백 불가는 조건부였다** · role 변경은 요청으로 반영 안 됨 |
|
||||||
|
|
||||||
|
## 시각 자료
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg) | 실험대 전체 구조 |
|
||||||
|
| [`diagrams/_style.md`](diagrams/_style.md) | 다이어그램 규약 (붉은 점선 = 죽인 것) |
|
||||||
|
| `diagrams/*.svg` | 실험별 구조도 12개 |
|
||||||
|
| `evidence/*/*.png` | Grafana · 브라우저 스크린샷 |
|
||||||
|
|
||||||
|
**B층(B-1·B-3·B-4·B-5)에는 Grafana 증거가 없다.** 안 찍은 것이 아니라
|
||||||
|
Prometheus 가 Redis·BFF·PostgreSQL 을 긁지 않는다 —
|
||||||
|
[`followup/04-observability-gap.txt`](evidence/followup/04-observability-gap.txt) 에 측정해 두었다.
|
||||||
|
|
||||||
## 문서 지도
|
## 문서 지도
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user