从 Nginx 切换到 Traefik

为什么选择 Traefik

如果追求极致的性能那自然选择 Nginx,但对于个人而言 Traefik 也是一个良好的选择,尤其是其服务发现的能力很好的解决了前端、后端应用增加、减少时需修改配置文件的问题。

Traefik 的架构

traefik architecture overview

由图所示,Traefik 会监听入口点(EntryPoints),路由器(Routers)连接到这些入口点,分析传入的请求,以查看它们是否与一组规则(Rule)匹配。如果匹配,则有中间件先经过中间件(Middlewares),再转发给对应的服务(Services)。

其中配置与 Nginx 相似,分为两部分:

  • 静态配置:traefik.toml,与 nginx.conf 类似
  • 动态配置:config/*.toml,与 conf.d/*.conf 类似

以 whoami 为示例

先创建 docker-compose.yml 文件,静态配置(traefik.toml)直接记录在容器启动配置中可以带来更好的体验:

version: '3.7'

services: 
  traefik:
    container_name: traefik
    image: traefik:v2.4.8
    restart: always
    ports: 
      - 80:80
      - 443:443
    networks: 
      - traefik
    command:
      # 全局配置
      - "--global.checkNewVersion=false"
      - "--global.sendAnonymousUsage=false"
      # ping
      - "--ping=true"
      # 日志
      - "--log.level=warn"
      - "--log.format=common"
      - "--accesslog=false"
      # api/dashboard
      - "--api=true"
      - "--api.insecure=true"
      # 入口点
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
      # 服务发现
      # docker 形式服务发现,接入 traefik network
      - "--providers.docker=true"
      - "--providers.docker.watch=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
      - "--providers.docker.swarmMode=false"
      - "--providers.docker.useBindPortIP=false"
      - "--providers.docker.network=traefik"
      # file 形式服务发现
      - "--providers.file=true"
      - "--providers.file.watch=true"
      - "--providers.file.directory=/etc/traefik/config"
      - "--providers.file.debugloggeneratedtemplate=true"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/:/etc/traefik/config/:ro
    labels: 
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      # dashboard http
      - "traefik.http.routers.traefik-dashboard-default.entrypoints=web"
      - "traefik.http.routers.traefik-dashboard-default.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-dashboard-default.service=dashboard@internal"
      # dashboard api
      - "traefik.http.routers.traefik-dashboard-api.entrypoints=web"
      - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.example.com`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api.service=api@internal"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      timeout: 5s
    logging: 
      driver: "json-file"
      options: 
        max-size: "1m"

  whoami:
    container_name: whoami
    image: traefik/whoami
    networks: 
      - traefik
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      # http entrypoints
      - "traefik.http.routers.whoami.entrypoints=web"
      - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"

# 先创建外部网卡
# docker network create traefik
networks: 
  traefik:
    external: true

可为动态配置抽象一些会公共使用的,增加 config.default.toml 文件。

1. 基础认证

基础认证采取简单的 BasicAuthopen in new window

利用 htpasswdopen in new window 工具生成密码:

htpasswd -nb yourname yourpassword

# 得到
# yourname:$apr1$NbtntO7x$43xzoIsnsgH.Sn2Utw33g.

config/default.toml 增加一项默认认证,动态修改生效,后续无需重启:

# 中间件在 docker cli 中使用时可用 `,` 分隔:middlewares=m1@file,m2@file

# gzip
[http.middlewares.gzip.compress]

# basic 认证
[http.middlewares.default-auth-user.basicAuth]
removeHeader = true
users = [
  "yourname:$apr1$NbtntO7x$43xzoIsnsgH.Sn2Utw33g."
]

traefikwhoamilabels 中增加:

version: '3.7'

services: 
  traefik:
    # 省略其他
    labels:
      - "traefik.http.routers.traefik-dashboard-default.middlewares=default-auth-user@file"
      - "traefik.http.routers.traefik-dashboard-api.middlewares=default-auth-user@file"
  whoami:
    # 省略其他
    labels:
      - "traefik.http.routers.whoami.middlewares=default-auth-user@file"

2. 升级为 HTTPS

  1. 先手动添加证书

    创建证书目录并移入证书:

    sudo mkdir ssl
    sudo mv /path/to/ssl/whoami.example.com.* ./ssl
    

    修改静态配置:

    version: '3.7'
    
    services:
     traefik:
       # 忽略其他
       volumes:
         - ./ssl/:/data/ssl/:ro
     whoami:
       # 忽略其他
       labels:
         # https entrypoints
         - "traefik.http.routers.whoami-secure.entrypoints=websecure"
         - "traefik.http.routers.whoami-secure.rule=Host(`whoami.example.com`)"
         - "traefik.http.routers.whoami-secure.tls=true"
    

    修改动态配置:

    # config/tls.toml
    [tls]
    [tls.options.default]
    minVersion = "VersionTLS12"
    maxVersion = "VersionTLS12"
    [tls.options.test-tls13]
    minVersion = "VersionTLS13"
    cipherSuites = [
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    ]
    
    [[tls.certificates]]
    certFile = "/data/ssl/whoami.example.com.crt"
    keyFile = "/data/ssl/whoami.example.com.key"
    
  2. 自动获取证书

    创建文件并赋权:

    sudo touch acme.json
    sudo chmod 600 acme.json
    

    修改静态配置文件:

    version: '3.7'
    
    services: 
      traefik:
        command:
          # tls
          - "--certificatesresolvers.letsencrypt.acme.email=acme@example.com"
          - "--certificatesresolvers.letsencrypt.acme.storage=acme.json"
          - "--certificatesresolvers.letsencrypt.acme.keyType=EC384"
          - "--certificatesresolvers.letsencrypt.acme.httpChallenge.entryPoint=web"
          - "--entrypoints.websecure.http.tls.certResolver=letsencrypt"
        labels:
          # dashboard https
          - "traefik.http.routers.traefik-dashboard-default.entrypoints=websecure"
          - "traefik.http.routers.traefik-dashboard-default.rule=Host(`traefik.example.com`)"
          - "traefik.http.routers.traefik-dashboard-default.service=dashboard@internal"
          # dashboard api
          - "traefik.http.routers.traefik-dashboard-api.entrypoints=websecure"
    

    重启服务后查看网页发现 https 已生效,且 acme.json 已被写入配置。

  3. 针对单个服务

    version: '3.7'
    
    services: 
      traefik:
        # 省略其他
        command:
          # 移除
         # - "--entrypoints.websecure.http.tls.certResolver=letsencrypt"
        labels:
          # 增加
          - "traefik.http.routers.traefik-dashboard-secure.tls=true"
          - "traefik.http.routers.traefik-dashboard-secure.tls.certresolver=letsencrypt"
    

3. 重定向

添加中间件:

# config/default.toml
[http.middlewares.https-redirect.redirectScheme]
scheme = "https"
permanent = true

使用:

version: '3.7'

services: 
  traefik:
    # 省略其他
    labels:
      # 增加
      - "traefik.http.routers.traefik-dash-default.middlewares=https-redirect@file"

4. 最终配置

version: '3.7'

services: 
  traefik:
    container_name: traefik
    image: traefik:v2.4.8
    restart: always
    ports: 
      - 80:80
      - 443:443
    networks: 
      - traefik
    command:
      - "--global.checkNewVersion=false"
      - "--global.sendAnonymousUsage=false"
      - "--ping=true"
      - "--log.level=warn"
      - "--log.format=common"
      - "--accesslog=false"
      - "--api=true"
      - "--api.insecure=true"
      - "--certificatesresolvers.letsencrypt.acme.email=acme@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=acme.json"
      - "--certificatesresolvers.letsencrypt.acme.keyType=EC384"
      - "--certificatesresolvers.letsencrypt.acme.httpChallenge.entryPoint=web"
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
      - "--entrypoints.websecure.http.tls.certResolver=letsencrypt"
      - "--entrypoints.websecure.http.middlewares=gzip@file"
      - "--providers.docker=true"
      - "--providers.docker.watch=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
      - "--providers.docker.swarmMode=false"
      - "--providers.docker.useBindPortIP=false"
      - "--providers.docker.network=traefik"
      - "--providers.file=true"
      - "--providers.file.watch=true"
      - "--providers.file.directory=/etc/traefik/config"
      - "--providers.file.debugloggeneratedtemplate=true"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/:/etc/traefik/config/:ro
      - ./ssl/:/data/ssl/:ro
      - ./acme.json/:/acme.json:rw
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.traefik-dashboard-default.middlewares=https-redirect@file"
      - "traefik.http.routers.traefik-dashboard-default.entrypoints=web"
      - "traefik.http.routers.traefik-dashboard-default.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-dashboard-secure.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard-secure.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik-dashboard-secure.service=dashboard@internal"
      - "traefik.http.routers.traefik-dashboard-secure.middlewares=default-auth-user@file"
      - "traefik.http.routers.traefik-dashboard-api.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.example.com`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api.service=api@internal"
      - "traefik.http.routers.traefik-dashboard-api.middlewares=default-auth-user@file"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      timeout: 5s
    logging: 
      driver: "json-file"
      options: 
        max-size: "1m"

  whoami:
    container_name: whoami
    image: traefik/whoami
    networks: 
      - traefik
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.whoami.middlewares=https-redirect@file"
      - "traefik.http.routers.whoami.entrypoints=web"
      - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
      - "traefik.http.routers.whoami-secure.entrypoints=websecure"
      - "traefik.http.routers.whoami-secure.rule=Host(`whoami.example.com`)"
      - "traefik.http.routers.whoami-secure.middlewares=default-auth-user@file"

# docker network create traefik
networks: 
  traefik:
    external: true

参考资料

  1. Traefikopen in new window
  2. Traefik 配置open in new window
  3. tomlopen in new window
  4. 苏洋 Tag#Traefikopen in new window