更好的反向代理工具 Traefik 配置入门——Docker 篇

10,279 阅读13分钟

前言

本人 Ghost 博客以及自建私有云 Nextcloud 均已经稳定运行了一年有余,期间经历过两次服务器迁移和多次版本升级,均未发生过数据丢失和其他问题,在迁移过程中也曾分享过如何将 Docker Volumes 安全的跨服务器迁移。目前博客和网盘服务分别搭建于两台服务器,并统一使用另一台服务器作反代。

在反代软件上,一直使用的都是 Nginx,最近在研究 K8s,也对比较流行的云原生反向代理工具 Traefik 产生了兴趣,于是便花了一点时间将反代服务器上的 Nginx 换成了 Traefik。一定要吐槽的一点是,Traefik 的官方文档写得实在太糟糕了,初次接触的用户,想通过只看官方文档把事情搞定,那是相当困难,所以我觉得非常有必要分享一下,让其他初次使用的用户避免踩坑。

目前 Traefik 已经更新到了 2.1 版本,其 2.x 版本和 1.x 版本差异较大,本文自然将基于最新版本,新版本增加的自动配置 HTTPS 功能和 TCP 代理功能都十分有价值。

主要目标

反向代理服务器的工作,自然就是截获服务器流量,并将流量转发至目标服务或地址,在这一基础上,如果转发的目标服务或地址是多个,就是负载均衡。Traefik 官方文档中的图片都是萌萌的,下面这张图就比较清晰的说明了 Traefik 或者说一般反代软件的功能:

Traefik 功能

本人目前的需求比较简单,就是将来自于pbeta.mewww.pbeta.me的访问流量转发至 博客IP:博客端口,将来自于cloud.pbeta.me的访问流量转发至 网盘IP:端口,另外,考虑到演示需要,本文将完成以下目标:

  • 域名访问 Traefik Dashboard
  • 将域名访问转发至 外部IP:端口
  • 将域名访问转发至 Docker 服务
  • 配置 HTTPS 访问(使用 LetsEncrypt)
  • 使用 Middlewares 实现 HTTP 访问自动跳转 HTTPS
  • 使用 Middlewares 为 Traefik Dashboard 增加密码验证

Traefik 配置说明

在安装之前,我觉得有必要讲一下 Traefik 这款工具的配置方式,搞清楚了基本的配置方式,在下面安装和配置的过程中才能有清晰的思路,以下内容主要参考了官方文档中的Configuration Introduction 页面

配置类型

配置类型

简单的说,Traefik 的配置包括静态配置和动态配置两种,前者是 Traefik 自身的配置,需要重启才能生效,后者则可以理解为被代理服务的配置,可以通过配置实现为即时生效。

如果配置内容不存在服务差异性,那就可以统一在静态配置中,否则就需要在动态配置中为每一个服务中单独添加,动态配置中的多个服务也可以共用同一配置内容,比如可以配置一个basicAuthMiddlewares,由多个服务共用。

配置内容位置

无论静态配置还是动态配置,都有两种配置方式:CLI 形式或者独立文件形式。

对于静态配置,一个是 Traefik 镜像启动时的启动参数,另一个是单独的配置文件(如 traefik.yml),官方文档提供示例时,也同时包括这两种形式,分别对应的是 CLIFile(YAML),不过在查看官方的配置示例时,你会发现还包括 File(TOML),TOML 文件是对 YAML 的一种改进,本文暂时都使用 YAML,读者可以自行转换。另外,官方文档中也说明了静态配置可以通过环境变量实现,本文暂不使用此方式。

配置项的优先级为配置文件 > 命令行参数 > 环境变量,请注意配置文件与命令行参数是互斥的,如果你选择使用配置文件,就不能再使用命令行参数,最终的配置并非二者的叠加

而对于动态配置,可以选择直接在服务的 docker-compose 文件最下方通过 labels 实现,比如这样:

# yaml
  whoami:
    # A container that exposes an API to show its IP address
    image: containous/whoami
    labels:
      - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)"

也可以写在单独的配置文件中,这一文件在 Traefik 中一般命名为 dynamic.yml,你可以将所有路由配置写在此文件中,为了清晰起见,本文统一使用后一种方式。

配置加载方式

通过上面的说明,我们可以知道,在 Docker 中使用 Traefik 大致可能会涉及到三个文件:

  • docker-compose.yml
  • traefik.yml(可选)
  • dynamic.yml

再次重复一次,如果使用独立配置文件来存放静态配置,那么 docker-compose.yml 文件中的 command 部分将不会生效。

有了以上的文件,如何加载呢?docker-compose.yml 中的配置自然不必考虑这个问题,对于其他两个文件的加载则必须要绑定至容器。

traefik.yml 文件的加载

