转载

使用Jenkins,Docker和Ansible进行持续集成和交付

【编者的话】本文介绍了使用Docker,Jenkins等技术实现应用开发,测试到部署的自动化。它是一种探索。重点在于流程中的代码检测、测试、部署。部署后要做的事情没有涉及。会在后面文章中介绍。

这篇文章努力提供一个持续集成、交付/部署工作流的可行方式。我会用 Jenkins , Docker , AnsibleVagrant 来建立两个服务器。一个作为Jenkins的服务器,另一个用来模拟生产环境。前者用来检查代码、测试和构建应用程序同时用来部署和部署后的测试。

你需要预先安装Vagrant和Git。剩下的工具会在本文的测试练习中下载。

CI/CD环境

我们用Vagrant和Ansible建立Jenkins环境。Vagrant会新建一个Ubuntu虚拟机然后运行 bootstrap.sh 脚本。脚本的唯一目的是安装Ansible。一旦安装好,Ansible能够确保下载Docker和运行Jenkins进程。

Jenkins会被打包在一个Docker容器中,并被Ansible部署。可以查看 Continuous Deployment: Implementation with Ansible and Docker 获取更多信息。

如果你不想自己实践,可以克隆这个GitHub仓库 jenkins-docker-ansible 。一旦下载好仓库,我们就可以用Vagrant启动 cd 虚拟机了。

language
git clone https://github.com/vfarcic/jenkins-docker-ansible.git
cd jenkins-docker-ansible
vagrant up cd

第一次在电脑上运行这条命令可能会花一些时间,所以我们可以利用vagrang创建、配置虚拟机的时间来看下面的步骤。

Vagrantfile 中有两行非常关键:

language
cd.vm.provision "shell", path: "bootstrap.sh"
cd.vm.provision :shell, inline: 'ansible-playbook /vagrant/ansible/cd.yml -c local'

首先运行 bootstrap.sh 脚本安装Ansible。我们可以使用 ANSIBLE PROVISIONER ,但是这需要我们在自己的主机上也安装Ansible。我觉得并没有必要,尤其是使用Windows的用户,在windows上安装配置Ansible一点儿也不简单。此外,我们我们需要在虚拟机中安装Ansible以完成从 cd 部署应用到 prod

【译者注:】cd和prod是本文中启动的两个虚拟机的名字。顾名思义,cd指持续部署VM,prod指生产环境VM。

bootstrap.sh执行结束后,Ansible剧本 cd.yml 开始运行:

language
- hosts: localhost
remote_user: vagrant
sudo: yes
roles:
- java
- docker
- registry
- jenkins

Ansible会运行java,docker,jenkins,registery role。Jenkins需要Java来运行slaves。Docker用来构建和运行容器。剩下的用Docker进程来运行。这时就不需要直接下载依赖、包或者其它应用。Registry role会运行Docker Registry。

这里是Jenkins role的任务列表:

- name: Directories are present
file: path="{{ item }}" state=directory
with_items: directories
  • name: Config files are present
    copy: src='{{ item }}' dest='{{ jenkins_directory }}/{{ item }}'
    with_items: configs
  • name: Plugins are present
    get_url: url='https://updates.jenkins-ci.org/{{ item }}' dest='{{ jenkins_directory }}/plugins'
    with_items: plugins
  • name: Build job directories are present
    file: path='{{ jenkins_directory }}/jobs/{{ item }}' state=directory
    with_items: jobs
  • name: Build jobs are present
    template: src=build.xml.j2 dest='{{ jenkins_directory }}/jobs/{{ item }}/config.xml' backup=yes
    with_items: jobs
  • name: Deployment job directories are present
    file: path='{{ jenkins_directory }}/jobs/{{ item }}-deployment' state=directory
    with_items: jobs
  • name: Deployment jobs are present
    template: src=deployment.xml.j2 dest='{{ jenkins_directory }}/jobs/{{ item }}-deployment/config.xml' backup=yes
    with_items: jobs
  • name: Container is running
    docker: name=jenkins image=vfarcic/jenkins ports=8080:8080 volumes=/data/jenkins:/jenkins
  • name: Reload
    uri: url= http://localhost:8080/reload method=POST status_code=302
    ignore_errors: yes
    }}}

