Server Programming/DevOps

Traefik 활용해보기

Dev.BeryL 2022. 2. 22. 16:38
728x90

Traefik ?

- Docker , K8S 등에 특화된 golang 기반의 reverse proxy 앱

- 설정을 raw text로 관리하는 것을 선호

- /volume1/docker 경로에 모든 파일을 저장한다고 가정, docker-compose로 설명

- lets encrypt 인증서를 입힐 수 있는 도메인이 필요

- 모든 연결이 https를 사용

- GUI가 아닌 쉘에서 진행

 

Traefik docker-compose로 올려보기

# 특정 docker-compose 문법을 사용하기 위해서는 3.x 이상이 필요합니다.
version: '3.7'
 
services:
  traefik:
    container_name: traefik
    image: traefik:latest
    restart: always
    # docker log를 쉘에서 확인하기 https://www.clien.net/service/board/cm_nas/13947705
    # 예전처럼 GUI에서 로그를 확인할거면 아래 두 줄 주석
    logging:
      driver: json-file
    command: # CLI arguments
      - --global.checkNewVersion=true
      - --global.sendAnonymousUsage=true
      # 2개의 entrypoint를 정의합니다. http 80포트 https 443포트
      # 아래 ports: 항목에도 해당 포트를 오픈(publish)해 주어야 합니다.
      # 또한 공유기로 포트 포워딩도 해줘야 합니다.
      # 80/443 포트를 못 연다면 https://www.clien.net/service/board/cm_nas/13753034
      # 다른 임의의 포트(예를들어 http는 1280 https는 12443)를 사용해도 될 것 같지만 확인해보지는 않았습니다.
      - --entryPoints.http.address=:80
      - --entryPoints.https.address=:443
      # 여기서 api는 dashboard를 올리기 위해 필요합니다.
      - --api=true
      # api에 http 연결을 허용하려면 아래 주석 해제
      # - --api.insecure=true
      # 최종 목적지가 https인 서비스를 연결할 때 아래 옵션이 필요합니다. 예를들어 ESXi webui
      - --serversTransport.insecureSkipVerify=true
      # 로그 관련 옵션은 한 번 찾아보세요.
      # https://docs.traefik.io/observability/access-logs/
      - --log=true
      - --log.level=INFO # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
      - --accessLog=true
      - --accessLog.filePath=/traefik.log
      - --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
      - --accessLog.filters.statusCodes=400-499
      # frontend라는 이름의 docker network에 있는 컨테이너들을 자동으로 인식해서 연결해줍니다.
      # 다만 자동으로 노출하지는 않고 각 컨테이너마다 traefik.enable=true를 주도록 했습니다.
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=frontend
      - --providers.docker.swarmMode=false
      # /rules 폴더에 있는 설정 파일들을 사용하며 그 중 traefik-dynamic.toml는 자동 인식해서 실시간으로 반영합니다.
      - --providers.file.directory=/rules # Load dynamic configuration from one or more .toml or .yml files in a directory.
      # - --providers.file.filename=traefik-conf.toml # Load dynamic configuration from a file.
      - --providers.file.watch=true # Only works on top level files in the rules folder
      # Lets encrypt 설정. 80 포트를 이용하는 http challenge 사용. dns challenge는 직접 해보세요.
      - --certificatesResolvers.leresolver.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # production이 아닌 staging을 인증서버로 사용, 테스트 이후 주석처리해서 최종 적용
      - --certificatesresolvers.leresolver.acme.httpchallenge=true
      - --certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=http
      - --certificatesresolvers.leresolver.acme.email=${TRF_LE_EMAIL}
      - --certificatesresolvers.leresolver.acme.storage=/acme.json
    # 두 네트워크에 모두 참여하고 있어야 소속 컨테이너에 traefik을 적용할 수 있음
    networks:
      - frontend
      - backend
    ports:
      # https://www.reddit.com/r/docker/comments/c1wrep/traefik_reverse_proxy_question_docker_overlay/
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${DOCKER_ROOT}/traefik/acme.json:/acme.json
      - ${DOCKER_ROOT}/traefik/traefik.log:/traefik.log
      - ${DOCKER_ROOT}/traefik/rules:/rules
    labels:
      - "traefik.enable=true"
      # HTTP-to-HTTPS Redirect
      # entrypoint http로 들어오는 모든 요청을 redirect-to-https라는 이름의 middleware를 적용한다.
      # 그 middleware가 https로 리다이렉트 해 줌
      - "traefik.http.routers.http-catchall.entrypoints=http"
      - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      # HTTP Routers
      # entrypoint https로 들어오는 요청 중 호스트명이 ${TRF_DOMAIN}이고 path가 /traefik이거나 /api면 
      # leresolver를 사용해서 인증서를 적용함. 이 라우팅의 이름은 traefik-rtr
      - "traefik.http.routers.traefik-rtr.entrypoints=https"
      - "traefik.http.routers.traefik-rtr.rule=Host(`${TRF_DOMAIN}`) && (PathPrefix(`/traefik`) || PathPrefix(`/api`))"
      - "traefik.http.routers.traefik-rtr.tls=true"
      - "traefik.http.routers.traefik-rtr.tls.certresolver=leresolver"
      # Middlewares
      - "traefik.http.routers.traefik-rtr.middlewares=traefik-stripprefix"
      - "traefik.http.middlewares.traefik-stripprefix.stripprefix.prefixes=/traefik"
      ## Services - API
      # traefik-rtr 라우팅의 최종 목적지인 service는 내부적으로만 오픈된 traefik api
      - "traefik.http.routers.traefik-rtr.service=api@internal"
 
