리버스 프록시 Caddy의 Caddyfile 매니저를 만들다

서버구축, 개발 등을 취미로 하다보면 방문하게 되는 곳이 서버 포럼 (https://svrforum.com)이다. 이곳에서 알게 되어 사용중인 것 중에 리버스 프록시인 Caddy가 있다.

리버스 프록시(Reverse Proxy)란?

Reverse Proxy는 클라이언트(사용자)들의 요청(request)를 받아 내부 서버(하나 또는 그 이상)로 전달하고 내부 서버의 응답을 요청한 클라이언트에게 돌려주는 서버를 말한다. 즉 클라이언트는 도메인 주소를 기준으로 요청하면 리버스 프록시는 클라이언트의 요청이 어떤 도메인 주소에 대한 요청인가에 따라 해당되는 내부 서버에게 요청을 전달하고 그 응답을 클라이언트에게 돌려주는 역할을 수행한다.

즉 클라이언트는 Reverse Proxy와 통신하게 되며 실제 내부 서버에는 직접 접근할 수 없도록 하는 일종의 네트워크 보안솔루션이다.

Caddy의 불편함

필자는 운좋게도 서울과 춘천 리전에 4 Core / 24 GB RAM의 ARM 기반 가상서버 2대(무료 티어)를 개인적으로 사용하고 있다. 스펙이 스펙이다 보니 성능이 차고 넘치기에 여러 용도의 어플리케이션들을 도커 기반의 가상 서버로 구축해 각각 도메인 주소 기반으로 각 어플리케이션에 접속할 수 있도록 하고 있다. 이때 하나의 IP 주소에 어플리케이션별로 도메인 주소를 할당해 어떤 도메인 주소로 접근했는지에 따라 적절한 어플리케이션 컨테이너로 연결해줄 때 Caddy가 사용된다.

그런데 문제는 Caddy의 설정이 너무나 번거롭다는 점이다. 왜냐하면 Caddy는 GUI를 제공하지 않기 때문이다. 그래서 설정이 변경될 때 마다 ssh 터미널을 실행하고 서버에 접속한 다음 설정 파일인 Caddyfile이 있는 경로까지 찾아가서 vi 같은 명령어로 파일을 열고 하나하나 수정하고 저장한 다음 Caddy를 재구동하거나 설정을 다시 로드시켜 주는 과정을 거쳐야 한다.

그래서 Caddyfile을 웹에서 열고 수정하고 저장하는 과정을 쉽게 할 수 없을까를 고민하다 웹 기반 Caddyfile 편집기를 만들게 되었다.


Caddyfile Manager의 컨테이너 구성

Caddy는 필자가 운영하고 있는 서울과 춘천리전의 두 대의 서버는 Portainer라는 컨테이너 관리자를 통해 관리하고 있다.

OCI-Seoul과 OCI-ChunCheon이 바로 그 두 서버다. 그리고 각 서버에는 다음과 같이 응용프로그램들을 Stack으로 구성해 설치했다. 스택은 연관성 있는 컨테이너들을 하나로 묶어 관리하는 개념의 컨테이너 구성단위다.

caddy-chuncheon 이라는 이름이 붙은 스택(컨테이너)이 그 아래의 여러 응용프로그램 스택(컨테이너)으로 들어오는 클라이언트의 요청을 처리해주는 리버스 프록시 역할을 수행한다.

caddy-chuncheon 스택에 들어가 보면 다음과 같이 두 개의 컨테이너가 포함되어 있다.

하나는 리버스 프록시인 Caddy다. 그리고 caddy-gui가 바로 Caddy의 Caddyfile을 웹 브라우저에서 열고 수정하고 편집한 다음 문법 오류를 검사하여 저장한 다음 적용하는 기능을 수행하는 CaddyManager다. 이 CaddyManager에 브라우저를 통해 접속할 때도 Caddy를 통해 접속하게 된다.

Portainer를 통해 Caddy를 설치할 때 Github 리포지토리에 올려놓은 CaddyManager 소스코드를 빌드하여 Caddy와 함께 Docker 컨테이너로 배포하게 된다.


CaddyManager 코드 작성 및 Github에 Push

CaddyManager 코드는 당연하게도 VSCode에 설치한 AI 에이전트의 도움을 받아 작성했다.

화면에서 볼 수 있듯 Node.js의 Express 프레임워크를 웹서버로 사용하여 코드를 작성했다. 그리고 Portainer를 통해 배포하기 위해 docker-compose.yml 도 함께 Github에 Push했다.

CaddyManager 빌드 과정에서의 문제점

Portainer를 통해 CaddyManager를 배포하기 위해서는 컨테이너 이미지를 Caddy 자체 컨테이너와 별도로 빌드하여 배포해야 하는데 그 과정에서 심각한 문제가 발생했다. Portainer에서 Github 리포지토리 주소를 기반으로 배포하면 HTTP 2.0으로 요청이 전송되어야 하는데 HTTP 1.1로 요청해 빌드가 실패하는 문제다. 다음과 같은 에러가 발생하며 디플로이(배포)가 실패한다.

Deployment error
Failed to deploy a stack: compose build operation failed: listing workers for Build: failed to list workers: Unavailable: connection error: desc = "error reading server preface: http2: failed reading the frame payload: http2: frame too large, note that the frame header looked like an HTTP/1.1 header" 

그런데 이 버그는 포테이너 관리자 웹이 설치된 서버에 디플로이(배포)할 때는 발생하지 않고 포테이너 웹이 설치되지 않은 포테이너 에이전트가 설치된 서버에 배포할 때만 발생한다.

코파일럿, 클로드 등의 AI 에이전트를 통해 docker-compose.yml과 코드를 넣고 질문을 해도 제대로 된 해결책을 제시해주지 못했다. 그러다가 우연히 관련된 오류가 보고된 건수가 상당히 많다는 코멘트가 AI의 응답에 포함되어 있는 것을 발견하고 Portainer의 고질적인 버그일 수도 있겠다는 생각을 하게 됐다.

그래서 이 문제를 우회할 수 있는 방법이 있냐고 물었더니 그제서야 빌드를 서버에서 하지 않고 Github Action을 통해 빌드하고 외부의 레지스트리에 빌드된 이미지를 Push한 다음 그 이미지를 기반으로 배포하는 방법이 있다는 답을 받을 수 있었다.

그래서 Github Action을 수행할 workflow를 작성하고 개발브랜치에서 main 브랜치에 Merge할 때 자동으로 빌드하고 Github의 레지스트리에 Push하도록 구성했다. 이 과정에서 Github가 빌드에 사용되는 가상머신 뿐만 아니라 빌드한 컨테이너 이미지를 저장할 수 있도록 ghcr.io 라는 도메인 주소로 자체적인 Registry를 제공한다는 것을 알게 되었다.

이 때 github의 ID와 자체적으로 생성하는 토큰을 통해 사용자 인증을 수행하여 보안을 유지하고 있었다. 그리고 레지스트리에 Push된 이미지는 Packges 라는 메뉴에서 확인할 수 있었다. 가끔씩 보게되는 저 Packages라는 메뉴는 뭘까? 라는 의문이 들긴 했었는데 이제야 그 용도를 알게 되었다.


CaddyManager 화면

CaddyManager에 웹 브라우저로 접속하면 Caddy의 사용자 인증이 적용되도록 구성했다. 그래서 로그인 창이 먼저 실행된다.

이 로그인은 CaddyManager 자체 사용자 인증 기능이 아닌 Caddy의 Basic Auth 인증 기능을 통해 구현한 사용자 인증이다. 사용자 인증을 거치고 나면 다음과 같이 Caddyfile을 불러와 편집창에 띄워준다.

기능은 실망스러울 정도로 단순하다. 그저 Caddyfile 편집과 저장 그리고 Reload 기능이 모두다. 아~ 저장할 때 설정에 문법 오류가 있는지 검사해주는 기능이 있다. 오류가 있으면 다음과 같이 오류를 보여준다.

docker-compose.yml

수시로 기억을 되살릴 수 있도록 docker-compose.yml 파일도 함께 올려둔다. 아래의 caddy-gui: 가 AI의 도움을 받아 코딩한 웹 기반 Caddyfile 편집기 컨테이너다.

version: '3.8'

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - "80:80"
      - "7790:443"
    environment:
      - TZ=Asia/Seoul
      - XDG_CONFIG_HOME=/config
      - XDG_DATA_HOME=/data
    volumes: # (for OCI)
      - /data/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /data/caddy/data:/data
      - /data/caddy/config:/config
    networks:
      - reverse_proxy

  caddy-gui:
    # The image owner is now dynamically set via the REPOSITORY_OWNER environment variable for CI/CD.
    image: ghcr.io/${REPOSITORY_OWNER}/caddymanager/caddy-gui:latest
    container_name: caddy-gui
    restart: unless-stopped
    ports:
      - "8890:80"
    volumes: # (for OCI)
      - /data/caddy/Caddyfile:/app/Caddyfile
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - reverse_proxy

networks:
  reverse_proxy:
    external: true

코드 중 다음 라인이 Github에서 Merge가 발생하면 자동으로 실행되는 Action에 의해 빌드되고 Github 레지스트리(ghcr.io)에 Push된 이미지를 기반으로 배포하는 것을 알 수 있는 코드다.

image: ghcr.io/${REPOSITORY_OWNER}/caddymanager/caddy-gui:latest

추후 기억을 상기시키는데 도움이 되길 바라며 첨부해둔다.

#reverse_proxy #Caddy #Caddyfile #CaddyfileManager

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다