diff --git a/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log b/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log new file mode 100644 index 0000000..e6bad9e --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log @@ -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 diff --git a/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log b/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log new file mode 100644 index 0000000..f9fd01b --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log @@ -0,0 +1 @@ +[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0 diff --git a/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log b/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log new file mode 100644 index 0000000..275afe6 --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log @@ -0,0 +1 @@ +[ 423ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0 diff --git a/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log b/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log new file mode 100644 index 0000000..58a3c36 --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log @@ -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] is deprecated. Please include @ 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 diff --git a/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log b/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log new file mode 100644 index 0000000..8ac9793 --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log @@ -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] is deprecated. Please include @ 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 diff --git a/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log b/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log new file mode 100644 index 0000000..ba710d3 --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log @@ -0,0 +1,14 @@ +[ 1319ms] [WARNING] is deprecated. Please include @ 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 diff --git a/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml b/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml new file mode 100644 index 0000000..a7fde5e --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml @@ -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] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml b/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml new file mode 100644 index 0000000..aaf9d7c --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml @@ -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] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml b/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml new file mode 100644 index 0000000..7b140eb --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml @@ -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 \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml b/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml new file mode 100644 index 0000000..8b3d9cd --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml @@ -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\" }" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml b/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml new file mode 100644 index 0000000..bfa5b33 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml @@ -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\" }" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml b/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml new file mode 100644 index 0000000..3165cde --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml @@ -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 \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml b/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml new file mode 100644 index 0000000..8bbdcf1 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml @@ -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 \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml b/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml new file mode 100644 index 0000000..8c6fd4c --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml @@ -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] \ No newline at end of file diff --git a/docs/diagrams/_style.md b/docs/diagrams/_style.md new file mode 100644 index 0000000..43dbdb3 --- /dev/null +++ b/docs/diagrams/_style.md @@ -0,0 +1,12 @@ +# 다이어그램 규약 + +| 표현 | 뜻 | +|---|---| +| 실선 상자 | 살아 있는 구성 요소 | +| 붉은 점선 상자 | 이 실험에서 죽이거나 막은 것 | +| ✂ 붉은 X | 주입 지점 | +| 실선 화살표 | 정상 경로 | +| 붉은 점선 화살표 | 실험에서 깨진 경로 | +| 회색 글씨 | 측정값 | + +SVG 는 GitHub 에서 그대로 렌더링되며 외부 폰트를 쓰지 않는다. diff --git a/docs/diagrams/a2-database-loss.svg b/docs/diagrams/a2-database-loss.svg new file mode 100644 index 0000000..f8dc35a --- /dev/null +++ b/docs/diagrams/a2-database-loss.svg @@ -0,0 +1,26 @@ + + + + + + + A-2 · PostgreSQL 정지 — 살아남는 노드가 없다 + keycloak-1캐시: 세션 N개 + keycloak-0캐시: 세션 M개 + 7800 · 살아 있다 + + + postgres ✗replicas=0 + + 양쪽 모두 NotReadyready 주소 = [] · 외부 503 + + up{job="keycloak"} = 1프로세스는 살아 있다 — up 은 못 잡는다 + diff --git a/docs/diagrams/a7-volatile-inversion.svg b/docs/diagrams/a7-volatile-inversion.svg new file mode 100644 index 0000000..d93e189 --- /dev/null +++ b/docs/diagrams/a7-volatile-inversion.svg @@ -0,0 +1,36 @@ + + + + A-7 · 같은 주입, 같은 관측, 정반대 결과 + + persistent (KC 26 기본) + + keycloak ×2 — 로컬 캐시 + + + PostgreSQL — 진실의 원천 + + volatile (KC 24 이전 방식) + + keycloak ×2 — 캐시가 곧 진실 + + + 클러스터 복제 (7800) + + + 뒤집힌 세 결과 + A-1 7800 차단 후 교차 refresh + 200400 Session not active + A-8 롤링 재시작 후 refresh + 200400 Session not active + A-2 DB 정지 중 새 로그인 + 500200 + persistentvolatile + diff --git a/docs/diagrams/b1-store-split.svg b/docs/diagrams/b1-store-split.svg new file mode 100644 index 0000000..24dcfe5 --- /dev/null +++ b/docs/diagrams/b1-store-split.svg @@ -0,0 +1,33 @@ + + + + + + + B-1 · Redis 는 세션만 옮기고 토큰은 두고 간다 + bff-0kc-lab-1 + bff-1kc-lab-2 + + Redis — Application Session + sessionRepository → RedisSessionRepository ✔ 옮겨졌다 + 필드: SPRING_SECURITY_CONTEXT · TTL 1772초 + + 프로세스 메모리 — OAuth2AuthorizedClient + InMemoryOAuth2AuthorizedClientService ✗ 그대로 + access token · refresh token 이 여기 있다 + + + + 그 결과 사용자에게 보이는 것 + principal: labuser ← 로그인은 되어 있다 + accessTokenStoredOnServer: false ← 토큰이 없다 + 완전히 로그아웃되는 편이 차라리 낫다 + diff --git a/docs/diagrams/b3-refresh-contention.svg b/docs/diagrams/b3-refresh-contention.svg new file mode 100644 index 0000000..5f614a4 --- /dev/null +++ b/docs/diagrams/b3-refresh-contention.svg @@ -0,0 +1,29 @@ + + + + + + + B-3 · 동시 refresh — 경쟁이 아니라 세션 파괴 + 같은 refresh token ×5 + + Keycloak + rotation ON · maxReuse=0 + 1× HTTP 200 + 4× HTTP 400 + + 재사용 탐지가 client session 을 제거한다 + user_session 은 남고 client_session = 0 (정상 세션은 1) + 그래서 오류가 "Session doesn't have required client" + + ★ 이긴 요청의 새 토큰도 곧바로 400 + 재시도로 회복 불가 → Q2 의 판정은 lock + diff --git a/docs/diagrams/b4-header-forgery.svg b/docs/diagrams/b4-header-forgery.svg new file mode 100644 index 0000000..e6753f1 --- /dev/null +++ b/docs/diagrams/b4-header-forgery.svg @@ -0,0 +1,37 @@ + + + + + + + B-4 · edge 가 설정하지 않은 헤더는 그대로 통과한다 + 공격자 + X-Auth-Request-Roles: admin + + nginx + proxy_set_header 한 것만 덮어쓴다 + X-Auth-Request-* 는 설정이 없다 + + + upstream + ['viewer','admin']둘 다 도착 · 검증 없음 + + 구분자 문제 + "admin,editor" 와 "role-with,comma" 가 + 도착 시점에 구별되지 않는다 + + 크기는 절벽이다 + 4KB 통과 · 8KB → Tomcat 400 + 16KB → 연결 끊김 (nginx) + + 헤더가 인가 근거가 되면 위조 가능성이 곧 권한 상승이다 + Q4 의 5문항 중 2·4번 해당 → Q4 자신의 기준으로 BFF 구조 + diff --git a/docs/diagrams/b5-redis-loss.svg b/docs/diagrams/b5-redis-loss.svg new file mode 100644 index 0000000..cfc0eee --- /dev/null +++ b/docs/diagrams/b5-redis-loss.svg @@ -0,0 +1,35 @@ + + + + + + + B-5 · 파드가 Ready 인 채로 계속 실패한다 + + bff ×2 + Ready = true + Service 에 그대로 남는다 + + + redis ✗replicas=0 + + health group 이 갈랐다 + /actuator/health → 503 + /actuator/health/readiness → 200 UP + redis 지표가 readiness 그룹에 없다 + + A-2 와 정반대 + A-2 Keycloak: DB 검사가 readiness 에 + → NotReady → 503 (명확) + B-5 BFF: 없음 → Ready 유지 + → HTTP 000 (멈춤) + 영속화: 볼륨 없이 AOF 만 켜면 appendonlydir 은 생기지만 파드 삭제로 전부 사라진다 — 볼륨이 먼저다 + diff --git a/docs/diagrams/b6-key-rotation.svg b/docs/diagrams/b6-key-rotation.svg new file mode 100644 index 0000000..e021a70 --- /dev/null +++ b/docs/diagrams/b6-key-rotation.svg @@ -0,0 +1,34 @@ + + + + + + + B-6 · 회전은 안전하고 옛 키를 버리는 순간이 위험하다 + + t0 — 키 A 만 + 발급 A · 검증 A + JWKS RS256 1개 + + + t1 — B 추가 (priority 200) + 발급 B · 검증 A+B + 옛 토큰 200 · 새 토큰 200 + + + t2 — A 제거 + 옛 토큰 즉시 401 + 캐시가 유예를 주지 않는다 + + 겹침 구간(t1~t2)의 최소 길이 = 옛 키로 서명된 것 중 가장 오래 사는 것의 수명 + access token 60초 · refresh token 1800초 → 최소 30분 + 모르는 kid 를 만나면 JWKS 를 다시 받으므로 제거가 즉시 반영된다. 유예는 옛 키를 남겨두는 기간으로 만든다. + diff --git a/docs/diagrams/c1-sso-structure.svg b/docs/diagrams/c1-sso-structure.svg new file mode 100644 index 0000000..6f87ddc --- /dev/null +++ b/docs/diagrams/c1-sso-structure.svg @@ -0,0 +1,31 @@ + + + + + + + C-1 · SSO 의 구조와 IdP 로그아웃의 한계 + + Keycloak user session ×1 + oqOjHekin4JU-BZjgQLjUByW + + + client session — bff-confidential + app1 · Redis 세션 + PostgreSQL 토큰 + + client session — oauth2-proxy + app2 · 쿠키 티켓 + Redis 세션 + + IdP 세션 삭제 ✗ + + app1 그대로 동작 + app2 그대로 동작 + diff --git a/docs/diagrams/d1-backup-restore.svg b/docs/diagrams/d1-backup-restore.svg new file mode 100644 index 0000000..a9618ae --- /dev/null +++ b/docs/diagrams/d1-backup-restore.svg @@ -0,0 +1,30 @@ + + + + + + + D-1 · 빈 데이터베이스가 200 을 냈다 + pg_dump395KB · 101 테이블 · 세션 포함 + + + DROP SCHEMA CASCADE남은 테이블 0 + + + 복구 1초오류 0건 · 재시작 0회 + + 테이블이 0개일 때 무엇이 깨졌는가 — 전부가 아니다 + /protocol/openid-connect/certs → 200 (realm 키가 캐시에 있다) + /.well-known → 500 토큰 발급 → 400 + + 헬스체크는 "DB 가 살아 있다"만 보고 "데이터가 있다"는 안 본다 + RPO = 백업 주기 + A-3 의 synchronous_commit 손실 · 덤프는 같은 호스트 /tmp 에 있었다 + diff --git a/docs/diagrams/d3-secret-exposure.svg b/docs/diagrams/d3-secret-exposure.svg new file mode 100644 index 0000000..5e6ee4d --- /dev/null +++ b/docs/diagrams/d3-secret-exposure.svg @@ -0,0 +1,31 @@ + + + + + + + D-3 · 네 경로 중 RBAC 만 실제로 감춘다 + + kubectl get -o jsonpath | base64 -d + POSTGRES_PASSWORD = lab-postgres-change-me + + 저장소 (at rest) + Encryption Disabled · state.db 에 평문 + + 파드 안 + KEYCLOAK_CLIENT_SECRET=... 환경변수 + + RBAC + default SA 는 get secrets 불가 + + describe 는 "14 bytes" 만 보여줘 감춰졌다는 착각을 준다 + base64 는 감추기 위한 것이 아니라 YAML 에 임의 바이트를 담기 위한 인코딩이다 + diff --git a/docs/diagrams/d4-cert-chain.svg b/docs/diagrams/d4-cert-chain.svg new file mode 100644 index 0000000..16be38d --- /dev/null +++ b/docs/diagrams/d4-cert-chain.svg @@ -0,0 +1,30 @@ + + + + + + + D-4 · 인증서 체인과 SAN 제약 + + 체인 4단계 · Verify return code: 0 + 0 CN=auth.hyeonworks.com + 1 Let's Encrypt YE2 + 2 ISRG Root YE + 3 ISRG Root X2 + + SAN 3개 · 와일드카드 아님 + auth · app1 · app2 + 네 번째 이름이 없다 + B-7 에서 Grafana 의 app2 를 빌려야 했다 + + 단계가 1개면 cert.pem, 2개 이상이면 fullchain.pem 이다 + 브라우저는 중간 인증서를 캐시하므로 cert.pem 실수는 캐시 없는 클라이언트에서만 드러난다 + diff --git a/docs/diagrams/lab-topology.svg b/docs/diagrams/lab-topology.svg new file mode 100644 index 0000000..eb39cd3 --- /dev/null +++ b/docs/diagrams/lab-topology.svg @@ -0,0 +1,57 @@ + + + + + + 개발 노트북 + 브라우저 · kubectl · Playwright + + https · tailnet 100.x · split DNS + + + test-server + Arch Linux · 12GB · WiFi only · sudo 는 비밀번호 필요 + + nginx :443 — TLS 종료 + auth / app1 / app2 (SAN 3개, 와일드카드 아님) + + + http · libvirt NAT (virbr0) + + + kc-lab-1 · 5120MB + k3s server · 10.42.0.0/24 + traefik ×1 + coredns + keycloak-1 + bff (1/2) + oauth2-proxy (1/2) + + prometheus (PVC) · grafana + 관측 스택은 여기 고정 — 죽이지 않는다 + + + kc-lab-2 · 4096MB + k3s agent · 10.42.1.0/24 + keycloak-0 + bff (2/2) + oauth2-proxy + + postgresPVC (노드 고정) + + redisPVC + AOF + 장애 주입은 여기 + + + + 7800 + JGroups + + A-0 에서 확인: 세션은 이 7800 이 아니라 postgres 를 통해 공유된다 + diff --git a/docs/evidence/d2-version-upgrade/d2-upgrade-window.png b/docs/evidence/d2-version-upgrade/d2-upgrade-window.png new file mode 100644 index 0000000..4b4e614 Binary files /dev/null and b/docs/evidence/d2-version-upgrade/d2-upgrade-window.png differ diff --git a/docs/evidence/followup/01-d2-forward-upgrade.txt b/docs/evidence/followup/01-d2-forward-upgrade.txt new file mode 100644 index 0000000..879a804 --- /dev/null +++ b/docs/evidence/followup/01-d2-forward-upgrade.txt @@ -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)] diff --git a/docs/evidence/followup/02-d2-rollback-same-schema.txt b/docs/evidence/followup/02-d2-rollback-same-schema.txt new file mode 100644 index 0000000..b4d6af6 --- /dev/null +++ b/docs/evidence/followup/02-d2-rollback-same-schema.txt @@ -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 diff --git a/docs/evidence/followup/03-b4-role-propagation.txt b/docs/evidence/followup/03-b4-role-propagation.txt new file mode 100644 index 0000000..d0dbbc5 --- /dev/null +++ b/docs/evidence/followup/03-b4-role-propagation.txt @@ -0,0 +1,9 @@ +=== ★ Keycloak 에서 email 을 바꾼다 === + 변경 시각: 15:27:52 + IdP 의 값: [ { + IdP 의 값: "email" : "changed-labuser@example.com" + IdP 의 값: } ] + +=== IdP 쪽 세션과 oauth2-proxy 세션 === + Redis 세션: 1 개 + (세션은 로그인 시점의 클레임을 담고 있다 — 이제 요청을 반복해 본다) diff --git a/docs/evidence/followup/04-observability-gap.txt b/docs/evidence/followup/04-observability-gap.txt new file mode 100644 index 0000000..8f080c9 --- /dev/null +++ b/docs/evidence/followup/04-observability-gap.txt @@ -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 대상에 추가해야 한다. diff --git a/docs/evidence/followup/README.md b/docs/evidence/followup/README.md new file mode 100644 index 0000000..820a980 --- /dev/null +++ b/docs/evidence/followup/README.md @@ -0,0 +1,17 @@ +# 후속 — 미측정으로 남겼던 항목을 채운 기록 + +2026-09-04 17:35–18: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 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다. diff --git a/docs/experiment-a2-database-loss.md b/docs/experiment-a2-database-loss.md index 37f01d0..eac488b 100644 --- a/docs/experiment-a2-database-loss.md +++ b/docs/experiment-a2-database-loss.md @@ -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. 결론부터 | 예측 | 결과 | diff --git a/docs/experiment-a7-volatile-comparison.md b/docs/experiment-a7-volatile-comparison.md index 091165e..73daa2c 100644 --- a/docs/experiment-a7-volatile-comparison.md +++ b/docs/experiment-a7-volatile-comparison.md @@ -9,6 +9,14 @@ --- +## 구조 + +![A-7 구조 — 두 모드의 데이터 흐름과 뒤집힌 결과](diagrams/a7-volatile-inversion.svg) + +> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md). + +--- + ## 0. 결론부터 — 비교표 | 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** | diff --git a/docs/experiment-b1-redis-session-store.md b/docs/experiment-b1-redis-session-store.md index 2040926..4508167 100644 --- a/docs/experiment-b1-redis-session-store.md +++ b/docs/experiment-b1-redis-session-store.md @@ -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. 결론부터 | | before | after | | diff --git a/docs/experiment-b3-refresh-token-contention.md b/docs/experiment-b3-refresh-token-contention.md index 15f6f79..27c87c2 100644 --- a/docs/experiment-b3-refresh-token-contention.md +++ b/docs/experiment-b3-refresh-token-contention.md @@ -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. 결론부터 **"하나는 성공하고 하나는 실패한다"가 아니다. 세션이 파괴된다.** diff --git a/docs/experiment-b4-edge-authorization-scope.md b/docs/experiment-b4-edge-authorization-scope.md index 1c50e14..2753815 100644 --- a/docs/experiment-b4-edge-authorization-scope.md +++ b/docs/experiment-b4-edge-authorization-scope.md @@ -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. 결론부터 | Q4 의 미지수 | 측정 결과 | diff --git a/docs/experiment-b5-redis-loss-persistence.md b/docs/experiment-b5-redis-loss-persistence.md index 17f818b..7e40350 100644 --- a/docs/experiment-b5-redis-loss-persistence.md +++ b/docs/experiment-b5-redis-loss-persistence.md @@ -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. 결론부터 | | 결과 | diff --git a/docs/experiment-b6-key-rotation.md b/docs/experiment-b6-key-rotation.md index 5698aad..8f8ff28 100644 --- a/docs/experiment-b6-key-rotation.md +++ b/docs/experiment-b6-key-rotation.md @@ -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. 결론부터 **질문이 두 갈래로 나뉜다.** diff --git a/docs/experiment-c1-multi-app-sso.md b/docs/experiment-c1-multi-app-sso.md index d06976e..3e61c02 100644 --- a/docs/experiment-c1-multi-app-sso.md +++ b/docs/experiment-c1-multi-app-sso.md @@ -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. 결론부터 | 물음 | 답 | diff --git a/docs/experiment-d1-backup-restore.md b/docs/experiment-d1-backup-restore.md index 8fc7f86..51b50a2 100644 --- a/docs/experiment-d1-backup-restore.md +++ b/docs/experiment-d1-backup-restore.md @@ -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. 결론부터 | 측정 | 값 | diff --git a/docs/experiment-d3-secret-management.md b/docs/experiment-d3-secret-management.md index eb37e3a..b516523 100644 --- a/docs/experiment-d3-secret-management.md +++ b/docs/experiment-d3-secret-management.md @@ -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. 결론부터 | 경로 | 감춰지는가 | diff --git a/docs/experiment-d4-certificate-renewal.md b/docs/experiment-d4-certificate-renewal.md index 65aecbc..5332d59 100644 --- a/docs/experiment-d4-certificate-renewal.md +++ b/docs/experiment-d4-certificate-renewal.md @@ -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. 결론부터 | 확인 | 결과 | diff --git a/docs/experiment-followup-untested-items.md b/docs/experiment-followup-untested-items.md new file mode 100644 index 0000000..fb2ad15 --- /dev/null +++ b/docs/experiment-followup-untested-items.md @@ -0,0 +1,249 @@ +# 후속 — 미측정으로 남겼던 항목을 채운다 + +브랜치 `feature/keycloak-followup-untested-items` · +증거 [`docs/evidence/followup/`](evidence/followup/) · +2026-09-04 17:35–18: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/ -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 는 그보다 훨씬 짧을 것이므로 해상도를 올려야 한다.