Profile picture

[Docker Swarm] 스웜(Swarm) 클러스터 Traefik 도입기

JaehyoJJAng2024년 06월 20일

개요

운영 중인 서비스는 대부분 서비스 포트를 외부에 노출하지 않고, reverse proxy를 서버 앞단에 두어 80번 포트로 들어오는 요청을 각 서비스에 분산하는 경우가 많다.

위와 같이 리버스 프록시를 앞단에 두는 이유는 다음과 같은 장점이 있기 때문이다.

  • 처리율 제한 알고리즘을 구현하여 서버 부하에 대비 가능
  • 직접적으로 외부에 port 정보를 노출하지 않기에 DDoS 공격으로부터 비교적 안전할 수 있음.

리버스 프록시를 사용하는 환경을 그림으로 표현한다면 다음과 같다.
image


클러스터 환경에서 Nginx의 문제점

도커 스웜 환경에서는 클러스터로 운영되기 때문에,

reverse proxy가 도메인이나 서버를 라우팅 하기 위해 여러 domain 정보를 알고 있어야 한다.

리버스 프록시로 가장 많이 사용하는 Nginx의 경우 클러스터 환경에서 서버가 discovery, 삭제, 새로운 domain이 추가 됐을 때 무중단 운영이 되지 않는 단점이 존재한다.


또한, 서버가 이미지 기반으로 scaling 되어야 하지만, Nginx는 도메인 정보를 다 들고 있는 config 파일 때문에 볼륨을 사용하지 않고 배포하려면 복잡한 프로세스를 새로운 서버마다 만들어줘야 한다.

이러한 단점을 보완하기 위해

go 언어 기반의 오픈소스 이면서 Docker, Kubernetes 환경을 지원해주는 Traefik을 도입해보려고 한다.


Traefik


Traefik은 go 언어로 구현된 reverse proxy 역할을 하는 오픈 소스 프로젝트이다.

docker.sock에 접근하여 docker swarm 정보를 읽어들여 새로운 서버를 자동으로 discovery하고, 도메인이 새로 추가되더라도 label 기반으로 자동 라우팅을 지원한다.

한마디로 Nginx와는 다르게 Traefik은 클러스터 환경에서 무중단 운영을 가능하게 해주는 리버스 프록시인 셈이다.

또한 Traefik은 라우팅에 대한 세밀한 설정(domain, port, network ..)을 Traefik이 아닌 새로 추가된 컨테이너에 label을 추가해주면 Traefik이 자동으로 추적하기 때문에

Nginx처럼 새로운 설정을 매번 컨테이너에 주입 해줄 필요가 없다.

Traefik을 도입한 환경을 그림으로 표현한다면 다음과 같다.
image


docker.sock

docker.sock은 docker API를 실행하는 주체인 docker daemon에 API 요청을 받는 클라이언트이다.

CLI에 docker 명령을 입력하면 Unix 소켓 기반인 docker.sock을 통해 daemon API를 호출하고 응답을 받는다.

Traefik은 이러한 요청들을 매번 감시하고 있다가 docker service가 새로 생기거나 제거 되는 이벤트가 발생하면 Traefik은 이를 감지하여 라우팅 대상에서 추가 또는 제외한다.


Traefik 구축하기


오버레이 네트워크 생성

  • 모든 노드에서 진행

실습에서 사용할 overlay 네트워크를 하나 생성해주도록 하자.

docker network create --driver overlay traefik-public

도메인 생성


Traefik을 실습하기 위해서는 도메인 구입이 필수적이다.

본인의 경우 가비아에서 가장 저렴한 .shop 도메인을 구입하였다.


노드별 label 업데이트

  • 마스터에서 진행

마스터 노드의 레이블을 다음과 같이 업데이트 해주겠다.

docker node update --label-add traefik-public.traefik-certificates=true $(docker info -f '{{.Swarm.NodeID }}')
[여기서 잠깐!] 마스터 노드에 레이블을 위처럼 업데이트 한 이유는?