networks:
  frontend:
    name: frontend
    driver: bridge
 
  backend:
    name: backend
    driver: bridge

 

.env 파일

docker-compose.yml에 환경변수로 대체 해놓은 것들이 있습니다. 이걸 지정해줍시다.

파일이 없으면 touch /volume1/docker/.env로 파일을 만들고 편집기로 열어서 아래 항목을 추가합니다.

# PATH
DOCKER_ROOT=/volume1/docker

# TRAEFIK
TRF_DOMAIN=dsm.mydomain.com
TRF_LE_EMAIL=example@gmail.com
mkdir /volume1/docker/traefik

touch /volume1/docker/traefik/acme.json
chmod 600 /volume1/docker/traefik/acme.json

touch /volume1/docker/traefik/traefik.log
mkdir /volume1/docker/traefik/rules

cd /volume1/docker
ls -al

docker-compose.yml
.env
traefik/rules
traefik/acme.json
traefik/traefik.log

# 위 항목 나오는지 확인 

traefik 서비스 올리기

이제 현재 위치가 /volume1/docker인지 확인하고 (pwd 명령어로 확인 가능)

docker-compose up -d traefik

으로 서비스를 올려봅니다. 명령어 실행 위치가 중요한 이유는 명령어 실행경로에 있는 .env 파일만 적용되기 때문입니다.

docker-compose logs traefik

으로 로그 확인해봅니다.

딱히 에러는 없겠지만 위에 적은 docker-compose.yml을 그대로 적용했다면 lets encrypt가 production이 아닌 staging (테스트 용도) 서버로 적용되어 있기 때문에 잘 되는 것을 확인했으면 그 줄을 주석처리

이제 아까 TRF_DOMAIN에 설정했던 도메인에 /traefik을 붙여서 브라우저로 접속하면 대시보드가 뜹니다. 저는 서브도메인을 안 좋아해서 서브패스로 대부분 서비스를 올립니다. 그래서 이 글에서는 dsm.mydomain.com/traefik으로 접속되도록 했지만 traefik.mydomain.com으로 하고 싶으시면 나중에 응용해서 하셔도 됩니다.

 

traefik 라우팅 이해하기

대시보드에 HTTP 항목을 살펴보면 지금 2개가 보일겁니다. 하나는 모든 http 요청을 https로 돌려주는 것. 그러므로 이제부터 모든 요청은 https :443으로 들어올겁니다. 여기는 tls (인증서) 적용이 필요 없죠. 그리고 두번째는 api를 이용한 대시보드로의 라우팅. 이걸 눌러서 들어가봅시다.

 

일단 :443포트로 들어오는 요청에 대해서 traefik-rtr이라는 이름의 라우팅 룰을 적용합니다. 룰은 왼쪽 아래에 자세히 나와있는데 호스트명이 dsm.mydomain.com이고 path가 /traefik이나 /api로 시작하는 모든 요청에 대해서... 라는 뜻입니다. 여기에 leresolver라는 인증서 처리자가 인증서도 입히고 api@internal이라는 서비스로 최종 연결하는데, 그 사이에 미들웨어가 이름에 걸맞게 중간에 껴들어 이것저것 처리를 합니다. 여기서는 traefik-stripprefix라는 이름의 미들웨어를 적용하는데 요청에서 /traefik을 달고 들어오면 이 prefix 떼고 최종 서비스에 전달하는겁니다.

