Python 持续继承和交付教程(二)
八、自动化部署的虚拟平台
在接下来的章节中,我们将探索工具 Go continuous delivery (GoCD)以及如何用它来打包、分发和部署软件。如果你想继续下去,用这些章节中描述的东西做实验,你将需要一些你能在上面做的机器。
如果您没有公共云或私有云来运行虚拟机(VM)并尝试这些示例,您可以使用本章介绍的工具在您的笔记本电脑或工作站上创建一个 VM 游乐场。即使您可以访问云解决方案,您也可能想要使用这里提供的一些脚本来设置和配置这些机器。
8.1 要求和资源使用
我们想在虚拟操场上做的事情是
-
构建 Debian 包。
-
将它们上传到本地的 Debian 仓库。
-
在一台或多台服务器上安装软件包。
-
运行一些精简和简单的 web 服务。
-
使用 Ansible 运行部署和配置脚本。
-
通过 GoCD 服务器和代理控制一切。
除了最后一项任务,所有这些任务都需要很少的资源。GoCD 服务器需要最多的资源,最低 1GB 内存,建议 2GB 内存。Go 服务器也是一个保存持久状态(比如配置和管道历史)的系统,您通常不想丢失这些状态。
因此,最简单的方法,也是我在这里采用的方法,是在主机上安装 Go 服务器,这是我通常使用的笔记本电脑或工作站。
然后是一个运行 Go 代理的虚拟机,Debian 包将在其上构建。另外两个虚拟机作为目标机器,新构建的包将在其上安装和测试。其中一个用作测试环境,另一个用作生产环境。
对于这三个虚拟机,工具提供的默认半 GB 内存已经足够了。如果您使用这个平台,并且主机上没有足够的内存,您可以尝试将这些虚拟机的内存使用减半。对于目标机器来说,甚至 200MB 就足够启动了。
8.2 介绍流浪者
layer 是传统虚拟化解决方案的抽象层,如 KVM 和 VirtualBox。它为您的虚拟机提供基础映像(称为盒),为您管理虚拟机,并为初始配置提供统一的 API。它还创建了一个虚拟专用网,允许主机与虚拟机通信,反之亦然。
要安装游民,可以从 www.vagrantup.com/downloads.html 下载安装程序,或者如果你用的是带包管理器的操作系统,比如 Debian 或者 RedHat,可以通过包管理器安装。在 Debian 和 Ubuntu 上,你可以用apt-get install vagrant来安装它(尽管不要用 2.0 系列,而是从流浪者的网站上安装 2.1 或更新的版本,如果通过软件包管理器只能获得 2.0 的话)。
你也要用同样的方法安装virtualbox,它充当了流浪汉的后端。安装了 After 后,运行以下命令:
$ vagrant plugin install vagrant-vbguest
这将安装一个浮动插件,该插件会自动在浮动框中安装访客工具,从而提高可配置性和可靠性。
要使用 vagger,您需要编写一个名为Vagrantfile的小 Ruby 脚本,它将一个或多个盒子实例化为 VM。您可以配置端口转发、专用或桥接网络,并在主机和来宾虚拟机之间共享目录。
vagrant命令行工具允许您使用vagrant up命令创建和配置虚拟机,使用vagrant ssh连接到虚拟机,使用vagrant status获取状态摘要,使用vagrant destroy再次停止和删除虚拟机。不带任何参数调用vagrant会给出可用选项的摘要。
你可能想知道为什么我们使用流浪者虚拟机而不是 Docker 容器。原因是 Docker 被优化为运行单个进程或进程组。但是对于我们的用例,我们必须运行至少三个进程:GoCD 代理和我们实际上想要在那里运行的应用;恰当地说,管理 Debian 仓库;和 HTTP 服务器,以允许对存储库的远程访问。
网络和流浪者设置
我们将使用地址从 172.28.128.1 到 172.28.128.254 的虚拟专用 IP 网络。当您将此范围内的一个或多个地址分配给虚拟机时,range 会自动将地址 172.28.128.1 分配给主机。
我已经将这几行添加到我的/etc/hosts文件中。这并不是绝对必要的,但它使与虚拟机的对话变得更加容易。
# Vagrant
172.28.128.1 go-server.local
172.28.128.3 testing.local
172.28.128.4 production.local
172.28.128.5 go-agent.local
我还在我的∾/.ssh/config文件中添加了几行。
Host 172.28.128.* *.local
User root
StrictHostKeyChecking no
IdentityFile /dev/null
LogLevel ERROR
不要对生产机器进行此操作。这仅在单台机器上的虚拟网络中是安全的,通过它您可以确保没有攻击者存在,除非他们已经危害了您的机器。
创建和销毁虚拟机在流浪者之地很常见,每次你重新创建它们,它们都会有新的主机密钥。如果没有这样的配置,您将花费大量时间来更新 SSH 密钥指纹。
这里介绍的
Vagrantfile和 Ansible playbook 可以在 GitHub 上的deployment-utils资源库的playground文件夹中找到。为了跟上,你可以这样使用它:
$ git clone https://github.com/python-ci-cd/deployment-utils.git
$ cd deployment-utils/playground
$ vagrant up
$ ansible-playbook setup.yml
清单 8-1 显示了为虚拟操场创建盒子的流浪者文件。
Vagrant.configure(2) do |config|
config.vm.box = "debian/stretch"
{
'testing' => "172.28.128.3",
'production' => "172.28.128.4",
'go-agent' => "172.28.128.5",
}.each do |name, ip|
config.vm.define name do |instance|
instance.vm.network "private_network", ip: ip,
auto_config: false
instance.vm.hostname = name + '.local'
end
end
config.vm.provision "shell" do |s|
ssh_pub_key = File.readlines("#{Dir.home}/.ssh/id_rsa.pub")
.first.strip
s.inline = <<-SHELL
mkdir -p /root/.ssh
echo #{ssh_pub_key} >> /root/.ssh/authorized_keys
SHELL
end
end
Listing 8-1Vagrantfile for the Playground
这个Vagrantfile假设您有一个 SSH 密钥对,并且公钥位于您的主目录下的.ssh/id_rsa.pub路径中,这是 Linux 上 RSA SSH 密钥的默认位置。它使用流浪者的shell provisioner 将公钥添加到虚拟机内部根用户的authorized_keys文件中,这样您就可以通过 SSH 在客户机上登录。(流浪者提供了一个vagrant ssh命令用于连接,没有这个额外的步骤,但我发现直接使用系统ssh命令更容易,主要是因为它不依赖于当前工作目录中Vagrantfile的存在。)
在带有Vagrantfile的目录中,您可以运行
$ vagrant up
启动和配置三台虚拟机。第一次做的时候要花几分钟,因为流浪汉要先下载基盒。
如果一切顺利,您可以通过调用vagrant status来检查这三个虚拟机是否正在运行,如下所示:
$ vagrant status
Current machine states:
testing running (virtualbox)
production running (virtualbox)
go-agent running (virtualbox)
This environment represents multiple VMs. The VMs are all
listed above with their current state. For more information
about a specific VM, run `vagrant status NAME`.
(在基于 Debian 的 Linux 系统上)您应该能够看到新创建的私有网络。
$ ip route | grep vboxnet
172.28.128.0/24 dev vboxnet1 proto kernel scope link
src 172.28.128.1
现在,您可以使用主机名ssh root@go-agent.local和testing.local和production.local登录虚拟机。
8.3 配置机器
为了配置虚拟机,我们从一个小的ansible.cfg文件开始(清单 8-2 )。
[defaults]
host_key_checking = False
inventory = hosts
pipelining=True
Listing 8-2ansible.cfg: A Configuration File for the Playground
禁用主机密钥检查只能在开发系统的可信虚拟网络中进行,而不能在生产环境中进行。
虚拟机及其 IP 列在清单文件中(清单 8-3 )。
[all:vars]
ansible_ssh_user=root
[go-agent]
agent.local ansible_ssh_host=172.28.128.5
[aptly]
go-agent.local
[target]
testing.local ansible_ssh_host=172.28.128.3
production.local ansible_ssh_host=172.28.128.4
[testing]
testing.local
[production]
production.local
Listing 8-3hosts Inventory File for the Playground
接下来是剧本(清单 8-4 ),它完成了运行 GoCD 代理、适当的存储库以及从go-agent虚拟机到目标虚拟机的 SSH 访问所需的所有配置。
---
- hosts: go-agent
vars:
go_server: 172.28.128.1
tasks:
- group: name=go system=yes
- name: Make sure the go user has an SSH key
user: >
name=go system=yes group=go generate_ssh_key=yes
home=/var/go
- name: Fetch the ssh public key, so we can distribute it.
fetch:
src: /var/go/.ssh/id_rsa.pub
dest: go-rsa.pub
fail_on_missing: yes
flat: yes
- apt: >
package=apt-transport-https state=present
update_cache=yes
- apt_key:
url: https://download.gocd.org/GOCD-GPG-KEY.asc
state: present
validate_certs: no
- apt_repository:
repo: 'deb https://download.gocd.org /'
state: present
- apt: package={{item}} state=present force=yes
with_items:
- openjdk-8-jre-headless
- go-agent
- git
- file:
path: /var/lib/go-agent/config
state: directory
owner: go
group: go
- copy:
src: files/guid.txt
dest: /var/lib/go-agent/config/guid.txt
owner: go
group: go
- name: Go agent configuration for versions 16.8 and above
lineinfile:
dest: /etc/default/go-agent
regexp: ^GO_SERVER_URL=
line: GO_SERVER_URL=https://{{ go_server }}:8154/go
- service: name=go-agent enabled=yes state=started
- hosts: aptly
tasks:
- apt: package={{item}} state=present
with_items:
- ansible
- aptly
- build-essential
- curl
- devscripts
- dh-systemd
- dh-virtualenv
- gnupg2
- libjson-perl
- python-setuptools
- lighttpd
- rng-tools
- copy:
src: files/key-control-file-gpg2
dest: /var/go/key-control-file
- command: killall rngd
ignore_errors: yes
changed_when: False
- command: rngd -r /dev/urandom
changed_when: False
- command: gpg --gen-key --batch /var/go/key-control-file
args:
creates: /var/go/.gnupg/pubring.gpg
become_user: go
become: true
changed_when: False
- shell: gpg --export --armor > /var/go/pubring.asc
args:
creates: /var/go/pubring.asc
become_user: go
become: true
- fetch:
src: /var/go/pubring.asc
dest: deb-key.asc
fail_on_missing: yes
flat: yes
- name: Bootstrap aptly repos on the `target` machines
copy:
src: ../add-package
dest: /usr/local/bin/add-package
mode: 0755
- name: Download an example package to fill the repo with
get_url:
url: https://perlgeek.de/static/dummy.deb
dest: /tmp/dummy.deb
- command: >
/usr/local/bin/add-package {{item}}
stretch /tmp/dummy.deb
with_items:
- testing
- production
become_user: go
become: true
- user: name=www-data groups=go
- name: Configure lighttpd to serve the aptly directories
copy:
src: files/lighttpd.conf
dest: /etc/lighttpd/conf-enabled/30-aptly.conf
- service: name=lighttpd state=restarted enabled=yes
- hosts: target
tasks:
- authorized_key:
user: root
key: "{{ lookup('file', 'go-rsa.pub') }}"
- apt_key:
data: "{{ lookup('file', 'deb-key.asc') }}"
state: present
- hosts: production
tasks:
- apt_repository:
repo: >
deb http://172.28.128.5/debian/production/stretch
stretch main
state: present
- hosts: testing
tasks:
- apt_repository:
repo:
deb http://172.28.128.5/debian/testing/stretch
stretch main
state: present
- hosts: go-agent
tasks:
- name: 'Checking SSH connectivity to {{item}}'
become: True
become_user: go
command: >
ssh -o StrictHostkeyChecking=No
root@"{{ hostvars[item]['ansible_ssh_host'] }}" true
changed_when: false
with_items:
- testing.local
- production.local
Listing 8-4File setup.yml: An Ansible Playbook for Configuring the Three VMs
这做了很多事情。它
-
安装和配置 GoCD 代理
- 它将一个具有固定 UID 的文件复制到 Go 代理的配置目录中,这样当您拆除机器并重新创建它时,Go 服务器将把它识别为与以前相同的代理。
-
通过以下方式授予
go-agent机器上的go用户对目标主机的 SSH 访问权限-
首先确保 Go 用户有一个 SSH 密钥
-
将公共 SSH 密钥复制到主机
-
随后使用
authorized_key模块将其分发到目标机器
-
-
为用户
go创建一个 GPG 密钥对- 因为 GPG 密钥创建对随机数使用大量熵,而虚拟机通常没有那么多熵,所以它首先安装
rng-tools并使用它来说服系统使用较低质量的随机性。同样,这是你永远不应该在生产环境中做的事情。
- 因为 GPG 密钥创建对随机数使用大量熵,而虚拟机通常没有那么多熵,所以它首先安装
-
将所述 GPG 密钥对的公钥复制到主机,并使用
apt_key模块将其分发给目标机 -
通过以下方式在
go-agent机器上创建一些合适的基于 Debian 的仓库-
将
add-package脚本从同一个存储库复制到go-agent机器上 -
用一个虚拟包运行它来实际创建存储库
-
安装和配置 lighttpd 以通过 HTTP 提供这些包
-
配置目标机器使用这些存储库作为包源
-
-
检查
go-agent机器上的Go用户确实可以通过 SSH 访问其他虚拟机
在使用ansible-playbook setup.yml运行剧本之后,您有一个 GoCD 代理等待连接到服务器。下一章将介绍 GoCD 服务器的安装。安装 GoCD 服务器后,您必须在 web 配置中激活代理并分配适当的资源(debian-stretch、build和aptly,如果您遵循本书中的示例)。
8.4 总结
通过管理虚拟机和专用网络,帮助你建立一个虚拟的 CD 游乐场。我们已经看到了一个可行的剧本,它配置这些机器来提供在主机上运行 GoCD 服务器所需的所有基础设施。
九、持续交付中的构建
前面的章节已经展示了从源代码到部署的基本步骤的自动化:构建、发布和部署。现在缺少的是将它们结合在一起的粘合剂:轮询源代码存储库,将包从构建服务器发送到存储库服务器并通常控制流程,当一个步骤失败时中止管道实例,等等。
我们将用 ThoughtWorks 的 Go 连续交付 1 (GoCD 或 Go)作为胶水。
9.1 关于 Go 持续交付
GoCD 是一个用 Java 编写的开源项目,它的 web 接口组件是用 Ruby on Rails 编写的。它在 2010 年开始作为专有软件,并在 2014 年开源。
您可以下载适用于 Windows、OSX、Debian 和基于 RPM 的 Linux 发行版以及 Solaris 的 GoCD。ThoughtWorks 提供 GoCD 的商业支持。
它由一个服务器组件组成,该组件保存管道配置、轮询源代码存储库的更改、调度和分发工作、收集工件、提供一个 web 界面来可视化和控制所有这些,并提供一个手动批准步骤的机制。
一个或多个代理连接到服务器并执行构建管道中的实际任务。
管道组织
GoCD 执行的每个构建、部署或测试工作都必须是管道的一部分。流水线由一个或多个线性排列的级组成。在一个阶段中,一个或多个作业潜在地并行运行,并被单独分配给代理。任务在一个作业中连续执行。
在任务中,您可以依赖于同一作业中以前的任务生成的文件,而在作业和阶段之间,您必须显式地捕获它们,并在以后将其作为工件进行检索。更多信息请见下文。
最常见的任务是执行外部程序。其他任务包括检索工件或特定于语言的东西,比如运行 Ant 或 Rake 构建。 2
管道可以触发其他管道,让你形成一个非循环的、有向的管道图(图 9-1 )。
图 9-1
GoCD 管道可以形成一个图。管道由连续的阶段组成,在这些阶段中,几个作业可以并行运行。任务在一个作业中连续执行。
作业与代理的匹配
当代理空闲时,它轮询服务器的工作。如果服务器有作业要运行,它使用两个标准来决定代理是否适合执行作业:环境和资源。
每个作业都是管道的一部分,如果您选择使用环境,管道就是环境的一部分。另一方面,每个代理被配置为一个或多个环境的一部分。代理仅接受来自其环境之一的管道的作业。
资源是用户定义的标签,描述代理必须提供的内容,在管道配置中,您可以指定作业需要哪些资源。例如,如果您定义一个作业需要使用phantomjs资源来测试一个 web 应用,那么只有您分配了该资源的代理才会执行该作业。将操作系统和版本作为资源添加是一个好主意。在前面的例子中,代理可能拥有phantomjs、debian和debian-stretch资源,为作业的作者提供了指定所需操作系统的粒度选择。
关于环境的一句话
GoCD 使得在特定环境中运行代理成为可能。例如,可以在每台测试机器和每台生产机器上运行 Go 代理,并将管道与代理环境相匹配,以确保安装步骤发生在正确环境中的正确机器上。如果您使用这个模型,您还可以使用 GoCD 将构建工件复制到需要它们的机器上。
我选择不这样做,因为我不想在我想部署的每台机器上安装 GoCD 代理。相反,我使用在 GoCD 代理上执行的 Ansible 来控制环境中的所有机器。这需要管理 Ansible 使用的 SSH 密钥,并通过 Debian 仓库分发包。但是因为 Debian 无论如何都需要一个存储库来解决依赖性,所以这并不是额外的负担。
材料
GoCD 中的一个素材有两个用途:它触发一个管道,它提供管道中的任务可以使用的文件。
我倾向于使用 Git 存储库作为素材,GoCD 可以轮询这些存储库,在新版本可用时触发管道。GoCD 代理还将存储库克隆到代理执行其作业的文件系统中。
有针对各种源代码控制系统的材料插件,比如 Subversion (svn)和 mercurial,还有将 Debian 和 RPM 包存储库视为材料的插件。
最后,管线可以作为其他管线的材料。使用此功能,您可以构建管线图。
史前古器物
GoCD 可以收集工件,这些工件是由一个作业生成的文件或目录。同一管道的后续部分,甚至是其他连接的管道,都可以检索这些工件。工件的检索不限于在同一台代理机器上创建的工件。
您还可以从 web 界面和 GoCD 服务器提供的REST API 中检索工件。 3
当磁盘空间变得不足时,可以将工件存储库配置为丢弃旧版本。
9.2 安装
为了使用 GoCD,您必须在一台机器上安装 GoCD 服务器,并在至少一台机器上安装 GoCD 代理。这可以与服务器在同一台机器上,也可以在不同的机器上,只要它可以通过端口 8153 和 8154 连接到 GoCD 服务器。
当您的基础设施和管道数量增长时,您可能会运行几个 Go 代理。
在 Debian 上安装 GoCD 服务器
要在基于 Debian 的操作系统上安装 GoCD 服务器,首先你必须确保你可以通过 HTTPS 下载 Debian 软件包。
$ apt-get install -y apt-transport-https
然后,您必须配置软件包源。
$ echo 'deb https://download.gocd.org /' \
> /etc/apt/sources.list.d/gocd.list
$ curl https://download.gocd.org/GOCD-GPG-KEY.asc \
| apt-key add -
最后安装。
$ apt-get update && apt-get install -y go-server
在 Debian 9 上,codename Stretch ,Java 8 开箱即用。在 Debian 的旧版本中,你可能不得不从其他来源安装 Java 8,比如 Debian Backports 。 4
现在,当您将浏览器指向 HTTPS Go 服务器的端口 8154(忽略 SSL 安全警告)或 HTTP 的端口 8153 时,您应该会看到 GoCD 服务器的 web 界面(图 9-2 )。
图 9-2
GoCD 的初始网络界面
如果你得到一个连接被拒绝的错误,检查/var/log/go-server/下的文件,寻找出错的提示。
为了防止未经认证的访问,您可以安装认证插件,例如基于密码文件的认证 5 或基于 LDAP 或 Active Directory 的认证。 6
在 Debian 上安装 GoCD 代理
在您想要执行自动构建和部署步骤的一台或多台机器上,您必须安装一个 Go 代理,它将连接到服务器并轮询它的工作。
关于 GoCD 代理的自动安装示例,请参见第八章。如果您想改为手动安装,您必须执行与安装 GoCD 服务器时相同的前三个步骤,以确保您可以从 GoCD 软件包资源库安装软件包。然后,当然,你安装 Go 代理。在基于 Debian 的系统上,如下所示:
$ apt-get install -y apt-transport-https
$ echo 'deb https://download.gocd.org /' >
/etc/apt/sources.list.d/gocd.list
$ curl https://download.gocd.org/GOCD-GPG-KEY.asc \
| apt-key add -
$ apt-get update && apt-get install -y go-agent
然后编辑文件/etd/default/go-agent。第一行应为
GO_SERVER_URL=https://127.0.0.1:8154/go
将变量更改为指向您的 GoCD 服务器,然后启动代理。
$ service go-agent start
几秒钟后,代理将联系服务器。当您在 GoCD 服务器的 web 界面中单击代理菜单时,您应该会看到代理(图 9-3 )。
图 9-3
GoCD 的代理管理界面截图。(lara是这里代理的主机名。)
第一次接触 GoCD 的 XML 配置
有两种方法可以配置 GoCD 服务器:通过 web 界面和 XML 格式的配置文件。您还可以通过 web 界面编辑 XML 配置。 7
虽然 web 界面是探索 GoCD 功能的好方法,但由于点击次数太多,它很快就变得令人讨厌。使用具有良好 XML 支持的编辑器可以更快地完成工作,而且它更适合于紧凑的解释,所以这就是我在这里选择的路线。您还可以在同一个 GoCD 服务器实例上使用这两种方法。
在管理菜单中,Config XML 项目允许您查看和编辑服务器配置。清单 9-1 是一个原始的 XML 配置,已经注册了一个代理。
<?xml version="1.0" encoding="utf-8"?>
<cruise
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="cruise-config.xsd"
schemaVersion="77">
<server artifactsdir="artifacts"
commandRepositoryLocation="default"
serverId="b2ce4653-b333-4b74-8ee6-8670be479df9">
</server>
<agents>
<agent hostname="lara" ipaddress="192.168.2.43"
uuid="19e70088-927f-49cc-980f-2b1002048e09" />
</agents>
</cruise>
Listing 9-1Baseline GoCD XML Configuration, with One Agent Registered
即使您遵循相同的步骤,代理的serverId和数据在您的安装中也会有所不同。
为了给代理一些资源,您可以将<agents>部分中的<agent .../>标记改为如清单 9-2 所示。
<agent hostname="lara" ipaddress="192.168.2.43"
uuid="19e70088-927f-49cc-980f-2b1002048e09">
<resources>
<resource>debian-stretch</resource>
<resource>build</resource>
<resource>aptly</resource>
</resources>
</agent>
Listing 9-2GoCD XML Configuration for an Agent with Resources
创建 SSH 密钥
对于 GoCD 来说,拥有一个没有密码的 SSH 密钥是很方便的,例如,能够通过 SSH 克隆 Git 存储库。要创建一个,请在服务器上运行以下命令:
$ su - go
$ ssh-keygen -t rsa -b 2048 -N " -f ~/.ssh/id_rsa
要么将生成的.ssh目录和其中的文件复制到每个代理的/var/go目录中(记得在最初创建时设置所有者和权限),要么在每个代理上创建一个新的密钥对。
9.3 管道中的建筑
触发 Debian 包的构建需要从 Git 存储库中获取源代码,将其配置为 GoCD 材料,然后调用带有一些选项的dpkg-buildpackage命令,最后收集结果文件。
这里(清单 9-3 )是构建python-matheval包的第一步,用 GoCD 的 XML 配置表示。
<pipelines group="deployment">
<pipeline name="python-matheval">
<materials>
<git
url="https://github.com/python-ci-cd/python-matheval.git"
dest="source" />
</materials>
<stage name="build" cleanWorkingDir="true">
<jobs>
<job name="build-deb" timeout="5">
<tasks>
<exec command="/bin/bash" workingdir="source">
<arg>-c</arg>
<arg>dpkg-buildpackage -b -us -uc</arg>
</exec>
</tasks>
<artifacts>
<artifact src="*.deb" dest="debian-packages/"
type="build" />
</artifacts>
<resources>
<resource>debian-stretch</resource>
<resource>build</resource>
</resources>
</job>
</jobs>
</stage>
</
pipeline>
</pipelines>
Listing 9-3Simple Approach to Building a Debian Package in GoCD
您可以在deployment-utils8存储库的gocd目录中找到这个配置和所有下面的 XML 配置。
最外面的标记是管道组,它有一个名称。它可用于对可用管道进行分类,也可用于管理权限。
第二层是带有名称的<pipeline>,它包含一个材料列表和一个或多个阶段。
目录布局
每次运行某个阶段中的作业时,分配给该作业的 GoCD 代理都会准备一个目录,在该目录中可以使用这些材料。在 Linux 上,这个目录默认为/var/lib/go-agent/pipelines/,后跟管道名。GoCD 配置中的路径相对于此路径。
例如,前面的 material 定义包含属性dest="source",所以这个 Git 存储库的工作副本的绝对路径是/var/lib/go-agent/pipelines/python-matheval/source。省略掉dest="..."会有效,并且会减少一个目录级别,但是这也会阻止我们在将来使用第二个材料。
可用材料类型和选项列表见配置参考 9 。插件可用于 10 添加更多的材料类型。
阶段、作业、任务和工件
管道中的所有阶段都是串行运行的,并且只有在前一个阶段成功的情况下,每个阶段才会运行。每个阶段都有一个名称,它既用于前端,也用于获取该阶段中产生的工件。
在前面的例子中,我给 stage 赋予了属性cleanWorkingDir="true",这使得 GoCD 删除在之前的构建过程中创建的文件,并放弃对版本控制下的文件的更改。这往往是一个很好的选择;否则,您可能会不知不觉地陷入前一个构建影响当前构建的境地,这对于调试来说确实很痛苦。
作业有可能在一个阶段中并行执行,并且由于与阶段相同的原因而命名。如果有几个代理可以运行这些作业,则它们只能并行运行。
GoCD 代理连续执行作业中的任务。我倾向于主要使用<exec>任务(和<fetchartifact>,你将在下一章看到),它们调用系统命令。他们遵循 UNIX 惯例,将退出状态 0 视为成功,其他都视为失败。
对于更复杂的命令,我在 Git 存储库中创建 shell、Perl 或 Python 脚本,并将存储库作为素材添加到管道中,这使得它们在构建过程中可用,而无需额外的工作。
我们示例中的<exec>任务调用/bin/bash -c 'dpkg-buildpackage -b -us -uc'。这是一个货邪教编程、 11 的案例,因为直接调用dpkg-buildpackage也可以。啊,好吧,我们可以稍后修改这个…
构建 Debian 包,并在源代码的 Git 检验中执行。它生成一个.deb文件,一个.changes文件,可能还有其他一些带有元数据的文件。它们是在管道的根目录中创建的,比 Git checkout 高一级。
因为这些是我们稍后想要处理的文件,至少是.deb文件,我们让 GoCD 将它们存储在一个名为工件库的内部数据库中。这就是配置中的<artifact>标签指示 GoCD 做的事情。
生成的包文件的名称取决于构建的 Debian 包的版本号(来自 Git 存储库中的debian/changelog文件),所以以后不容易通过名称引用它们。这就是dest="debian-packages/"发挥作用的地方:它让 GoCD 将工件存储在一个有固定名称的目录中。随后的阶段可以通过固定的目录名从这个目录中检索所有工件文件。
行动中的管道
如果没有出错(从来没有出错过,对吗?),图 9-4 大致展示了运行新管道后 web 界面的样子。
图 9-4
成功运行构建阶段后的管道概述
每当 Git 存储库中有新的提交时,GoCD 都会很高兴地构建一个 Debian 包并存储起来以备将来使用。自动化构建,耶!
版本回收被认为有害
当构建 Debian 包时,工具通过查看debian/changelog文件的顶部来确定最终包的版本号。这意味着,每当有人在没有新的 changelog 条目的情况下推送代码或文档变更时,生成的 Debian 包与前一个包具有相同的版本号。
大多数 Debian 工具假设包名、版本和架构的元组唯一地标识了包的修订。将旧版本号的包的新版本塞进一个存储库中肯定会引起麻烦。大多数存储库管理软件只是简单地拒绝接受循环使用某个版本的包的副本。在要安装软件包的目标机器上,如果版本号保持不变,升级软件包不会有任何作用。
构建唯一的版本号
有几个来源可以用来生成唯一的版本号。
-
随机性(例如,以 UUIDs 的形式)
-
当前日期和时间
-
Git 存储库本身
-
GoCD 公开的几个有用的环境变量 12
后者大有可为。GO_PIPELINE_COUNTER是一个单调计数器,每次 GoCD 运行管道时它都会增加,所以这是一个很好的版本号来源。GoCD 允许手动重新运行阶段,所以最好与GO_STAGE_COUNTER结合使用。就 shell 脚本而言,使用$GO_PIPELINE_COUNTER.$GO_STAGE_COUNTER作为版本字符串听起来是一种不错的方法。
但是,还有更多。GoCD 允许您使用特定版本的材料触发管道,因此您可以运行新的管道来构建旧版本的软件。如果这样做,使用GO_PIPELINE_COUNTER作为版本字符串的第一部分并不能反映旧代码库的使用。
git describe是一种计算提交次数的既定方法。默认情况下,它打印存储库中的最后一个标记,如果HEAD没有解析为与该标记相同的提交,它会添加自该标记以来的提交次数和以g为前缀的缩写 SHA1 哈希,例如,提交4232204的2016.04-32-g4232204,这是在标记2016.04之后的 32 次提交。选项--long强制它总是打印提交次数和散列,即使 HEAD 指向一个标签。
我们不需要版本号的提交散列,因此构建合适版本号的 shell 脚本如下所示。
#!/bin/bash
set -e
set -o pipefail
v=$(git describe --long |sed 's/-g[A-Fa-f0-9]*$//')
version="$v.${GO_PIPELINE_COUNTER:-0}.${GO_STAGE_COUNTER:-0}"
Bash 的${VARIABLE:-default}语法是让脚本在 GoCD 代理环境之外工作的好方法。这个脚本需要在 Git 存储库中设置一个标记。如果没有,则失败,并显示来自git describe的消息:
fatal: No names found, cannot describe anything.
围绕构建的其他零碎内容
现在我们有了一个惟一的版本字符串,我们必须指示构建系统使用这个版本字符串。这通过用期望的版本号在debian/changelog中写入一个新条目来实现。debchange工具为我们实现了自动化。要使它可靠地工作,有几个选项是必需的。
export DEBFULLNAME='Go Debian Build Agent'
export DEBEMAIL='go-noreply@example.com'
debchange --newversion=$version --force-distribution -b \
--distribution="${DISTRIBUTION:-stretch}" 'New Version'
当我们想要在管道的后期引用这个版本号时(是的,将会有更多),在一个文件中提供它是很方便的。在输出中包含它也很方便,所以我们在脚本中还需要两行。
echo $version
echo $version > ../version
当然,必须触发实际的构建,如下所示:
dpkg-buildpackage -b -us -uc
将它插入 GoCD
为了让 GoCD 可以访问这个脚本,并让它处于版本控制之下,我将这个脚本放入一个 Git 存储库中,命名为debian-autobuild,并将这个存储库作为一个素材添加到管道中(清单 9-4 )。
<pipeline name="python-matheval">
<materials>
<git
url="https://github.com/python-ci-cd/python-matheval.git"
dest="source" materialName="python-matheval" />
<git
url="https://github.com/python-ci-cd/deployment-utils.git"
dest="deployment-utils" materialName="deployment-utils" />
</materials>
<stage name="build" cleanWorkingDir="true">
<jobs>
<job name="build-deb" timeout="5">
<tasks>
<exec command="../deployment-utils/debian-autobuild"
workingdir="source" />
</tasks>
<artifacts>
<artifact src="version" type="build"/>
<artifact src="*.deb" dest="debian-packages/"
type="build" />
</artifacts>
<resources>
<resource>debian-stretch</resource>
<resource>build</resource>
</resources>
</job>
</jobs>
</stage>
</pipeline>
Listing 9-4GoCD Configuration for Building Packages with Distinct Version Numbers
现在,GoCD 在每次提交到 Git 存储库时自动构建 Debian 包,并给每个包一个不同的版本字符串。
9.4 总结
GoCD 是一个开源工具,可以轮询您的 Git 存储库,并通过专用代理触发构建。它是通过 web 界面配置的,可以通过点击助手或提供 XML 配置。
必须注意为每个版本构造有意义的版本号。Git 标记、自最后一个标记以来的提交次数以及 GoCD 公开的计数器是构建这种版本号的有用组件。
Footnotes 12
<ant>和<rake>任务执行同名的专门构建器,并允许您指定目标和构建文件。详见 https://docs.gocd.org/current/configuration/configuration_reference.html#ant 。
3
https://api.gocd.org/current/ 。
4
https://backports.debian.org/ 。
5
https://github.com/gocd/gocd-filebased-authentication-plugin 。
6
https://github.com/gocd/gocd-ldap-authentication-plugin 。
7
从 GoCD 版本 16.7 开始,管道配置可以交换到外部版本控制库,并且通过插件,甚至可以用不同的格式编写,比如 YAML。虽然这似乎是一个非常有前途的方法,但介绍它超出了本书的范围。
8
https://github.com/python-ci-cd/deployment-utils 。
9
https://docs.gocd.org/current/configuration/configuration_reference.html#materials 。
10
11
维基百科,《货物邪教编程》, https://en.wikipedia.org/wiki/Cargo_cult_programming ,2018 年。
12
https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html 。
十、在管道中分发和部署包
前一章给我们留下了 GoCD 管道的开端。它会在每次新的提交被推送到 Git 时自动构建一个 Debian 包,并为每次构建生成一个唯一的版本号。最后,它捕获构建的包和包含版本号的文件version,作为工件。接下来的任务是将它上传到 Debian 仓库,并部署到目标机器上。
10.1 管道上传
第六章,关于分发软件包,已经介绍了一个小程序,用于创建和填充 Debian 知识库。如果您将它添加到那一章的deployment-utils Git 存储库中,那么您可以自动上传新构建的带有这个额外 GoCD 配置的包(清单 10-1 ),在构建阶段之后插入。
<stage name="upload-testing">
<jobs>
<job name="upload-testing">
<tasks>
<fetchartifact pipeline="" stage="build"
job="build-deb" srcdir="debian-packages"
artifactOrigin="gocd">
<runif status="passed" />
</fetchartifact>
<exec command="/bin/bash">
<arg>-c</arg>
<arg>deployment-utils/add-package testing stretch *.deb</arg>
</exec>
</tasks>
<resources>
<resource>aptly</resource>
</resources>
</job>
</jobs>
</stage>
Listing 10-1GoCD Configuration for Uploading a Freshly Built Package to the testing Repository
您猜对了,fetchartifact任务获取存储在 GoCD 服务器的工件库中的工件。在这里,它获取目录python-matheval,前一阶段将 Debian 包上传到这个目录中。管道名称的空字符串指示 GoCD 使用当前管道。
在add-package脚本的调用中,testing指的是环境的名称(可以自由选择,只要一致),而不是 Debian 项目的测试发行版。
最后,aptly资源选择具有相同资源的 GoCD 代理来运行作业(参见图 10-1 )。如果您预计您的设置会有所增长,那么您应该有一台单独的机器来为这些存储库服务。在其上安装 GoCD 代理,并为其分配该资源。您甚至可以为测试和生产存储库准备单独的机器,并给它们更多特定的资源(比如aptly-testing和aptly-production)。
图 10-1
Aptly 存储库所在的机器有一个 GoCD 代理,它从 GoCD 服务器中检索 Debian 包作为工件。目标机器将存储库配置为软件包源。
用户帐户和安全性
在前面的示例配置中,add-package脚本以go系统用户的身份运行,默认情况下,该用户在基于 Linux 的系统上的主目录是/var/go。这将在诸如/var/go/aptly/testing/stretch/的目录中创建存储库。
在第六章中,假设 Aptly 在自己的系统用户账户下运行。您仍然需要给予go用户权限来将包添加到存储库中,但是您可以防止go用户修改现有的存储库,更重要的是,防止用户从用来签署包的 GPG 密钥获得访问权。
如果您将存储库置于一个单独的用户之下,您需要一种跨越用户帐户障碍的方法,对于命令行应用来说,传统的方法是允许go用户通过sudo命令调用add-package。但是为了获得实际的安全性好处,您必须将add-package命令复制到一个位置,在那里go用户没有写权限。否则,可以访问go用户帐户的攻击者可以修改这个命令,做他/她认为合适的任何事情。
假设您打算将其复制到/usr/local/bin,您可以添加这一行:
/etc/sudoers
到文件(列表 10-2 )。
go ALL=(aptly) NOPASSWD: /usr/local/bin/add-package
Listing 10-2/etc/sudoers Line That Allows the go User to Execute add-package As User aptly
然后,不是调用add-package <environment> <distribution> <deb package>,而是将它改为
$ sudo -u aptly --set-home /usr/local/bin/add-package \
<environment> <distribution> <deb package>
--set-home标志告诉sudo将HOME环境变量设置为目标用户的主目录,这里是aptly。
如果你选择不走sudo路线,你必须调整网络服务器配置,以提供来自/var/go/aptly/而不是/home/aptly/aptly的文件。
10.2 在管道中部署
在第七章中,我们看到了如何通过 Ansible 升级(或安装,如果尚未安装的话)一个包(见图 10-2 ,如下所示:
$ ansible -i testing web -m apt \
-a 'name=python-matheval state=latest update_cache=yes'
其中,testing是与环境同名的清单文件,web是要部署到的主机组,python-matheval是软件包的名称。
图 10-2
GoCD 代理运行 Ansible,通过 SSH 连接到目标机器,安装所需的软件包
在 GoCD 中,你可以在upload-testing阶段(清单 10-3 )之后,作为一个单独的阶段来完成这项工作。
<stage name="deploy-testing">
<jobs>
<job name="deploy-testing">
<tasks>
<exec command="ansible" workingdir="deployment-utils/ansible/">
<arg>--inventory-file=testing</arg>
<arg>web</arg>
<arg>-m</arg>
<arg>apt</arg>
<arg>-a</arg>
<arg>name=python-matheval state=latest update_cache=yes</arg>
<runif status="passed" />
</exec>
</tasks>
</job>
</jobs>
</stage>
Listing 10-3GoCD Configuration for Automatically Installing a Package
这里假设您将库存文件添加到了deployment-utils Git 仓库的ansible目录中,并且 Debian 仓库已经在目标机器上配置好了,正如第七章中所讨论的。
10.3 结果
要运行新阶段,可以通过点击 web 前端管道概览中的“播放”三角形来触发管道的完整运行,或者在管道历史视图中手动触发该阶段。您可以登录目标机器,检查软件包是否安装成功。
$ dpkg -l python-matheval
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture Description
+++-==============-============-============-==============
ii python-matheval 0.1-0.7.1 all Web service
并验证该服务正在运行
$ systemctl status python-matheval
python-matheval.service - Package installation informati
Loaded: loaded (/lib/systemd/system/python-matheval.ser
Active: active (running) since Sun 2016-03-27 13:15:41
Process: 4439 ExecStop=/usr/bin/hypnotoad -s /usr/lib/py
Main PID: 4442 (/usr/lib/packag)
CGroup: /system.slice/python-matheval.service
├─4442 /usr/lib/python-matheval/python-matheval
├─4445 /usr/lib/python-matheval/python-matheval
├─4446 /usr/lib/python-matheval/python-matheval
├─4447 /usr/lib/python-matheval/python-matheval
└─4448 /usr/lib/python-matheval/python-matheval
您还可以从主机检查服务是否在端口 8080 上响应,正如它应该做的那样。
$ curl --data '["+", 5]' -XPOST http://172.28.128.3:8800
5
10.4 一直到生产
上传和部署到生产环境的工作方式与测试环境相同。因此,所有需要做的就是复制最后两条管道的配置,用production替换每次出现的testing,并添加一个手动批准按钮,这样生产部署仍然是一个有意识的决定(列出 10-4 )。
<stage name="upload-production">
<approval type="manual" />
<jobs>
<job name="upload-production">
<tasks>
<fetchartifact pipeline="" stage="build" job="build-deb" srcdir="debian-packages" artifactOrigin="gocd">
<runif status="passed" />
</fetchartifact>
<exec command="/bin/bash">
<arg>-c</arg>
<arg>deployment-utils/add-package production \ stretch *.deb</arg>
</exec>
</tasks>
<resources>
<resource>aptly</resource>
</resources>
</job>
</jobs>
</stage>
<stage name="deploy-production">
<jobs>
<job name="deploy-production">
<tasks>
<exec command="ansible" workingdir="deployment-utils/ansible/">
<arg>--inventory-file=production</arg>
<arg>web</arg>
<arg>-m</arg>
<arg>apt</arg>
<arg>-a</arg>
<arg>name=python-matheval state=latest update_cache=yes</arg>
<runif status="passed" />
</exec>
</tasks>
</job>
</jobs>
</stage>
Listing 10-4GoCD Configuration for Distributing in, and Deploying to, the Production Environment
这里唯一真正的新闻是第二行
<approval type="manual" />
这使得 GoCD 只有在有人单击 web 界面中的批准箭头时才进行到这个阶段。
您还必须用您的一台或多台服务器的列表填写名为production的清单文件。
10.5 成就解锁:基本连续交付
简而言之,管道
-
由源代码中的提交自动触发
-
从每次提交自动构建 Debian 包
-
将其上传到测试环境的存储库中
-
自动将其安装在测试环境中
-
在手动批准后,将其上传到生产环境的存储库中
-
在生产中自动安装新版本
从源代码中的 Git 提交到生产环境中运行的软件的自动化部署的基本框架已经就绪。
十一、管道改进
前一章中的管道已经非常有用,并且比手工构建、分发和安装更好。尽管如此,仍有改进的余地。我将讨论如何将它更改为总是部署在同一管道实例中构建的确切版本,如何在安装后运行冒烟测试,以及如何从 Go continuous delivery (GoCD)配置中提取模板,以便它变得易于重用。
11.1 回滚和安装特定版本
在前几章中开发的部署管道总是安装最新版本的包。因为构建版本号的逻辑通常会产生单调递增的版本号,所以这通常是以前在同一个管道实例中构建的包。
然而,我们真的希望管道部署在管道的同一个实例中构建的确切版本。明显的好处是,它允许您重新运行旧版本的管道,安装旧版本,有效地让您回滚。
或者,您可以为修补程序构建第二个管道,基于同一个 Git 存储库但不同的分支。当您需要修补程序时,您只需暂停常规管道并触发修补程序管道。在这种情况下,如果您总是安装最新版本,为修补程序找到合适的版本字符串几乎是不可能的,因为它必须高于当前安装的版本,但也低于下一个常规版本。哦,请全部自动完成。
安装特定版本的一个不太明显的好处是,它可以检测目标机器的包源代码配置中的错误。如果部署脚本只安装可用的最新版本,并且由于错误没有在目标机器上配置存储库,那么安装过程将变成一个无声的无操作,如果软件包已经安装在一个旧版本中的话。
履行
要做两件事:弄清楚要安装哪个版本的包,然后去做。如何用 Ansible 安装一个特定版本的包(清单 11-1 )已经在第七章解释过了。
- apt: name=foo=1.00 state=present force=yes
Listing 11-1Ansible Playbook Fragment for Installing Version 1.00 of Package foo
更通用的方法是使用同一章中介绍的角色custom_package_installation。
- hosts: web roles:
role: custom_package_installation
package: python-matheval
您可以用ansible-playbook --extra-vars=package_version=1.00...调用它。
将这个剧本作为文件ansible/deploy-python-matheval.yml添加到deployment-utils Git 存储库中。找到要安装的版本号也有一个简单但可能不明显的解决方案:将版本号写入文件;将此文件作为工件收集到 GoCD 中;然后,当需要安装时,获取工件并从中读取版本号。在撰写本文时,GoCD 没有更直接的方法通过管道传播元数据。
将版本传递给 Ansible playbook 的 GoCD 配置如清单 11-2 所示。
<job name="deploy-testing">
<tasks>
<fetchartifact pipeline="" stage="build" job="build-deb" srcfile="version" artifactOrigin="gocd" />
<exec command="/bin/bash" workingdir="deployment-utils/ansible/">
<arg>-c</arg>
<arg>ansible-playbook --inventory-file=testing
--extra-vars="package_version=$(< ../../version)" deploy-python-matheval.yml</arg>
</exec>
</tasks>
</job>
Listing 11-2GoCD Configuration for Installing the Version from the version File
(<arg>...</arg> XML 标签必须在一行上,这样 Bash 会将其解释为一个命令。此处多行显示只是为了可读性。)
Bash 的$(...)打开一个子进程,这也是一个 Bash 进程,并将该子进程的输出插入命令行。< ../../version是读取文件的一种简单方式,这是 XML,需要对小于号进行转义。
生产部署配置看起来非常相似,只是使用了--inventory-file=production。
试试看!
为了测试特定于版本的包安装,您必须拥有至少两次捕获了version工件的管道运行。如果还没有,可以将提交推送到源存储库,GoCD 会自动获取它们。
可以用dpkg -l python-matheval查询目标机器上安装的版本。在最后一次运行之后,应该安装在该管道实例中构建的版本。
然后,您可以从以前的管道重新运行部署阶段,例如,在管道的历史视图中,将鼠标悬停在阶段上,然后单击上面带有触发重新运行的箭头的圆圈(图 11-1 )。
图 11-1
在管道的历史视图中,将鼠标悬停在一个完整的阶段(通过或失败)上会显示一个重新运行该阶段的图标
当 stage 完成运行时,您可以再次检查目标机器上已安装的软件包版本,以验证旧版本确实已经部署。
11.2 在管道中运行冒烟测试
部署应用时,测试应用的新版本是否能正常工作是非常重要的。通常,这是通过冒烟测试来完成的——这是一个非常简单的测试,但它测试了应用的许多方面:应用进程运行,它绑定到它应该绑定的端口,以及它可以响应请求。通常,这意味着配置和数据库连接也是相同的。
什么时候抽烟?
烟尘测试一次覆盖很多领域。一个单独的测试可能需要一个正常工作的网络、正确配置的防火墙、web 服务器、应用服务器、数据库等等。这是一个优点,因为这意味着它可以检测许多种类的错误,但这也是一个缺点,因为这意味着诊断能力较低。当它出现故障时,您不知道是哪个组件的问题,必须重新调查每个故障。
冒烟测试也比单元测试贵得多。它们往往需要更多的时间来编写,需要更长的时间来执行,并且在面对配置或数据更改时更加脆弱。因此,典型的建议是进行少量的冒烟测试,可能是 1 到 20 次,或者可能是 1%的单元测试。
举个例子,如果你要为 Web 开发一个航班搜索和推荐引擎,你的单元测试将覆盖用户可能遇到的不同场景,并且引擎会产生最好的建议。在冒烟测试中,你只需检查你是否能输入旅行的起点、目的地和日期,以及你是否能得到一个飞行建议列表。如果该网站上有一个成员资格区域,您将测试没有凭据无法访问该区域,并且可以在登录后访问该区域。三次烟雾测试,差不多吧。
白盒烟雾测试
上面提到的例子基本上是黑盒冒烟测试,因为它们不关心应用的内部,并且像用户一样对待应用。这是非常有价值的,因为归根结底,你关心用户的体验。
有时,应用的某些方面不容易进行冒烟测试,但经常出现故障,足以保证自动冒烟测试。例如,应用可能会缓存来自外部服务的响应,因此简单地使用某个功能并不能保证能够使用这个特定的通信通道。
一个实用的解决方案是让应用提供某种自我诊断,比如应用从一个 web 页面测试其自身配置的一致性,检查所有必需的数据库表是否存在,以及外部服务是否可达。然后,单个冒烟测试可以调用状态页,并在状态页不可访问或报告错误时引发错误。这是一个白盒烟雾测试。
白盒冒烟测试的状态页面可以在监控检查中重用,但是在部署过程中显式检查它们仍然是一个好主意。白盒烟雾测试不应该取代黑盒烟雾测试,而是对其进行补充。
样本黑盒烟雾测试
python-matheval应用提供了一个简单的 HTTP 端点,因此任何 HTTP 客户端都可以进行冒烟测试。使用curl命令行 HTTP 客户端,请求可能如下所示:
$ curl --silent -H "Accept: application/json" \
--data '["+", 37, 5]' \
-XPOST http://127.0.0.1:8800/
42
检查输出是否符合预期的一个简单方法是通过grep将它传送出去。
$ curl --silent -H "Accept: application/json" \
--data '["+", 37, 5]' \
-XPOST http://127.0.0.1:8800/ | grep ⁴²$
42
输出与之前相同,但是如果输出偏离预期,退出状态是非零的。
在管道和滚动发布中增加烟雾测试
一个简单的在交付管道中集成冒烟测试的方法是在每个部署阶段之后添加一个冒烟测试阶段(也就是说,一个在测试部署之后,一个在生产部署之后)。如果应用的某个版本在测试环境中未通过冒烟测试,此设置会阻止该版本进入生产环境。因为冒烟测试只是一个 shell 命令,它以非零退出状态指示失败,所以将它作为命令添加到您的部署系统中是微不足道的。
如果您的应用只有一个实例在运行,这是您能做的最好的事情。但是,如果您有一个机器群,并且有几个应用实例在某种负载平衡器后面运行,则可以在升级期间单独对每个实例进行冒烟测试,如果太多实例未通过冒烟测试,则可以中止升级。
所有成功的大型科技公司都通过检查来保护他们的生产系统,甚至更精细的版本。
这种滚动升级的一个简单方法是为每个包的部署扩展 Ansible playbook,并让它在移动到下一台机器之前运行每台机器的冒烟测试(清单 11-3 和 11-4 )。
---
- hosts: web
serial: 1
max_fail_percentage: 1
tasks:
- apt:
update_cache: yes
package: python-matheval={{package_version}}
state: present
force: yes
- local_action: >
command ../smoke-tests/python-matheval
"{{ansible_host}}"
changed_when: False
Listing 11-4File ansible/deploy-python-matheval.yml: A Rolling Deployment Playbook with Integrated Smoke Test
#!/bin/bash
curl --silent -H "Accept: application/json" \
--data '["+", 37, 5]' –XPOST http://$1:8800/ \
| grep ⁴²$
Listing 11-3File smoke-tests/python-matheval: A Simple HTTP-Based Smoke Test
随着冒烟测试的数量随着时间的推移而增长,将它们全部塞进 Ansible playbook 是不现实的,这样做还会限制可重用性。在这里,它们位于部署工具库的一个单独文件中。 1 另一个选择是从冒烟测试中构建一个包,并将它们安装在运行 Ansible 的机器上。
虽然在安装了该服务的机器上执行 smoke tests 命令很容易,但作为本地操作(即在启动 Ansible playbook 的控制主机上)运行它也会测试网络和防火墙部分,从而更真实地模拟实际使用场景。
11.3 配置模板
当您有多个软件包要部署时,您需要为每个软件包构建一个管道。只要部署管道在结构上足够相似——通常使用相同的打包格式和相同的安装技术——您就可以重用该结构,方法是从第一个管道中提取一个模板,并对其进行多次实例化,以创建具有相同结构的单独管道。
如果您仔细查看之前开发的管道 XML 配置,您可能会注意到它并不是非常特定于python-matheval项目。除了 Debian 发行版和部署手册的名称,这里的所有内容都可以被 Debian 打包的任何软件重用。
为了使管道更加通用,您可以将参数(简称 params )定义为管道内部的第一件事,在<materials>部分之前(列表 11-5 )。
<params>
<param name="distribution">stretch</param>
<param name="deployment_playbook">deploy-python-matheval.yml</param>
</params>
Listing 11-5Parameter Block for the python-matheval Pipeline, to Be Inserted Before the Materials
然后用占位符#{distribution}替换每个阶段定义中所有出现的stretch,用#{deployment_playbook}替换deploy-python-matheval.yml,这样就得到如下 XML 片段
<exec command="/bin/bash">
<arg>-c</arg>
<arg>deployment-utils/add-package \
testing #{distribution} *.deb</arg>
</exec>
和
<exec command="/bin/bash" workingdir="deployment-utils/ansible/">
<arg>-c</arg>
<arg>ansible-playbook --inventory-file=testing
--extra-vars="package_version=$(< ../../version)"
#{deployment_playbook}</arg>
</exec>
泛化的下一步是将阶段移动到一个模板。这也可以通过编辑 XML 配置来完成,或者在 web 界面中使用管理➤管道,然后单击名为 python-matheval 的管道旁边的提取模板链接来完成。
如果选择debian-base作为模板名,XML 中的结果看起来如清单 11-6 所示。
<pipelines group="deployment">
<pipeline name="python-matheval" template="debian-base">
<materials>
<git url=
"https://github.com/python-ci-cd/python-matheval.git"
dest="source" materialName="python-matheval" />
<git url=
"https://github.com/python-ci-cd/deployment-utils.git"
dest="deployment-utils"
materialName="deployment-utils" />
</materials>
<params>
<param name="distribution">stretch</param>
<param name="deployment_playbook">deploy-python-matheval.yml</param>
</params>
</pipelines>
<templates>
<pipeline name="debian-base">
<!-- stages definitions go here -->
</pipeline>
</templates>
Listing 11-6GoCD Configuration for Pipeline matheval Using a Template
这个软件包特有的一切现在都在管道定义中,可重用的部分在模板中。唯一的例外是deployment-utils存储库,它必须单独添加到每个管道中,因为 GoCD 无法将材料移动到模板中。
为另一个应用添加部署管道现在只需要指定 URL、目标(即 Ansible 清单文件中的组名)和分发。在下一章你会看到一个例子。一旦您习惯了工具,这相当于不到五分钟的工作。
11.4 避免重建踩踏
当您有相当数量的管道时,您会注意到一种不幸的模式。每当您向deployment-utils存储库提交时,都会触发所有管道的重建。这是一种资源浪费,并且会占用一个或多个构建代理,因此基于实际源代码更改的包构建会延迟到所有构建工作完成之后。
GoCD 的材料有一个忽略过滤器,这是为了避免当只有文档发生变化时昂贵的重建(清单 11-7 )。您可以使用它来忽略对存储库中所有文件的更改,从而避免重建混乱。
<git url="https://github.com/python-ci-cd/deployment-utils.git"
dest="deployment-utils" materialName="deployment-utils">
<filter>
<ignore pattern="*" />
<ignore pattern="**/*" />
</filter>
</git>
Listing 11-7GoCD Material Definition That Avoids Triggering the Pipeline
*过滤器匹配顶层目录中的所有文件,**/*匹配子目录中的所有文件。
当您将所有管道中的deployment-utils材料的材料配置更改为这些忽略过滤器时,对deployment-utils存储库的新提交不会触发任何管道。启动管道时,GoCD 仍然轮询材料并使用最新版本。与所有管线一样,所有阶段的材料版本都是相同的。
忽略存储库中的所有文件是一种笨拙的工具,需要您手动触发项目的管道,以对部署行动手册进行更改。因此,从 GoCD 版本 16.6 开始,您可以用invertFilter="true"反转过滤条件,以创建白名单(清单 11-8 )。
<git url="https://github.com/python-ci-cd/deployment-utils.git"
invertFilter="true" dest="deployment-utils"
materialName="deployment-utils">
<filter>
<ignore pattern="ansible/deploy-python-matheval.yml" />
</filter>
/git>
Listing 11-8Using White Lists in GoCD Materials to Selectively Trigger on Changes to Certain Files
每个管道的这种白名单配置导致对deployment-utils储存库的提交仅触发与变更相关的管道。
11.5 摘要
当您将管道配置为部署在管道的同一实例中构建的完全相同的版本时,您可以使用它来安装旧版本或进行回滚。
管道模板允许您提取管道之间的共性,并且只维护这些共性一次。参数带来了支持不同软件包所需的多样性。
Footnotes 1https://github.com/python-ci-cd/deployment-utils 。
十二、安全性
自动化部署对您的应用和基础架构的安全性有什么影响?事实证明,这既有安全优势,也有需要警惕的事情。
12.1 集权的危险
在部署管道中,控制部署的机器必须能够访问部署软件的目标机器。在最简单的情况下,部署机器上有一个私有的 SSH 密钥,目标机器将访问权授予该密钥的所有者。
这是一个明显的风险,因为获得部署机器(GoCD 代理或控制代理的 GoCD 服务器)访问权限的攻击者可以使用这个密钥连接到所有目标机器,从而获得对它们的完全控制。
一些可能的缓解措施包括:
-
实现部署机器的强化设置(例如,使用 SELinux 或 grsecurity)。
-
用密码保护 SSH 密钥,并通过触发部署的同一通道提供密码,比如通过 GoCD 服务器中的加密变量。
-
使用硬件令牌来存储 SSH 部署密钥。硬件令牌对于基于软件的密钥提取是安全的。
-
拥有单独的部署和构建主机。构建主机往往需要安装更多的软件,这暴露了更大的攻击面。
-
您还可以为每个环境配备单独的部署机器,并使用单独的凭证。
-
在目标机器上,通过上述 SSH 密钥只允许非特权访问,并使用类似于
sudo的东西,只允许某些特权操作。
每种缓解措施都有自己的成本和缺点。为了说明这一点,请注意,如果攻击者仅设法获得文件系统的副本,那么密码保护的 SSH 密钥会有所帮助,但如果攻击者获得机器上的 root 权限,从而可以获得包含解密的 SSH 密钥的内存转储,那么就没有帮助了。
基于硬件的秘密存储可以很好地防止密钥被盗,但是它使得虚拟系统的使用更加困难,并且必须购买和配置。
sudo方法在限制攻击传播方面非常有效,但是它需要在目标机器上进行大量的配置,并且您需要一种安全的方式来部署它。所以,你遇到了一个鸡和蛋的问题,需要付出额外的努力。
另一方面,如果您没有交付管道,部署必须手动进行。所以,现在你有同样的问题,必须让人类访问目标机器。大多数组织都提供某种安全的机器来存储操作员的 SSH 密钥,并且您面临着与部署机器相同的风险。
12.2 安全修复上市时间
与手动部署相比,即使相对较慢的部署管道仍然非常快。当发现漏洞时,这种快速且自动化的部署过程可以大大缩短部署修补程序的时间。
同样重要的是,笨拙的手动发布过程诱使操作人员绕过安全修复走捷径,从而跳过了质量保证过程的一些步骤。当这个过程自动化且快速时,坚持这个过程比跳过它更容易,所以即使在有压力的情况下,它实际上也会被执行。
12.3 审计和软件材料清单
一个好的部署管道跟踪软件包的哪个版本是在什么时候构建和部署的。这允许人们回答诸如“我们的这个安全漏洞有多长时间了?”,“报告问题后多久在生产中修补了漏洞?”,甚至可能是“谁批准了引入漏洞的变更?”
如果您还使用基于存储在版本控制系统中的文件的配置管理,那么您甚至可以针对配置来回答这些问题,而不仅仅是针对软件版本。
简而言之,部署管道为审计提供了足够的数据。
一些法规要求您在某些情况下记录软件物料清单 1 ,例如,对于医疗设备软件。这是软件中包含的组件的记录,例如库及其版本的列表。虽然这对于评估许可证违规的影响很重要,但对于找出特定版本的库中哪些应用受到漏洞的影响也很重要。
惠普安全部门 2015 年的一份报告发现,44%的被调查漏洞可能是由至少两年前已知(并可能已修补)的漏洞造成的。这反过来意味着,通过跟踪您在何处使用哪个软件版本、订阅已知漏洞的新闻简报或订阅源,以及定期使用修补版本重建和重新部署您的软件,您可以将安全风险减半。
连续交付系统不会自动为您创建这样的软件物料清单,但它会为您提供一个可以插入这样的系统的地方。
12.4 摘要
连续交付提供了对新发现的漏洞做出快速且可预测的反应的能力。同时,部署管道本身也是一个攻击面,如果没有适当的保护,它可能会成为入侵者的诱人目标。
最后,部署管道可以帮助您收集数据,这些数据可以提供对具有已知漏洞的软件的使用的洞察,从而使您在修补这些安全漏洞时能够彻底。
Footnotes 1维基百科,《软件物料清单》, https://en.wikipedia.org/wiki/Software_bill_of_materials ,2018。
十三、状态管理
连续交付(CD)对于一个无状态的应用来说很好也很容易,也就是说,对于一个没有持久存储数据的应用来说。安装新的应用版本是一项简单的任务,只需要安装新的二进制文件(或者源代码,如果是未编译的语言),停止旧的实例,并启动新的实例。
一旦有持久状态要考虑,事情就变得更复杂了。在这里,我将考虑带有模式的传统关系数据库。您可以通过使用无模式的“noSQL”数据库来避免一些问题,但是您并不总是拥有这种奢侈。如果您真的采用无模式,您必须在应用代码中处理旧的数据结构,而不是通过部署过程。
随着模式的改变,您可能需要考虑数据迁移,这可能涉及到用默认值填充缺失值或者从不同的数据源导入数据。一般来说,这种数据迁移与模式迁移符合相同的模式,即要么执行一段 SQL 和数据 定义语言(DDL ) 1 ,要么运行直接与数据库对话的外部命令。
13.1 代码和数据库版本之间的同步
状态管理很困难,因为代码通常被绑定到数据库模式的一个版本上。在一些情况下,这可能会导致问题。
-
数据库更改通常比应用更新慢。如果应用的版本 1 只能处理模式的版本 1,而应用的版本 2 只能处理模式的版本 2,那么您必须停止应用的版本 1,进行数据库升级,并且只有在数据库迁移完成之后才启动应用的版本 2。
-
回滚到应用的前一个版本,以及它的数据库模式版本,变得非常痛苦。通常,数据库更改或其回滚都可能丢失数据,因此您不能轻松地跨这些界限进行自动释放和回滚。
为了详细说明最后一点,考虑向数据库的表中添加一列的情况。在这种情况下,更改的回滚(再次删除列)会丢失数据。相反,如果最初的更改是删除一列,这一步通常不能逆转。您可以重新创建相同类型的列,但数据会丢失。即使您存档了已删除的列数据,新行也可能已被添加到表中,并且这些新行没有存档的数据。
13.2 分离应用和数据库版本
有一些工具可以帮助您将数据库模式可重复地转换到一个定义的状态,但是它不能为您解决回滚过程中潜在的数据丢失问题。唯一可行的方法是在应用开发人员和数据库管理员之间建立协作,并将有问题的更改分解成多个步骤。
假设您想要的更改是删除一个有NOT NULL约束的列。简单地在一个步骤中删除列会带来上一节中概述的问题。相反,您可以执行以下步骤:
-
部署一个能够处理从列中读取
NULL值的应用版本,即使还不允许使用NULL值。 -
等到您确定不希望回滚到无法处理
NULL值的应用值。 -
部署数据库更改,使列可为空(或给它一个默认值)。
-
等到您确定不想回滚到该列为
NOT NULL的模式版本。 -
部署不再使用该列的新版本的应用。
-
等到您确定不希望回滚到使用该列的应用版本。
-
部署完全删除该列的数据库更改。
有些场景允许您跳过其中的一些步骤,或者将多个步骤合并为一个步骤。向表中添加列是一个类似的过程,如下所示:
-
部署一个数据库更改,添加带有默认值(或允许
NULL值)的新列。 -
部署写入新列的应用版本。
-
可以选择运行一些迁移来填充旧行的列。
-
可选地部署一个数据库变更,添加开始时不可能的约束(如
NOT NULL)。…在步骤之间有适当的等待。
模式更改的示例
假设您有一个由 PostgreSQL 数据库支持的 web 应用,并且目前该应用将登录尝试记录到数据库中。因此,该模式如下所示:
CREATE TABLE users (
id SERIAL,
email VARCHAR NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE login_attempts (
id SERIAL,
user_id INTEGER NOT NULL REFERENCES users (id),
success BOOLEAN NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
source_ip VARCHAR NOT NULL,
PRIMARY KEY(id)
);
随着 web 应用负载的增加,您意识到您正在为数据库创建不必要的写负载,并开始记录到外部日志服务。在数据库中,您真正需要的是最后一次成功登录的日期和时间(您的 CEO 坚持要求您在每次登录时显示该日期和时间,因为一名审计员确信这将提高安全性)。
所以,你最终想要的模式是这样的:
CREATE TABLE users (
id SERIAL,
email VARCHAR NOT NULL,
last_login TIMESTAMP NOT NULL,
PRIMARY KEY(id)
);
一个直接的数据库更改脚本将会有
DROP TABLE login_attempts;
ALTER TABLE users
ADD COLUMN last_login TIMESTAMP NOT NULL;
但是这存在前面提到的问题,即它将模式版本与应用版本联系在一起,而且在没有默认值和没有为其提供值的情况下,您不能引入一个NOT NULL列。
让我们把它分解成不受这些问题困扰的独立步骤。
创建新列,可空
第一步是添加可选的新列users.last_login(通过允许NULL值)。如果起点是模式的版本 1,这就是版本 2:
CREATE TABLE users (
id SERIAL,
email VARCHAR NOT NULL,
last_login TIMESTAMP,
PRIMARY KEY(id)
);
-- table login_attempts omitted, because it's unchanged.
运行 apgdiff,另一个 PostgreSQL Diff 工具、、 2 、针对这两个方案文件给出了我们:
$ apgdiff schma-1.sql schema-2.sql
ALTER TABLE users
ADD COLUMN last_login TIMESTAMP;
这是从模式 1 到模式 2 的正向迁移脚本。请注意,我们不一定需要回滚脚本,因为每个可以处理模式版本 1 的应用版本也可以处理模式版本 2(除非应用做了类似于SELECT * FROM users的傻事,并期望得到一定数量或顺序的结果。我会假设应用没有那么愚蠢)。
这个迁移脚本可以在 web 应用运行时应用于数据库,而不会有任何停机时间。
MySQL 有一个不幸的特性,即模式更改不是事务性的,在模式更改期间它们会锁定整个表,这抵消了您从增量数据库更新中获得的一些优势。
为了减轻这种情况,有一些外部工具可以解决这一问题,方法是创建表的修改副本,逐渐将数据从旧表复制到新表,最后进行重命名以替换旧表。GitHub 的 gh-ost 3 就是这样一个工具。
这些工具通常只对外键约束提供有限的支持,所以在使用它们之前要仔细评估。
当模式更改完成后,您可以部署新版本的 web 应用,每当成功登录时,该应用都会写入到users.last_login。请注意,该应用版本必须能够处理从该列读取的NULL值,例如,通过返回到表login_attempts来确定最后一次登录尝试。
该应用版本还可以停止向表login_attempts中插入新条目。更保守的方法是将该步骤推迟一段时间,这样您就可以安全地回滚到较旧的应用版本。
数据迁移
最后,users.last_login应该是NOT NULL,所以你必须为它的位置NULL生成值。这里,表last_login是这种数据的来源。
UPDATE users
SET last_login = (
SELECT login_attempts.timestamp
FROM login_attempts
WHERE login_attempts.user_id = users.id
AND login_attempts.success
ORDER BY login_attempts.timestamp DESC
LIMIT 1
)
WHERE users.last_login IS NULL;
如果NULL值仍然存在,比如说,因为用户从未成功登录,或者因为表last_login没有足够远地返回,您必须有一些回退,这可以是一个固定值。在这里,我选择简单的方法,简单地使用NOW()作为退路。
UPDATE users SET last_login = NOW() WHERE last_login IS NULL;
当应用运行时,这两个更新可以再次在后台运行。此次更新后,users.last_login中不应显示更多的NULL值。等待几天后,验证确实如此,是时候应用必要的约束了。
应用约束,清理
一旦您确信在列last_login中没有丢失值的行,并且您不会回滚到引入丢失值的应用版本,您可以部署停止使用表login_attempts的应用版本,处理表login_attempts,然后应用NOT NULL约束(参见图 13-1 )。
图 13-1
应用和数据库更新步骤的顺序。每个数据库版本都与之前和之后的应用版本兼容,反之亦然。
DROP TABLE login_attempts;
ALTER TABLE users
ALTER COLUMN last_login SET NOT NULL;
总的来说,一个逻辑数据库更改已经被分散到三次数据库更新(两次模式更新和一次数据迁移)和两次应用更新中。
这使得应用开发更加费力,但是您获得了操作上的优势。其中一个优势是保持应用在任何时候都可以发布到生产环境中。
先决条件
如果在几个步骤中部署单个逻辑数据库更改,则必须进行几次部署,而不是一次引入代码和模式更改的大型部署。只有当部署(至少大部分)是自动化的,并且组织提供了足够的连续性,您可以实际完成变更过程时,这才是可行的。
如果开发人员不断地灭火,他们很可能从来没有时间添加最终期望的NOT NULL约束,并且一些未被发现的 bug 将导致以后信息的丢失。
您还应该建立某种问题跟踪器,使用它您可以跟踪模式迁移的路径,以确保没有未完成的,例如,在开发人员离开公司的情况下。
工具作业
不幸的是,我不知道有哪种工具能够完全支持我所描述的数据库和应用发布周期。一般来说,有一些工具可以管理模式更改。例如,Sqitch 4 和 Flyway 5 是管理数据库变更和回滚的通用框架。
在较低的层次上,有一些工具,比如 apgdiff,可以比较新旧模式,并使用这种比较来生成 DDL 语句,将您从一个版本带到下一个版本。这种自动生成的 DDL 可以构成 Sqitch 或 Flyway 随后管理的升级脚本的基础。
一些 ORM 还带有框架,承诺为您管理模式迁移。仔细评估它们是否允许在不丢失数据的情况下进行回滚。
结构
如果您将应用部署与模式部署分离,那么您必须拥有至少两个可单独部署的包:一个用于应用,一个用于数据库模式和模式迁移脚本。如果您希望或必须支持数据库模式的回滚,您必须记住,您需要与新模式相关联的元数据才能回滚到旧版本。
模式版本 5 的数据库描述不知道如何从版本 6 回滚到版本 5,因为它对版本 6 一无所知。因此,您应该始终安装最新版本的模式文件包,并将已安装的版本与当前活动的数据库版本分开。控制模式迁移的工具可以独立于应用及其模式,因此应该存在于第三个软件包中。
没有银弹
没有一个解决方案可以在部署期间自动管理您的所有数据迁移。您必须仔细设计应用和数据库的更改,以分离并分别部署它们。这通常是应用开发方面的更多工作,但它为您购买了部署和回滚的能力,而不会受到数据库更改的阻碍。
工具可用于某些部分,但通常不适用于全局。必须有人跟踪应用和模式的版本,或者将它们自动化。
13.3 摘要
数据库中保存的状态会使应用升级变得复杂。
不兼容的数据结构和模式更改可以分解成几个较小的步骤,每个步骤都与前一个步骤兼容。
这允许在不停机的情况下升级应用,代价是必须进行多次应用和模式部署。
Footnotes 1维基百科,《数据定义语言》, https://en.wikipedia.org/wiki/Data_definition_language ,2018 年。
2
3
https://github.com/github/gh-ost/ 。
4
5
十四、总结和展望
读完这本书后,你应该对如何以及为什么为一个 Python 项目实现持续集成(CI)和持续交付(CD)有一个坚实的理解。这不是一个小任务,但是许多例子应该可以让您很快开始,甚至只有某些方面的实现也可以给您带来好处。在一个协作的环境中,展示这些好处更容易让其他人相信,在工具链和过程改进上花费时间是值得的。
14.1 接下来是什么?
在这最后一章中,让我们看看一些概念,它们可以帮助你发展一个更成熟的软件开发过程,并与 CI 和 CD 相结合。
提高质量保证
提高软件质量就像增加应用的单元测试覆盖率一样简单。然而,并不是所有的错误都可以通过这种方式捕获,例如,性能退化或您以前没有想到的错误。
为了捕捉性能回归,您可以创建一个单独的性能测试环境,并在该环境中运行一组预定义的负载和性能测试。您可以将此作为另一个阶段添加到部署管道中。
处理意想不到的案件更难,因为,根据定义,他们会出其不意地抓住你。对于某些类型的应用,自动模糊化可以找到导致应用崩溃的输入,并将这些输入作为示例提供给开发人员。
有一些架构方法可以使您的应用对意外的用户输入和错误场景更加健壮,但是从工具的角度来看,您能做的最好的事情就是使应用对这种错误的反应更加健壮。
专门的错误跟踪器可以帮助您识别这样的错误。它们让开发人员更深入地了解如何重现和诊断这些错误。例如, Sentry 1 是一款开源的集中式错误跟踪器,提供托管解决方案。
韵律学
在更大的系统和组织中,收集和聚合指标是保持系统可管理性的一个要求。甚至出现了将监测建立在时间序列数据基础上的趋势。
在部署系统的上下文中,您可以收集的一些数据点包括每个阶段或任务的开始日期和持续时间、它构建的版本以及该特定版本的特征,包括性能数据、使用数据(如参与率)、生成的工件的大小、发现的缺陷和漏洞等等。
理解收集到的数据并不总是容易的,关于这方面的书籍已经有很多了。尽管如此,建立一种收集各种指标的方法并创建有助于解释它们的仪表板是一个好主意。
基础设施自动化
配置管理对于扩展基础设施至关重要,但是像 Ansible 这样的工具不足以满足所有的需求和规模。
数据库中的配置、机密管理
随着配置数据量的增长,将其保存在纯文本文件中变得不切实际。因此,您必须维护一个配置数据库,并使用 Ansible 的动态库存机制 2 将数据传播到配置管理系统中。
然而,在数据库中存储密码、私钥和其他秘密总是一件棘手的事情。你需要在数据库上安装一个应用,以避免将这些秘密泄露给不应该访问它们的用户。
这样的应用已经存在。专用的秘密管理系统以加密的形式存储秘密,并小心地控制对它们的访问。这类应用的例子有 Square 的 Keywhiz 、??、??、,或者《流浪者》的作者 HashiCorp 的 Vault 、 4 。
秘密管理系统通常提供插件来创建服务帐户,如 MySQL 或 PostgreSQL 数据库帐户,并在无人参与的情况下轮换其密码。至关重要的是,这也意味着没有人会看到随机生成的密码。
除了将应用配置推送到运行应用的机器或容器中,您还可以构建您的应用来从中央位置获取配置。这样的中心位置通常被称为服务发现系统。CoreOS 项目的 etcd 、5和 HashiCorp 的consult、 6 等工具,让管理大量配置变得更加容易。它们还提供了额外的特性,比如对服务的基本监控,以及只向消费者公开服务端点的工作实例。
举例来说,考虑一个需要大量配置数据的应用可以只被提供一个秘密密钥,用于针对服务发现系统的认证以及关于它在哪个环境中运行的信息。然后,应用从中央服务中读取所有其他配置。如果应用需要访问存储服务,并且有多个实例可以提供该服务,则监控服务会确保应用获得一个工作实例的地址。
这种服务发现方法允许一种称为不可变基础设施的模式。这意味着您只需构建一个容器(比如 Docker 容器,甚至是一个虚拟机映像),然后,您就可以通过各种测试环境传播整个容器,而不是只传播您的应用。集群管理系统提供连接到服务发现系统的凭证;否则,容器保持不变。
基础设施作为代码
如前几章所述,传统的 CD 系统通常局限于源代码控制系统中的一个分支,因为只有一个用于部署代码的测试环境。
云基础设施改变了游戏规则。它允许对整个环境的声明性描述,包括几个数据库、服务和虚拟服务器。实例化一个新环境就变成了执行一个命令的事情,允许您将每个分支部署到一个新的、独立的环境中。
14.2 结论
自动化部署使软件开发和操作更加高效和愉快。我已经向您展示了对它的温和而实用的介绍,反过来,使您能够向您的组织介绍 CD。
对于一个开发软件的组织来说,这是一个很大的进步,但它也是自动化基础设施的一小部分,是通往高效且有弹性的软件开发过程的一小部分。
Footnotes 1https://getsentry.com/welcome/ 。
2
http://docs.ansible.com/ansible/developing_inventory.html 。
3
https://square.github.io/keywhiz/ 。
4
www.hashicorp.com/blog/vault.html 。
5
https://github.com/coreos/etcd 。
6
7
8
https://aws.amazon.com/cloudformation/ 。