大规模场景下的多服务部署和管理是一件很难的事情。幸运的是,Docker Stack为解决该问题而生。Docker Stack 通过提供期望状态、滚动升级、简单易用、扩缩容、健康检查等特性简化了应用的管理。这些功能都封装在一个完美的声明式模型当中。
14.1 使用Docker Stack 部署应用——简介
在笔记本上测试和部署应用很简单。但是这只能算是业余选手。在真实的生产环境进行多服务的应用部署和管理,这才是专业选手的水平。
幸运的是,Stack正为此而生!Stack能够在单个声明文件中定义复杂的多服务应用。Stack还提供了简单的方式来部署应用并管理完整的生命周期:初始化部署—>健康检查—>扩容—>更新—>回滚,以及其他功能。
步骤很简单。在Compose文件中定义应用,然后通过 docker stack deploy 命令完成部署和管理。Compose文件中包含了构成应用所需的完整服务栈,此外还包括了卷、网络、安全以及应用所需的其他架构。然后基于该文件使用docker stack deploy 命令来部署应用。
Stack是基于Docker Swarm之上来完成应用的部署。因此诸如安全等高级特性,其实都是来自Swarm。简而言之,Docker适用于开发和测试。Docker Stack则适用于大规模场景和生产环境。
14.2 使用Docker Stack 部署应用——详解
从体系结构上来讲,Stack位于Docker应用层级的最顶端。Stack基于服务进行构建,而服务又基于容器,如下图所示。
14.2.1 简单应用
本章的示例使用的应用是 AtSea Shop,这个应用我们在前面曾经下载过了(多阶段构建Docker)。该应用是个多服务应用,并且利用了认证和安全相关的技术。应用的架构图如下:
如果还没有下载这个项目,请使用git来下载。
git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git
该应用代码由若干的目录和源码文件组成,我们重点关注 docker-stack.yml 这个文件。该文件通常被称为Stack文件,在该文件中定义了应用及其依赖。
在该文件整体结构中,定义了4种关键字。其中包括 version、services、networks、secrets。与我们之前在Swarm章节介绍的一样,前3个关键字是同样的含义。secrets定义的是应用用到的密钥。
我们展开顶级菜单,就会看到5个服务,3个网络和4个密钥。
version: "3.2"
services:
reverse_proxy:
database:
appserver:
visualizer:
payment_gateway:
networks:
front-tier:
back-tier:
payment:
secrets:
postgres_password:
staging_token:
revprox_key:
revprox_cert:
14.2.2 深入分析Stack文件
Stack文件就是Docker Compose文件。唯一的要求就是version:一项需要是3.0或者更高的值。在Docker根据某个Stack文件部署应用的时候,首先会检查并创建networks:关键字对应网络。如果网络不存在,Docker会进行创建。下面我们详细看下这几个模块。
- 网络
networks:
front-tier:
back-tier:
payment:
driver: overlay
driver_opts:
encrypted: 'yes'
这里定义了3个网络,默认情况下网络都是使用overlay驱动,新建对应的覆盖类型的网络。但是payment网络比较特殊,需要对数据层加密。
默认情况下,覆盖网络的所有控制层都是加密的。如果需要加密数据层,有两种选择。
- 在 docker network create 命令中指定 -o encrypted 参数。
- 在stack文件中的 driver_opts 之下指定 encrypted:'yes'
数据层加密会导致额外开销,而影响额外开销大小的因素有很多,比如流量的类型和流量的多少。但是,通常额外开销会在10%的范围之内。
正如前面提到的,全部的3个网络均会先于密钥和服务被创建。
- 密钥
密钥数据顶级对象,在当前Stack文件中定义了4个。
secrets:
postgres_password:
external: true
staging_token:
external: true
revprox_key:
external: true
revprox_cert:
external: true
注意,4个密钥都被定义为external。这意味着在Stack部署之前,这些密钥必须存在。当然在应用部署时按需创建密钥也是可以的,只需要将 file:
- 服务
部署中主要的操作都在服务这个环节。每个服务都是一个JSON集合,其中包含了一系列关键字。下面分别介绍着5个服务。
(1) reverse_proxy 服务
reverse_proxy:
image: dockersamples/atseasampleshopapp_reverse_proxy
ports:
- "80:80"
- "443:443"
secrets:
- source: revprox_cert
target: revprox_cert
- source: revprox_key
target: revprox_key
networks:
- front-tier
reverse_proxy服务定义了镜像、端口、密钥和网络。
image关键字是服务对象中唯一的必填项,这个关键字定义了将要用于构建服务副本的Docker镜像。默认情况下会从Docker Hub拉取镜像,如果要从第三方服务中拉取,则需要自己添加对应的第三方镜像仓库服务API的DNS名称。
Docker Stack和Docker Compose的一个区别是,Stack不支持构建。这意味着在部署Stack之前,所有镜像都必须提前构建完成。
ports关键字定义了两个关键字。默认情况下,所有端口映射都采用Ingress模式。这意味着Swarm集群中每个节点的对应端口都会映射并且是可以访问的,即使那些没有运行副本的节点。另一种方式是Host模式,端口只映射了运行副本的Swarm节点上。但是Host模式需要使用完整的格式去配置,我们在前面曾经介绍过。
secrets 关键字定义了两个密钥,这两个密钥必须在顶级关键字 secerts 下定义,并且必须在系统中已经存在。密钥以普通文件的形式被挂载到服务副本当中。文件的名称就是stack文件中定义的 target 属性的值,其在Linux下的路径为 /run/secrets,Linux将 /run/secrets作为内存文件系统挂载。
networks关键字确保服务所有副本都会连接到front-tier网络。网络相关定义必须位于顶级关键字networks之下,如果定义的网络不存在,Docker会以Overlay网络方式新建一个网络。
(2)database服务
数据库服务除了定义上述的内容之外,还应用了环境变量和部署约束。
database:
image: dockersamples/atsea_db
environment:
POSTGRES_USER: gordonuser
POSTGRES_DB_PASSWORD_FILE: /run/secrets/postgres_password
POSTGRES_DB: atsea
networks:
- back-tier
secrets:
- postgres_password
deploy:
placement:
constraints:
- 'node.role == worker'
environment关键字允许在服务副本中注入环境变量。在该服务中,使用了3个环境变量来定义数据库用户、数据库密码的位置(挂载到每个服务副本的密钥)以及数据库服务的名称。
该服务还在deploy关键字下定义了部署约束。这样保证了当前服务只会运行在Swarm集群的worker节点之上。部署约束是一种拓扑感知定时任务,是一种很好的优化调度选择的方式。Swarm目前允许通过如下几种方式进行调度。
- 节点ID,如 node.id == qwertyuasdads
- 节点名称,如 node.hostname==wrk-12
- 节点角色,如 node.role != manager
- 节点引擎标签,如 engine.labels.operatingsystem==ubuntu16.04
- 节点自定义标签,如 node.labels.zone==prod1
(3) appserver 服务
appserver:
image: dockersamples/atsea_app
networks:
- front-tier
- back-tier
- payment
deploy:
replicas: 2
update_config:
parallelism: 2
failure_action: rollback
placement:
constraints:
- 'node.role == worker'
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
secrets:
- postgres_password
appserver 服务使用了一个镜像,连接到3个网络,并且挂载了一个密钥。此外,appserver服务还在deploy关键字下引入了一些额外的特性。
接下来我们进一步了解deploy关键字新增的内容。
replicas: 2 设置了期望服务的副本数量为2,默认为1.如果服务正在运行,需要调整副本数。可以调整stack文件中的 replicas 的数值,然后重新部署stack。重新部署stack并不会影响那些没有改动的服务。
update_config 定义了服务在滚动升级的时候应该如何操作。在这里是每次更新两个副本,升级失败之后会自动回滚。回滚会基于之前的服务定义启动新的副本。failure_action 的默认操作时pause,会在服务升级失败后阻止其他副本的升级。failure_action还支持continue。
restart_policy 定义了Swarm针对容器异常退出的重启策略。当前服务的重启策略是:如果某个副本以非0返回值退出,会立即重启当前副本。重启最多尝试3次,每次都是等待之多120s来检测是否成功。每次重启的间隔是5s。
(4) visualizer 服务
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8001:8080"
stop_grace_period: 1m30s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
update_config:
failure_action: rollback
placement:
constraints:
- 'node.role == manager'
除了指定镜像、定义端口映射、更新配置以及部署约束之外,这里还挂载了一个指定卷,并且定义了容器的优雅停止方式。
stop_grace_period 属性调整了默认为10s的优雅停止时长。
volumes关键字用于挂载提前创建的卷或者主机目录到某个服务副本中。在这里挂载Docker主机的 /var/run/docker.sock 目录到每个副本的 /var/run/docker.sock 路径。这意味着在服务副本中任何对 /var/run/docker.sock 的读写操作都会指向 Docker 主机的对应目录。
docker.sock 文件时Docker提供的套接字,Docker daemon通过该套接字对其他进程暴露其API终端。这意味着如果给某个容器访问该文件的权限,就是允许该容器接受全部的API终端,即等级与给予了容器查询和管理Docker daemon 的能力。在大部分场景下这是不允许的,但是这是个测试环境的示例应用。
该服务需要Docker套接字访问权限的原因时需要以图形化的方式展示当前Swarm中的服务。为了实现这个目标,当前服务需要能访问管理节点 Docker daemon ,当前服务通过部署约束的方式,强制服务服务只能部署在管理节点之上,同时将Docker套接字绑定到每个服务副本中。
(5) payment_gateway 服务
payment_gateway:
image: dockersamples/atseasampleshopapp_payment_gateway
secrets:
- source: staging_token
target: payment_token
networks:
- payment
deploy:
update_config:
failure_action: rollback
placement:
constraints:
- 'node.role == worker'
- 'node.labels.pcidss == yes'
在这里,payment_gateway 服务被要求只能允许在符合PCI DSS(支付卡行业标准)标准的节点之上。为了能使其生效,读者可以将某个自定义节点标签应用到Swarm集群中符合要求的节点之上。本书会在搭建应用部署实验环境的时候完成这个操作。
因为这里定义了两个部署约束,也就是只有同时满足 pcidss=yes 并且时worker的节点才会被部署。
14.2.3 部署应用
在部署应用之前,有几个前置处理需要完成。分别是:
- Swarm模式:应用将采用Docker Stack 部署,而Stack依赖Swarm模式。
- 标签: 某个Swarm worker节点需要自定义标签
- 密钥: 应用所需的密钥需要在部署前创建完成。
- 搭建应用实验环境
同Swarm的实验一样,这里我们使用三个Dokcer主机来搭建Swarm集群,其中包括1个管理节点和2个工作节点。初始化并添加工作节点的操作这里不重复操作了,请看前面的章节。
实验的节点情况如下:
[pangcm@docker01 ~]$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
g6kx0krb978aqlexv6nd2i66x * docker01 Ready Active Leader 19.03.5
px24lw8vvy3kh0ge61oge9cjo docker02 Ready Active 19.03.4
zha2x49djdhuqch35f6gm4efh docker03 Ready Active 19.03.5
(1) 添加节点标签(pcidss)
在前面我们分析过了payment_gateway 服务配置了部署约束,限制了只有运行在 pcidss=yes 标签的工作节点之上。这里我们在 docker02 节点上增加该标签。
docker node update --label-add pcidss=yes docker02
可以使用 docker node inspect docker02 命令去确认标签
[pangcm@docker01 ~]$ docker node inspect docker02|grep pcidss
"pcidss": "yes"
(2) 创建密钥
密钥中有3个是需要加密的key的,所以我们要先创建加密的key,然后把加密 key 放到 Docker 密钥文件当中。
创建键值对
## 不断enter 即可
openssl req -newkey rsa:4096 -nodes -sha256 -keyout domain.key -x509 -days 365 -out domain.crt
创建 revprox_cert、revprox_key、postgres_password的密钥
docker secret create revprox_cert domain.crt
docker secret create revprox_key domain.key
docker secret create postgres_password domain.key
创建stage_token密钥
echo staging |docker secret create staging_token -
列出所有的密钥:
[pangcm@docker01 ~]$ docker secret ls
ID NAME DRIVER CREATED UPDATED
zp591zcfknxprzhbn06r3rrwh postgres_password 31 minutes ago 31 minutes ago
tzfh3qgajlnf6yh9s82yw77ws revprox_cert 35 minutes ago 35 minutes ago
f109x6mtz1ueerxwxoy772ujv revprox_key 34 minutes ago 34 minutes ago
3bmwnmddfgyy8ij98csmfldjm staging_token 34 minutes ago 34 minutes ago
这里我们就完成了全部的前置准备,下面开始部署。
- 部署示例应用
##进入到项目目录下
cd atsea-sample-shop-app
##使用docker stack deploy 命令部署
docker stack deploy -c docker-stack.yml seastack
这里我们指定了stack文件,并把stack命名为 seastack。我们可以运行 docker network ls 以及docker service ls 命令来擦看应用的网络和服务情况。下面是命令输出需要注意的地方。
网络是先于服务创建的。这是因为服务依赖于网络,所以网络需要在服务启动前创建。Docker将Stack名称附加到由他创建的任何资源名称前作为前缀。在本例中,Stack名为seastack,所以所有资源名称的格式都如: seastack_
另外一个需要注意的是查看网络状态的时候会看到多了一个 seastack_default 的网络。该网络我们没有定义,为什么会创建呢?要知道每个服务都要连接到网络的,我们的visualizer服务没有指定具体的网络。于是Docker就创建了这样的一个网络,并且把visualizer连接到该网络。
我们可以通过 docker stack ls 和docker stack ps 去查看 stack的更多信息。
## docker stack ls
[pangcm@docker01 atsea-sample-shop-app]$ docker stack ls
NAME SERVICES ORCHESTRATOR
seastack 5 Swarm
## docker stack ps
[pangcm@docker01 atsea-sample-shop-app]$ docker stack ps seastack
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
aioybx1kz4vv seastack_appserver.1 dockersamples/atsea_app:latest docker03 Running Preparing 10 hours ago
i5k69i7gekyc seastack_database.1 dockersamples/atsea_db:latest docker02 Running Running 10 hours ago
8ulzn7q95axq seastack_appserver.1 dockersamples/atsea_app:latest docker02 Shutdown Failed 10 hours ago "task: n
...
可以看到 appserver 在docker02节点启动失败过,通过docker stack ps 可以看到Stack中每个服务的概况,包括服务副本所在的节点、当前状态、期望状态和异常细节。
如果要查看具体某个服务的详细信息,可以使用 docker service logs 命令。
在服务都起来之后,我们使用浏览器访问 https://xxxx 即可访问这个应用的首页。
14.2.4 管理应用
Stack是一组相关联的服务和基础设施,需要进行统一的部署和管理。这意味着Stack是由普通的Docker资源构建而来:网络、卷、密钥、服务等。我们可以通过普通的Docker命令对其进行查看和重新配置,如:docker netwoerk、docker volume、docker secret、docker service等。
在这样的一个前提下,通过docker service 命令来管理 Stack中的某个服务是可行的。一个简单的例子是通过 docker service scale 命令来扩充appserver的服务副本数,但我们不推荐这么操作。
推荐的方式是通过声明式方式修改,即将Stack文件作为配置的唯一声明。这样,所有Stack相关的改动都需要体现在Stack文件中,然后重新部署应用所需的Stack文件。
举例,我们把appserver的副本数目从2修改为3,把visuaizer服务的优雅停止时间增加到2分钟,我们修改stack文件。
appserver:
deploy:
replicas: 3
update_config:
parallelism: 2
failure_action: rollback
visualizer:
ports:
- "8001:8080"
stop_grace_period: 2m
然后重新部署
docker stack deploy -c docker-stack.yml seastack
如果要删除某个 Stack,使用 docker stack rm 命令。一定要谨慎,删除Stack不会进行二次确认。该命令会把网络和服务都删除,但是密钥不会被删除,原因是密钥是我们在部署前就创建并存在的。当然,卷也是不会被删除的。
14.3 使用Docker Stack 部署应用——命令
docker stack deploy 用于根据 Stack 文件部署和更新Stack服务的命令。
docker stack ls 会列出 Swarm 集群中全部的Stack,包括每个Stack拥有多少服务。
docker stack ps 列出某个已经部署的Stack相关详情。该命令支持Stack名称作为其主要参数,列举了服务副本在节点的分布情况,以及期望状态和当前状态。
docker stack rm 命令用于从Swarm集群中移除Stack。移除操作执行前并不会进行二次确认。
14.4 本章小结
Stack是Docker原生的部署和管理多服务应用的解决方案。Stack默认集成在Docker引擎中,并且提供了简单的声明式接口对应用进行部署和全生命周期管理。
在本章开始提供了应用代码以及一些基础设施需求,比如网络、端口、卷和密钥。接下来的内容完成了应用的容器化,并且将全部应用服务和基础设施需求集成到一个声明式的Stack文件当中。在Stack文件中设置了服务副本数、滚动升级以及重启策略。然后通过 docker stack deploy 命令基于Stack文件完成了应用的部署。
对于已部署应用的更新操作,应该通过修改Stack文件完成。首先需要从源码管理系统中检出Stack文件,更新该文件,然后重新部署应用,最后将改动后的Stack文件重新提交到源码控制系统中。
因为Stack文件中定义了像服务副本数这样的内容,所以读者需要自己维护多个Stack文件以用于不同的环境,比如dev、test以及prod。