• 欢迎访问蜷缩的蜗牛博客 蜷缩的蜗牛
  • 微信搜索: 蜷缩的蜗牛 | 联系站长 kbsonlong@qq.com
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

【转载】利用Docker开启持续交付之路

Docker 蜷缩的蜗牛 1年前 (2017-04-17) 29次浏览 已收录 0个评论

持续交付即 Continuous Delivery,简称 CD,随着 DevOps 的流行正越来越被传统企业所重视。持续交付讲求以短周期、小细粒度,自动化的方式频繁的交付软件,在这个过 程中要求开发、测试、用户体验等角色紧密合作,快速收集反馈,从而不断改善软件质量并减少浪费。然而,在我所接触的传统企业中,对于持续交付实践的实施都 还非常初级,坦白说,大部分还停留的手工生成发布包,手工替换文件进行部署的阶段,这样做无疑缺乏管理且容易出错。如果究其原因,我想主要是因为构建一个 可实际运行且适合企业自身环境的持续发布流程并不简单。然而,Docker 作为轻量级的基于容器的解决方案,它对系统侵入性低,容易移植,天生就适合做自 动化部署,这些特性非常有助于降低构建持续交付流程的复杂度。本文将通过一个实际案例分享我们在一个真实项目中就如何使用 Docker 构建持续发布流程的 经验总结,这些实践也许不是最先进的,但确是非常实际和符合当时环境的。

项目背景

我们的客户来自物流行业,由于近几年业务的飞速发展,其老的门户网站对于日常访问和订单查询还勉强可以支撑,但每当遇到像双十一这样访问量成倍增长的情况就很难招架了。因此,客户希望我们帮助他们开发一个全新的门户网站。

新网站采用了动静分离的策略,使用 Java 语言,基于 REST 架构,并结合 CMS 系统。简单来说,可以把它看成是时下非常典型的一个基于 Java 的 Web 应用,它具体包含如下几个部分:

  • 基于 Jersey 的动态服务(处理客户端的动态请求)
  • 二次开发的 OpenCMS 系统,用于静态导出站点
  • 基于 js 的前端应用并可以打包成为一个 OpenCMS 支持的站点
  • 后台任务处理服务(用于处理实时性要求不高的任务,如:邮件发送等)

以下是系统的逻辑软件架构图:

利用 Docker 开启持续交付之路

面临的挑战以及为什么选择 Docker

在设计持续交付流程的过程中,客户有一个非常合理的需求:是否可以在测试环境中尽量模拟真实软件架构(例如:模拟静态服务器的水平扩展),以便尽早 发现潜在问题?基于这个需求,可以尝试将多台机器划分不同的职责并将相应服务按照职责进行部署。然而,我们遇到的第一个挑战是:硬件资源严重不足尽 管客户非常积极的配合,但无奈于企业内部层层的审批制度。经过两个星期的努力,我们很艰难的申请到了两台四核 CPU 加 8G 内存的物理机(如果申请虚拟机可 能还要等一段时间),同时还获得了一个 Oracle 数据库实例。因此,最终我们的任务就变为把所有服务外加持续集成服务器(Jenkins)全部部署在这 两台机器上,并且,还要模拟出这些服务真的像是分别运行在不同职责的机器上并进行交互。如果采用传统的部署方式,要在两台机器上完成这么多服务的部署是非 常困难的,需要小心的调整和修改各个服务以及中间件的配置,而且还面临着一旦出错就有可能耗费大量时间排错甚至需要重装系统的风险。第二个挑战是:企业内 部对 UAT(与产品环境配置一致,只是数据不同)和产品环境管控严格,我们无法访问,也就无法自动化。这就意味着,整个持续发布流程不仅要支持自动化部 署,同时也要允许下载独立发布包进行手工部署。

最终,我们选择了 Docker 解决上述两个挑战,主要原因如下:

  • Docker 是容器,容器和容器之间相互隔离互不影响,利用这个特性就可以非常容易在一台机器上模拟出多台机器的效果
  • Docker 对操作系统的侵入性很低,因其使用 LXC 虚拟化技术(Linux 内核从 2.6.24 开始支持),所以在大部分 Linux 发行版下不需要安装额外的软件就可运行。那么,安装一台机器也就变为安装 Linux 操作系统并安装 Docker,接着它就可以服役了
  • Docker 容器可重复运,且 Docker 本身提供了多种途径分享容器,例如:通过 export/import 或者 save/load 命令以文件的形式分享,也可以通过将容器提交至私有 Registry 进行分享,另外,别忘了还有 Docker Hub

下图是我们利用 Docker 设计的持续发布流程:

利用 Docker 开启持续交付之路

图中,我们专门设计了一个环节用于生成唯一发布包,它打包所有 War/Jar、数据库迁移脚本以及配置信息。因此,无论是手工部署还是利用 Docker 容器自动化部署,我们都使用相同的发布包,这样做也满足持续交付的单一制品原则(Single Source Of Truth,Single Artifact)。

Docker 与持续集成

持续集成(以下简称 CI)可以说是当前软件开发的标准配置,重复使用率极高。而将 CI 与 Docker 结合后,会为 CI 的灵活性带来显著的提升。由于我们项目中使用 Jenkins,下面会以 Jenkins 与 Dcoker 结合为例进行说明。

1.创建 Jenkins 容器

相比于直接把 Jenkins 安装到主机上,我们选择把它做为 Docker 容器单独使用,这样就省去了每次安装 Jenkins 本身及其依赖的过程,真正做到了拿来就可以使用。

Jenkins 容器使创建一个全新的 CI 变的非常简单,只需一行命令就可完成:

docker run -d -p 9090:8080 ——name jenkins jenkins:1.576

该命令启动 Jenkins 容器并将容器内部 8080 端口重定向到主机 9090 端口,此时访问:主机 IP:9090,就可以得到一个正在运行的 Jenkins 服务了。

为了降低升级和维护的成本,可将构建 Jenkins 容器的所有操作写入 Dockerfile 并用版本工具管理,如若需要升级 Jenkins,只要重新 build 一次 Dockerfile:

FROM ubuntu
ADD sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y -q wget
RUN wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | apt-key add -
ADD jenkins.list /etc/apt/sources.list.d/
RUN apt-get update
RUN apt-get install -y -q jenkins
ENV JENKINS_HOME /var/lib/jenkins/
EXPOSE 8080
CMD ["java", "-jar", "/usr/share/jenkins/jenkins.war"]

每次 build 时标注一个新的 tag:

docker build -t jenkins:1.578 —rm .

另外,建议使用 Docker volume 功能将外部目录挂载到 JENKINS_HOME 目录(Jenkins 会将安装的插件等文件存放在这个目录),这样保证了升级 Jenkins 容 器后已安装的插件都还存在。例如:将主机/usr/local/jenkins/home 目录挂载到容器内部/var/lib/jenkins:

docker run -d -p 9090:8080 -v /usr/local/jenkins/home:/var/lib/jenkins ——name jenkins jenkins:1.578

2. 使用 Docker 容器作为 Jenkins 容器的 Slave

在使用 Jenkins 容器时,我们有一个原则:不要在容器内部存放任何和项目相关的数据。因为运行中的容器不一定是稳定的,而 Docker 本身也可能有 Bug,如果把项目数据存放在容器中,一旦出了问题,就有丢掉所有数据的风险。因此,我们建议 Jenkins 容器仅负责提供 Jenkins 服务而不负责构建,而是把构建工作代理给其他 Docker 容器做。

例如,为了构建 Java 项目,需要创建一个包含 JDK 及其构建工具的容器。依然使用 Dockerfile 构建该容器,以下是示例代码(可根据项目实际需要安装其他工具,比如:Gradle 等):

FROM ubuntu
RUN apt-get update && apt-get install -y -q openssh-server openjdk-7-jdk
RUN mkdir -p /var/run/sshd
RUN echo 'root:change' |chpasswd
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

在这里安装 openssh-server 的原因是 Jenkins 需要使用 ssh 的方式访问和操作 Slave,因此,ssh 应作为每一个 Slave 必须安装的服务。运行该容器:

docker run -d -P —name java java:1.7

其中,-P 是让 Docker 为容器内部的 22 端口自动分配重定向到主机的端口,这时如果执行命令:

docker ps
804b1d9e4202       java:1.7           /usr/sbin/sshd -D     6 minutes ago       Up 6 minutes       0.0.0.0:49153->22/tcp   java

端口 22 被重定向到了 49153 端口。这样,Jenkins 就可以通过 ssh 直接操作该容器了(在 Jenkins 的 Manage Nodes 中配置该 Slave)。

有了包含构建 Java 项目的 Slave 容器后,我们依然要遵循容器中不能存放项目相关数据的原则。此时,又需要借助 volume:

docker run -d -v /usr/local/jenkins/workspace:/usr/local/jenkins -P —name java java:1.7

这样,我们在 Jenkins Slave 中配置的 Job、Workspace 以及下载的源码都会被放置到主机目录/usr/local/jenkins/workspace 下,最终达成了不在容器中放置任何项目数据的目标。

