在 Docker Swarm Mode 集群使用 Traefik 反向代理

最近在两台服务器上部署 Docker Swarm Mode 集群. 整整花了三天时间才搞定. 最终的技术方案是 Docker Swarm + Convoy + Traefik + Portainer. 简单总结下过程.

安装 Docker 和 Docker Compose

Docker 这里推荐两种安装方式, 一种参照 https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/#install-using-the-repository , 另一种是使用 Daocloud 提供的一键安装脚本. 同时我们也安装下 Docker compose, 毕竟是容器编排常用的东西, 不管用不用的上(我原本是需要用的, 后来换了方案就暂时没有用了).

  1. 官方安装
$ apt-get update
$ apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
$ add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ apt-get update
$ apt-get install docker-ce
  1. Daocloud 脚本安装
$ curl -sSL https://get.daocloud.io/docker | sh

接着安装 Docker-compose

$ curl -L https://get.daocloud.io/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

安装 Convoy 配置 NFS

volume 一般是宿主机到容器之间的, 这在集群会出现数据共享的问题. 所以我们需要共享存储, 我试了两个, 一个是 azurefile 一个是 convoy. azurefile 似乎有点问题, 而且要创建 azure 账号, 而且服务要收费. 所以我这里推荐用 convoy.

convoy 可以查阅 https://github.com/rancher/convoy , 我们可以通过下面的脚本来快速安装 https://gist.github.com/pi0/55d1cfee4d201ffcd125441c8e56c841 . 我们先在 Master 进行操作.

注意更改 ${VFS_PATH:/mnt}. 然后给权限, 执行就好了.

我们通过 NFS 里实现服务器文件共享, 因此还需要对 NFS 进行一定配置. 参考文章 https://www.digitalocean.com/community/tutorials/how-to-set-up-an-nfs-mount-on-ubuntu-16-04

在 Master 执行:

$ apt install nfs-kernel-server

在 Worker 执行:

$ apt install nfs-common

在 master 上, 假设 /mnt 使我们作为 NFS 的地方:

$ chown nobody:nogroup /mnt

同样在 Master 上, 修改你的 /etc/exports 文件, 加入你的 Worker. 然后重启服务

$ systemctl restart nfs-kernel-server

确保防火墙没问题. 接着在 Worker 挂载 Master 的 NFS 目录.

$ mount 172.21.137.191:/mnt /mnt

如果想要 Worker 重启自动挂载的话, 还需要编辑 /etc/fstab 文件 (该文件谨慎编辑)

$ 172.21.137.191:/mnt     /mnt    nfs     auto,nofail,noatime,nolock,intr,actimeo=1800 0 0

可以检查下是否成功. 如果成功了之后, Worker 安装 Convoy, 修改 path 为该挂载点即可.

部署 Swarm 集群

https://portainer.io/install.html

首先初始化一个 Swarm 集群

$ docker swarm init

屏幕会打印出一条 docker swarm join --token xxx 的信息, 在子节点运行该命令即可加入到该 Swarm 中来.

配置 Traefik

首先创建一个 overlay 网络, 例如 traefik-net.

$ docker network create --driver overlay traefik-net --attachable

创建一个数据卷 traefik

$ concoy create traefik

然后在数据卷 traefik 中新建两个文件, traefik.tomlacme.json. 前者是 Traefik 的配置文件, 后者是存放 SSL 证书的. Traefik 的配置文件:

# Example Trafik.toml
debug = true
defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  compress = true
    [entryPoints.https.tls]

[retry]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "ruiming.me"
watch = true
exposedbydefault = false

[acme]
email = "ruiming.zhuang@gmail.com"
storage = "acme.json"
entryPoint = "https"
onHostRule = true

该配置文件开启了调试模式, 并配置了 HTTP 自动跳转 HTTPS 以及自动申请 SSL 证书. 可以根据自己情况进行修改, 你也可以在给相应的 Serivce 或者 Container 传入 label 来覆盖.

创建 Traefik 服务:

$ docker service create \
--name traefik \
--constraint=node.role==manager \
--publish 80:80 \
--publish 443:443 \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/mnt/traefik/traefik.toml,target=/traefik.toml \
--mount type=bind,source=/mnt/traefik/acme.json,target=/acme.json \
--network traefik-net \
traefik \
--docker \
--docker.swarmmode \
--docker.domain=ruiming.me \
--docker.watch \
--web

可以创建下如下一个简单的服务来测试下:

$ docker service create \
--name whoami \
--label traefik.port=80 \
--label traefik.frontend.rule=Host:test.ruiming.me \
--label traefik.docker.network=traefik-net \
--network traefik-net \
emilevauge/whoami

注意 traefik.frontend.rule 这个地方, 配置的前端规则, 也就是反向代理规则. 这里我觉得非常强大的地方在于它不仅仅支持根据 host 来匹配, 它支持几乎全部的 http request header. Traefik 称之为 Matcher. Matcher 支持很多规则, 例如 method, headers, path 等, 具体可以看 https://docs.traefik.io/basics/#matchers .

在这里, Host 是 test.ruiming.me, 可以随便改.

然后测试下:

$ curl -H host:test.ruiming.me localhost

服务数量扩大之后测试:

$ docker service scale whoami=3

把容器数量设置为 3, 一般 swarm 就会在 worker 部署有 whoami, 这时候我们再多次测试下, 如果都能成功, 就说明正常工作了, 不然的话检查下防火墙设置(mark 一下, 在这里因为防火墙的原因踩了大坑...)

安装 Portainer

为了便捷管理 Swarm 集群, 我们可以使用 Portainer.

首先创建一个 volume

$ convoy create portainer

创建 Portainer 服务

$ docker service create \
--name portainer \
--publish 9000:9000 \
--replicas=1 \
--constraint 'node.role == manager' \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--mount src=portainer,dst=/data \
--label traefik.port=9000 \
--label traefik.frontend.rule=Host:dashboard.ruiming.me \
--label traefik.docker.network=traefik-net \
--network traefik-net \
portainer/portainer \
-H unix:///var/run/docker.sock

注意几个 Traefik 相关的 Label. 服务启动之后浏览器访问 dashboard.ruiming.me 即可, 域名可以自己修改下, 但要解析到该服务器来.

portainer

Portainer 对 Docker Swarm Mode 的支持还是不错的.

还可以根据需要安装 visualizer, 官方的一个 Docker Swarm 可视化工具, 不过其实 Portainer 已经自带了. 可装可不装.

docker service create \
--name=viz \
--publish=5000:8080/tcp \
--constraint=node.role==manager \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--label traefik.port=8080 \
--label traefik.frontend.rule=Host:swarm.ruiming.me \
--label traefik.docker.network=traefik-net \
--network traefik-net \
dockersamples/visualizer

总结

Docker Swarm 集群的部署比较简单, 并且本身自带反向代理. 但是如果我们想根据 host 来进行反向代理的话, 就需要借助一些工具. 常见的例如 Nginx, HAProxy 还有我现在使用的 Traefik. 另外理论上也可以使用 swarm-ingress-router 来实现, 之所以说是理论上, 是因为我没有成功, 但我猜原因是我防火墙的问题, 因为那时候 Traefik 配置一样失败, 最后调整了防火墙配置就可以了. swarm-ingress-router 功能也比较简单, 所以配置好了 Traefik 之后我就没有再去尝试.

之所以选择的 Traefik, 是因为我不想自己配置反向代理, 希望可以通过指定容器 Label 或者 Environment 方式来实现; 希望可以自动获取和维护 Let's Encrypt 证书, 不想自己去维护特别是 Let's Encrypt 证书只有三个月有效期; 再者就是我希望其提供尽可能多的配置项, 因为既然不用自己手写配置文件, 就应该提供比较丰富的配置选项出来, 比如 Gzip.

Nginx 可以通过 nginx, docker-gen, letsencrypt-nginx-proxy-companion 三个容器来实现, 事实上在单机环境我就是这么做的. 但在集群下会有无法获悉其他服务器上容器创建销毁的问题, 以及无法准确得到容器服务内网 IP 的问题. 我一开始尝试用 Nginx 来解决花了很长时间也没搞定. 详细的可以参考下方的 **配置 Nginx (参考) ** .

HAProxy 我没有尝试, 作为反向代理使用的话应该 HAProxy 是非常有资格的, 但我不知道他有没有配套的 SSL 解决方案.

Traefik 是最能满足我需求的, 实际体验也非常好.

配置 Nginx (参考)

利用 nginx, docker-gen, letsencrypt-nginx-proxy-companion 这三个容器, 可以做到监听 docker 容器的创建和销毁, 根据被创建或销毁的环境变量和发布端口, 自动生成反向代理配置文件并重启 Nginx.