首先我们创建存放Jenkins插件和roles的目录。为了加快构建需要的容器,我们还在主机上创建了存放ivy文件(SBT可能需要用)的目录。这样每次构建Docker容器时不需要重复下载依赖了。

创建好目录后我们会复制Jenkins的文件和插件。

下一步是Jenkins的jobs。因为所有的jobs都会做相同的工作,所以我们根据需要使用两个模板(`build.xml.j2`和`deployment.xml.j2`)来创建jobs。

最后,一旦job的配置文件传到Jenkins服务器里,我们就能确认Jenkins容器启动并且正确运行了。

所有的Ansible和Jenkins源代码都可以在[jenkins-docker-ansible](https://github.com/vfarcic/jenkins-docker-ansible/tree/master/ansible/roles/jenkins)找到。

下面是`build.xml.j2`模板中的关键部分:

{{{

sudo docker build -t 192.168.50.91:5000/{{ item }}-tests docker/tests/

sudo docker push 192.168.50.91:5000/{{ item }}-tests

sudo docker run -t --rm

-v $PWD:/source

-v /data/.ivy2:/root/.ivy2/cache

192.168.50.91:5000/{{ item }}-tests

sudo docker build -t 192.168.50.91:5000/{{ item }} .

sudo docker push 192.168.50.91:5000/{{ item }}

上面所有的 {{ item }} 都会被Ansible中的变量值代替。因为所有的构建job都执行相同的流程,对于所有的job我们可以使用相同的模板以及提供简单的变量值就够了。在这篇文章中, main.yml 中的变量值如下:

language
jobs:
- books-service

Ansible运行时,每个** {{ item }} 会被替换为 books-service jobs**中的变量对应我们需要的item值。jobs中的变量不需要一次性匹配添加完但要根据需要逐步添加。

接着我们会看到下面这样:

language
jobs:
- books-service
- authentication-service
- shopping-cart-service
- books-ui

开始用Ansible部署时,来自模板的执行命令如下:

sudo docker build -t 192.168.50.91:5000/books-service-tests docker/tests/
sudo docker push 192.168.50.91:5000/books-service-tests
sudo docker run -t --rm
-v $PWD:/source
-v /data/.ivy2:/root/.ivy2/cache
localhost:5000/books-service-tests
sudo docker build -t 192.168.50.91:5000/books-service .
sudo docker push 192.168.50.91:5000/books-service

首先我们构建测试容器并push到私有registry中。然后运行测试。如果没有错误,我们会构建 books-service 容器,push到私有registry中。从这里开始, books-service已经被测试、构建结束了,准备被部署。

Docker出现之前,我所有的Jenkins服务构建到最后留下一堆jobs。因为使用大量不同的框架、语言和库,所以大部分jobs都不一样。管理大量的jobs很累人而且容易出错,这就不仅仅是复杂的问题了。管理slaves和依赖同样需要大量的时间。

Docker使问题简单很多。如果我们能保证每个项目有它自己的测试和应用容器,那所有的jobs就能做同样的事情了:构建测试容器并运行,若没有错误就构建应用容器并push到私有仓库。最后,我们只需要部署它。如果每个项目有它们自己的Dockerfile,那所有项目的构建流程都类似。另一个优点是-因为有了Docker我们不需要在服务器上安装任何东西,我们唯一需要的就是能运行容器的Docker。

jobs的构建和从Dockerfile构建容器相似,部署应用就与此不同,它更复杂一点。虽然应用不可变并且被封装在容器里,但是仍然有一些环境变量、连接 和/或 数据卷需要设置。这里就是Ansible施展拳脚的地方。我们可以使Jenkins的部署job相同但是它们的Ansible playbook名称不同。【译者注:这句个人理解不是太准确,贴出原句: We can have every Jenkins deployment job the same with only name of the Ansible playbook differing 】。这样执行部署的jobs很容易运行部署应用的Ansible role了。这在大多数情况下都很简单。如果不使用Docker部署的话,两者的差异是巨大的。在使用Docker时我们只需要考虑数据(应用和依赖都被打包在容器里里了),没有Docker我们要考虑下载什么、更新什么以及这些变化会对服务器或虚拟机里的其它应用带来哪些影响。这也是企业不愿意更新技术栈的原因之一,例如,仍然使用java 5(或者更低)。

作为例子,下面是Ansible中 books-service 中列出的例子:

- name: Directory is present
file:
path=/data/books-service/db
state=directory
  • name: Latest container is pulled
    shell: sudo docker pull 192.168.50.91:5000/books-service
  • name: Container is absent
    docker:
    image=192.168.50.91:5000/books-service
    name=books-service
    state=absent
  • name: Container is running
    docker:
    name=books-service
    image=192.168.50.91:5000/books-service
    ports=9001:8080
    volumes=/data/books-service/db:/data/db
    state=running

我们要确保存储数据的目录存在,拉取最新的容器,移除运行中的进程启动新的。

让我们回来看文章开始时创建的 cd 虚拟机。如果 vagrant up cd 命令执行结束,那整个VM中的Jenkins,Docker,和Registry都启动并运行起来了。

现在我们可以打开 http://localhost:8080 使用Jenkins了。Ansible的tasks没有创建的凭证,我们需要手工创建。

- 点击 Manage Jenkins > Manage Nodes > CD > Configure

- 点击 Credentials 部分的 Add 按钮

- 输入 vagrant 作为用户名和密码,点击 Add 按钮

- 选中 Credentials 部分新创建的key

- 点击 SaveLaunch slave agent

这些步骤本可以自动完成,但是安全起见我更喜欢手动配置。

现在启动了CD slave,它指向我们用Vagrant创建的cd虚拟机,并提供给所有的jobs使用(即使部署的job在另一台机器里执行)。

现在准备运行 books-service job。在Jenkins的主页,点击 books-service job ,就启动了第一次构建(也可以点击 Build now 手动构建),可以在 Build History 模块查看构建过程, Console Output 可以查看日志。第一次构建Docker容器可能会花一些时间。一旦job完成就会运行 books-service-deployment job,但是我们仍然没有生产环境的VM而且Jenkins job运行的Ansible playbook也可能连不上生产环境的VM。一会儿我们再来考虑这个。现在我们将要做的是检测代码、运行测试、构建容器并push到私有registry中。

这种设置的主要优点是除了 cd 上的Docker外不需要再额外下载任何东西,因为所有一切都在容器里搞定了。我们就没必要为了下载提供编写和测试的各种框架、库而头疼了。也不会有不同版本应用间的依赖冲突了。最后,Jenkins的jobs也变得很简单,因为用于应用测试、构建、部署的逻辑全部放在了Docker文件里。换句话说,不管Jenkins要管理多少项目或应用,整个流程维护起来都很简单、一点儿不痛苦。

如果我们约定命名规范(比如本文中的例子),创建新的job就更简单了。要做的就是在Ansible配置文件 ansible/roles/jenkins/defaults/main.yml 中添加新的变量,运行 vagrant provision cd **或者直接在CD VM中运行 ansible-playbook /vagrant/ansible/cd.yml -c local**。

下面展示了如何将改变应用到CD服务器中(包括添加新的Jenkins Job):

[在主机的克隆仓库目录下执行]

language
vagrant provision cd

或者

language
vagrant ssh cd
ansible-playbook /vagrant/ansible/cd.yml -c local
exit

** books-service**被安排每隔5分钟从仓库更新代码。这很耗资源且运行很慢。更好的设置是使用GitHub hook。有了DitHub hook只有每次push代码到仓库时才会触发构建。更多信息参见 GitHub Plugin 。类似的设置可以应用到任何其它类型的代码仓库。

生产环境

为了更贴近与真实情形,生产环境会另起一个虚拟机。目前还不需要在上面安装任何软件。随后Jenkins会运行Ansible,Ansible要确保服务器正确启动来部署每个应用。prod VM的创建方式和cd VM的相同。

[在克隆仓库目录执行命令]

language
vagrant up prod

cd 不同, prod 需要一个Ubuntu系统就够了,不需要包和额外的依赖。

现在我们启动并运行了 prod 环境,唯一剩下的是生成SSH Key并把它加到 cd 里。

[在克隆仓库目录执行命令]

language
vagrant ssh prod
ssh-keygen # Simply press enter to all questions
exit
vagrant ssh cd
ssh-keygen # Simply press enter to all questions
ssh-copy-id 192.168.50.92 # Password is "vagrant"
exit

所有要做的都在这了。我们也启动了部署应用的生产环境了。现在回到Jenkins ( http://localhost:808 0)运行 books-service-deployment job,如果您到这一步时 books-service 还没执行结束,请耐心等待直到它结束, books-service-deployment 会自动开始。一切job都结束时,服务会被启动运行在9001端口。

现在我们添加一些信息到 books-service

[在克隆仓库目录执行命令]

vagrant ssh prod
curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 1, "title": "My First Book", "author": "John Doe", "description": "Not a very good book"}' http://localhost:9001/api/v1/books
curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 2, "title": "My Second Book", "author": "John Doe", "description": "Not a bad as the first book"}' http://localhost:9001/api/v1/books
curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 3, "title": "My Third Book", "author": "John Doe", "description": "Failed writers club"}' http://localhost:9001/api/v1/books
exit

然后看看服务是否返回了正确的数据。在浏览器中打开 http://localhost:9001/api/v1/books 网址。你会看到之前用 crul 命令添加的3本书的信息。

我们的服务已经部署并且启动运行了。每次修改代码时,都会重复相同的流程:Jenkins会克隆代码、运行测试、构建容器、推送到registry,最后在目标服务器运行容器。

虚拟机创建,配置,构建和部署花了很多时间。但是,从现在开始大部分事情(Docker镜像IVY依赖等)下载后,再次运行的时候会非常快(不需要再重复下载)。只要把新建的Docker镜像push到registry。从这一刻起,快速就是整个流程最重要的优势了。

总结

有了Docker我们可以探索构建、测试、部署应用新的途径。容器技术的优点之一是它很简单,因为它具有不变性和自举的特点。这就没有什么理由让服务器下载运行大量依赖的包了。也不再需要做那些该死的维护不同版本应用或者是新建一个虚拟机来测试部署应用了。

Dokcer不仅让服务器配置变得简单。为每个配置提供Docker文件也意味着Jenkins job更易于维护。不再需要成百上千个jobs了并且每个job对应应用测试部署的文件都不同,有了Docker我们很简单就能让所有jobs都一样。用Dockerfile构建、测试,最后用Ansible部署Docker容器。(或者区其它工具比如 Fig )

我们没有涉及到项目部署后的测试(功能测试、集成测试、压测等),这一步对成功的持续交付或部署是必须的。我们也漏掉了部署 零宕机 应用的方式。我们会在下一篇文章中的项目给出方法。(另一篇文章中)我们会在这次结束的地方开始,并且更深入地探索应用部署后要做的事情。

文章中涉及的源代码在这里 jenkins-docker-ansible

原文链接: Continuous Integration, Delivery or Deployment with Jenkins, Docker and Ansible (翻译:adolphlwq)

==========================================

译者介绍 adolphlwq -南京信息工程大学本科大四学生,对Docker充满兴趣,喜欢运动。现在正努力充电希望快速进步。

正文到此结束
Loading...