当 Traefik 启动时,将会在以下位置搜索配置文件,名称可以是 traefik.toml、traefik.yml 或者 traefik.yaml:

  • /etc/traefik/
  • $XDG_CONFIG_HOME/
  • $HOME/.config/
  • . (the working directory).

另外,可以通过添加形式如 --configFile=foo/bar/myconfigfile.toml 的启动参数覆盖默认行为。

根据官方文档的以上描述,考虑到我们是在 Docker 环境中运行 Traefik,所以建议将放置配置文件的目录绑定到容器的 /etc/traefik 目录即可。

dynamic.yml 文件的加载

动态配置文件的加载,则需要使用 Traefik 的 providers 启动参数,形式如下:

  • CLI 形式
--providers.file.directory=/etc/traefik
--providers.file.filename=dynamic.yml
--providers.file.watch=true
  • 配置文件形式
providers:
  file:
    directory: "/etc/traefik"
    filename: "dynamic.yml"
    watch: true

这三行启动参数的含义非常容易理解,在官方文档中我没有注意到有默认目录,所以我们一定需要指定目录,指定配置文件名,最后一行的watch=true将开启动态配置项的即时生效,不再需要重启容器。

traefik.yml 文件的加载类似,在 Docker 环境运行 Traefik 的情况下,我们需要将存放 dynamic.yml 的目录绑定至容器的 /etc/traefik 目录,这里我们使用了同一目录,这样就不必分别绑定两个目录了。

通过以上的描述,读者应该基本明白了 Traefik 整个配置的运作方式,更加高级的用法可以自行研究官方文档。

Traefik 的安装

我们使用 Docker 安装 Traefik,下文将以官方文档中的相关示例为模板,修正其错误,完成初步配置及安装。

我们使用的配置文件来自于官方文档中Docker-compose with let's encrypt: TLS Challenge,内容如下:

version: "3.3"

services:

  traefik:
    image: "traefik:v2.0.0-rc3"
    container_name: "traefik"
    command:
      #- "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
      #- "--certificatesresolvers.mytlschallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.mytlschallenge.acme.email=postmaster@mydomain.com"
      - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
    ports:
      - "443:443"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  whoami:
    image: "containous/whoami"
    container_name: "simple-service"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.mydomain.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=mytlschallenge"

官方文档中的这一示例,使用了 CLI 配置静态配置的方式,但其提供的这一示例主要存在以下几个问题:

  • 缺少 80 端口绑定,只能使用 HTTPS
  • 没有开启 Dashboard
  • 没有绑定动态配置文件目录

下面我们对其针对性的修改,另外为了清晰,我们将示例中提供的 whoami 应用在后面作为单独应用运行。

根据前文对 Traefik 配置方式的讲解,我们知道我们既可以选择在 docker-compose.yml 文件中配置,也可以使用独立的 traefik.yml 配置,本人建议使用独立的 traefik.yml 配置,下文也只提供这一种方式,熟悉了以后,读者可以自行切换。

静态配置里可以有哪些内容,可以参考官方文档

我们的配置文件内容如下:

  • docker-compose.yml 文件
version: "3.3"
services:
  traefik:
    image: "traefik:latest" # 我们直接部署最新版本,可自行调整
    container_name: "traefik"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      # 自动申请的证书存放位置,我们需要在当前目录创建 letsencrypt 目录
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # 绑定我们的配置目录
      - "./config:/etc/traefik"
  • ./config/traefik.yml 文件
providers:
  docker: {}
  file:
    directory: "/etc/traefik"
    filename: "dynamic.yml"
    watch: true

log:
  level: DEBUG

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

certificatesResolvers:
  mytlschallenge:
    acme:
      email:  "yourname@domain"
      storage:  "/letsencrypt/acme.json"
      tlsChallenge: {}

api:
  dashboard: true
  • ./config/dynamic.yml 文件

由于我们暂时不转发任何服务,此文件暂时为空。

启动我们的 docker-compose.yml 文件:

docker-compose up -d

稍等片刻,就能看到服务创建成功,使用 docker ps 查看,可以看到容器正常运行。

traefik-docker-1.png

配置域名访问 Traefik Dashboard

Traefik 的 Dashboard 实际上是其 API 功能的图形化展示,所以我们需要开启配置中的 API 参数和 Dashboard 参数,参考官方文档:

If you enable the API, a new special service named api@internal is created and can then be referenced in a router. And then define a routing configuration on Traefik itself with the dynamic configuration.

如果开启了 api,就会创建一个叫做 api@internal 的服务,我们上面所使用的 traefik.yml 配置中,已经打开了 api,也打开了 dashboard

官方配置中有一行 - "--api.insecure=true" 被我们无视掉了,有兴趣的朋友阅读这里

