3P by GN⁺ | ★ favorite | 댓글 1개
  • Kamal Proxy는 웹 애플리케이션 앞에서 동작하는 작은 HTTP 프록시로, 진행 중인 트래픽을 끊지 않고 새 인스턴스로 배포를 전환하도록 설계됨
  • 새 인스턴스는 kamal-proxy deploy로 등록되며, 프록시가 HTTP 헬스 체크를 통과한 뒤에만 새 트래픽을 해당 인스턴스로 라우팅함
  • 배포 명령은 이전 인스턴스의 트래픽이 모두 빠질 때까지 기다리므로, 성공적으로 반환된 뒤에는 진행 중 요청 중단 없이 이전 인스턴스를 제거할 수 있음
  • 단일 프록시에서 여러 애플리케이션을 운영할 수 있도록 호스트 기반 라우팅, 경로 기반 라우팅, 자동 TLS와 수동 TLS 인증서 지정 기능을 제공함
  • Kamal Proxy는 Kamal 배포 도구의 일부로 설계됐지만, 단독 실행이나 다른 배포 도구의 구성 요소로도 사용할 수 있음

Kamal Proxy가 하는 일

  • Kamal Proxy는 제로 다운타임 배포 조율을 쉽게 하기 위한 작은 HTTP 프록시임
  • 웹 애플리케이션을 Kamal Proxy 뒤에서 실행하면, 진행 중인 트래픽을 중단하지 않고 변경 사항을 배포할 수 있음
  • 이 동작을 위해 애플리케이션 쪽의 특별한 협력은 필요하지 않음
  • Kamal Proxy는 컨테이너 패키징과 프로비저닝을 포함하는 Kamal의 일부로 동작하도록 설계됨
  • 단독으로 사용하거나 다른 배포 도구의 일부로 사용할 수도 있음

실행과 배포 흐름

  • 프록시 인스턴스는 kamal-proxy run 명령으로 실행함
  • 설정 파일은 없으며, 기본값이 애플리케이션에 맞지 않을 때 옵션을 지정할 수 있음
    • 기본 HTTP 포트는 80
    • 다른 포트로 실행하려면 kamal-proxy run --http-port 8080 사용
    • 전체 옵션은 kamal-proxy help run에서 확인 가능
  • 트래픽을 웹 애플리케이션으로 보내려면 애플리케이션 인스턴스를 프록시에 deploy
  • 배포 대상 인스턴스는 hostname:port 형식으로 지정함
    • 예: kamal-proxy deploy service1 --target web-1:3000
    • 이 명령은 web-1:3000service1 서비스 이름으로 등록함

제로 다운타임 전환 방식

  • 새 인스턴스가 등록되면 Kamal Proxy는 즉시 HTTP 헬스 체크를 실행해 접근 가능성과 동작 여부를 확인함
  • 헬스 체크가 성공하면 새 트래픽을 새 인스턴스로 라우팅하기 시작함
  • 새 인스턴스가 적절한 시간 안에 healthy 상태가 되지 못하면 deploy 명령은 배포를 중단하고 0이 아닌 종료 코드를 반환함
  • 각 배포는 이전에 배포된 인스턴스의 모든 트래픽을 새 인스턴스로 넘김
  • deploy 명령은 이전 인스턴스에서 트래픽이 모두 빠질 때까지 기다린 뒤 반환함
    • 성공적으로 반환된 뒤에는 이전 인스턴스를 제거해도 진행 중 요청을 끊지 않음
  • 새 인스턴스가 healthy 상태일 때만 트래픽을 받고, 이전 인스턴스는 완전히 drain된 뒤 제거되므로 배포가 제로 다운타임으로 진행됨

헬스 체크 설정

  • 기본 헬스 체크는 각 서비스에 대해 초당 1회 /up으로 GET 요청을 보냄
  • 200 응답을 healthy 상태로 간주함
  • 애플리케이션에 맞게 헬스 체크를 바꾸려면 deploy 플래그를 사용함
    • --health-check-path
    • --health-check-port
    • --health-check-timeout
    • --health-check-interval
  • 헬스 체크 경로 변경 예시:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
  • 메인 서비스와 다른 포트에서 헬스 엔드포인트를 노출하는 경우:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080