traefik을 매니저 노드에서만 배포하고 나머지 노드에서는 배포되지 않도록 하기 위해 고유한 레이블을 매니저 노드에 부여하였다.


traefik 도커 컴포즈 작성

아래와 같이 docker-compose-traefik.yaml을 작성해보자.


sample

  • 서비스 종류에 따라 적
services:
  서비스이름:
    image: traefik
    ports:
      - target: 80
        published: 80
        mode: host
        protocol: tcp
      - target: 443
        published: 443
        mode: host
        protocol: tcp
      - 9001:9001
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/log/traefik:/var/log/traefik
      - traefik-certificates:/certificates
    deploy:
      mode: global
      placement:
        constraints:
          - node.labels.traefik-public.traefik-certificates == true
      labels:
        - traefik.enable=true
        - traefik.docker.network=<도커네트워크>
        - traefik.constraint-label=<도커네트워크>
        - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
        - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
        - traefik.http.routers.<도커네트워크>-http.rule=Host(`<도메인주소>`)
        - traefik.http.routers.<도커네트워크>-http.entrypoints=traefik-ui
        - traefik.http.routers.<도커네트워크>-http.middlewares=https-redirect
        - traefik.http.routers.<도커네트워크>-https.rule=Host(`<도메인주소>`)
        - traefik.http.routers.<도커네트워크>-https.entrypoints=traefik-ui
        - traefik.http.routers.<도커네트워크>-https.tls=true
        - traefik.http.routers.<도커네트워크>-https.service=api@internal
        - traefik.http.routers.<도커네트워크>-https.tls.certresolver=le
        - traefik.http.routers.<도커네트워크>-https.middlewares=admin-auth
        - traefik.http.services.<도커네트워크>.loadbalancer.server.port=8080
        - "traefik.http.middlewares.non-www-to-www.redirectregex.regex=^https?://(?:www\\.)?(.+)"
        - "traefik.http.middlewares.non-www-to-www.redirectregex.permanent=true"
        - "traefik.http.middlewares.non-www-to-www.redirectregex.replacement=https://www.$${1}"
      command:
      - --providers.swarm.network=<도커네트워크>
      - --providers.swarm.constraints=Label(`traefik.constraint-label`, <도커네트워크>)

      #v3 버전 전용 코드
      - --providers.swarm.exposedbydefault=false
      - --providers.swarm.endpoint=unix:///var/run/docker.sock
      - --entrypoints.http.address=:80
      - --entrypoints.https.address=:443
      - --certificatesresolvers.le.acme.email=<이메일주소>
      - --certificatesresolvers.le.acme.storage=/certificates/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      - --accesslog.bufferingsize=100
      - --accesslog.filepath=/var/log/traefik/traefik-access.log
      - --accesslog.fields.defaultmode=keep
      - --accesslog.fields.names.ClientUsername=drop
      - --accesslog.fields.headers.defaultmode=keep
      - --accesslog.fields.headers.names.User-Agent=keep
      - --accesslog.fields.headers.names.Authorization=drop
      - --accesslog.fields.headers.names.Content-Type=keep
      - --log
      - --api
      - --entrypoints.traefik-ui.address=:<변경할traefik-ui_포트번호>
    networks:
      - "traefik-public"

volumes:
  traefik-certificates: {}

networks:
  traefik-public:
    name: traefik-public
    external: true

본인이 작성한 코드