然后按官方文档说明,为 Traefik 自身添加这一动态路由,所以我们修改 dynamic.yml 文件:

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.domain.com`)"
      service: api@internal

Rule 的配置除了 Host 还有 PathPathPrefix 等,配置逻辑运算符来实现配置,具体可以查看这里。另外,记得将域名提前解析到服务器 IP,如果是本地虚拟机测试,请自行修改 hosts 文件。

之后重新运行 docker-compose up -d 即可,此时访问绑定的域名就能看到 Traefik 的 Dashboard 了:

traefik-docker-2-dashboard.png
traefik-docker-3-dashboard2.png

配置域名转发至 Docker 服务

当我们启动一个 Docker 服务后,Traefik 就能发现服务并为其创建默认路由,需要注意的一点是,如果 Traefik 要能够与新的 Docker 服务进行通信,必须将其加入到同一网络,我们使用 docker-compose.yml 启动 Traefik 时,就创建了 traefik_default 网络,新加入的容器需要手动加入该网络。

加入网络的方式很多,可以自行搜索,使用 docker run 时通过 --network traefik_default,或者使用docker-compose.yml 时添加:

networks:
  - traefik_default

下面我们使用 Traefik 提供的 whoami 应用进行测试,使用 docker run 启动应用:

docker run -d -P --name whoami --net traefik_default containous/whoami

之后我们查看 Dashboard,就会发现 Traefik 已经自动发现了服务,服务名为 whoami@docker 。下面我们为该服务配置路由,打开我们的 ./config/dynamic.yml 文件,在下方增加 whoami 路由配置内容:

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
    whoami:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`) && Path(`/whoami`)"
      service: whoami@docker

配置完成并保存后,在 Dashboard 就可以看到新添加的路由了,此时我们访问 http://traefik.pbeta.cn/whoami 就能访问到我们的服务了:

traefik-docker-4.png

你会发现我们并没有告诉要转发服务的哪个端口,这是因为 Traefik 智能的进行了处理,如果服务只暴露一个端口,Traefik 就会自动选择该端口,但如果服务暴露多个端口,你必须手动指定,具体请参考官方文档。

配置转发外部服务

我们使用本机来模拟这一需求,在我们的服务器上运行一个 Ghost 服务,但是并不加入 traefik_default 网络,而是在 Traefik 中通过 服务器互联网IP : 服务端口 来转发。

启动一个 Ghost 容器:

docker run -d -p 2368:2368 --name ghost  ghost

我们暴露了服务器的 2368 端口,此时直接在外部访问 服务器IP:端口 就能访问到网站(如果你防火墙开放了 2368 端口的话)

traefik-docker-5.png

下面我们就配置 Traefik 将 traefik.pbeta.cn/ghost 转发至该地址,继续修改我们的 dynamic.yml ,分别添加一个服务和一个路由,完整的 dynamic.yml 文件如下:

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
    whoami:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`) && Path(`/whoami`)"
      service: whoami@docker
    ghost:
      entryPoints:
        - "web"
      rule: "Host(`ghost.pbeta.cn`) "
      service: ghost

  services:
    ghost:
      loadBalancer:
        servers:
        - url: "http://47.74.154.202:2368/"

保存之后访问 http://ghost.pbeta.cn 即可打开我们刚启动的 Ghost 网站:

traefik-docker-6.png

配置 HTTPS 访问

前面我们一直使用的是 web 这一入口,通过简单配置即可实现 HTTPS 访问服务。官方文相关页面有较多示例,可供参考。本文将只讲解使用 Let's Encrypt 自动配置 HTTPS。

前面提供的 traefik.yml 中已经包括了相关的配置:

certificatesResolvers:
  mytlschallenge:
    acme:
      email:  "yourname@domain"
      storage:  "/letsencrypt/acme.json"
      tlsChallenge: {}

我们为这一 certificatesRsolver 命名为了 mytlschallenge,之后在路由配置中添加 tls 部分使用该 resolver 即可,所以我们打开 dynamic.yml,进行编辑,为服务额外添加一条路由配置:

api-tls:
  entryPoints:
    - "websecure"
  rule: "Host(`traefik.pbeta.cn`)"
  service: api@internal
  tls:
    certResolver: "mytlschallenge"

保存之后稍等片刻,就已经配置好了 HTTPS:

traefik-docker-7.png

可以在多个网站使用同一个 certificatesResolver,但为了避免冲突,支持通过 option 来进行区分,一个完整的配置示例如下:

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
    api-tls:
      entryPoints:
        - "websecure"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
      tls:
        certResolver: "mytlschallenge"
        options: traefik

    whoami:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`) && Path(`/whoami`)"
      service: whoami@docker
    whoami-tls:
      entryPoints:
        - "websecure"
      rule: "Host(`traefik.pbeta.cn`) && Path(`/whoami`)"
      service: whoami@docker
      tls:
        certResolver: "mytlschallenge"
        options: traefik
    ghost:
      entryPoints:
        - "web"
      rule: "Host(`ghost.pbeta.cn`)"
      service: ghost
    ghost-tls:
      entryPoints:
        - "websecure"
      rule: "Host(`ghost.pbeta.cn`)"
      service: ghost
      tls:
        certResolver: "mytlschallenge"
        options: ghost

  services:
    ghost:
      loadBalancer:
        servers:
        - url: "http://47.74.154.202:2368/"