라우팅 기능

  • 호스트 기반 라우팅

    • 호스트 기반 라우팅을 사용하면 하나의 Kamal Proxy 인스턴스로 같은 서버의 여러 애플리케이션에 트래픽을 라우팅할 수 있음
    • 배포 시 특정 호스트를 지정할 수 있음
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com
    • 이 방식으로 배포된 인스턴스는 지정된 호스트의 트래픽만 받음
    • 각 애플리케이션에 별도 호스트를 지정하면 포트 충돌 없이 같은 서버에서 여러 애플리케이션을 실행할 수 있음
    • 특정 호스트는 한 번에 하나의 서비스만 라우팅할 수 있음
    • 이미 service1app1.example.com을 사용 중이면 service2가 같은 호스트로 배포될 때 오류가 반환됨
    • service1을 제거한 뒤에는 service2를 같은 호스트에 배포할 수 있음
  • 경로 기반 라우팅

    • 경로 기반 라우팅은 요청 경로에 따라 트래픽을 다른 서비스로 나누는 애플리케이션에 사용함
    • 서비스는 서로 다른 경로 접두사 아래에 마운트될 수 있음
    • /api로 시작하는 요청을 web-1로 보내는 예: kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api
    • 나머지를 web-2로 보내는 예: kamal-proxy deploy service2 --target web-2:3000
    • 기본적으로 경로 접두사는 upstream으로 전달되기 전에 제거됨
    • /api/users/123 요청은 web-1/users/123으로 전달됨
    • 원래 경로를 그대로 전달하려면 --strip-path-prefix=false를 지정함

TLS 지원

  • Kamal Proxy는 애플리케이션용 TLS 인증서를 자동으로 획득하고 갱신할 수 있음
  • 자동 TLS는 배포 시 --tls 플래그를 추가해 활성화함
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
  • 자동 TLS에는 호스트 지정이 필요함
    • 임의 호스트명에 대해 악의적으로 인증서를 요청하지 못하게 하기 위한 조건임
  • 경로 기반 라우팅에서 TLS 옵션은 루트 경로에 설정해야 함
    • 같은 호스트의 다른 경로에 배포된 서비스는 루트 경로에 지정된 TLS 설정을 사용함
  • 수동으로 획득한 TLS 인증서, 자체 인증 기관, Cloudflare origin certificate을 설치해야 하는 경우 인증서 파일과 개인 키 경로를 직접 지정할 수 있음
    • --tls-certificate-path cert.pem
    • --tls-private-key-path key.pem

환경 변수와 빌드

  • Docker 컨테이너 실행 같은 환경에서는 run 옵션을 환경 변수로 지정할 수 있음
  • kamal-proxy run은 설정된 환경 변수가 있으면 각 옵션 값을 환경 변수에서 읽음
    • kamal-proxy run --http-port 8080
    • HTTP_PORT=8080 kamal-proxy run
  • 환경 변수 이름이 기존 환경과 충돌하면 KAMAL_PROXY_ 접두사를 붙여 구분할 수 있음
    • KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
  • 로컬 빌드는 동작하는 Go 환경에서 make로 수행함
  • Docker 컨테이너로 빌드하려면 make docker를 사용함
  • 프록시 명령을 시험해볼 수 있는 Docker Compose 설정은 example 폴더에 있음

댓글과 토론