services:
  traefik:
    image: traefik
    ports:
      - target: 80
        published: 80
        mode: host
        protocol: tcp
      - target: 443
        published: 443
        mode: host
        protocol: tcp
      - 9001:9001
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /var/log/traefik:/var/log/traefik
      - traefik-certificates:/certificates
    deploy:
      mode: global
      update_config:
        order: stop-first
      placement:
        constraints:
          - node.labels.traefik-public.traefik-certificates == true
      labels:
        - traefik.enable=true
        - traefik.docker.network=traefik-public
        - traefik.constraint-label=traefik-public
        - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
        - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
        - traefik.http.routers.traefik-public-http.rule=Host(`test.shop`)
        - traefik.http.routers.traefik-public-http.entrypoints=traefik-ui
        - traefik.http.routers.traefik-public-http.middlewares=https-redirect
        - traefik.http.routers.traefik-public-https.rule=Host(`test.shop`)
        - traefik.http.routers.traefik-public-https.entrypoints=traefik-ui
        - traefik.http.routers.traefik-public-https.tls=true
        - traefik.http.routers.traefik-public-https.service=api@internal
        - traefik.http.routers.traefik-public-https.tls.certresolver=le
        - traefik.http.services.traefik-public.loadbalancer.server.port=8080
        - "traefik.http.middlewares.non-www-to-www.redirectregex.regex=^https?://(?:www\\.)?(.+)"
        - "traefik.http.middlewares.non-www-to-www.redirectregex.permanent=true"
        - "traefik.http.middlewares.non-www-to-www.redirectregex.replacement=https://www.$${1}"
    command:
      - --providers.swarm.network=traefik-public
      # - --providers.swarm.constraints=Label(`traefik.constraint-label`, 'traefik-public')
      - --providers.swarm.exposedbydefault=false
      - --providers.swarm.endpoint=unix:///var/run/docker.sock
      - --entrypoints.http.address=:80
      - --entrypoints.https.address=:443
      - --certificatesresolvers.le.acme.email=<email>
      - --certificatesresolvers.le.acme.storage=/certificates/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      - --accesslog.bufferingsize=100
      - --accesslog.filepath=/var/log/traefik/traefik-access.log
      - --accesslog.fields.defaultmode=keep
      - --accesslog.fields.names.ClientUsername=drop
      - --accesslog.fields.headers.defaultmode=keep
      - --accesslog.fields.headers.names.User-Agent=keep
      - --accesslog.fields.headers.names.Authorization=drop
      - --accesslog.fields.headers.names.Content-Type=keep
    networks:
      - "traefik-public"

volumes:
  traefik-certificates: {}

networks:
  traefik-public:
    name: traefik-public
    external: true

위처럼 작성했다면

이제 HTTPS를 설정할 서비스(fastapi, node-js)와 연동 해주기만 된다.


앱과 연동하기

docker-compose-app.yaml

  • 서비스 이름에 맞게 traefik.http.routers.<서비스이름>을 채워넣어주면 된다.

sample

services:
  <서비스이름>:
    image: <배포할이미지명>
    networks:
      - <traefik이배포된overlay네트워크>
    deploy:
      labels:
      - "traefik.enable=true"
      - "traefik.http.routers.<서비스명>.rule=Host(`도메인명`)"
      - "traefik.http.routers.<서비스명>.entrypoints=http"
      - "traefik.http.routers.<서비스명>.middlewares=https-redirect"
      - "traefik.http.routers.<서비스명>-https.rule=Host(`도메인명`)"
      - "traefik.http.routers.<서비스명>-https.entrypoints=https"
      - "traefik.http.routers.<서비스명>-https.tls=true"
      - "traefik.http.routers.<서비스명>-https.tls.certresolver=le"
      - "traefik.docker.network=traefik-public"
      - "traefik.constraint-label=traefik-public"
      - "traefik.http.services.flask-https.loadbalancer.server.port=<컨테이너포트>"

networks:
  traefik-public:
    name: traefik-public
    external: true

본인이 작성한 코드

services:
  flask: # 서비스 이름
    image: 192.168.219.114:5000/fastapi-helloworld
    networks:
      - traefik-public
    deploy:
      labels:
      - "traefik.enable=true"
      - "traefik.http.routers.flask.rule=Host(`test.shop`)"
      - "traefik.http.routers.flask.entrypoints=http"
      - "traefik.http.routers.flask.middlewares=https-redirect"
      - "traefik.http.routers.flask-https.rule=Host(`test.shop`)"
      - "traefik.http.routers.flask-https.entrypoints=https"
      - "traefik.http.routers.flask-https.tls=true"
      - "traefik.http.routers.flask-https.tls.certresolver=le"
      - "traefik.docker.network=traefik-public"
      - "traefik.constraint-label=traefik-public"
      - "traefik.http.services.flask-https.loadbalancer.server.port=8080"
    expose:
      - 8080
