개요
운영 중인 서비스는 대부분 서비스 포트를 외부에 노출하지 않고, reverse proxy를 서버 앞단에 두어 80번 포트로 들어오는 요청을 각 서비스에 분산하는 경우가 많다.
위와 같이 리버스 프록시를 앞단에 두는 이유는 다음과 같은 장점이 있기 때문이다.
- 처리율 제한 알고리즘을 구현하여 서버 부하에 대비 가능
- 직접적으로 외부에 port 정보를 노출하지 않기에 DDoS 공격으로부터 비교적 안전할 수 있음.
리버스 프록시를 사용하는 환경을 그림으로 표현한다면 다음과 같다.
클러스터 환경에서 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을 도입한 환경을 그림으로 표현한다면 다음과 같다.
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을 추가해주도록 하자.
그리고 앱과 연동하기에서 만들었던 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/)