자물쇠는 잠겼는데 503이었습니다 — 인증서 다음에 남아 있던 것들
와일드카드 인증서가 발급된 뒤 임의의 하위 주소를 브라우저에 쳐봤습니다. 주소창의 자물쇠는 잠겼고, 화면에는 503이 떠 있었습니다. 인증서는 끝났는데 아직 아무것도 되지 않았다는 뜻이었습니다.
앞 글에서 리졸버를 갈아끼워 와일드카드 인증서를 받은 이야기를 했습니다. 이 글은 그 다음에 남아 있던 것들에 대한 기록입니다. 결론부터 적자면, 인증서와 DNS는 끝났는데 고객사 주소를 실제로 붙이지는 못했습니다. 앞에 남은 일이 둘 있었고, 그중 하나는 인프라 문제가 아니었습니다.
503이 뜬다는 건 인증서가 성공했다는 뜻입니다 — TLS 층과 라우팅 층
503의 원인 자체는 단순했습니다. 인증서는 와일드카드로 정상 발급되지만, 그 주소를 받아줄 앱이 없습니다. 앞 글에서 인증서를 받으려고 둔 그 우선순위 낮은 라우터가 하는 일은 "이 이름을 서빙한다고 선언하는 것"까지이고, 그 뒤에 붙은 것이 아직 없습니다.
중요한 것은 원인이 아니라 두 가지가 서로 다른 층에서 일어난다는 사실이었습니다. 자물쇠가 잠겼다는 것은 TLS 종료가 성공했다는 뜻입니다. 그 다음에 오는 라우팅, 그러니까 "이 호스트로 들어온 요청을 어느 앱으로 보낼 것인가"는 완전히 별개의 질문입니다. 503이라는 숫자 자체가 그 순서를 말해줍니다. 프록시가 이름은 알아들었는데 뒤로 넘길 곳이 없을 때 내는 응답이니까요.
| 층 | 성공하면 보이는 것 | 실패하면 보이는 것 |
|---|---|---|
| TLS 종료 — 인증서 | 주소창의 자물쇠 | 브라우저가 페이지 대신 띄우는 경고 화면 |
| 라우팅 — 어느 앱으로 보낼지 | 앱이 만든 화면 | 프록시가 만든 5xx |
이 표를 미리 갖고 있지 않으면 503 앞에서 인증서를 의심하게 됩니다. 인증서가 가장 최근에 건드린 것이고, 발급 권한을 어디까지 줄지 따지는 일처럼 조심스러운 작업을 막 끝낸 참이니 자연스러운 의심입니다. 그런데 503은 인증서가 성공한 뒤에만 볼 수 있는 화면입니다. 인증서가 문제였다면 브라우저는 503을 보여주지도 못하고 그 앞에서 멈췄을 것입니다. 자물쇠가 잠겼다는 것 자체가 인증서를 용의선상에서 빼주는 증거인데, 그걸 증거로 읽으려면 두 층을 나눠 보는 눈이 먼저 있어야 합니다.
와일드카드가 없앤 단계는 배포가 아니었습니다
인증서가 나왔다고 새 주소가 동작하는 것이 아니라, 앱에 그 주소를 등록하고 다시 배포해야 비로소 화면이 뜹니다. 주소가 하나 늘 때마다 이 과정이 반복됩니다.
2편에서 도메인 추가가 배포 행위가 된다고 적었던 것이 여기서 그대로 돌아왔습니다. 그때는 고객 소유 도메인을 하나씩 받아줄 때의 이야기였는데, 서브도메인에서도 성질이 같았습니다. 와일드카드가 없애준 것은 주소마다 인증서를 따로 받는 단계이지 주소마다 배포하는 단계가 아니었습니다.
인프라만 놓고 보면 주소 하나 붙이는 일은 DNS 레코드 한 줄과 인증서 자동 발급으로 끝나는, 30분짜리 작업처럼 보입니다. 그 30분이 틀린 것은 아닙니다. 다만 30분 뒤에 뜨는 화면이 503이라는 것까지가 그 30분에 포함되어 있었습니다.
인증서는 결승선이 아니라 시작선이었습니다
앞의 네 글을 쓰는 동안 저는 계속 인증서를 목적지처럼 다뤘습니다. 발급에 필요한 권한을 좁히고, 못 덮는 범위를 따지고, 어느 프록시로 받을지를 골랐습니다. 정작 받고 나서 처음 본 화면이 503이었다는 것은, 그 네 글이 전부 준비에 대한 글이었다는 뜻이기도 합니다.
화면이 떠도 요청은 옛 주소로 나갑니다
다음 벽은 앱 쪽에 있었습니다. 웹 앱이 API 주소를 절대주소로 박아서 빌드합니다. 이 상태로 새 주소를 붙이면 화면은 뜨는데, 그 화면이 보내는 요청은 원래 주소로 나갑니다.
브라우저에게 그건 다른 출처입니다. API가 허용하는 출처 목록(CORS)에는 방금 만든 주소가 들어 있지 않으니 요청이 막힙니다. 인증 쿠키를 실어 보내는 요청이라면 조건이 하나 더 붙습니다. 서버가 그 출처를 정확히 지목하고 자격증명을 함께 허용해야 하는데, 목록에 없는 주소를 지목해줄 리가 없습니다.
3편에서 세션 비용 이야기를 하며 브라우저가 지금 보고 있는 그 오리진의 /api/*를 부르게 하자는 해법을 제시했었습니다. 그 해법이 여기서 착수 전 선행 조건으로 되돌아왔습니다. 그때는 커스텀 도메인에 대비하는 이야기였는데, 지금은 커스텀 도메인 없이 서브도메인만으로도 필요합니다. 예측이 맞았다기보다 예측했던 것보다 일찍 왔다는 쪽이 정확하겠습니다.
그리고 이 값은 다시 띄워서는 안 바뀝니다
고치는 일 자체는 어렵지 않습니다. 상대경로로 바꾸면 됩니다. 다만 한 가지를 같이 알고 있어야 합니다. 이 주소는 빌드 시점에 번들 안으로 굳는 값이라, 컨테이너 환경변수를 갈아끼우고 다시 띄우는 것으로는 바뀌지 않습니다. 재빌드가 필요합니다.
빌드 시점
API 주소(환경변수) → 번들 파일 안에 문자열로 굳음
배포 시점
컨테이너 환경변수 교체 → 이미 굳은 문자열은 그대로 → 재빌드 필요
설정은 코드에 넣지 말고 환경에 두라는 원칙이 있습니다. 12-factor app의 config 항목으로 알려진 그것인데, 빌드 시점에 굳는 값은 이름만 환경변수일 뿐 사실상 코드 쪽에 있습니다. 그래서 "환경변수 하나 바꾸면 되는 일"로 예상하고 작업 시간을 잡으면 어긋납니다.
여기까지는 빌드 설정을 읽고 안 것입니다. 새 주소를 실제로 붙여서 요청이 막히는 것을 본 것은 아닙니다. 붙이기 전에 고치기로 했으니, 앞으로도 그 화면을 볼 일이 없기를 바라고 있습니다.
A 고객사 주소로 B 계정이 로그인하면 어떻게 할 것인가
마지막 하나는 성격이 달랐습니다. 이건 인프라 문제가 아니라 제품 결정입니다.
acme.example.com으로 들어온 사람이 다른 고객사에 속한 계정으로 로그인을 시도할 수 있습니다. 막을 것인가, 아니면 자기 주소로 보낼 것인가. 둘 다 말이 되고, 둘 다 다른 코드를 부릅니다. 앞의 벽들처럼 "이렇게 하면 된다"가 없습니다.
정하지 않으면 기본값이 골라집니다
여기서 제가 놓칠 뻔한 것은 정하지 않는 것도 하나의 선택이라는 점이었습니다. 아무것도 쓰지 않은 채 주소를 붙이면 그냥 로그인이 됩니다. 아무도 고르지 않았지만 고른 것과 똑같은 상태가 만들어집니다.
그래서 문제는 무엇을 고를지가 아니라 언제 고르는지였습니다. 붙여놓고 나중에 정하면 그때는 이미 그 상태의 세션이 생겨 있습니다. 그다음에 정책을 바꾸는 일은 코드 수정이 아니라, 이미 만들어진 세션과 그 세션이 남긴 데이터를 어떻게 할 것이냐는 문제가 됩니다. 같은 결정인데 며칠 사이에 코드 수정에서 데이터 마이그레이션으로 승격됩니다.
3편에서 저는 목적을 먼저 정해야 두 번 짓지 않는다고 썼는데, 그 글은 "주소를 어떻게 줄 것인가"까지만 봤습니다. "그 주소로 누가 들어오는가"는 보지 않았습니다. 앞의 네 글을 통틀어 없던 질문이고, 인프라 쪽을 아무리 깔끔하게 정리해도 나오지 않는 질문이기도 합니다.
그래서 인증서까지만 해 두고 멈췄습니다
남은 둘의 성질이 다르다는 것이 멈춘 이유였습니다. 절대주소는 언제 고쳐도 값이 같습니다. 빌드를 다시 하면 되고, 틀렸으면 되돌리면 됩니다. 반면 A 주소로 B 계정이 들어오는 문제는 붙이는 순간부터 값이 올라갑니다. 되돌릴 수 있는 결정과 되돌릴 수 없는 결정이 섞여 있었고, 되돌릴 수 없는 쪽이 아직 정해지지 않았습니다.
그래서 순서를 이렇게 잡았습니다. 되돌리기 어려운 것부터 정하고, 되돌릴 수 있는 것은 그다음에 합니다. 인증서와 DNS는 되돌리는 값이 아직 설정 수준이라 먼저 해둬도 크게 손해가 아니었고, 실제로 해뒀습니다. 물론 앞 글에 적었듯 그쪽에서도 제가 의도한 것보다 넓게 건드린 것이 하나 있었으니, 되돌릴 수 있다는 말을 너무 편하게 쓰지는 않으려 합니다. 어쨌든 여기서 멈춘 것이 게으름인지 신중함인지는 솔직히 지금도 잘 모르겠습니다. 다만 붙여놓고 정하는 쪽보다는 낫다는 판단이었습니다.
다섯 편을 쓰고 나서 돌이켜 생각해보니, 이 시리즈에서 제가 매번 물었던 것은 사실 같은 질문이었습니다. 1편에서는 "이 권한을 주면 최악의 경우 무엇을 잃는가"였고, 2편에서는 "먼저 만든 쪽이 나중에 버려지지 않는가"였고, 3편에서는 "무엇을 위해 이걸 하는가"였고, 앞 글에서는 "내가 바꾼 것이 어디까지 적용되는가"였습니다. 전부 되돌릴 수 있는가를 다르게 물은 것이었다는 생각이 듭니다.
인프라 작업은 되돌리기 쉬워 보입니다. 설정 한 줄을 지우면 되니까요. 그런데 그 설정 위로 사용자와 데이터가 올라오기 시작하면, 그때부터 그것은 설정이 아니라 이력입니다. 이력은 지운다고 없던 일이 되지 않습니다.
관련 글
리졸버를 갈아끼웠습니다 — 순단은 3초, 바뀐 것은 인증서 갱신 경로 전부
설계 세 편을 쓰고 3주 뒤에 실제로 발급했습니다. 계획서에는 기존 리졸버를 건드리지 않겠다고 적어뒀는데 실행에서는 갈아끼웠고, 그 대가로 순단 3초와 '기존 인증서의 갱신 경로가 리졸버 단위로 전부 바뀐다'는 사실이 따라왔습니다. 확인한 것과 아직 확인하지 않은 것도 함께 적었습니다.
멀티테넌트 주소 설계 — 목적을 먼저 정해야 두 번 짓지 않습니다
테넌트마다 주소를 준다는 요구는 세 가지 서로 다른 목적을 감추고 있었습니다. 목적을 확정하기 전에 인프라를 지으면 그중 하나는 버려집니다. 그리고 커스텀 도메인의 진짜 비용은 TLS가 아니라 세션이었습니다.
와일드카드가 못 덮는 것 — Traefik DNS-01과 Caddy on-demand TLS를 가른 지점
와일드카드 인증서를 손에 넣고 나서야 그것이 한 단계밖에 못 덮는다는 사실이 문제가 됐습니다. 고객사 소유 도메인을 받으려면 애초에 다른 메커니즘이 필요했고, 그 판단이 프록시 선택 자체를 다시 보게 만들었습니다.