미들웨어 추가하기

미들웨어는 라우팅이 관장하는 애드온 같은 느낌의 옵션입니다.

https://doc.traefik.io/traefik/middlewares/overview/

도커에 올린 HTTP 서비스 연결하기

여기서 도커에 올린 서비스라 함은 docker-traefik과 같은 도커 데몬 아래에서 돌아가는 서비스를 말합니다. 여담으로 외부에 있는 docker도 같이 연결해주면 좋겠다 싶었는데 아직은 지원하지 않더군요.

guacamole

  guac:
    image: guacamole/guacamole
    container_name: guac
    restart: always
    logging:
      driver: json-file
    networks:
      - backend
    environment:
      MYSQL_DATABASE: guacamole_db
      MYSQL_USER: guacamole_user
      MYSQL_PASSWORD: ${GUAC_MYSQL_PASS}
      GUACD_HOSTNAME: guacd
      MYSQL_HOSTNAME: guac-mysql
    links:
      - guacd
      - guac-mysql
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=backend"
      # HTTP Routers
      - "traefik.http.routers.guac-rtr.entrypoints=https"
      - "traefik.http.routers.guac-rtr.rule=Host(`${TRF_DOMAIN}`) && PathPrefix(`/guacamole`)"
      - "traefik.http.routers.guac-rtr.tls=true"
      - "traefik.http.routers.guac-rtr.tls.certresolver=leresolver"
      # Middlewares
      - "traefik.http.routers.guac-rtr.middlewares=mid-secure-headers@file,mid-rate-limit@file"
      # HTTP Services
      - "traefik.http.routers.guac-rtr.service=guac-svc"
      - "traefik.http.services.guac-svc.loadbalancer.server.port=8080"

  guac-mysql:
    image: mysql:latest
    container_name: guac-mysql
    restart: always
    logging:
      driver: json-file
    networks:
      - backend
    volumes:
      - "${DOCKER_ROOT}/guacamole/mysql:/var/lib/mysql"
      - "${DOCKER_ROOT}/guacamole/scripts:/tmp/scripts"
    environment:
      MYSQL_ROOT_PASSWORD: ${GUAC_MYSQL_ROOT_PASS}

  guacd:
    image: guacamole/guacd
    container_name: guacd
    restart: always
    logging:
      driver: json-file
    networks:
      - backend
    volumes:
      - "${DOCKER_ROOT}/guacamole/D2Coding:/usr/share/fonts/truetype/D2Coding:ro"

제 docker-compose.yml에서 guacamole 관련 컨테이너 설정만 발췌했습니다. guacamole이 daemon과 db를 포함해서 3개의 컨테이너로 이루어진 서비스라 나머지는 참고만 하시고 제일 첫번째 guac 아래 labels를 보시면 됩니다.

    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=backend"
      # HTTP Routers
      - "traefik.http.routers.guac-rtr.entrypoints=https"
      - "traefik.http.routers.guac-rtr.rule=Host(`${TRF_DOMAIN}`) && PathPrefix(`/guacamole`)"
      - "traefik.http.routers.guac-rtr.tls=true"
      - "traefik.http.routers.guac-rtr.tls.certresolver=leresolver"
      # Middlewares
      - "traefik.http.routers.guac-rtr.middlewares=mid-secure-headers@file,mid-rate-limit@file"
      # HTTP Services
      - "traefik.http.routers.guac-rtr.service=guac-svc"
      - "traefik.http.services.guac-svc.loadbalancer.server.port=8080"

첫 두줄은 "traefik backend 네트워크에 있는 날 연결해라"라는 뜻이고, 다음으로 guac-rtr 이란 이름의 라우팅 룰을 정의해서

  1. entrypoint https로 들어오는 요청 중 호스트명이 ${TRF_DOMAIN}이고 path가 /guacamole인 것에 대해,
  2. 인증서 입히고,
  3. basicAuth를 제외한 middlewares.toml에 있는 두 개의 미들웨어를 추가하고,
  4. guac-svc로 연결하라는 뜻입니다.

