大概会比写 nginx 反代舒服的 Traefik (对于 Docker 服务来说)

common.Created 10932 common.chars / common.mor

用 docker 的话 你基本上装完就不用碰 traefik 的配置

FFW 群友如是说。

经典的 Nginx 反代方案

自从 Node.js、Go、Python 等语言的发展,越来越多的程序不再像 PHP 一样需要和一个 CGI 服务配合使用,而是都提供了自己的 HTTP 服务器实现,只需要前端将请求转发至一个特定的端口(或者 socket),然后再把得到的回应转送给用户。然后 nginx 的配置就都变成了这样:

# sites-enabled/service.conf
server {
    listen 80;
    listen 443 ssl http2;
    server_name service.domain;
    location / {
        proxy_pass http://127.0.0.1:2077;
        include    revproxy_common.conf;
    }
    include tls_security_common.conf;
}
nginx

核心内容只有一行看似无害的 location,以及一行 proxy_pass 接后端地址,对吧?

它怎么了?

这几行都算是好理解(只是理解而已!好不好写另说,尤其是 location),问题在于共有配置的一些坑:

  • 要想让网站上的 Websocket 生效,需要让 HTTP 连接能够 Upgrade,例如配置几个 header:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
nginx
allow 103.21.244.0/22;
# 下略
deny all;
nginx
ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
ssl_verify_client on;
nginx

以及一些不再列出详细解的常见问题:

  • 对于遇到 413 Payload Too Large 的情况,可能需要修改上传大小限制 (client_max_body_size)
  • 让后端获取到正确的 Host
  • 正确地配置 PHP
  • 还有最麻烦的 location