通过上面的实践,我们成功的将一个 Docker 容器配置成了 Jenkins 的 Slave。相比直接将 Jenkins 安装到主机上的方式,Jenkins 容器的解决方案带来了明显的好处:

  • 重用更加简单,只需一行命令就可获得 CI 的服务;
  • 升级和维护也变的容易,只需要重新构建 Jenkins 容器即可;
  • 灵活配置 Slave 的能力,并可根据企业内部需要预先定制具有不同能力的 Slave,比如:可以创建出具有构建 Ruby On Rails 能力的 Slave,可以创建出具有构建 NodeJS 能力的 Slave。当 Jenkisn 需要具备某种能力的 Slave 时,只需要 docker run 将该容器启动,并配置为 Slave,Jenkins 就立刻拥有了构建该应用的能力。

如果一个组织内部项目繁多且技术栈复杂,那么采用 Jenkins 结合 Docker 的方案会简化很多配置工作,同时也带来了相率的提升。

Docker 与自动化部署

说到自动化部署,通常不仅仅代表以自动化的方式把某个应用放置在它应该在的位置,这只是基本功能,除此之外它还有更为重要的意义:

  • 以快速且低成本的部署方式验证应用是否在目标环境中可运行(通常有 TEST/UAT/PROD 等环境);
  • 以不同的自动化部署策略满足业务需求(例如:蓝绿部署);
  • 降低了运维的成本并促使开发和运维人员以端到端的方式思考软件开发(DevOps)。

在我们的案例中,由于上述挑战二的存在,导致无法将 UAT 乃至产品环境的部署全部自动化。回想客户希望验证软件架构的需求,我们的策略是:尽量使测试环境靠近产品环境。

  1. 标准化 Docker 镜像

很多企业内部都存在一套叫做标准化的规范,在这套规范中定义了开发中所使用的语言、工具的版本信息等等,这样做可以统一开发环境并降低运维团队负担。在我们的项目上,依据客户提供的标准化规范,我们创建了一系列容器并把它们按照不同的职能进行了分组,如下图:

利用 Docker 开启持续交付之路

图中,我们把 Docker 镜像分为三层:基础镜像层、服务镜像层以及应用镜像层,下层镜像的构建依赖上层镜像,越靠上层的镜像越稳定越不容易变。

基础镜像层

  • 负责配置最基本的、所有镜像都需要的软件及服务,例如上文提到的 openssh-server

服务镜像层

  • 负责构建符合企业标准化规范的镜像,这一层很像 SaaS

应用镜像层

  • 和应用程序直接相关,CI 的产出物

分层后, 由于上层镜像已经提供了应用所需要的全部软件和服务,因此可以显著加快应用层镜像构建的速度。曾经有人担心如果在 CI 中构建镜像会不会太慢?经过这样的分层就可以解决这个问题。

在 Dockerfile 中使用 FROM 命令可以帮助构建分层镜像。例如:依据标准化规范,客户的产品环境运行 RHEL6.3,因此在测试环境中,我 们选择了 centos6.3 来作为所有镜像的基础操作系统。这里给出从构建 base 镜像到 Java 镜像的方法。首先是定义 base 镜像的 Dockerfile:

FROM centos
# 可以在这里定义使用企业内部自己的源
RUN yum install -y -q unzip openssh-server
RUN ssh-keygen -q -N "" -t dsa -f /etc/ssh/ssh_host_dsa_key && ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key
RUN echo 'root:changeme' | chpasswd
RUN sed -i "s/#UsePrivilegeSeparation.*/UsePrivilegeSeparation no/g" /etc/ssh/sshd_config \
&& sed -i "s/UsePAM.*/UsePAM no/g" /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

接着,构建服务层基础镜像 Java,依据客户的标准化规范,Java 的版本为:jdk-6u38-linux-x64:

FROM base
ADD jdk-6u38-linux-x64-rpm.bin /var/local/
RUN chmod +x /var/local/jdk-6u38-linux-x64-rpm.bin
RUN yes | /var/local/jdk-6u38-linux-x64-rpm.bin &>/dev/null
ENV JAVA_HOME /usr/java/jdk1.6.0_38
RUN rm -rf var/local/*.bin
CMD ["/usr/sbin/sshd", "-D"]

如果再需要构建 JBoss 镜像,就只需要将 JBoss 安装到 Java 镜像即可:

FROM java
ADD jboss-4.3-201307.zip /app/
RUN unzip /app/jboss-4.3-201307.zip -d /app/ &>/dev/null && rm -rf /app/jboss-4.3-201307.zip
ENV JBOSS_HOME /app/jboss/jboss-as
EXPOSE 8080
CMD ["/app/jboss/jboss-as/bin/run.sh", "-b", "0.0.0.0"]

这样,所有使用 JBoss 的应用程序都保证了使用与标准化规范定义一致的 Java 版本以及 JBoss 版本,从而使测试环境靠近了产品环境。

  1. 更好的组织自动化发布脚本

为了更好的组织自动化发布脚本,版本化控制是必须的。我们在项目中单独创建了一个目录:deploy,在这个目录下存放所有与发布相关的文件,包括:用于自动化发布的脚本(shell),用于构建镜像的 Dockerfile,与环境相关的配置文件等等,其目录结构是:

├── README.md
├── artifacts   # war/jar,数据库迁移脚本等
├── bin         # shell 脚本,用于自动化构建镜像和部署
├── images       # 所有镜像的 Dockerfile
├── regions     # 环境相关的配置信息,我们只包含本地环境及测试环境
└── roles       # 角色化部署脚本,会本 bin 中脚本调用

这样,当需要向某一台机器上安装 java 和 jboss 镜像时,只需要这样一条命令:

bin/install.sh images -p 10.1.2.15 java jboss

而在部署的过程中,我们采用了角色化部署的方式,在 roles 目录下,它是这样的:

├── nginx
│   └── deploy.sh
├── opencms
│   └── deploy.sh
├── service-backend
│   └── deploy.sh
├── service-web
│   └── deploy.sh
└── utils.sh

这里我们定义了四种角色:nginx,opencms,service-backend 以及 service-web。每个角色下都有自己的发布脚本。例如:当需要发布 service-web 时,可以执行命令:

bin/deploy.sh -e test -p 10.1.2.15 service-web

该脚本会加载由-e 指定的 test 环境的配置信息,并将 service-web 部署至 IP 地址为 10.1.2.15 的机器上,而最终,bin/deploy.sh 会调用每个角色下的 deploy.sh 脚本。

角色化后,使部署变的更为清晰明了,而每个角色单独的 deploy 脚本更有利于划分责任避免和其他角色的干扰。

  1. 构建本地虚拟化环境

通常在聊到自动化部署脚本时,大家都乐于说这些脚本如何简化工作增加效率,但是,其编写过程通常都是痛苦和耗时,需要把脚本放在相应的环境中反复执 行来验证是否工作正常。这就是我为什么建议最好首先构建一个本地虚拟化环境,有了它,就可以在自己的机器上反复测试而不受网络和环境的影响。

Vagrant(http://www.vagrantup.com/)是很好的本地虚拟化工具,和 Docker 结合可以很容易的在本地搭建起与测试环境几乎相同的环境。以我们的项目为例,可以使用 Vagrant 模拟两台机器,以下是 Vagrantfile 示例:

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.define "server1", primary: true do |server1|
server1.vm.box = "raring-docker"
server1.vm.network :private_network, ip: "10.1.2.15"
end
config.vm.define "server2" do |server2|
server2.vm.box = "raring-docker"
server2.vm.network :private_network, ip: "10.1.2.16"
end
end

由于部署脚本通常采用 SSH 当方式连接,所以,完全可以把这两台虚拟机看做是网络中两台机器,调用部署脚本验证是否正确。限于篇幅,这里就不多说了。

4 构建企业内部的 Docker Registry

上文提到了诸多分层镜像,如何管理这些镜像?如何更好的分享?答案就是使用 Docker Registry。Docker Registry 是一个镜像仓库,它允许你向 Registry 中提交(push)镜像同时又可以从中下载(pull)。

构建本地的 Registry 非常简单,执行下面的命令:

docker run -p 5000:5000 registry

更多关于如何使用 Registry 可以参考地址:https://github.com/docker/docker-registry

当搭建好 Registry 后,就可以向它 push 你的镜像了,例如:需要将 base 镜像提交至 Registry:

docker push your_registry_ip:5000/base:centos

而提交 Java 和 JBoss 也相似:

docker push your_registry_ip:5000/java:1.6
docker push your_registry_ip:5000/jboss:4.3

使用下面的方式下载镜像:

docker pull your_registry_ip:5000/jboss:4.3

总结

本文总结我们在实际案例中使用 Docker 一些实践,它给我们的印象就是非常灵活,几乎是一个多面手,给整个流程带来了极大的灵活性和扩展性,并且也展现了极好的性能,符合它天生就为部署而生的特质。

来自:http://insights.thoughtworkers.org/start-continuous-delivery-with-docker/


蜷缩的蜗牛 , 版权所有丨如未注明 , 均为原创丨 转载请注明【转载】利用 Docker 开启持续交付之路
喜欢 (0)
[]
分享 (0)

您必须 登录 才能发表评论!