Caddy 2.11.6 turns a silent config failure on /load into an HTTP 400
Caddy 2.11.6, /load의 조용한 설정 실패를 HTTP 400으로 알립니다
Caddy 2.11.4는 잘못된 설정을 받아들이고 HTTP 200을 반환한 뒤 기존 서버까지 응답 불능으로 만들었습니다. 2.11.6과 2.11.7에서는 일부 설정 오류를 400으로 거부하며 기존 설정을 유지합니다. 2.11.7은 이전 Unix 소켓 연결이 약 2분간 멈추던 문제도 즉시 거부로 바꿉니다.
- 주제
에디터 노트
설정 적용 API가 200을 반환해도 실제 서비스가 살아 있다는 보장은 없습니다. 2.11.4의 음수 가중치 사례에서는 200 뒤에 데이터 경로가 중단됐고, 새 버전은 해당 설정을 400으로 거부하면서 기존 설정을 유지했습니다. 댓글에서 제안한 데이터 경로 확인은 이 간극을 직접 검사하는 방법입니다. 글쓴이의 테스트도 새 리스너만 확인하면 이전 Unix 소켓의 100~120초 지연을 놓칠 수 있음을 보여줍니다. 다만 검증 강화는 명시된 오류 사례에 한정됩니다. 글쓴이는 중복 ID와 잘못된 정수 입력이 이전 버전에서도 거부됐다고 구분했고, 모든 잘못된 설정을 새 버전이 잡는다고 주장하지 않습니다. 따라서 400 응답 개선은 배포 자동화의 오판을 줄이지만, 재적용 뒤 서비스 상태와 이전 소켓을 확인하는 검사는 별도로 남습니다. 댓글의 문서 처리 경험담은 개인 사례이며 이 테스트의 결과를 추가로 검증하는 근거로 보지는 않습니다.
AI 요약
Caddy 관리 API의 /load는 JSON 설정을 받아 실행 중인 설정을 교체합니다. 글쓴이는 Caddy 2.11.4에 음수 가중치가 포함된 weighted_round_robin 설정을 POST했습니다. API는 HTTP 200을 반환했지만 실제 서비스 포트는 빈 응답을 내며 중단됐습니다. 반면 2.11.6과 2.11.7은 같은 설정에 HTTP 400과 오류 내용을 반환했고, 기존의 정상 설정은 계속 요청을 처리했습니다.
설정 검증 변경
2.11.7은 weighted_round_robin에 양수 가중치가 하나도 없는 설정을 거부합니다. 가중치가 0인 항목은 허용하며, 규칙은 모든 가중치가 양수여야 한다는 뜻이 아니라 최소 하나가 양수여야 한다는 뜻입니다. forward_auth에 uri를 중복 선언하면 2.11.4에서는 경고 없이 마지막 값만 적용했지만, 2.11.7에서는 중복 선언 오류가 발생합니다. 닫는 중괄호가 빠진 map 핸들러의 대상 자리표시자도 2.11.7에서 거부됩니다.
모든 설정 오류가 새로 검출되는 것은 아닙니다. 라우트의 중복 @id는 2.11.4에서도 HTTP 400으로 거부됐고, 정수 필드인 file_limit에 3.5를 넣는 경우도 Go JSON 디코더가 이미 거부했습니다. 변경 사항은 duplicate named_routes, 중복 forward_auth 하위 지시어, 잘못된 weighted_round_robin 가중치, 모호한 map 입력 등 특정 사례를 다룹니다. 따라서 2xx 응답만 확인하는 배포 자동화는 2.11.4 이전 버전에서 설정 적용 성공으로 오판할 수 있습니다. 글쓴이는 재적용 후 데이터 경로를 확인하는 자체 헬스 체크를 없애지 말라고 권합니다.
Unix 소켓 재적용 동작
글쓴이는 2.11.6에서 서버의 리스너를 unix//tmp/sock_a에서 unix//tmp/sock_b로 옮기는 설정을 재적용했습니다. 이전 소켓 파일은 남아 있었고, 해당 경로에 연결하면 약 100~120초 동안 시간 초과가 발생한 뒤 연결 거부로 바뀌었습니다. 2.11.7에서는 재적용 직후 이전 소켓 파일이 사라졌고, 연결 시도도 즉시 거부됐습니다. 새 소켓이 응답하는지만 확인하면 이전 경로의 지연을 발견하지 못할 수 있으므로 글쓴이는 리스너를 옮길 때 새 경로와 이전 경로를 모두 확인하라고 설명합니다.
재현 방법과 결과
글쓴이는 Docker 이미지 caddy:2.11.4와 caddy:2.11.7에서 정상 설정을 적용한 뒤 음수 가중치 설정을 POST했습니다. 2.11.4는 잘못된 설정에도 200을 반환했고 이후 서비스 요청은 연결 실패를 보였습니다. 2.11.7은 400을 반환하고 기존 서비스 응답을 유지했습니다. 변경을 배포하는 CI 작업이나 컨트롤 플레인이 /load의 2xx만 성공 기준으로 삼는다면 실행 중인 Caddy 버전을 확인해야 합니다.
dev.to 반응
- @robertpaschal — 거짓말하는 200 응답은 제가 다루는 모든 컨트롤 플레인에 적용하고 싶습니다. 서버가 죽었는데 재적용 엔드포인트가 성공을 알리는 상황은 시끄러운 실패보다 나쁩니다. 아무도 호출을 받지 않고 들여다보지 않으며 가장 먼저 알아채는 사람은 사용자입니다. 설정 재적용을 운영하면서 배운 원칙은 컨트롤 플레인 응답만 믿지 않는 것입니다. 매번 데이터 경로를 확인하고 배포 로그가 아니라 실제로 제공되는 내용을 비교합니다. 오래된 소켓이 멈추는 문제도 다른 모습으로 같은 문제입니다. 아무도 서비스하지 않는 소켓에서 2분간 멈추면 느린 하위 서비스처럼 보여 당직자가 엉뚱한 서비스를 2분간 쫓습니다. 설정 변경이 적용됐다고 했지만 실제로는 반영되지 않은 문제를 문서 처리 파이프라인에서 겪었습니다. 로그에는 안 나오고 출력물에만 드러났습니다. 재적용 뒤 오래된 소켓 경로도 확인하시나요, 아니면 새 리스너만 확인하시나요?
- @alexgeorgiev17 — 네, "배포 로그가 아니라 데이터 경로를 확인하라"는 것이 이 글의 결론에 가깝습니다. 400 응답은 특정 오류 목록만 해결하며 모든 오류를 해결하지는 않습니다. 이전 소켓은 실제 테스트에서 확인했습니다. 그랬기 때문에 2분간의 지연을 발견했습니다. 리스너를 옮긴 뒤 약 0.5초 간격으로 이전 경로를 확인했습니다. 2.11.6에서는 파일이 남아 있었고 100~120초쯤까지 연결이 시간 초과됐습니다. 새 리스너만 확인했다면 새 소켓은 바로 응답했으므로 모든 것이 정상으로 보였을 겁니다. 그러니 리스너를 옮긴다면 두 경로를 모두 확인하세요. 새 경로는 응답해야 하고 이전 경로는 사라졌거나 즉시 거부해야 합니다. 이전 경로에 짧은 연결 제한 시간을 두는 것이 유용합니다. 거부는 0밀리초 만에 돌아오고 멈춤은 제한 시간만큼 기다리게 하므로 결과를 바로 구분할 수 있습니다.
- @robertpaschal — 0.5초 간격 확인은 이 결과를 재현 가능하게 만드는 세부 사항이고, 거부와 멈춤을 구분하는 점은 제가 적용할 부분입니다. 문서 처리 파이프라인에서도 설정 변경이 적용됐지만 로그에는 안 나오고 출력물에만 드러나는 함정이 있었습니다. 그래서 살아 있어야 하는 대상만이 아니라 죽어 있어야 하는 대상도 확인하는 법을 배웠습니다. 100~120초 동안 남아 있는 현상은 응답하지 않는 소켓이 거부하는 소켓보다 더 나쁠 수 있음을 보여줍니다. 멈춤은 제한 시간을 조용히 소모합니다. 이제 리스너를 옮길 때마다 이전 경로에 짧은 제한 시간의 확인을 두겠습니다.
원문: dev.to / 번역·요약: Trawling