서브 도메인으로 쓰려면 "traefik.http.routers.guac-rtr.rule=Host(guac.mydomain.com)" 이런식으로 바꿔도 되겠지만, 서비스는 여전히 /guacamole에서 되고 있으므로 a) guac.mydomain.com/guacamole로 쓰거나 b) 서버 설정을 바꿔주거나 c) 미들웨어 AddPrefix 같은걸 추가해서 path 수정해주면 될겁니다.

최종 서비스 연결은 저런 식으로 서버의 포트만 지정해주면 됩니다. 포트가 8080인 이유는 guacamole 이미지가 8080포트를 기본으로 노출하고 있고 (이미지 만들때 지정가능) 같은 지정 네트워크에 소속되어 있는 컨테이너들끼리는 따로 포트 설정없이 접근할 수 있기 때문입니다.

transmission (webui only)

    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=backend"
      ## HTTP Routers
      - "traefik.http.routers.tr-rtr.entrypoints=https"
      - "traefik.http.routers.tr-rtr.rule=Host(`$TRF_DOMAIN`) && PathPrefix(`/transmission`)"
      - "traefik.http.routers.tr-rtr.tls=true"
      - "traefik.http.routers.tr-rtr.tls.certresolver=leresolver"
      ## Middlewares
      - "traefik.http.routers.tr-rtr.middlewares=mid-secure-headers@file,mid-rate-limit@file"
      ## HTTP Services
      - "traefik.http.routers.tr-rtr.service=tr-svc"
      - "traefik.http.services.tr-svc.loadbalancer.server.port=9091"

트랜스미션 webui, rpc를 연결하는 컨테이너 라벨 예시입니다. guacamole과 거의 같습니다.

 

로컬에서 동작하는 HTTP 서비스 연결하기

도커로 동작하지 않으니 더이상 라벨로 설정할 수가 없습니다. 이때는 file provider를 이용합니다. 미리 볼륨 매핑해둔 rules 폴더 아래에 traefik-conf.toml 파일을 만들고 설정을 하나씩 추가해 나가겠습니다.

touch /volume1/docker/traefik/rules/traefik-conf.toml

DSM

아래 내용을 traefik-conf.toml에 추가해 주세요.

[http.routers.dsm-rtr]
    rule = "Host(`dsm.mydomain.com`)"
    service = "dsm-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.dsm-rtr.tls]
    certResolver = "leresolver"
[http.services.dsm-svc.loadbalancer]
    [[http.services.dsm-svc.loadbalancer.servers]]
        url = "<http://192.168.1.75:5000>"

앞에 traefik만 붙지 않았지 도커 라벨에 쓰는 것과 대동소이합니다. 마지막에 url을 이용해서 DSM 내부 ip와 port를 적어줍니다.

docker-traefik을 up 할 필요 없이 파일만 저장하면 바로 변경사항을 인식하고 연결해줍니다.

포토스테이션

[http.routers.photo-station-rtr]
    rule = "Host(`dsm.mydomain.com`) && PathPrefix(`/photo`)"
    service = "photo-station-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.photo-station-rtr.tls]
    certResolver = "leresolver"
[http.services.photo-station-svc.loadbalancer]
    [[http.services.photo-station-svc.loadbalancer.servers]]
        url = "<http://192.168.1.75:81>"

전 80/443을 traefik에 할당하기 위해 https://www.clien.net/service/board/cm_nas/13753034CLIEN 기존에 80에서 동작하던 서비스를 81로 돌려서 저렇습니다.

WEBDAV

[http.routers.dav-rtr]
    rule = "Host(`dav.mydomain.com`)"
    service = "dav-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.dav-rtr.tls]
    certResolver = "leresolver"
[http.services.dav-svc.loadbalancer]
    [[http.services.dav-svc.loadbalancer.servers]]
        url = "<http://192.168.1.75:5005>"

webdav도 http 서비스라 동일하게 하시면 됩니다.

ESXi (webui)

[http.routers.esxi-rtr]
    rule = "Host(`esxi.mydomain.com`)"
    service = "esxi-svc"
    entryPoints = ["https"]
    middlewares = ["mid-secure-headers"]
[http.routers.esxi-rtr.tls]
    certResolver = "leresolver"
[http.services.esxi-svc.loadbalancer]
    [[http.services.esxi-svc.loadbalancer.servers]]
        url = "<https://192.168.1.11:443>"