但是, docker-gen 是通过 bind docker.sock 实现的, 所以它只能捕获到宿主机的容器变化, 而对于其他 Swarm 节点是无能为力的. 有一种方法就是通过监听 Swarm Master 服务来实现, 设置环境变量DOCKER_HOST 就好, 不过问题也不少, 似乎 IP 地址会有问题.

可以参考这两个 issue:

https://github.com/jwilder/nginx-proxy/issues/97

https://github.com/jwilder/nginx-proxy/issues/520

首先我们可以创建一个使用 overlay 的 network, 如下命名为 proxy. 我们把 Nginx 和相关的需要放在同一个网络里的 Web 服务都加入到该网络中来.

$ docker network create --driver overlay proxy --attachable

当 master 调度任务到 worker 时, worker 自动拷贝了该网络. 也就是说, 不同于 bridge 和 host 用于容器和宿主机. overlay 是在集群中进行使用的. Docke Swarm 初始化后会自动创建基于 overlay 的 ingress 和 docker_gwbridge 网络. 不过建议自己手动创建一个网络使用, --attachable 可以让通过 docker run 运行的容器也能加入到该网络来. 默认情况下, overlay 网络只有 docker service create 的方式才能加入.

然后我们进入 Portainer, 默认暴露在了 9000 端口. 初次进入 Portainer 可以配置密码.

Nginx 我们需要创建四个 volumn 出来, 分别是 nginx-conf, nginx-vhost, nginx-html, nginx-ssl. 当然你也可以自己换个名字. 注意 driver 选择 convoy. 这样无论容器部署到了哪个节点都可以正常运行. 我们一次性要创建这三个容器:

  • nginx
  • jwilder/docker-gen
  • jrcs/letsencrypt-nginx-proxy-companion

这三个容器加起来, 可以做到监听容器创建和销毁, 自动生成 Nginx 配置文件, 自动申请 Let's Encrypt 证书. 自动重启 Nginx. 由于一些原因, 最好使用 docker-compose 来创建服务, 如果服务器还未安装 docker-compose, 自行安装.

虽然我们并没有把 Nginx 部署到 Swarm 上面, 但是为了以防万一, 数据卷我们还是使用 convoy driver.

然后将下面配置写入 docker-compose.yml (参考 https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion) 中, 执行

version: '3'
services:
  nginx:
    image: nginx
    labels:
        com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro

  nginx-gen:
    image: jwilder/docker-gen
    command: -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    container_name: nginx-gen
    restart: unless-stopped
    depends_on:
      - nginx
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - /mnt/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro # 手动获取 nginx.tmpl 并根据实际路径进行修改

  nginx-letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: nginx-letsencrypt
    restart: unless-stopped
    depends_on:
      - nginx
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:rw
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      NGINX_DOCKER_GEN_CONTAINER: "nginx-gen"
      NGINX_PROXY_CONTAINER: "nginx"

volumes:
  nginx-conf:
    external: true
  nginx-vhost:
    external: true
  nginx-html:
    external: true
  nginx-certs:
    external: true

networks:
  default:
    external:
      name: nginx-proxy

注意 nginx-gen 服务的最后一个 volume, 我们需要手动获取下该文件:

$ curl https://raw.githubusercontent.com/jwilder/nginx-proxy/master/nginx.tmpl > nginx.tmpl

按照你实际情况修改该 volume 路径.

接着部署就可以了:

$ docker-compose up -d

在浏览器输入 ip 访问, 应该可以连入到 Nginx 了.

我们可以修改一开始创建的 Portainer.

同时加上以下三个环境变量:

  • LETSENCRYPT_HOST
  • VIRTUAL_HOST
  • LETSENCRYPT_EMAIL

配置好后, 容器重启, nginx-gen 和 nginx-letsencrypt 监听到容器变化, 配置反向代理和 SSL 并写入 volume 然后重启 Nginx. 此时访问你输入的域名就可以了.

$ docker-compose restart

类似的, 当我们部署一个新网站, 只要让它加入到 Nginx 所在的那个 overlay 网络, 数据卷都用 convoy driver. 并配置上面三个环境变量. 就可以做到分布式快速部署了.

创建私有仓库 (参考)

集群中部署私有仓库, 可以避免每个服务器都去构建一遍. 部署了私有仓库后, 我们只需要从私有仓库拉取镜像就可以了. 集群的每个节点创建该服务都从私有仓库拿, 这样速度会快很多. 私有仓库搭建非常简单, 直接创建一个 registry 服务就好了. 不需要什么配置, 让他加入到 ingress 网络就好, 这样集群的所有机子都可以访问.