Alpha 서버 cloudflared 구조 완전 해설

EC2 alpha 호스트에서 돌고 있는 6개 Cloudflare Tunnel 컨테이너 — 두 가지 배포 패턴과 그 코드 · 2026-07-10 실측

1. 한눈에 — 무엇이 돌고 있나

ssh alpha docker ps -a 에 잡히는 cloudflared 컨테이너는 6개. 전부 동일 이미지 cloudflare/cloudflared:latest, 전부 동일 Cloudflare 계정(a=33cdf220…) 소속, 전부 restart: unless-stopped. 각 컨테이너는 하나의 독립 서비스 스택에 붙어 그 스택을 외부로 노출하는 egress-only 리버스 프록시다.

컨테이너공개 호스트Origin(내부)네트워크패턴
grindaai-kms-astro-cloudflaredkms.rinda.aiweb:80grindaai-kms-astro-netA config
grindaai-mcp-cloudflaredmcp.rinda.aimcp:8080grindaai-mcp-netA config
rinda-html-cloudflaredhtml.rinda.aiapp:3300rinda-html-netB token
ai-real-estate-cloudflared-1admin.eodisalji.com 외frontend:5173estate-networkB token
pps-infisical-cloudflared(Infisical 셀프호스트)infisical 서비스infisical-standalone_defaultB token
kailab-cloudflaredkailab.grinda.aihost 포트hostB token

2. 공통 동작 원리 — 왜 포트를 안 여나

모든 스택의 핵심은 호스트에 inbound 포트를 하나도 열지 않는다는 것이다. cloudflared 는 서버 → Cloudflare 엣지 방향으로 아웃바운드 QUIC 연결(UDP 7844) 만 맺고, 트래픽은 그 연결의 역방향으로 흘러 들어온다.

브라우저→ HTTPS → Cloudflare 엣지
icn01 / icn06
← QUIC(아웃바운드) ← cloudflared
컨테이너
→ 내부 DNS → origin
web:80

3. 두 가지 배포 패턴

겉보기 CMD 는 비슷하지만 ingress(어떤 호스트를 어디로 보낼지) 규칙이 어디에 사는가가 근본적으로 다르다.

패턴 A Named Tunnel + config.yml

ingress 규칙이 서버 파일에 산다.

  • CMD: tunnel --config /etc/cloudflared/config.yml run
  • 볼륨 마운트 ./cloudflared:/etc/cloudflared:ro
  • 파일 2개: config.yml(터널ID+ingress) + creds.json(터널 시크릿)
  • ingress 를 git 으로 코드 관리 가능 → 리뷰·버전 추적 유리
  • 사용처: kms, mcp

패턴 B Token(원격 관리)

ingress 규칙이 Cloudflare 대시보드에 산다.

  • CMD: tunnel run --token <JWT> 또는 TUNNEL_TOKEN env
  • 서버엔 설정 파일 없음 — 토큰 하나가 전부
  • ingress 는 Zero Trust 대시보드에서 편집 (서버 재배포 불필요)
  • 토큰 = base64 JSON {a,t,s} (계정·터널ID·시크릿)
  • 사용처: rinda-html, ai-real-estate, infisical, kailab

4. 패턴 A 코드 — config.yml 방식 (kms 예시)

4-1. docker-compose 서비스

# grindaai-kms-astro/docker-compose.yml
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: grindaai-kms-astro-cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate --config /etc/cloudflared/config.yml run
    volumes:
      - ./cloudflared:/etc/cloudflared:ro   # config.yml + creds.json 주입 (읽기전용)
    depends_on: [web]                    # origin 이 먼저 떠야 함
    networks: [kmsastronet]              # origin 과 같은 네트워크 → web:80 직결

4-2. config.yml — ingress 규칙 (실측)

# /home/ec2-user/grindaai-kms-astro/cloudflared/config.yml
tunnel: cfbb3bca-caee-46d0-b007-fd9c172b357c   # 터널 UUID
credentials-file: /etc/cloudflared/creds.json

ingress:
  - hostname: kms.rinda.ai
    service: http://web:80          # 이 호스트로 온 트래픽을 web:80 으로
  - service: http_status:404          # catch-all (필수, 맨 마지막)

4-3. creds.json — 터널 자격증명 (구조만)

{
  "AccountTag":   "33cdf2206b3653878e6bca1694506a49",
  "TunnelID":     "cfbb3bca-caee-46d0-b007-fd9c172b357c",
  "TunnelSecret": "Fo8Q1PHrkR…"  ← 재발급 시크릿 (gitignore, 서버에만)
}
재발급: cloudflared tunnel create grindaai-kms-astro → 새 creds.json 생성. config.yml 의 tunnel UUID 도 함께 갱신.

5. 패턴 B 코드 — token 방식

5-1. env 로 주입 (rinda-html, 권장 형태)

# rinda-html-share/docker-compose.yml — 완전 독립 스택
  app:
    image: rinda-html-share
    container_name: rinda-html-app
    # ports 섹션 없음 → 호스트로 아무 포트도 안 염 (충돌 0)
    networks: [htmlnet]

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run
    environment:
      TUNNEL_TOKEN: ${TUNNEL_TOKEN:?TUNNEL_TOKEN required in .env}  # 없으면 기동 실패
    depends_on: [app]
    networks: [htmlnet]                 # app:3300 직결

5-2. CLI 인자로 주입 (ai-real-estate)

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN}
    depends_on: [backend, frontend]
    networks: [estate-network]
CLI 인자 방식은 docker inspect·ps 에 토큰이 그대로 노출된다(위 실측에서도 보임). env 방식(TUNNEL_TOKEN)이 더 안전 — 프로세스 인자에 시크릿이 안 남는다.

5-3. 토큰 구조 — base64 JSON

eyJhIjoiMzNj… 를 base64 디코드하면:

{
  "a": "33cdf2206b3653878e6bca1694506a49",   # account tag (6개 터널 전부 동일 = 같은 CF 계정)
  "t": "84252dff-b3ed-4f35-9490-6848604e1df5", # tunnel id
  "s": "+wVEJtAa/nyD… (redacted)"          # tunnel secret
}

token 방식 = creds.json 의 세 필드를 base64 로 뭉친 것. 차이는 ingress 를 어디서 관리하느냐뿐이다.

6. 특이 케이스

kailab — network: host

다른 5개는 전용 bridge 네트워크를 쓰지만 kailab 만 host 네트워크. 온프렘 H100(청사 air-gapped) 접근 패턴상 호스트 포트로 직접 붙기 위함. origin 을 서비스명이 아닌 localhost:PORT 로 지정.

ai-real-estate — 멀티 ingress

로그의 ingressRule=5 는 대시보드에 ingress 규칙이 6개(0~5) 걸려 있다는 뜻. admin.eodisalji.com 은 frontend:5173 로, API·SSE 등 여러 호스트를 한 터널이 처리.

7. distroless 이미지 — 디버깅 주의

cloudflare/cloudflared:latestdistroless — 셸·cat·ls 가 없다. docker exec … cat config.ymlexec: "cat": executable file not found 로 실패한다. 설정 확인은 컨테이너 안이 아니라 호스트 마운트 경로에서:
ssh alpha cat /home/ec2-user/grindaai-kms-astro/cloudflared/config.yml

8. 운영 상태 (2026-07-10 실측)

9. 핵심 요약