docker-traefik을 올릴때 command 항목에서 "--serversTransport.insecureSkipVerify=true"가 필수입니다. 이상하게 rateLimit를 적용하면 로그인이 안되는군요.

Transmission/Flexget api/webui

[http.routers.tr-rtr]
    rule = "Host(`dsm.mydomain.com`) && PathPrefix(`/transmission`)"
    service = "tr-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.tr-rtr.tls]
    certResolver = "leresolver"
[http.routers.fg-rtr]
    rule = "Host(`dsm.mydomain`) && PathPrefix(`/flexget`)"
    service = "fg-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.fg-rtr.tls]
    certResolver = "leresolver"
[http.services.tr-svc.loadbalancer]
    [[http.services.tr-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:9091>"
[http.services.fg-svc.loadbalancer]
    [[http.services.fg-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:3539>"

거의 복붙입니다.

Portainer

[http.routers.portainer-rtr]
    rule = "Host(`dsm.mydomain.com`) && PathPrefix(`/portainer`)"
    service = "portainer-svc"
    entryPoints = ["https"]
    middlewares = ["portainer-stripprefix", "mid-rate-limit", "mid-secure-headers"]
[http.routers.portainer-rtr.tls]
    certResolver = "leresolver"
[http.services.portainer-svc.loadbalancer]
    [[http.services.portainer-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:9000>"
[http.middlewares.portainer-stripprefix.stripPrefix]
    prefixes = ["/portainer"]

portainer는 패스없이 동작하기 때문에 stripPrefix 미들웨어로 날려줘야합니다.

Plex와 Tautulli

[http.routers.plex-rtr]
    rule = "Host(`plex.mydomain.com`)"
    service = "plex-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.plex-rtr.tls]
    certResolver = "leresolver"
[http.services.plex-svc.loadbalancer]
    [[http.services.plex-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:32400>"

[http.routers.tt-rtr]
    rule = "Host(`plex.mydomain.com`) && PathPrefix(`/tt`)"
    service = "tt-svc"
    entryPoints = ["https"]
    middlewares = ["mid-rate-limit", "mid-secure-headers"]
[http.routers.tt-rtr.tls]
    certResolver = "leresolver"
[http.services.tt-svc.loadbalancer]
    [[http.services.tt-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:8181>"

우분투 VM의 docker에서 돌고 있는 plex와 tautulli를 하나의 서브 도메인 아래에 묶었습니다. 저처럼 tautulli가 /tt 아래에서 돌게 하려면 tautulli 설정인 config.ini에서 http_root = /tt로 지정해줘야합니다.

SJVA와 Filebrowser

[http.routers.sjva-rtr]
    rule = "Host(`sjva.mydomain.com`)"
    service = "sjva-svc"
    entryPoints = ["https"]
[http.routers.sjva-rtr.tls]
    certResolver = "leresolver"
[http.services.sjva-svc.loadbalancer]
    [[http.services.sjva-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:9999>"
# sjva-filebrowser
[http.routers.sjva-filebrowser-rtr]
    rule = "Host(`sjva.mydomain .com`) && PathPrefix(`/filebrowser`)"
    service = "sjva-filebrowser-svc"
    entryPoints = ["https"]
[http.routers.sjva-filebrowser-rtr.tls]
    certResolver = "leresolver"
[http.services.sjva-filebrowser-svc.loadbalancer]
    [[http.services.sjva-filebrowser-svc.loadbalancer.servers]]
        url = "<http://192.168.1.99:9998>"

마찬가지로 SJVA와 함께 딸려오는 filebrowser를 하나의 서브 도메인 아래에 묶었습니다. SJVA를 도커로 올릴때 환경변수(FB_BASEURL=/filebrowser)를 추가해주면 filebrowser가 서브패스에서 동작합니다.

docker-traefik이 같이 동작하는 도커 서비스 뿐만 아니라 우분투 VM의 도커 컨테이너, ESXi, DSM 서비스까지 모두 아우를 수 있습니다. 마찬가지로 오라클 클라우드에서 2개의 인스턴스를 동일한 가상클라우드네트워킹에 할당하고 하나의 인스턴스에 traefik을 올린 뒤 다른 인스턴스로 리버스 프록싱 해줄 수도 있습니다. (물론 포트 설정이 필요합니다.)

반응형