Hacker News 의견들
  • 동작 이름으로 deploy를 고른 건 이상함
    이미 과부하된 용어라 “앱이 배포됐나? 아니 프록시에 배포됐나?” 같은 혼란이 생길 듯함
    bind, intercept, 그냥 proxy 같은 말을 쓸 수도 있었을 텐데 왜 deploy인지 모르겠음

    • “배포됨”은 실행 중이고 프록시에 등록됨이라는 뜻으로 보면 됨
    • 인입 트래픽이 프록시에서 온다면, deploy가 “프록시 트래픽이 새 앱 인스턴스로 흐르기 시작했다”는 뜻 말고 달리 무엇이 될 수 있는지 모르겠음
  • 요즘은 다른 서버들로도 무중단 배포를 쉽게 할 수 있는데, 이걸 별도 앱 전체로 만든 선택이 흥미로움
    예를 들어 Unix 소켓을 지원하는 앱+웹 프록시는 파일을 옮기는 것만으로 무중단이 가능함
    원자적이고, curl로 워밍업 요청도 보낼 수 있음
    등록 시스템까지 만드는 건 과해 보임

    • 이건 Kamal(https://kamal-deploy.org)의 아주 작은 일부일 뿐임
      Kamal은 클라우드에서 자체 하드웨어로 옮기며 수백만 달러를 절약하는 데 쓴 배포 도구임(https://basecamp.com/cloud-exit)
    • 이 방식에 대해 좀 더 설명해 줄 수 있으면 좋겠음
      참고할 글이 있다면 더 배워보고 싶음
  • 이건 주로 Docker Swarm에서 서비스 컨테이너를 교체할 때 트래픽이 끊기는 근본 문제를 처리하려는 것으로 보임
    Cloud 66에서 JAMStack 서버를 만들 때 같은 문제를 겪었고, 자체 프록시 대신 Caddy를 썼으며 Traefik도 충분히 적합했을 것 같음
    Kamal이 왜 k8s나 k3s 대신 Swarm을 골랐는지는 모르겠지만, 복잡성은 어딘가에 자리 잡아야 하고 숨길 수는 없어서 결국 자체 프록시로 간 듯함
    직접 써보진 않았지만 WebSockets, SSE, HTTP/3, 각종 압축과 암호화 지원을 계속 쫓아다니게 될 것 같아 꽤 회의적임

    • Kamal은 “Kubernetes는 너무 복잡하다”는 전제 위에 만들어진 느낌이고, 그 정당화에서 출발해 Kubernetes가 하는 상당 부분을 다시 만들고 있음
      단순하게 시작한 역방향 프록시가 결국 피하려던 복잡성을 계속 흡수하게 되는 예로 보임
      넓고 확장 가능한 실행 시스템에는 경쟁자가 필요하다고 생각하지만, Kamal은 피하려던 복잡성으로 뒷걸음질치는 작업처럼 보임
      Kubernetes가 희망 상태 관리 프레임워크로서 가진 역량과 유연성이 오히려 더 도드라져 보임
    • 오케스트레이션과 프록시를 섞어 보고 있는 것 같음
      Docker를 계속 쓰면서도 프록시는 Caddy, Traefik, Envoy를 쓸 수 있음
      Kubernetes도 결국 이들을 인그레스 컨트롤러로 쓰는 경우가 많음
    • Kamal이 Docker Swarm을 쓴다는 건 어디서 본 건지 모르겠음
      보기에는 Swarm을 쓰지 않는 것 같음
    • Kamal은 평범한 docker run 명령을 쓰며, Swarm이나 오케스트레이션은 없음
      기본적으로 Capistrano를 대체하는 단순한 도구임
    • 그게 이유라면 이 주제에 대해 아주 기초적인 조사도 안 한 느낌임
      컨테이너 오케스트레이션 서비스의 기본은 인그레스 컨트롤러로 연결과 블루/그린 배포를 처리하는 것임
      더 정확히는 수년, 길게는 수십 년 전부터 이 용례를 처리해 온 수많은 역방향 프록시에 부여된 역할명이 인그레스 컨트롤러
  • 무중단 배포가 일반적으로 어떻게 동작하는지 간단히 설명해 줄 수 있으면 좋겠음
    두 버전의 앱이 동시에 떠 있고 새 트래픽이 새 버전으로 라우팅되는 구조라고 추측함
    그런데 단일 데이터베이스를 쓰고 새 앱 버전이 스키마 변경을 가져오면, 시작 시 마이그레이션 스크립트가 스키마를 바꿀 텐데 이전 앱 버전은 예전 스키마를 기대함
    이걸 어떻게 처리하는지 궁금함

    • 첫 단계는 마이그레이션과 배포를 분리하는 것임
      많은 프레임워크가 코드 배포 시 마이그레이션을 실행하지만, 실제로는 실행 시점을 수동으로 제어하는 편이 좋음
      각 코드 버전은 현재 스키마와 미래 마이그레이션 이후 스키마 모두에서 동작해야 해서 사실상 하위 호환 코드가 됨
      흐름은 “현재/미래 스키마 모두에서 동작하는 새 코드 배포 → 확인 → 마이그레이션 실행 → 확인 → 사라진 스키마 대응 코드 정리”가 됨
    • 마이그레이션은 하위 호환이어야 해서 데이터베이스 스키마가 앱의 두 버전을 모두 지원할 수 있어야 함
      무중단 배포나 롤링 배포를 하려면 치러야 하는 추가 비용이고, 큰 회사들은 대체로 이렇게 함
    • 정말 무중단 배포가 필요할 때의 방법은 다른 답변들이 설명했지만, 대부분의 조직과 대부분의 마이그레이션에서는 DB 마이그레이션으로 인한 중단 시간이 사실상 0과 구분되지 않음
      특히 지역 단위 사용자를 대상으로 하고 조용한 시간대를 골라 배포할 수 있다면 더 그렇다
    • 직접 써보진 않았지만 Xata가 적어도 Postgres에서는 DB 마이그레이션에 꽤 깔끔한 해법을 만든 것 같음
      앱 두 버전이 동시에 실행될 수 있음
      https://xata.io/blog/multi-version-schema-migrations
    • 맞음, 어느 시점에는 두 버전이 모두 실행 중이어야 함
      로드 밸런서가 Server2에서 연결을 받기 시작하고 Server1에서는 새 연결을 받지 않음
      이후 Server1의 모든 연결이 닫히면 Server1을 끊음
      서로 다른 서버일 수도 있고, 한 서버의 여러 워커일 수도 있음
      그 기간에는 다른 답변들처럼 마이그레이션이 하위 호환이어야 함
  • Kamal 2는 현재 RC(https://github.com/basecamp/kamal/releases)이고, 자동 SSL을 지원해 한 서버에서 여러 앱을 Kamal로 쉽게 돌릴 수 있게 된다니 기대됨

    • autocert 패키지를 쓰는데, 이건 최소한의 구현에 가까움
      취약하고, Let's Encrypt의 발급 제한과 동시 인증서 제한 때문에 프록시 인스턴스를 수평 확장하기 어려움
      Caddy/Certmagic은 공유 저장소에 데이터를 써서 인증서를 하나만 발급하고 모든 인스턴스가 저장소를 통해 재사용·조율하게 해 이 문제를 해결함
      발급자 대체, 발급 제한 회피, ARI 지원도 없음
      업스트림이 가능해질 때까지 요청을 붙잡는 것도 Caddy는 reverse_proxytry_durationtry_interval을 설정하면 잘 처리함
      프록시 헤더 처리도 신뢰할 IP를 고려하지 않아, 활성화하면 누군가 X-Forwarded-For를 설정해 IP를 위조할 수 있음
      기본값이 꺼져 있는 건 다행이지만 경고도 없음
      단순함이 목적이라는 건 이해하지만, 현재 상태로는 덜 익었고 더 나은 선택지가 너무 많아 실제로 쓰고 싶지 않음
  • 이걸 어떻게 쓰는지 잘 모르겠고 뭔가 놓친 것 같음
    예제는 web 서비스 복제본 4개를 시작함
    그중 example-web-1 같은 복제본 하나에 배포해서 서비스를 만들 수 있다면, 나머지 3개 복제본은 무엇을 하는지 모르겠음
    web을 업데이트해 무중단 배포를 하려면 web 서비스에서 빌드 명령을 실행하고, 어떤 식으로든 이 서비스를 시작한 뒤, 새 대상에 대해 deploy를 실행해야 할 것 같음
    그런데 docker compose up --build --force-recreate web을 실행하면 기존 복제본이 내려가서 전부 무의미해짐

    • 첫 질문의 다른 복제본들은 Docker가 VIP나 DNS 요청에서 여러 IP를 반환하는 방식으로 로드 밸런싱함[0]
      이 프록시가 DNS 요청에서 반환된 여러 레코드를 대상으로 균형 분산하는지는 확인하지 않았지만, 적어도 VIP 기반 로드 밸런싱이면 기대한 대로 동작할 것임
      두 번째 업데이트 부분은 덜 명확함
      같은 네트워크 안에 다른 이름의 서비스를 띄우고 kamal-proxy deploy를 하는 기대일 수도 있음
      서비스 이름에 버전 번호를 넣는 식일 수 있고, 빠르게 되돌리고 싶다면 이전 버전을 뜨거운 상태로 유지하는 것도 말이 됨
      [0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
    • 그냥 k8s rollout restart deployment를 쓰지 않을 이유가 뭔지 모르겠음
      아니면 DNS나 라우터를 두 백엔드 사이에서 전환해도 됨
  • 이게 트래픽 일시 정지 패턴을 구현한 건지 궁금함
    프록시가 트래픽을 몇 초 동안 사실상 멈추는 방식으로, 들어온 요청은 평소보다 몇 초 더 걸려 보이지만 짧은 지연 뒤 완료됨
    그 몇 초 동안 작은 DB 마이그레이션이나 5초 안에 끝낼 수 있는 조금 더 복잡한 차단형 인프라 변경을 실행할 수 있음

    • 이걸 실제 운영 환경에서 해본 적이 있는지 궁금함
      꽤 위험하게 들리고, 앱 서버 회사에서 몇 년 일했지만 이 패턴은 처음 들음
      다만 Django 공동 창시자라면 여러 배포를 봤을 테니 그냥 넘기기 어렵긴 함
    • 솔직히 위험하게 살기 패턴처럼 들림
    • Caddy는 이걸 함
      예전에 이 주제로 이야기했던 기억이 있음
  • Traefik이나 검증된 다른 도구를 쓰지 않고 자체 프록시를 만들기로 한 이유를 어디서 설명했는지 궁금함

  • 타임아웃을 설정하는 방법이 있는지 궁금함
    https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
    https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
    https://blog.cloudflare.com/exposing-go-on-the-internet/

  • NIH임. 더 덧붙일 말이 없음