nginx 是功能相当强大的工具(例如我们之前提过它可以处理 SMTP),只用它来做反代未免大材小用,而且有时候会碰到非常、特别、超级令人烦躁的坑(例如 location1

Traefik 是什么?

Traefik 由 Traefik Labs 开发的云原生应用代理 ("The Cloud Native Application Proxy"),你可以在 CNCF 的 landscape 上看到它。从和它并列在 Service Proxy 分类的产品,像是 HAProxy、OpenELB,以及喜闻乐见的 Nginx (和 Tengine)等等,我们就能看出它的用途:负载均衡和反向代理。刚好是我们希望做的事情。

它的文档在这里: https://doc.traefik.io/traefik/

这玩意似乎主要是给 Kubernetes 等集群用的,网络上的大量教程也是假设它用在 Kubernetes 上(所以大量的示例配置都充斥着 NodePortIngress,以及 kubectl 之类的东西,这些都是 Kubernetes 相关的名词),不过它也可以很不错地配合单机 Docker 运作。除此之外,它还支持很多其它的配置来源 (Provider)、包括 ECS、Rancher(小 k8s?)、Etcd、Redis、HTTP,当然还有文件。接下来的文章会给出一些主要通过 docker-compose.yml 上的 label 配置 Traefik 的示例。

几条 Traefik 小知识

Entrypoint、Router、Service

Traefik 上最重要的有三件事:Entrypoint、Router 和 Service。

Entrypoint 描述请求如何进入 Traefik。每个 entrypoint 都由地址、端口和协议组成,例如 127.0.0.1:3179/udp 就代表监听 127.0.0.1 地址上 3179 端口的 UDP 协议;或者 :8080 就代表监听所有 interface 上 8080 端口的 TCP 协议(协议默认是 TCP)。

Router 对请求进行匹配,以决定它要发给哪个 Service 进行处理,或者要经过什么样的 middleware 等等。最常见的请求匹配模式当然就是 Host (也包括 TLS 请求的 SNI 标记),其它的还有 PathPrefixPathHeadersClientIP,甚至 Query 等。

Service 就是后端的服务入口啦。负载均衡后端列表、流量分配策略等的配置都放在这边。

静态配置和动态配置

Traefik 上的配置有两种:静态配置 (static configuration) 和动态配置 (dynamic configuration)。

静态配置主要是用来配置各个 Provider(这个后面会提到)、Entrypoint,以及运行日志、访问日志、遥测、mTLS 根证书之类。它可以通过配置文件、命令行参数和环境变量三者之一(而非三个配置来源合并)传入 Traefik。对静态配置的更改需要重新启动 Traefik 以应用。

动态配置则用来配置其它的所有东西。Traefik 三件套中,除了 Entrypoint 通过静态配置设置之外,另外的 Router 和 Service,还有各种其它的东西,例如 Middleware 啦、证书配置啦,都用动态配置来设置。它的来源 (provider) 可就多啦:例如 Docker 容器的 label(本文主要使用这个 provider)、Kubernetes 的 Ingress、Amazon ECS 容器的 label、etcd 里的配置,以及 redis 里的配置等等。除了这些之外,动态配置还有一个来源,就是配置文件 (file)。这些配置文件和静态配置的配置文件的内容可不太一样,记得不要把配置文件里的条目写错地方了。

官方文档参考在这里

基础配置

举一个静态配置的例子(注意这是静态配置哦):

# traefik.yml
api:
  dashboard: true # 启用 Dashboard API

entryPoints:
  # 普通的 HTTP 出口配置,监听所有 interface 的 80 端口
  web:
    address: ":80"
  # 监听特定 IP/端口
  web-local:
    address: "127.0.0.1:8867"
  # 普通的 HTTPS 出口配置
  websecure:
    address: ":443"
    http:
      # 默认启用 TLS
      tls: {}

providers:
  # 选择 Docker 的 Provider
  docker: {}
  # 选择 file 的 Provider
  file:
    # 动态配置的路径
    directory: /dynamicConf
    # 监视动态配置自动重载
    watch: true

# 如果有要配置的 mTLS CA,可以放在这里
serversTransport:
  rootCAs:
    - /ssl/origin_pull.pem
yml

运行 Traefik 的时候,需要指定静态配置文件的位置(或者,也可以把配置文件放在几个静态配置的搜索路径):

traefik --configFile=/traefik.yml
sh

对于 docker-compose,则是大概这样:

version: "3.8"

services:
  traefik:
    image: traefik:2.8
    command: --configFile=/traefik.yml
    volumes:
      # 挂载本地 Docker socket 到容器内
      - /var/run/docker.sock:/var/run/docker.sock
      # 挂载静态配置文件,只读
      - ./conf/traefik.yml:/traefik.yml:ro
      # 挂载动态配置文件,只读
      - ./dynamicConf:/dynamicConf:ro
    labels:
      # 在这个 container 上关闭 Traefik 特性
      - "traefik.enable=false"
    # 让 Traefik 使用主机网络,这样才能监听主机的接口等等
    network_mode: host
yml

配合一个动态配置(这是动态配置哦),依旧以 docker-compose 配置形式呈现:

version: "3"

services:
  whoami:
    image: traefik/whoami
    labels:
      # 配置一个名为 whoami 的 service
      # 将进入这个 service 的请求打入此容器的 80 端口
      # (如果这个容器只 expose 了一个端口,也可以省略这条,默认使用这个 expose 的端口)
      - "traefik.http.services.whoami.loadbalancer.server.port=80"
      # 配置一个名为 whoami 的 router(router 和 service 的名字不会冲突)
      # 所有从 web 这个 entrypoint 进入的,
      # Host 为 localhost 的请求都进入这个 service
      - "traefik.http.routers.whoami.rule=Host(`localhost`)"
      - "traefik.http.routers.whoami.entrypoints=web"
yml

跑起来:

docker-compose up
yml

之后打开 localhost,就可以看到关于此连接的信息:

Hostname: 9ae731c457fe
IP: 127.0.0.1
IP: 172.18.0.2
RemoteAddr: 172.18.0.1:54514
GET / HTTP/1.1
...
X-Real-Ip: 127.0.0.1
text

此时,Traefik 就已经将请求转发到 whoami 这个容器中的对应端口了。

其它的配置示例

Dashboard

Traefik 提供一个 dashboard,可以用来查看各个 entrypoint、router 及 service 的情况。由于这里不会用到额外的容器,我们会把配置写在文件动态配置中。

# dynamicConf/dashboard.yml
http:
  routers:
    dashboard:
      rule: Host(`localhost`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
      service: api@internal
      entrypoints:
        - web
yml

之后保存文件(如果在前面启用了动态配置自动重载)或者重新启动 Traefik 服务,就可以访问 http://localhost/dashboard/ 看到目前 Traefik 的配置状况。

TLS 和 mTLS

Traefik 是从一个证书库中选择使用的 TLS 证书的。因此,需要在动态配置中列出使用的所有证书。Traefik 会选择第一个可用的证书用来为客户端提供服务。示例配置如下:

(如果在 Docker 里配置 Traefik,那么这些路径都会是容器内的路径,记得把证书和私钥挂载到容器中。)

本地使用的 mTLS 证书对除了放在静态配置中,也可以放在这边。

tls:
  certificates:
    - certFile: /ssl/domain1.cer
      keyFile: /ssl/domain1.key
    - certFile: /ssl/domain2.cer
      keyFile: /ssl/domain2.key
  stores:
    default:
      defaultCertificate:
        certFile: /ssl/domain1.cer
        keyFile: /ssl/domain1.key
  options:
    default:
      clientAuth:
        caFiles:
          # mTLS CA 证书
          # 和静态配置中的 serversTransport.rootCAs 差不多
          # 配置其中一处应该就可以
          - /ssl/origin_pull.pem
        # 要求验证客户端证书
        clientAuthType: RequireAndVerifyClientCert
yml

IP 白名单

Traefik 有丰富的 middleware 机制。这里我们向动态配置中添加一个名为 ip-whitelistipWhiteList 类 middleware 解决这个问题。

# dynamicConf/ipwhitelist.yml
http:
  middlewares:
    ip-whitelist:
      ipWhiteList:
        sourceRange:
          # https://www.cloudflare.com/ips/
          - "103.21.244.0/22"
          - "103.22.200.0/22"
          # ...
          - "2a06:98c0::/29"
          - "2c0f:f248::/32"
yml

我们可以把这个 middleware 单独应用到特定的 router(例如,通过 Docker label 配置):

services:
  app:
    # ...
    labels:
      - "traefik.http.routers.router-name.middlewares=ip-whitelist@file"
    # ...
yml

也可以把这个 middleware 应用到一个特定的 entrypoint 下的所有 router:

entryPoints:
  websecure:
    address: ":443"
    http:
      middlewares:
        - cf-ip-whitelist@file
      tls: {}
    # ...
yml

注意单独指定 router 的 middleware 列表的时候,这个列表会在全 entrypoint 的 middleware 列表基础上追加,而不是覆盖全 entrypoint 的 middleware 配置。因此,目前我们建议只将所有 router 共用的 middleware 放在 entrypoint 级别的 middleware 列表中。

操作请求回复的 Header

这个也可以用 middleware 做。用法和其它的 middleware 一致。这里给出在动态配置文件中描述 middleware 配置的示例:

http:
  middlewares:
    do-headers:
      headers:
        customResponseHeaders:
          Permissions-Policy: "interest-cohert=()"
          # ...
yml

路径重写

同样是很常见的操作,我们也还是使用 middleware 完成。不过,路径重写配置一般都针对于特定服务,因此我们在 Docker 的 label 中描述这个配置。

Traefik 提供了多种用于路径重写的 middleware:

举个例子,如果要将 https://example.com/prefix/test 在服务端变成 http://service/test

services:
  app:
    # ...
    labels:
      - "traefik.http.middlewares.strip-this-path.stripprefix.prefixes=/prefix/"
      # 因为是在 Docker label 中配置的 middleware,这个 middleware 的全名是 strip-this-path@docker
      # 但因为我们也是在 Docker label 中引用的这个 middleware,所以 @docker 可以省略
      - "traefik.http.routers.router-name.middlewares=strip-this-path"
    # ...
yml

X-Forwarded-For 的信任来源 IP 名单

这是 entrypoint 级别配置的一部分

entryPoints:
  websecure:
    address: ":443"
    forwardedHeaders:
      trustedIPs:
        - "103.21.244.0/22"
        - "103.22.200.0/22"
        # ...
        - "2a06:98c0::/29"
        - "2c0f:f248::/32"
yml

ACME

Traefik 也支持通过 ACME 服务自动向 Let's Encrypt 等提供商申请证书,从而进行自动化证书管理。为了做到这点,我们需要在静态配置中配置 certificatesResolvers

certificatesResolvers:
  le-resolver:
    acme:
      email: letsencrypt-notice@example.org
      storage: /etc/traefik/acme/acme.json 
      keyType: EC384
      # 使用 TLS-ALPN-01 challenge
      tlsChallenge: {}
      # 如果希望申请泛域名证书,则需要使用 DNS-01 challenge
      # 需要配置一个 provider ,并按照 go-acme 的要求配置所需的凭据
      # dnsChallenge:
      #   provider: cloudflare
yml

有读者就会好奇:我们还没有配置申请证书的域名呢!别急,我们在 router 的动态配置中完成剩下的部分:

services:
  app:
    # ...
    labels:
      traefik.http.routers.emby.tls.certresolver: le-resolver
      traefik.http.routers.emby.tls.domains[0].main: "site.example.com"
      # 泛域名证书的也是填在这里
      # traefik.http.routers.emby.tls.domains[0].main: "*..example.com"
    # ...
yml

注释

  1. 踩坑 nginx 数次的加藤桑.jpg


common.Permanent link: https://blog.outv.im/2022/traefik/

common.Comments

common.LIKE this

common.Webmentions

common.webmention_response_0