networks:
  traefik-public:
    name: traefik-public
    external: true

domain rule을 부여하여 test.shop로 요청이 들어온다면 라우팅이 될 것이다.

또한 port 정보는 8080이라는 것을 traefik에게 알려주게된다.


참고로 192.168.219.114:5000/fastapi-helloworld Dockerfile은 다음과 같다.

FROM python:3.10-slim
WORKDIR /usr/src/app
RUN pip install fastapi uvicorn
COPY . .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--reload", "--port", "8080", "--host", "0.0.0.0"]

여러 서비스 연결 방법

현재 도메인(test.shop) 접근 시 하나의 서비스(flask:8080)로만 연결된다.

이때 만약, 다른 서비스도 노출하고 싶다면 어떻게 구분을 시켜야하는걸까?


방법은 간단하다. 앱 서비스를 배포할 때 포트를 다르게하여 배포하는 것이다.


하지만 포트로 구분하는 방법보다는 구매한 도메인에서 서브도메인을 새로 추가하여 운영하는 것이 제일 바람직하다.


방법은 다음과 같다.

  • 1. CNAME 추가
  • 2. 서비스명 키워드 전부 변경

먼저 구입한 도메인 플랫폼에서 CNAME을 추가해주도록 하자.
image


그리고 앱과 연동하기에서 만들었던 docker-compose-app.yaml을 복사하고 다음과 같이 수정해주자.

cp -R docker-compose-app.yaml docker-compose-nginx.yaml

sample

services:
  앱이름:
    image: 앱이미지
    networks:
      - traefik-public
    deploy:
      labels:
      - "traefik.enable=true"
      - "traefik.http.routers.<앱이름>.rule=Host(`도메인명`)"
      - "traefik.http.routers.<앱이름>.entrypoints=http"
      - "traefik.http.routers.<앱이름>.middlewares=https-redirect"
      - "traefik.http.routers.<앱이름>-https.rule=Host(`도메인명`)"
      - "traefik.http.routers.<앱이름>-https.entrypoints=https"
      - "traefik.http.routers.<앱이름>-https.tls=true"
      - "traefik.http.routers.<앱이름>-https.tls.certresolver=le"
      - "traefik.docker.network=traefik-public"
      - "traefik.constraint-label=traefik-public"
      - "traefik.http.services.<앱이름>-https.loadbalancer.server.port=<컨테이너포트번호>" # 컨테이너 포트 변경
    expose:
      - 80
networks:
  traefik-public:
    name: traefik-public
    external: true

본인이 작성한 코드

services:
  nginx:
    image: nginx:latest
    networks:
      - traefik-public
    deploy:
      labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nginx.rule=Host(`nginx.test.shop`)" # 서브 도메인명으로 변경
      - "traefik.http.routers.nginx.entrypoints=http"
      - "traefik.http.routers.nginx.middlewares=https-redirect"
      - "traefik.http.routers.nginx-https.rule=Host(`nginx.test.shop`)" # 서브 도메인명으로 변경
      - "traefik.http.routers.nginx-https.entrypoints=https"
      - "traefik.http.routers.nginx-https.tls=true"
      - "traefik.http.routers.nginx-https.tls.certresolver=le"
      - "traefik.docker.network=traefik-public"
      - "traefik.constraint-label=traefik-public"
      - "traefik.http.services.nginx-https.loadbalancer.server.port=80" # 컨테이너 포트 변경
    expose:
      - 80
networks:
  traefik-public:
    name: traefik-public
    external: true

마무리

이로써 도커 스웜과 traefik을 이용하여 무중단으로 서버를 운영하기 위한 설정이 끝났다.

실제로 무중단 운영이 가능한지 테스트를 진행해보기 위해서는 아래와 같은 테스트 도구를 사용하면 된다.
[jmeter]](https://jmeter.apache.org/)


Loading script...