应用不可变的基础架构实践有 充分的 理由 ;其中之一是您不必担心您的服务器包含以前部署的残留物。此外,您的部署过程可以非常干净,因为它们不必处理中间件和代码实时更新的细节,而是创建图像/容器和包含它们的基础设施。
在 Cloudify,我们有许多工件需要构建。从 Docker 和机器映像到可能可重定位的 Python virtualenvs、node.js tar 文件和 Windows 二进制文件。
我们希望我们的构建过程是不可变的。
流浪汉
由于我们不是一家 SaaS 公司,而是一家开源软件提供商,我们认为:为什么不使用相同的不可变基础设施概念来构建我们的工件呢?这实际上意味着每次我们想要创建一个二进制文件时都会启动一个新环境。启动临时环境的最简单方法是什么? 流浪汉 。
假设我们要创建一个 Docker 镜像。我们创建一个包含 2 个虚拟机的“Vagrantfile”:
# -*- mode: ruby -*-
# vi: set ft=ruby :
AWS_ACCESS_KEY_ID = ENV['AWS_ACCESS_KEY_ID']
AWS_ACCESS_KEY = ENV['AWS_ACCESS_KEY']
BASE_BOX_NAME = 'ubuntu/trusty64'
Vagrant.configure('2') do |config|
config.vm.define "ubuntu" do |ubuntu|
#dummy box, will be overridden
ubuntu.vm.box = "dummy"
ubuntu.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box"
ubuntu.vm.provider :aws do |aws, override|
aws.access_key_id = AWS_ACCESS_KEY_ID
aws.secret_access_key = AWS_ACCESS_KEY
# official ubuntu 14.04 64bit box
aws.ami = "ami-036eaa74"
aws.region = "eu-west-1"
aws.instance_type = "m3.medium"
aws.keypair_name = "vagrant_build"
override.ssh.username = "ubuntu"
override.ssh.private_key_path = "/home/.ssh/aws/vagrant_build.pem"
aws.tags = {
"Name" => "vagrant docker images build",
}
aws.security_groups = "vagrant_cfy_build"
end
# need to sync folders
ubuntu.vm.synced_folder "../../", "/cloudify-packager", create: true
ubuntu.vm.provision "shell", path: "provision.sh", privileged: false
end
config.vm.define :local do |local|
local.vm.provider :virtualbox do |vb|
vb.customize ['modifyvm', :id, '--memory', '1024']
end
local.vm.box = BASE_BOX_NAME
local.vm.hostname = 'local'
local.vm.synced_folder "../../", "/cloudify-packager", create: true
local.vm.provision "shell", path: "provision.sh", privileged: false
end
end
第一个 AWS VM 用于我们的正式构建过程。
由于 Vagrant 对 IaaS 的抽象,我们可以选择适合特定构建的机器属性,从而更容易优化构建过程。例如,如果我们知道我们有一个针对特定工件的漫长而复杂的构建过程,我们可能希望为构建机器提供大量资源(反之亦然)。
通过使用 Vagrant,我们还能够提供本地可执行的构建系统。第二个 VM 允许人们在本地构建他们需要的任何工件,而不是绑定到特定的构建系统。
`provision.sh` 脚本只是生成工件所需的脚本。在这种特殊情况下,我们安装和配置 Docker 和 docker-compose ;在驻留在同步目录中的 Dockerfile 上运行“docker-compose build ...”并将图像保存到 tar。然后,我们自动将 tar 推送到 S3。上例中的“cloudify-packager”包含构建过程所需的文件,因此它们会自动 rsync-ed 并在 VM 中使用。
另一个例子是为不同的发行版创建 Cloudify 代理 。我们需要在 Ubuntu Trusty and Precise、CentOS 6.4、Debian Jessie 和 Windows Server 2013 上编译我们的代理。
我们有一个“Vagrantfile”,其中包含基于 AWS 和 Virtualbox 的 虚拟机,以及与我们需要的操作系统和发行版相匹配的图像。我们提供一个支持多发行版的“provision.sh”文件来构建所有基于 Linux 的代理,以及另一个专门用于 Windows 的“provision.windows.bat”文件。
剩下的就是运行 `Vagrant up debian_jessie_aws` 和噗!,生成一个 debian jessie 代理。
脱钩天堂
这个过程将每个工件的生成机制与管理它的特定构建系统完全分离。它还允许我们并行化我们的构建过程,因为我们可以在我们选择的任何 IaaS 供应商中启动无限数量的 Vagrant 机器(只要该供应商有 Vagrant 插件)并且不必依赖我们的 Jenkins 集群的容量或我们为构建过程编写生成机器的逻辑。
此外,我们使用哪个构建系统来创建工件并不重要。这只是在不同的环境中执行相同的命令。
这种解耦机制还允许我们为客户提供一种自行生成工件的方法。作为开源软件,`git clone` 和 `Vagrant up` 是客户构建自己的工件所需的全部。
可读日志
Vagrant 在其配置过程中提供了可读性很好的日志。这使我们能够相对轻松地调试与构建相关的问题。将来,我们打算将这些日志发送到外部 ELK 堆栈、Logentries 或 Loggly,以便能够更深入地分析我们的构建过程。
帕克进来玩的地方
显然,我们对每次启动 VM 时都安装所有构建要求不感兴趣。
我们将使用 Packer 构建包含构建过程基本要求的图像。
我们将使用一个包含配置的 packerfile.json 文件来创建一个包含 Docker 的镜像,稍后我们的 Vagrantfile 将使用它。 (是的。我们可以使用 Docker 构建器,但因为它在本地机器上运行,而且我们宁愿启动一个新的、干净的虚拟机,所以我们这样做。)
# -*- mode: ruby -*-
# vi: set ft=ruby :
AWS_ACCESS_KEY_ID = ENV['AWS_ACCESS_KEY_ID']
AWS_ACCESS_KEY = ENV['AWS_ACCESS_KEY']
BASE_BOX_NAME = 'ubuntu/trusty64'
Vagrant.configure('2') do |config|
config.vm.define "ubuntu" do |ubuntu|
#dummy box, will be overridden
ubuntu.vm.box = "dummy"
ubuntu.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box"
ubuntu.vm.provider :aws do |aws, override|
aws.access_key_id = AWS_ACCESS_KEY_ID
aws.secret_access_key = AWS_ACCESS_KEY
# official ubuntu 14.04 64bit box
aws.ami = "ami-036eaa74"
aws.region = "eu-west-1"
aws.instance_type = "m3.medium"
aws.keypair_name = "vagrant_build"
override.ssh.username = "ubuntu"
override.ssh.private_key_path = "/home/.ssh/aws/vagrant_build.pem"
aws.tags = {
"Name" => "vagrant docker images build",
}
aws.security_groups = "vagrant_cfy_build"
end
# need to sync folders
ubuntu.vm.synced_folder "../../", "/cloudify-packager", create: true
ubuntu.vm.provision "shell", path: "provision.sh", privileged: false
end
config.vm.define :local do |local|
local.vm.provider :virtualbox do |vb|
vb.customize ['modifyvm', :id, '--memory', '1024']
end
local.vm.box = BASE_BOX_NAME
local.vm.hostname = 'local'
local.vm.synced_folder "../../", "/cloudify-packager", create: true
local.vm.provision "shell", path: "provision.sh", privileged: false
end
end
在这种情况下,`provision.sh` 安装 Docker 和 `docker-compose` 并公开 Docker API(用于 docker-compose)。然后在 Vagrantfile 中使用生成的 AMI。另一个例子是生成需要 ruby、 fpm 和 packman 的代理程序包。我们可以使用 Packer 为每个发行版创建一个包含它们的图像,而且……很有趣!
有缺点,但即便如此......
这个解决方案并不完美。启动 VM 需要时间,并且是一个容易出现环境故障的过程。此外,它花费更多的钱并暴露出一些与安全相关的问题(显然,在安全方面,无 VM 比 VM 更好)。
赢!
我们已经看到它是值得的。在进行转换之前,不干净的机器正在生成我们的工件,我们不断地偶然发现失败的构建。为多个工件构建流程的并行执行添加几分钟并不是一个破坏交易的因素。机器没有足够长的时间被破坏(当然安全组配置正确);当这个解决方案为我们提供了每次都能获得干净环境的能力时,每月几百美元不是问题。
顺便说一句,通过在云上构建提供更快的 VM 配置,如本地 OpenStack 安装、Exoscale(托管 Cloudstack)或 Digital-Ocean,也可以减少启动 VM 的时间。我们选择 AWS 是因为它的稳定性和可用性。在不久的将来,我们可能(并且很可能会)切换到不同的提供商。谁知道呢……也许我们会为此开始使用 Docker……
所以?
自从我们开始如上所述构建我们的工件以来,我们大多只遇到构建过程逻辑的问题和(很少)缺少先决条件。通过使用包含尽可能多的先决条件的图像,可以大大减少这些。