docs: fill the untested items and record why the B layer has no graphs

The forward upgrade to 26.7.3 was zero downtime across 87 samples, and since databasechangelog stayed at 210 the rollback to 26.7.0 also succeeded, which narrows D-2's conclusion: rolling back fails when the schema moved, not because of the version number. The row count is the check.

Role changes never reach the upstream through request repetition; the session is a snapshot taken at login and only a new session picks up the new claim. Auditing the docs also surfaced that Prometheus scrapes only keycloak, kubelet, node-exporter and itself, so the B-layer experiments have no metrics to screenshot rather than missing screenshots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-04 16:18:33 +09:00
co-authored by Claude Opus 5
parent b5528fae87
commit 98a74e90a5
45 changed files with 1210 additions and 0 deletions
@@ -0,0 +1,2 @@
[ 19340ms] [ERROR] Failed to load resource: the server responded with a status of 403 () @ https://app2.hyeonworks.com/oauth2/callback?state=rKipZCUv8W5a-xgYheJbjBsInoD5Il1AaF1RlM_RB2s%3A%2Fapi%2Fecho&session_state=Mw52KcQijFB9Bq4rN-C4SF5Y&iss=https%3A%2F%2Fauth.hyeonworks.com%2Frealms%2Fkeycloak-patterns&code=f9a4835a-2af3-b886-bd04-10b5347ee8d2.Mw52KcQijFB9Bq4rN-C4SF5Y.80431dbc-af81-4673-9790-ad06d1570b2e:0
[ 20374ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1 @@
[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1 @@
[ 423ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
@@ -0,0 +1,8 @@
[ 1127ms] [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
[ 2377ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&panes=%7B%22h4a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%22now-30m%22%2C%22to%22%3A%22now%22%7D%7D%7D&orgId=1:0
[ 2472ms] [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
[ 3501ms] [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
[ 5119ms] [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
[ 8511ms] [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
[ 14956ms] [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
[ 28065ms] [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
@@ -0,0 +1,10 @@
[ 1362ms] [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
[ 1874ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788502680000%22%2C%22to%22%3A%221788503520000%22%7D%7D%7D:0
[ 2907ms] [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
[ 3998ms] [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
[ 6253ms] [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
[ 9426ms] [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
[ 12495ms] [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
[ 24486ms] [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
[ 31338ms] [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
[ 46196ms] [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
@@ -0,0 +1,14 @@
[ 1319ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22vendor_statistics_approximate_entries_unique%7Bcache%3D%5C%22sessions%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22sessions%20%5Cuce90%5Cuc2dc%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788497040000%22%2C%22to%22%3A%221788499080000%22%7D%7D%7D:0
[ 5941ms] [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
[ 11107ms] [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
[ 14234ms] [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
[ 17005ms] [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
[ 23049ms] [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
[ 30565ms] [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
[ 44135ms] [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
[ 54992ms] [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
[ 63653ms] [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
[ 70658ms] [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
[ 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
@@ -0,0 +1,16 @@
- generic [ref=f55e3]:
- banner [ref=f55e4]:
- generic [ref=f55e5]: keycloak-patterns
- main [ref=f55e6]:
- heading "Sign in to your account" [level=1] [ref=f55e8]
- generic [ref=f55e12]:
- generic [ref=f55e13]:
- generic [ref=f55e14]: Username or email
- textbox "Username or email" [ref=f55e17]
- generic [ref=f55e18]:
- generic [ref=f55e19]: Password
- generic [ref=f55e21]:
- textbox "Password" [ref=f55e24]
- button "Show password" [ref=f55e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f55e27]:
- button "Sign In" [ref=f55e30] [cursor=pointer]
@@ -0,0 +1,16 @@
- generic [ref=f55e3]:
- banner [ref=f55e4]:
- generic [ref=f55e5]: keycloak-patterns
- main [ref=f55e6]:
- heading "Sign in to your account" [level=1] [ref=f55e8]
- generic [ref=f55e12]:
- generic [ref=f55e13]:
- generic [ref=f55e14]: Username or email
- textbox "Username or email" [ref=f55e17]: labuser
- generic [ref=f55e18]:
- generic [ref=f55e19]: Password
- generic [ref=f55e21]:
- textbox "Password" [ref=f55e24]: labpass
- button "Show password" [ref=f55e26] [cursor=pointer]:
- generic [aria-hidden] [ref=f55e27]:
- button "Sign In" [ref=f55e30] [cursor=pointer]
@@ -0,0 +1,17 @@
- generic [ref=f56e1]:
- generic [ref=f56e3]:
- generic [ref=f56e4]: "403"
- heading "Forbidden" [level=1] [ref=f56e6]
- generic [ref=f56e8]:
- paragraph [ref=f56e9]: More Info
- generic [ref=f56e10] [cursor=pointer]:
- separator [ref=f56e12]
- generic [ref=f56e13]:
- button "Go back" [ref=f56e16] [cursor=pointer]
- button "Sign in" [ref=f56e19] [cursor=pointer]
- contentinfo [ref=f56e20]:
- paragraph [ref=f56e22]:
- text: Secured with
- link "OAuth2 Proxy" [ref=f56e23] [cursor=pointer]:
- /url: https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy
- text: version v7.7.1
@@ -0,0 +1 @@
- generic [ref=f57e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMDNZMkZrTUdZM01tRmlZekkwWldFell6a3lOREJpTW1KaE5UZGpOVEJoWWcuZVVmckp5VHRqY1VsXzdiV1hiX3hwdw==|1788503135|yDSo7VdgRSlvoVfj9raHPClKTlQiWDg7FauLfoUayw4=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.0.53\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
@@ -0,0 +1 @@
- generic [ref=f58e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMHpabUV5TVdKaVpEa3lOemRrTlRFMU9USTBaV00wWWpGaE16bGhNak0zT1EuN2tTa3dnWUdISDkwMGFSSTVOSUFFUQ==|1788503202|snWKU5IRfRLoD9-bXEodGjEgfHeAw8PQaoHnecpFH90=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"changed-labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.1.132\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
@@ -0,0 +1,175 @@
- generic [ref=f59e1]:
- generic [ref=f59e4]:
- link "Skip to main content" [ref=f59e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f59e7]:
- generic [ref=f59e8]:
- link [ref=f59e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f59e11]
- generic [ref=f59e14]:
- button "Search or jump to..." [ref=f59e18] [cursor=pointer]
- generic [ref=f59e19]: ctrl+k
- generic [ref=f59e23]:
- button "New" [ref=f59e24] [cursor=pointer]
- button "Help" [ref=f59e30] [cursor=pointer]
- button "News" [ref=f59e33] [cursor=pointer]
- button "Profile" [ref=f59e36] [cursor=pointer]:
- img "User avatar" [ref=f59e37]
- generic [ref=f59e38]:
- button "Open menu" [ref=f59e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f59e43]:
- list [ref=f59e44]:
- listitem [ref=f59e45]:
- link "Home" [ref=f59e46] [cursor=pointer]:
- /url: /
- listitem [ref=f59e50]:
- link "Explore" [ref=f59e51] [cursor=pointer]:
- /url: /explore
- listitem [ref=f59e55]:
- generic "Prometheus" [ref=f59e56]
- generic [ref=f59e57]:
- generic [ref=f59e60]:
- button "Copy shortened URL" [ref=f59e61] [cursor=pointer]
- button "Open copy link options" [ref=f59e64] [cursor=pointer]
- button "Toggle top search bar" [ref=f59e68] [cursor=pointer]
- main [ref=f59e74]:
- generic [ref=f59e76]:
- heading "Explore" [level=1] [ref=f59e77]
- generic [ref=f59e82]:
- navigation "Explore toolbar" [ref=f59e84]:
- navigation "Search links" [ref=f59e86]:
- generic [ref=f59e87]:
- button "Content outline" [expanded] [ref=f59e89] [cursor=pointer]:
- generic [ref=f59e92]: Outline
- generic [ref=f59e97] [cursor=pointer]:
- img "Prometheus logo" [ref=f59e99]
- textbox "Select a data source" [ref=f59e100]:
- /placeholder: Prometheus
- generic [ref=f59e104]:
- button "Split the pane" [ref=f59e106] [cursor=pointer]:
- generic [ref=f59e109]: Split
- button "Add" [ref=f59e111] [cursor=pointer]
- generic [ref=f59e116]:
- 'button "Time range selected: Last 30 minutes" [ref=f59e117] [cursor=pointer]'
- button "Zoom out time range" [ref=f59e122] [cursor=pointer]
- generic [ref=f59e126]:
- button "Cancel" [ref=f59e127] [cursor=pointer]
- button "Auto refresh turned off. Choose refresh time interval" [ref=f59e129] [cursor=pointer]
- generic [ref=f59e133]:
- generic [ref=f59e137]:
- button "Collapse outline" [expanded] [ref=f59e139] [cursor=pointer]:
- img "arrow-from-right" [ref=f59e140]
- generic [ref=f59e142]:
- button "Content outline item collapse button" [ref=f59e143] [cursor=pointer]:
- img "angle-right" [ref=f59e144]
- button "Queries" [ref=f59e146] [cursor=pointer]:
- img "arrow" [ref=f59e147]
- generic [ref=f59e154]:
- generic [ref=f59e156]:
- generic [ref=f59e157]:
- generic "Query editor row" [ref=f59e159]:
- generic [ref=f59e160]:
- generic [ref=f59e162]:
- generic [ref=f59e163]:
- button "Collapse query row" [expanded] [ref=f59e164] [cursor=pointer]
- generic [ref=f59e167]:
- button "Query editor row title A" [ref=f59e168] [cursor=pointer]:
- generic [ref=f59e169]: A
- emphasis [ref=f59e170]: (Prometheus)
- generic [ref=f59e171]:
- button "Show data source help" [ref=f59e173] [cursor=pointer]
- button "Duplicate query" [ref=f59e177] [cursor=pointer]
- button "Hide response" [ref=f59e181] [cursor=pointer]
- button "Remove query" [ref=f59e185] [cursor=pointer]
- button "Drag and drop to reorder" [ref=f59e188]:
- img "Drag and drop to reorder" [ref=f59e189]
- generic [ref=f59e192]:
- generic [ref=f59e193]:
- button "Kick start your query" [ref=f59e194] [cursor=pointer]
- generic [ref=f59e197]:
- generic [ref=f59e198] [cursor=pointer]: Explain
- generic [ref=f59e199]:
- checkbox "Explain Toggle switch" [ref=f59e200]
- generic "Toggle switch" [ref=f59e201] [cursor=pointer]
- radiogroup [ref=f59e206]:
- generic [ref=f59e207]:
- radio "Builder" [ref=f59e208] [cursor=pointer]
- generic [ref=f59e209] [cursor=pointer]: Builder
- generic [ref=f59e210]:
- radio "Code" [checked] [ref=f59e211] [cursor=pointer]
- generic [ref=f59e212] [cursor=pointer]: Code
- generic [ref=f59e214]:
- generic [ref=f59e216]:
- button "Loading metrics..." [disabled] [ref=f59e217] [cursor=pointer]
- code [ref=f59e224]:
- generic [ref=f59e225]:
- generic [ref=f59e230]: vendor_cluster_size
- textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e235]: vendor_cluster_size
- 'button "Options Legend: cluster_size {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e241] [cursor=pointer]':
- generic [ref=f59e245]:
- heading "Options" [level=6] [ref=f59e246]
- generic [ref=f59e247]:
- generic [ref=f59e248]: "Legend: cluster_size {{pod}}"
- generic [ref=f59e249]: "Format: Time series"
- generic [ref=f59e250]: "Step: auto"
- generic [ref=f59e251]: "Type: Range"
- generic [ref=f59e252]: "Exemplars: false"
- generic "Query editor row" [ref=f59e254]:
- generic [ref=f59e255]:
- generic [ref=f59e257]:
- generic [ref=f59e258]:
- button "Collapse query row" [expanded] [ref=f59e259] [cursor=pointer]
- generic [ref=f59e262]:
- button "Query editor row title B" [ref=f59e263] [cursor=pointer]:
- generic [ref=f59e264]: B
- emphasis [ref=f59e265]: (Prometheus)
- generic [ref=f59e266]:
- button "Show data source help" [ref=f59e268] [cursor=pointer]
- button "Duplicate query" [ref=f59e272] [cursor=pointer]
- button "Hide response" [ref=f59e276] [cursor=pointer]
- button "Remove query" [ref=f59e280] [cursor=pointer]
- button "Drag and drop to reorder" [ref=f59e283]:
- img "Drag and drop to reorder" [ref=f59e284]
- generic [ref=f59e287]:
- generic [ref=f59e288]:
- button "Kick start your query" [ref=f59e289] [cursor=pointer]
- generic [ref=f59e292]:
- generic [ref=f59e293] [cursor=pointer]: Explain
- generic [ref=f59e294]:
- checkbox "Explain Toggle switch" [ref=f59e295]
- generic "Toggle switch" [ref=f59e296] [cursor=pointer]
- radiogroup [ref=f59e301]:
- generic [ref=f59e302]:
- radio "Builder" [ref=f59e303] [cursor=pointer]
- generic [ref=f59e304] [cursor=pointer]: Builder
- generic [ref=f59e305]:
- radio "Code" [checked] [ref=f59e306] [cursor=pointer]
- generic [ref=f59e307] [cursor=pointer]: Code
- generic [ref=f59e309]:
- generic [ref=f59e311]:
- button "Loading metrics..." [disabled] [ref=f59e312] [cursor=pointer]
- code [ref=f59e319]:
- generic [ref=f59e320]:
- generic [ref=f59e325]: "up{job=\"keycloak\"}"
- textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e330]: "up{job=\"keycloak\"}"
- 'button "Options Legend: up {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e336] [cursor=pointer]':
- generic [ref=f59e340]:
- heading "Options" [level=6] [ref=f59e341]
- generic [ref=f59e342]:
- generic [ref=f59e343]: "Legend: up {{pod}}"
- generic [ref=f59e344]: "Format: Time series"
- generic [ref=f59e345]: "Step: auto"
- generic [ref=f59e346]: "Type: Range"
- generic [ref=f59e347]: "Exemplars: false"
- generic [ref=f59e348]:
- button "Add query" [ref=f59e349] [cursor=pointer]
- button "Query history" [ref=f59e353] [cursor=pointer]
- button "Query inspector" [ref=f59e357] [cursor=pointer]
- generic:
- main
- generic [ref=f59e364]:
- alert
- alert
- complementary
- complementary
@@ -0,0 +1,56 @@
- generic [ref=f62e4]:
- link "Skip to main content" [ref=f62e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f62e7]:
- generic [ref=f62e8]:
- link [ref=f62e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f62e11]
- generic [ref=f62e14]:
- button "Search or jump to..." [ref=f62e18] [cursor=pointer]
- generic [ref=f62e19]: ctrl+k
- generic [ref=f62e23]:
- button "New" [ref=f62e24] [cursor=pointer]
- button "Help" [ref=f62e30] [cursor=pointer]
- button "News" [ref=f62e33] [cursor=pointer]
- button "Profile" [ref=f62e36] [cursor=pointer]:
- img "User avatar" [ref=f62e37]
- generic [ref=f62e38]:
- button "Open menu" [ref=f62e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f62e43]:
- list [ref=f62e44]:
- listitem [ref=f62e45]:
- link "Home" [ref=f62e46] [cursor=pointer]:
- /url: /
- listitem [ref=f62e50]:
- link "Explore" [ref=f62e51] [cursor=pointer]:
- /url: /explore
- listitem [ref=f62e55]:
- generic "Prometheus" [ref=f62e56]
- generic [ref=f62e57]:
- button "Show more items" [ref=f62e60] [cursor=pointer]
- button "Toggle top search bar" [ref=f62e64] [cursor=pointer]
- main [ref=f62e70]:
- generic [ref=f62e72]:
- heading "Explore" [level=1] [ref=f62e73]
- generic [ref=f62e78]:
- navigation "Explore toolbar" [ref=f62e80]:
- navigation "Search links" [ref=f62e82]:
- generic [ref=f62e83]:
- button "Content outline" [expanded] [ref=f62e85] [cursor=pointer]:
- generic [ref=f62e88]: Outline
- generic [ref=f62e93] [cursor=pointer]:
- img "Prometheus logo" [ref=f62e95]
- textbox "Select a data source" [ref=f62e96]:
- /placeholder: Prometheus
- button "Show more items" [ref=f62e102] [cursor=pointer]
- generic [ref=f62e106]:
- button "Collapse outline" [expanded] [ref=f62e112] [cursor=pointer]:
- img "arrow-from-right" [ref=f62e113]
- generic [ref=f62e119]:
- generic [ref=f62e122]:
- button "Add query" [ref=f62e123] [cursor=pointer]
- button "Query history" [ref=f62e127] [cursor=pointer]
- button "Query inspector" [ref=f62e131] [cursor=pointer]
- generic:
- main
@@ -0,0 +1,29 @@
- generic [ref=f65e4]:
- link "Skip to main content" [ref=f65e5] [cursor=pointer]:
- /url: "#pageContent"
- banner [ref=f65e7]:
- generic [ref=f65e8]:
- link [ref=f65e10] [cursor=pointer]:
- /url: /
- img "Grafana" [ref=f65e11]
- generic [ref=f65e14]:
- button "Search or jump to..." [ref=f65e18] [cursor=pointer]
- generic [ref=f65e19]: ctrl+k
- generic [ref=f65e23]:
- button "New" [ref=f65e24] [cursor=pointer]
- button "Help" [ref=f65e30] [cursor=pointer]
- button "News" [ref=f65e33] [cursor=pointer]
- button "Profile" [ref=f65e36] [cursor=pointer]:
- img "User avatar" [ref=f65e37]
- generic [ref=f65e38]:
- button "Open menu" [ref=f65e40] [cursor=pointer]
- navigation "Breadcrumbs" [ref=f65e43]:
- list [ref=f65e44]:
- listitem [ref=f65e45]:
- link "Home" [ref=f65e46] [cursor=pointer]:
- /url: /
- listitem [ref=f65e50]:
- generic "Explore" [ref=f65e51]
- button "Toggle top search bar" [ref=f65e53] [cursor=pointer]
- main [ref=f65e59]:
- heading "Explore" [level=1] [ref=f65e62]
+12
View File
@@ -0,0 +1,12 @@
# 다이어그램 규약
| 표현 | 뜻 |
|---|---|
| 실선 상자 | 살아 있는 구성 요소 |
| 붉은 점선 상자 | 이 실험에서 죽이거나 막은 것 |
| ✂ 붉은 X | 주입 지점 |
| 실선 화살표 | 정상 경로 |
| 붉은 점선 화살표 | 실험에서 깨진 경로 |
| 회색 글씨 | 측정값 |
SVG 는 GitHub 에서 그대로 렌더링되며 외부 폰트를 쓰지 않는다.
+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">A-2 · PostgreSQL 정지 — 살아남는 노드가 없다</text>
<rect class="box" x="30" y="46" width="180" height="52"/><text class="t" x="120" y="68" text-anchor="middle">keycloak-1</text><text class="s" x="120" y="86" text-anchor="middle">캐시: 세션 N개</text>
<rect class="box" x="480" y="46" width="180" height="52"/><text class="t" x="570" y="68" text-anchor="middle">keycloak-0</text><text class="s" x="570" y="86" text-anchor="middle">캐시: 세션 M개</text>
<path class="ln" d="M210,72 L480,72"/><text class="s" x="345" y="66" text-anchor="middle">7800 · 살아 있다</text>
<path class="bad" d="M120,100 L300,150"/><path class="bad" d="M570,100 L400,150"/>
<rect class="dead" x="270" y="156" width="160" height="52"/>
<text class="r" x="350" y="178" text-anchor="middle">postgres ✗</text><text class="s" x="350" y="196" text-anchor="middle">replicas=0</text>
<rect class="dead" x="30" y="228" width="290" height="52"/>
<text class="r" x="175" y="250" text-anchor="middle">양쪽 모두 NotReady</text><text class="s" x="175" y="268" text-anchor="middle">ready 주소 = [] · 외부 503</text>
<rect class="box" x="370" y="228" width="290" height="52"/>
<text class="t" x="515" y="250" text-anchor="middle">up{job="keycloak"} = 1</text><text class="s" x="515" y="268" text-anchor="middle">프로세스는 살아 있다 — up 은 못 잡는다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+36
View File
@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 330" width="720" height="330" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
</style>
<defs><marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker></defs>
<text class="h" x="16" y="24">A-7 · 같은 주입, 같은 관측, 정반대 결과</text>
<text class="h" x="180" y="52" text-anchor="middle">persistent (KC 26 기본)</text>
<rect class="box" x="30" y="62" width="300" height="54"/>
<text class="t" x="180" y="84" text-anchor="middle">keycloak ×2 — 로컬 캐시</text>
<path class="ln" d="M180,116 L180,140"/>
<rect class="ok" x="70" y="142" width="220" height="40"/>
<text class="g" x="180" y="167" text-anchor="middle">PostgreSQL — 진실의 원천</text>
<text class="h" x="540" y="52" text-anchor="middle">volatile (KC 24 이전 방식)</text>
<rect class="box" x="390" y="62" width="300" height="54"/>
<text class="t" x="540" y="84" text-anchor="middle">keycloak ×2 — 캐시가 곧 진실</text>
<path class="ln" d="M470,116 L470,140"/><path class="ln" d="M610,140 L610,116"/>
<rect class="ok" x="430" y="142" width="220" height="40"/>
<text class="g" x="540" y="167" text-anchor="middle">클러스터 복제 (7800)</text>
<rect class="box" x="30" y="202" width="660" height="112"/>
<text class="t" x="360" y="224" text-anchor="middle">뒤집힌 세 결과</text>
<text class="s" x="200" y="248" text-anchor="middle">A-1 7800 차단 후 교차 refresh</text>
<text class="g" x="430" y="248" text-anchor="middle">200</text><text class="r" x="560" y="248" text-anchor="middle">400 Session not active</text>
<text class="s" x="200" y="272" text-anchor="middle">A-8 롤링 재시작 후 refresh</text>
<text class="g" x="430" y="272" text-anchor="middle">200</text><text class="r" x="560" y="272" text-anchor="middle">400 Session not active</text>
<text class="s" x="200" y="296" text-anchor="middle">A-2 DB 정지 중 새 로그인</text>
<text class="r" x="430" y="296" text-anchor="middle">500</text><text class="g" x="560" y="296" text-anchor="middle">200</text>
<text class="s" x="430" y="230" text-anchor="middle">persistent</text><text class="s" x="560" y="230" text-anchor="middle">volatile</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 330" width="700" height="330" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-1 · Redis 는 세션만 옮기고 토큰은 두고 간다</text>
<rect class="box" x="30" y="46" width="140" height="46"/><text class="t" x="100" y="66" text-anchor="middle">bff-0</text><text class="s" x="100" y="82" text-anchor="middle">kc-lab-1</text>
<rect class="box" x="30" y="104" width="140" height="46"/><text class="t" x="100" y="124" text-anchor="middle">bff-1</text><text class="s" x="100" y="140" text-anchor="middle">kc-lab-2</text>
<rect class="ok" x="330" y="46" width="330" height="60"/>
<text class="t" x="495" y="68" text-anchor="middle">Redis — Application Session</text>
<text class="s" x="495" y="86" text-anchor="middle">sessionRepository → RedisSessionRepository ✔ 옮겨졌다</text>
<text class="s" x="495" y="100" text-anchor="middle">필드: SPRING_SECURITY_CONTEXT · TTL 1772초</text>
<rect class="dead" x="330" y="122" width="330" height="60"/>
<text class="r" x="495" y="144" text-anchor="middle">프로세스 메모리 — OAuth2AuthorizedClient</text>
<text class="s" x="495" y="162" text-anchor="middle">InMemoryOAuth2AuthorizedClientService ✗ 그대로</text>
<text class="s" x="495" y="176" text-anchor="middle">access token · refresh token 이 여기 있다</text>
<path class="ln" d="M170,69 L330,69"/><path class="ln" d="M170,127 L330,80"/>
<path class="bad" d="M170,140 L330,150"/>
<rect class="box" x="30" y="210" width="630" height="90"/>
<text class="t" x="345" y="234" text-anchor="middle">그 결과 사용자에게 보이는 것</text>
<text class="s" x="345" y="256" text-anchor="middle">principal: labuser ← 로그인은 되어 있다</text>
<text class="s" x="345" y="272" text-anchor="middle">accessTokenStoredOnServer: false ← 토큰이 없다</text>
<text class="r" x="345" y="292" text-anchor="middle">완전히 로그아웃되는 편이 차라리 낫다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-3 · 동시 refresh — 경쟁이 아니라 세션 파괴</text>
<rect class="box" x="30" y="46" width="150" height="40"/><text class="t" x="105" y="71" text-anchor="middle">같은 refresh token ×5</text>
<path class="ln" d="M180,66 L280,66"/>
<rect class="box" x="280" y="42" width="180" height="48"/><text class="t" x="370" y="62" text-anchor="middle">Keycloak</text>
<text class="s" x="370" y="80" text-anchor="middle">rotation ON · maxReuse=0</text>
<path class="ln" d="M460,58 L560,58"/><rect class="box" x="560" y="42" width="110" height="24"/><text class="s" x="615" y="58" text-anchor="middle">1× HTTP 200</text>
<path class="bad" d="M460,78 L560,78"/><rect class="dead" x="560" y="66" width="110" height="24"/><text class="s" x="615" y="82" text-anchor="middle">4× HTTP 400</text>
<rect class="dead" x="120" y="120" width="460" height="70"/>
<text class="r" x="350" y="144" text-anchor="middle">재사용 탐지가 client session 을 제거한다</text>
<text class="s" x="350" y="164" text-anchor="middle">user_session 은 남고 client_session = 0 (정상 세션은 1)</text>
<text class="s" x="350" y="180" text-anchor="middle">그래서 오류가 "Session doesn't have required client"</text>
<rect class="dead" x="120" y="210" width="460" height="60"/>
<text class="r" x="350" y="234" text-anchor="middle">★ 이긴 요청의 새 토큰도 곧바로 400</text>
<text class="s" x="350" y="254" text-anchor="middle">재시도로 회복 불가 → Q2 의 판정은 lock</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 290" width="700" height="290" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-4 · edge 가 설정하지 않은 헤더는 그대로 통과한다</text>
<rect class="box" x="30" y="46" width="130" height="60"/><text class="t" x="95" y="68" text-anchor="middle">공격자</text>
<text class="s" x="95" y="86" text-anchor="middle">X-Auth-Request-</text><text class="s" x="95" y="100" text-anchor="middle">Roles: admin</text>
<path class="ln" d="M160,76 L250,76"/>
<rect class="box" x="250" y="40" width="180" height="72"/><text class="t" x="340" y="62" text-anchor="middle">nginx</text>
<text class="s" x="340" y="82" text-anchor="middle">proxy_set_header 한 것만 덮어쓴다</text>
<text class="r" x="340" y="100" text-anchor="middle">X-Auth-Request-* 는 설정이 없다</text>
<path class="bad" d="M430,76 L520,76"/>
<rect class="dead" x="520" y="46" width="150" height="60"/>
<text class="r" x="595" y="68" text-anchor="middle">upstream</text>
<text class="s" x="595" y="86" text-anchor="middle">['viewer','admin']</text><text class="s" x="595" y="100" text-anchor="middle">둘 다 도착 · 검증 없음</text>
<rect class="box" x="30" y="134" width="310" height="66"/>
<text class="t" x="185" y="156" text-anchor="middle">구분자 문제</text>
<text class="s" x="185" y="176" text-anchor="middle">"admin,editor" 와 "role-with,comma" 가</text>
<text class="s" x="185" y="192" text-anchor="middle">도착 시점에 구별되지 않는다</text>
<rect class="box" x="360" y="134" width="310" height="66"/>
<text class="t" x="515" y="156" text-anchor="middle">크기는 절벽이다</text>
<text class="s" x="515" y="176" text-anchor="middle">4KB 통과 · 8KB → Tomcat 400</text>
<text class="s" x="515" y="192" text-anchor="middle">16KB → 연결 끊김 (nginx)</text>
<rect class="dead" x="30" y="222" width="640" height="50"/>
<text class="r" x="350" y="244" text-anchor="middle">헤더가 인가 근거가 되면 위조 가능성이 곧 권한 상승이다</text>
<text class="s" x="350" y="262" text-anchor="middle">Q4 의 5문항 중 2·4번 해당 → Q4 자신의 기준으로 BFF 구조</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-5 · 파드가 Ready 인 채로 계속 실패한다</text>
<rect class="box" x="30" y="46" width="180" height="76"/>
<text class="t" x="120" y="68" text-anchor="middle">bff ×2</text>
<text class="g" x="120" y="90" text-anchor="middle">Ready = true</text>
<text class="s" x="120" y="110" text-anchor="middle">Service 에 그대로 남는다</text>
<path class="bad" d="M210,84 L330,84"/>
<rect class="dead" x="330" y="58" width="160" height="52"/>
<text class="r" x="410" y="80" text-anchor="middle">redis ✗</text><text class="s" x="410" y="98" text-anchor="middle">replicas=0</text>
<rect class="box" x="30" y="146" width="310" height="110"/>
<text class="t" x="185" y="168" text-anchor="middle">health group 이 갈랐다</text>
<text class="r" x="185" y="192" text-anchor="middle">/actuator/health → 503</text>
<text class="g" x="185" y="214" text-anchor="middle">/actuator/health/readiness → 200 UP</text>
<text class="s" x="185" y="238" text-anchor="middle">redis 지표가 readiness 그룹에 없다</text>
<rect class="box" x="360" y="146" width="310" height="110"/>
<text class="t" x="515" y="168" text-anchor="middle">A-2 와 정반대</text>
<text class="s" x="515" y="192" text-anchor="middle">A-2 Keycloak: DB 검사가 readiness 에</text>
<text class="s" x="515" y="208" text-anchor="middle">→ NotReady → 503 (명확)</text>
<text class="s" x="515" y="230" text-anchor="middle">B-5 BFF: 없음 → Ready 유지</text>
<text class="r" x="515" y="248" text-anchor="middle">→ HTTP 000 (멈춤)</text>
<text class="s" x="16" y="284">영속화: 볼륨 없이 AOF 만 켜면 appendonlydir 은 생기지만 파드 삭제로 전부 사라진다 — 볼륨이 먼저다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 260" width="700" height="260" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">B-6 · 회전은 안전하고 옛 키를 버리는 순간이 위험하다</text>
<rect class="ok" x="30" y="46" width="190" height="76"/>
<text class="t" x="125" y="68" text-anchor="middle">t0 — 키 A 만</text>
<text class="s" x="125" y="88" text-anchor="middle">발급 A · 검증 A</text>
<text class="s" x="125" y="108" text-anchor="middle">JWKS RS256 1개</text>
<path class="ln" d="M220,84 L255,84"/>
<rect class="ok" x="255" y="46" width="190" height="76"/>
<text class="t" x="350" y="68" text-anchor="middle">t1 — B 추가 (priority 200)</text>
<text class="g" x="350" y="88" text-anchor="middle">발급 B · 검증 A+B</text>
<text class="s" x="350" y="108" text-anchor="middle">옛 토큰 200 · 새 토큰 200</text>
<path class="bad" d="M445,84 L480,84"/>
<rect class="dead" x="480" y="46" width="190" height="76"/>
<text class="t" x="575" y="68" text-anchor="middle">t2 — A 제거</text>
<text class="r" x="575" y="88" text-anchor="middle">옛 토큰 즉시 401</text>
<text class="s" x="575" y="108" text-anchor="middle">캐시가 유예를 주지 않는다</text>
<rect class="box" x="30" y="146" width="640" height="60"/>
<text class="t" x="350" y="168" text-anchor="middle">겹침 구간(t1~t2)의 최소 길이 = 옛 키로 서명된 것 중 가장 오래 사는 것의 수명</text>
<text class="s" x="350" y="190" text-anchor="middle">access token 60초 · refresh token 1800초 → 최소 30분</text>
<text class="s" x="16" y="234">모르는 kid 를 만나면 JWKS 를 다시 받으므로 제거가 즉시 반영된다. 유예는 옛 키를 남겨두는 기간으로 만든다.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" width="700" height="300" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">C-1 · SSO 의 구조와 IdP 로그아웃의 한계</text>
<rect class="box" x="220" y="42" width="260" height="52"/>
<text class="t" x="350" y="64" text-anchor="middle">Keycloak user session ×1</text>
<text class="s" x="350" y="82" text-anchor="middle">oqOjHekin4JU-BZjgQLjUByW</text>
<path class="ln" d="M300,96 L180,130"/><path class="ln" d="M400,96 L520,130"/>
<rect class="box" x="60" y="134" width="240" height="52"/>
<text class="t" x="180" y="156" text-anchor="middle">client session — bff-confidential</text>
<text class="s" x="180" y="174" text-anchor="middle">app1 · Redis 세션 + PostgreSQL 토큰</text>
<rect class="box" x="400" y="134" width="240" height="52"/>
<text class="t" x="520" y="156" text-anchor="middle">client session — oauth2-proxy</text>
<text class="s" x="520" y="174" text-anchor="middle">app2 · 쿠키 티켓 + Redis 세션</text>
<rect class="dead" x="220" y="206" width="260" height="40"/>
<text class="r" x="350" y="231" text-anchor="middle">IdP 세션 삭제 ✗</text>
<path class="bad" d="M300,246 L200,262"/><path class="bad" d="M400,246 L500,262"/>
<rect class="ok" x="60" y="256" width="240" height="34"/><text class="g" x="180" y="278" text-anchor="middle">app1 그대로 동작</text>
<rect class="ok" x="400" y="256" width="240" height="34"/><text class="g" x="520" y="278" text-anchor="middle">app2 그대로 동작</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 270" width="700" height="270" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-1 · 빈 데이터베이스가 200 을 냈다</text>
<rect class="box" x="30" y="46" width="180" height="46"/><text class="t" x="120" y="66" text-anchor="middle">pg_dump</text><text class="s" x="120" y="82" text-anchor="middle">395KB · 101 테이블 · 세션 포함</text>
<path class="ln" d="M210,69 L270,69"/>
<rect class="dead" x="270" y="42" width="180" height="54"/>
<text class="r" x="360" y="64" text-anchor="middle">DROP SCHEMA CASCADE</text><text class="s" x="360" y="84" text-anchor="middle">남은 테이블 0</text>
<path class="ln" d="M450,69 L510,69"/>
<rect class="ok" x="510" y="42" width="160" height="54"/>
<text class="g" x="590" y="64" text-anchor="middle">복구 1초</text><text class="s" x="590" y="84" text-anchor="middle">오류 0건 · 재시작 0회</text>
<rect class="box" x="30" y="118" width="640" height="76"/>
<text class="t" x="350" y="140" text-anchor="middle">테이블이 0개일 때 무엇이 깨졌는가 — 전부가 아니다</text>
<text class="g" x="350" y="162" text-anchor="middle">/protocol/openid-connect/certs → 200 (realm 키가 캐시에 있다)</text>
<text class="r" x="350" y="182" text-anchor="middle">/.well-known → 500 토큰 발급 → 400</text>
<rect class="dead" x="30" y="210" width="640" height="46"/>
<text class="r" x="350" y="232" text-anchor="middle">헬스체크는 "DB 가 살아 있다"만 보고 "데이터가 있다"는 안 본다</text>
<text class="s" x="350" y="250" text-anchor="middle">RPO = 백업 주기 + A-3 의 synchronous_commit 손실 · 덤프는 같은 호스트 /tmp 에 있었다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 280" width="700" height="280" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-3 · 네 경로 중 RBAC 만 실제로 감춘다</text>
<rect class="dead" x="30" y="46" width="310" height="60"/>
<text class="r" x="185" y="68" text-anchor="middle">kubectl get -o jsonpath | base64 -d</text>
<text class="s" x="185" y="88" text-anchor="middle">POSTGRES_PASSWORD = lab-postgres-change-me</text>
<rect class="dead" x="360" y="46" width="310" height="60"/>
<text class="r" x="515" y="68" text-anchor="middle">저장소 (at rest)</text>
<text class="s" x="515" y="88" text-anchor="middle">Encryption Disabled · state.db 에 평문</text>
<rect class="dead" x="30" y="120" width="310" height="60"/>
<text class="r" x="185" y="142" text-anchor="middle">파드 안</text>
<text class="s" x="185" y="162" text-anchor="middle">KEYCLOAK_CLIENT_SECRET=... 환경변수</text>
<rect class="ok" x="360" y="120" width="310" height="60"/>
<text class="g" x="515" y="142" text-anchor="middle">RBAC</text>
<text class="s" x="515" y="162" text-anchor="middle">default SA 는 get secrets 불가</text>
<rect class="box" x="30" y="200" width="640" height="60"/>
<text class="t" x="350" y="222" text-anchor="middle">describe 는 "14 bytes" 만 보여줘 감춰졌다는 착각을 준다</text>
<text class="s" x="350" y="244" text-anchor="middle">base64 는 감추기 위한 것이 아니라 YAML 에 임의 바이트를 담기 위한 인코딩이다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 250" width="700" height="250" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.r{font-size:12px;fill:#cf222e;font-weight:600}.g{font-size:12px;fill:#1a7f37;font-weight:600}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
.bad{stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;fill:none;marker-end:url(#b)}
</style>
<defs>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker>
<marker id="b" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#cf222e"/></marker>
</defs>
<text class="h" x="16" y="24">D-4 · 인증서 체인과 SAN 제약</text>
<rect class="ok" x="30" y="46" width="300" height="104"/>
<text class="t" x="180" y="68" text-anchor="middle">체인 4단계 · Verify return code: 0</text>
<text class="s" x="180" y="90" text-anchor="middle">0 CN=auth.hyeonworks.com</text>
<text class="s" x="180" y="106" text-anchor="middle">1 Let's Encrypt YE2</text>
<text class="s" x="180" y="122" text-anchor="middle">2 ISRG Root YE</text>
<text class="s" x="180" y="138" text-anchor="middle">3 ISRG Root X2</text>
<rect class="box" x="360" y="46" width="310" height="104"/>
<text class="t" x="515" y="68" text-anchor="middle">SAN 3개 · 와일드카드 아님</text>
<text class="s" x="515" y="90" text-anchor="middle">auth · app1 · app2</text>
<text class="r" x="515" y="114" text-anchor="middle">네 번째 이름이 없다</text>
<text class="s" x="515" y="134" text-anchor="middle">B-7 에서 Grafana 의 app2 를 빌려야 했다</text>
<rect class="box" x="30" y="168" width="640" height="60"/>
<text class="t" x="350" y="190" text-anchor="middle">단계가 1개면 cert.pem, 2개 이상이면 fullchain.pem 이다</text>
<text class="s" x="350" y="212" text-anchor="middle">브라우저는 중간 인증서를 캐시하므로 cert.pem 실수는 캐시 없는 클라이언트에서만 드러난다</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" width="760" height="470" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
<style>
.t{font-size:13px;fill:#1f2328}.s{font-size:11px;fill:#59636e}.h{font-size:14px;font-weight:600;fill:#1f2328}
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.host{fill:#f6f8fa;stroke:#59636e;stroke-width:1.6;rx:8}
.node{fill:#fff;stroke:#0969da;stroke-width:1.6;rx:8}
.ln{stroke:#59636e;stroke-width:1.4;fill:none;marker-end:url(#a)}
</style>
<defs><marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#59636e"/></marker></defs>
<rect class="box" x="290" y="12" width="180" height="38"/>
<text class="t" x="380" y="30" text-anchor="middle">개발 노트북</text>
<text class="s" x="380" y="44" text-anchor="middle">브라우저 · kubectl · Playwright</text>
<path class="ln" d="M380,52 L380,80"/>
<text class="s" x="392" y="70">https · tailnet 100.x · split DNS</text>
<rect class="host" x="40" y="84" width="680" height="66"/>
<text class="h" x="56" y="106">test-server</text>
<text class="s" x="56" y="122">Arch Linux · 12GB · WiFi only · sudo 는 비밀번호 필요</text>
<rect class="box" x="430" y="94" width="270" height="46"/>
<text class="t" x="565" y="112" text-anchor="middle">nginx :443 — TLS 종료</text>
<text class="s" x="565" y="128" text-anchor="middle">auth / app1 / app2 (SAN 3개, 와일드카드 아님)</text>
<path class="ln" d="M240,152 L200,186"/><path class="ln" d="M520,152 L560,186"/>
<text class="s" x="330" y="172" text-anchor="middle">http · libvirt NAT (virbr0)</text>
<rect class="node" x="40" y="190" width="320" height="250"/>
<text class="h" x="56" y="212">kc-lab-1 · 5120MB</text>
<text class="s" x="56" y="228">k3s server · 10.42.0.0/24</text>
<rect class="box" x="56" y="238" width="130" height="26"/><text class="t" x="121" y="255" text-anchor="middle">traefik ×1</text>
<rect class="box" x="196" y="238" width="148" height="26"/><text class="t" x="270" y="255" text-anchor="middle">coredns</text>
<rect class="box" x="56" y="272" width="130" height="26"/><text class="t" x="121" y="289" text-anchor="middle">keycloak-1</text>
<rect class="box" x="196" y="272" width="148" height="26"/><text class="t" x="270" y="289" text-anchor="middle">bff (1/2)</text>
<rect class="box" x="56" y="306" width="288" height="26"/><text class="t" x="200" y="323" text-anchor="middle">oauth2-proxy (1/2)</text>
<rect class="box" x="56" y="340" width="288" height="46"/>
<text class="t" x="200" y="358" text-anchor="middle">prometheus (PVC) · grafana</text>
<text class="s" x="200" y="374" text-anchor="middle">관측 스택은 여기 고정 — 죽이지 않는다</text>
<rect class="node" x="400" y="190" width="320" height="250"/>
<text class="h" x="416" y="212">kc-lab-2 · 4096MB</text>
<text class="s" x="416" y="228">k3s agent · 10.42.1.0/24</text>
<rect class="box" x="416" y="238" width="288" height="26"/><text class="t" x="560" y="255" text-anchor="middle">keycloak-0</text>
<rect class="box" x="416" y="272" width="140" height="26"/><text class="t" x="486" y="289" text-anchor="middle">bff (2/2)</text>
<rect class="box" x="566" y="272" width="138" height="26"/><text class="t" x="635" y="289" text-anchor="middle">oauth2-proxy</text>
<rect class="box" x="416" y="306" width="140" height="46"/>
<text class="t" x="486" y="324" text-anchor="middle">postgres</text><text class="s" x="486" y="340" text-anchor="middle">PVC (노드 고정)</text>
<rect class="box" x="566" y="306" width="138" height="46"/>
<text class="t" x="635" y="324" text-anchor="middle">redis</text><text class="s" x="635" y="340" text-anchor="middle">PVC + AOF</text>
<text class="s" x="560" y="374" text-anchor="middle">장애 주입은 여기</text>
<path d="M360,290 L400,290" stroke="#0969da" stroke-width="1.6" fill="none" marker-end="url(#a)"/>
<path d="M400,300 L360,300" stroke="#0969da" stroke-width="1.6" fill="none" marker-end="url(#a)"/>
<text class="s" x="380" y="284" text-anchor="middle">7800</text>
<text class="s" x="380" y="318" text-anchor="middle">JGroups</text>
<text class="s" x="40" y="460">A-0 에서 확인: 세션은 이 7800 이 아니라 postgres 를 통해 공유된다</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

@@ -0,0 +1,32 @@
=== D-1 절차대로 먼저 백업 ===
백업: 395375 bytes
마이그레이션 전: 210
세션 전: 3
=== ★ 정방향 업그레이드 + 1초 간격 가용성 측정 ===
시작: 15:22:59
partitioned roll out complete: 2 new pods have been updated...
완료: 15:24:26
=== 업그레이드 중 외부 응답 시계열 ===
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 200 200 200
200 응답: 87 회
비200 : 0
0 회
=== 업그레이드 후 ===
quay.io/keycloak/keycloak:26.7.3
Keycloak 26.7.3
마이그레이션 후: 210 (전: 210)
세션 후: 3 (전: 3)
=== 스키마 마이그레이션이 실제로 있었는가 ===
(없으면 26.7.0→26.7.3 에 스키마 변경이 없다는 뜻)
=== 파드 상태와 클러스터 ===
keycloak-0 1/1 Running restarts=0
keycloak-1 1/1 Running restarts=0
cluster: [keycloak-1-11418(v=16.0.14)|47] (2) [keycloak-1-11418(v=16.0.14), keycloak-0-58996(v=16.0.14)]
@@ -0,0 +1,20 @@
=== ★ 가설: 스키마 변경이 없으면 롤백이 된다 (26.7.3 → 26.7.0) ===
시작: 15:25:08
partitioned roll out complete: 2 new pods have been updated...
완료: 15:25:53
200 응답: 43 회 / 비200: 1
keycloak-0 1/1 Running restarts=0
keycloak-1 1/1 Running restarts=0
Keycloak 26.7.0
마이그레이션: 210
세션: 3
=== 롤백 중 응답 시계열 (비200 위치) ===
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200 000 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
200 200 200 200
비200 값: 000
=== 대조: 정방향 업그레이드 때는 ===
200: 87 / 비200: 0
@@ -0,0 +1,9 @@
=== ★ Keycloak 에서 email 을 바꾼다 ===
변경 시각: 15:27:52
IdP 의 값: [ {
IdP 의 값: "email" : "changed-labuser@example.com"
IdP 의 값: } ]
=== IdP 쪽 세션과 oauth2-proxy 세션 ===
Redis 세션: 1 개
(세션은 로그인 시점의 클레임을 담고 있다 — 이제 요청을 반복해 본다)
@@ -0,0 +1,17 @@
=== Prometheus 가 실제로 긁는 대상 (2026-09-04 18:10 KST) ===
keycloak 2개
kubelet 2개
node-exporter 2개
prometheus 1개
=== B층 구성 요소의 지표가 있는가 ===
redis_up 시계열 0개
redis_connected_clients 시계열 0개
redis_memory_used_bytes 시계열 0개
pg_up 시계열 0개
pg_stat_database_numbackends 시계열 0개
→ B-1·B-2·B-3·B-5 는 Grafana 증거를 만들 수 없다.
스크린샷을 안 찍은 것이 아니라 긁는 대상에 없다.
보완하려면 redis_exporter · postgres_exporter · BFF 의 /actuator/prometheus 를
scrape 대상에 추가해야 한다.
+17
View File
@@ -0,0 +1,17 @@
# 후속 — 미측정으로 남겼던 항목을 채운 기록
2026-09-04 17:3518:20 KST
해설: [`docs/experiment-followup-untested-items.md`](../../experiment-followup-untested-items.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-d2-forward-upgrade.txt` | **D-2 정방향** 26.7.0 → 26.7.3. 백업 396KB · **87회 요청 전부 200(무중단)** · 마이그레이션 210 → 210(스키마 변경 없음) · 세션 3 유지 · Infinispan 16.0.12 → 16.0.14 |
| `02-d2-rollback-same-schema.txt` | **스키마가 안 바뀌면 롤백이 된다** — 26.7.3 → 26.7.0 성공. 다만 전환 순간 `000` 1회(3초 타임아웃) |
| `03-b4-role-propagation.txt` | **B-4 ③** IdP 에서 값을 바꿔도 **12회 요청·6초 동안 옛 값**. 세션 삭제 후 재인증에서야 새 값 |
| `04-observability-gap.txt` | **B층에 관측이 없다** — Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다. Redis·BFF·PostgreSQL 지표가 0개 |
## 핵심 세 줄
1. **"롤백은 안 된다" 는 조건부였다.** 스키마가 바뀌었으면 안 되고, 안 바뀌었으면 된다 — D-2 의 결론을 정밀화한다.
2. **role 변경은 요청 횟수와 무관하게 반영되지 않는다.** `--cookie-refresh` 가 없으면 쿠키 만료나 재인증까지 옛 값이 간다.
3. **B층 실험에 Grafana 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다.
+9
View File
@@ -9,6 +9,15 @@
--- ---
## 구조
![A-2 구조 — DB 정지 시 살아남는 노드가 없다](diagrams/a2-database-loss.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| 예측 | 결과 | | 예측 | 결과 |
@@ -9,6 +9,14 @@
--- ---
## 구조
![A-7 구조 — 두 모드의 데이터 흐름과 뒤집힌 결과](diagrams/a7-volatile-inversion.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
---
## 0. 결론부터 — 비교표 ## 0. 결론부터 — 비교표
| 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** | | 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** |
@@ -10,6 +10,15 @@
--- ---
## 구조
![B-1 구조 — 세션만 Redis 로, 토큰은 프로세스 메모리에](diagrams/b1-store-split.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| | before | after | | | | before | after | |
@@ -12,6 +12,15 @@
--- ---
## 구조
![B-3 구조 — 동시 refresh 가 client session 을 제거한다](diagrams/b3-refresh-contention.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
**"하나는 성공하고 하나는 실패한다"가 아니다. 세션이 파괴된다.** **"하나는 성공하고 하나는 실패한다"가 아니다. 세션이 파괴된다.**
@@ -10,6 +10,15 @@
--- ---
## 구조
![B-4 구조 — 설정하지 않은 헤더는 통과한다](diagrams/b4-header-forgery.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| Q4 의 미지수 | 측정 결과 | | Q4 의 미지수 | 측정 결과 |
@@ -8,6 +8,15 @@
--- ---
## 구조
![B-5 구조 — readiness 그룹이 갈랐다](diagrams/b5-redis-loss.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| | 결과 | | | 결과 |
+9
View File
@@ -12,6 +12,15 @@
--- ---
## 구조
![B-6 구조 — 겹침 구간과 제거 시점](diagrams/b6-key-rotation.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
**질문이 두 갈래로 나뉜다.** **질문이 두 갈래로 나뉜다.**
+9
View File
@@ -11,6 +11,15 @@
--- ---
## 구조
![C-1 구조 — user session 1 : client session N](diagrams/c1-sso-structure.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| 물음 | 답 | | 물음 | 답 |
+9
View File
@@ -8,6 +8,15 @@
--- ---
## 구조
![D-1 구조 — 파괴와 복구, 그리고 캐시가 가린 것](diagrams/d1-backup-restore.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| 측정 | 값 | | 측정 | 값 |
+9
View File
@@ -6,6 +6,15 @@
--- ---
## 구조
![D-3 구조 — 네 경로 중 하나만 막는다](diagrams/d3-secret-exposure.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| 경로 | 감춰지는가 | | 경로 | 감춰지는가 |
@@ -6,6 +6,15 @@
--- ---
## 구조
![D-4 구조 — 체인과 SAN 제약](diagrams/d4-cert-chain.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터 ## 0. 결론부터
| 확인 | 결과 | | 확인 | 결과 |
+249
View File
@@ -0,0 +1,249 @@
# 후속 — 미측정으로 남겼던 항목을 채운다
브랜치 `feature/keycloak-followup-untested-items` ·
증거 [`docs/evidence/followup/`](evidence/followup/) ·
2026-09-04 17:3518:20 KST
23개 실험을 마치며 **세 항목을 "못 했다" 로 남겼다.** 그중 둘을 채우고,
셋째(D-4 강제 갱신)는 권한이 필요해 별도로 진행한다.
---
## 0. 결론부터
| 항목 | 결과 |
|---|---|
| **D-2 정방향 업그레이드** | **무중단.** 87회 요청 전부 200 |
| **그리고 D-2 의 결론이 정밀해졌다** | **"롤백 불가" 는 조건부다** — 스키마가 바뀌었을 때만 |
| **B-4 ③ role 변경 반영 시점** | **요청 횟수와 무관하다.** 세션이 새로 만들어져야 한다 |
| **B층에 Grafana 증거가 없는 이유** | **관측 대상에 없다.** 안 찍은 것이 아니다 |
---
## 1. D-2 정방향 업그레이드 — 26.7.0 → 26.7.3
### 개념 — 왜 이 방향을 못 했었나
D-2 를 처음 할 때 26.7.0 보다 새 이미지를 몰라 **역방향(26.0)만 시험**했다.
태그 목록을 조회하니 26.7.1 / 26.7.2 / **26.7.3** 이 있었다.
```bash
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
```
### 절차 — D-1 의 교훈대로 백업이 먼저다
```bash
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/pre-2673.sql # 396333 bytes
# 1초 간격으로 외부 진입점을 찍으면서 태그를 바꾼다
( for i in $(seq 1 150); do
printf "%s " "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 https://auth.hyeonworks.com/realms/master)"
sleep 1
done > /tmp/avail.txt ) &
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.3
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
```
### 결과 — 무중단
```
200 200 200 ... (87회)
200 응답: 87 회
비200 : 0 회
소요: 15:22:59 → 15:24:26 (87초)
```
![업그레이드 구간의 cluster_size 와 up](evidence/d2-version-upgrade/d2-upgrade-window.png)
**파드가 하나씩 교체되며 `cluster_size` 가 2 → 1 → 2 를 두 번 반복한다.**
각 파드의 `up` 시계열이 끝나고 새 시계열이 시작되는 것이 함께 보인다.
```
마이그레이션: 210 → 210 ← 스키마 변경 없음
세션: 3 → 3 ← 유지
Infinispan: 16.0.12 → 16.0.14
restarts=0
```
---
## 2. ★ 그래서 D-2 의 결론을 정밀화한다
**"스키마 변경이 없었다면 롤백이 될 것" 이라는 가설이 생겼고, 시험했다.**
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
```
200 응답: 43 회 / 비200: 1
Keycloak 26.7.0
마이그레이션: 210 · 세션: 3
restarts=0
```
**롤백이 성공했다.**
| 버전 차 | `databasechangelog` | 롤백 |
|---|---|---|
| 26.7.0 → **26.0** | 체크섬 불일치 | **불가** (`ValidationFailedException`) |
| 26.7.0 ↔ **26.7.3** | **210 → 210, 변화 없음** | **가능** |
> **처음 D-2 에서 "롤백은 안 된다" 고 쓴 것은 과했다.**
> 정확히는 **"스키마가 바뀌었으면 안 된다"** 이고,
> **패치 릴리스처럼 스키마가 그대로면 태그를 되돌리는 것으로 충분하다.**
>
> 판단 기준은 버전 번호가 아니라 **`databasechangelog` 의 행 수가 바뀌었는가**다.
```bash
# 업그레이드 전후로 이것만 비교하면 롤백 가능 여부를 안다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
```
### 전환 순간의 `000` 1회
```
200 ×24 000 200 ×19
```
**서버 오류가 아니라 `--max-time 3` 타임아웃**이다. 파드 전환 순간 요청 하나가
3초를 넘겼다. 정방향에서는 0회였다.
> **"무중단" 은 관측 해상도에 달려 있다.** 1초 간격·3초 타임아웃으로는
> 44회 중 1회가 걸렸다. **더 촘촘히 보면 더 보일 것이다.**
---
## 3. B-4 ③ — role 변경은 언제 반영되는가
### 왜 못 했었나
B-4 를 할 때 **oauth2-proxy 가 아직 배포되지 않아** "proxy session" 이
존재하지 않았다. B-7 에서 배포했으므로 이제 측정할 수 있다.
### 방법
`X-Auth-Request-Roles` 대신 **이미 전달되고 있는 `x-forwarded-email`** 을 썼다.
role 을 헤더로 내보내려면 추가 설정이 필요한데, **"IdP 의 클레임 변경이
언제 반영되는가" 라는 질문은 어느 클레임이든 같다.**
```bash
# 1. 기준선
fetch('/api/echo') → x-forwarded-email = labuser@example.com
# 2. IdP 에서 바꾼다
kcadm.sh update users/<id> -r keycloak-patterns -s email=CHANGED-labuser@example.com
# 3. 반복 요청
for (i=1..12) fetch('/api/echo') # 0.5초 간격
```
### 결과 — 반영되지 않는다
```
1: labuser@example.com
2: labuser@example.com
...
12: labuser@example.com ← 12회 · 6초 동안 옛 값
```
세션을 지우고 재인증시키자
```
재인증 후 email = changed-labuser@example.com
```
### 개념 — 세션은 로그인 시점의 스냅샷이다
```
로그인 → IdP 가 준 클레임을 세션에 담는다
이후 요청 → 세션에서 읽어 헤더로 내보낸다
└─ IdP 를 다시 부르지 않는다
IdP 에서 변경 → 세션은 모른다
```
**oauth2-proxy 에 `--cookie-refresh` 가 설정되어 있지 않다.**
설정하면 그 주기마다 토큰을 갱신하며 클레임을 다시 받는다.
| 설정 | 반영 시점 |
|---|---|
| 지금 (`--cookie-refresh` 없음) | **쿠키 만료(1시간) 또는 재인증까지 안 됨** |
| `--cookie-refresh=5m` | 최대 5분 |
> **Q4 는 "몇 번째 요청부터 반영되는지" 를 물었는데, 답은 "요청으로는 안 된다" 이다.**
> 요청 횟수가 아니라 **세션의 나이**가 정한다.
>
> **이것이 Q4 의 설계 판단 2번(role·tenant 변경이 즉시 반영돼야 하는가)에
> 직접 답한다** — 즉시가 필요하면 헤더 방식은 맞지 않는다.
---
## 4. B층에 Grafana 증거가 없는 이유
문서 감사에서 **B-1·B-3·B-4·B-5 에 스크린샷이 없는 것**이 드러나
소급해서 찍으려다 원인을 확인했다.
```
=== Prometheus 가 실제로 긁는 대상 ===
keycloak 2개
kubelet 2개
node-exporter 2개
prometheus 1개
=== B층 구성 요소의 지표가 있는가 ===
redis_up 시계열 0개
redis_connected_clients 시계열 0개
pg_up 시계열 0개
pg_stat_database_numbackends 시계열 0개
```
**Redis 도 PostgreSQL 도 BFF 도 긁는 대상에 없다.**
> **스크린샷을 안 찍은 것이 아니라 지표가 없다.**
> A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 `/metrics` 를
> 내놓고 그것을 scrape 대상에 넣어뒀기 때문이다.
>
> **관측은 "나중에 붙이는 것" 이 아니라 실험 설계에 포함되어야 한다.**
> A-2 에서 `kube-state-metrics` 가 없다는 것을, A-6 에서 응답 시간
> 히스토그램이 없다는 것을 찾았는데, **B층 전체가 빠져 있던 것은
> 문서 감사를 하고서야 드러났다.**
### 보완하려면
| 대상 | 방법 |
|---|---|
| Redis | `redis_exporter` 사이드카 또는 Deployment |
| PostgreSQL | `postgres_exporter` |
| BFF | 이미 actuator 가 있다 — `/actuator/prometheus` 노출 + scrape 추가 |
| 파드 readiness | `kube-state-metrics` (A-2 에서 이미 찾은 항목) |
---
## 5. 남은 것 — D-4 강제 갱신
```
$ sudo -n -l
sudo: a password is required
```
**호스트 sudo 가 비밀번호를 요구해 `certbot renew --force-renewal`
`systemctl reload nginx` 를 실행할 수 없다.** 사람이 함께 있어야 한다.
측정 계획은 준비되어 있다.
```bash
# 측정 쪽 (내가 실행)
while true; do
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 https://auth.hyeonworks.com/realms/master)"
sleep 0.2
done
# 주입 쪽 (사람이 실행)
sudo certbot renew --force-renewal
```
**0.2초 간격으로 재는 이유** — 2절에서 1초 간격으로는 전환을 거의 못 잡았다.
nginx reload 는 그보다 훨씬 짧을 것이므로 해상도를 올려야 한다.