tls:
  options:
    traefik: {}
    ghost: {}

这样我们的三个路由的 HTTPS 访问都就配置好了,你可以注意到我们使用了2个 options,因为 traefik.pbeta.cnghost.pbeta.cn 需要不同的证书,所以必须以此进行区分,options 中也可以自行根据文档说明自行添加配置内容。

使用 Middlewares

Traefik 提供了非常多的中间件,这些中间件能实现你需要的大部分功能,这里分别以简单权限认证和 HTTP 自动跳转 HTTPS 为例进行介绍。

为了避免配置文件过长,下面只以 Dashboard 进行示例。

实现自动跳转 HTTPS

Middlewares 的使用非常简单,在动态配置文件中的 http (或tcp)下添加 middlewares,之后在每一个路由中使用即可,我们添加一个自动跳转 HTTPS 的 Middleware:

  middlewares:
    redirect:
      redirectScheme:
        scheme: https

之后我们在以 web 为入口的服务添加这一 middleware

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
      middlewares:
        - redirect
    api-tls:
      entryPoints:
        - "websecure"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
      tls:
        certResolver: "mytlschallenge"
        options: traefik

  middlewares:
    redirect:
      redirectScheme:
        scheme: https

tls:
  options:
    traefik: {}

保存文件之后,访问 http://traefik.pbeta.cn 将会自动跳转至 https://traefik.pbeta.cn

实现密码认证

这里需要使用的中间件是 BasicAuth,具体的使用方式可以参阅官方文档

在 K8s 中我们使用 Secret 资源来管理密码等,但在 Docker 中无法通过这种方式,需要使用 users 或者 usersFile 参数管理密码,这里我们使用 users

密码的创建需要借助于 htpasswd,安装方式如下:

  • CentOS
yum -y install httpd-tools
  • Ubuntu
apt install apache2-utils

我们使用 htpasswd 命令来生成一用户名为test 密码 为test1234 的密码对:

echo $(htpasswd -nb test test1234)

得到的信息是 test:$apr1$k.xiBHjv$VdavfNvly69vNZvkKpB2j0

需要注意一点,由于我们是在单独配置文件中使用,所以无须进行转义,如果是以CLI 形式使用的话,所有 $ 符号必须写两次,可以使用官方文档中的命令来生成转义后的密码 echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g

下面我们就使用此密码对来创建我们的 BasicAuth Middleware,打开 dynamic.yml 文件进行修改,在上面创建的 redirect 下添加如下内容:

test-auth:
  basicAuth:
    users:
      - "test:$apr1$k.xiBHjv$VdavfNvly69vNZvkKpB2j0"

之后在前面的路由下的 middlewares 下添加 test-auth,完整的 dynamic.yml 文件如下:

http:
  routers:
    api:
      entryPoints:
        - "web"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
      middlewares:
        - redirect
        - test-auth
    api-tls:
      entryPoints:
        - "websecure"
      rule: "Host(`traefik.pbeta.cn`)"
      service: api@internal
      middlewares:
        - test-auth
      tls:
        certResolver: "mytlschallenge"
        options: traefik

  middlewares:
    redirect:
      redirectScheme:
        scheme: https
    test-auth:
      basicAuth:
        users:
          - "test:$apr1$k.xiBHjv$VdavfNvly69vNZvkKpB2j0"
tls:
  options:
    traefik: {}

保存文件后,再次访问 http://traefik.pbeta.cn,就会发现需要密码登录了:

traefik-docker-9.png

输入密码后即可正常访问。

总结

通过以上内容,我们对 Traefik 的配置方式有了比较深入的了解,并实现了我们的目标,Traefik 的官方文档虽然杂乱,但不失详尽,有了本文提供的基础知识,再结合官方文档,使用 Traefik 实现各种高级功能应该并非难事。

在配置过程中遇到任何问题,可以在评论区中提问,本人将尽力协助。

参考文档

  1. Traefik 官方文档
  2. What api.insecure do exactly?
  3. Comparison of Traefik 1.7 and Traefik 2.0 with Docker