CoreOS那些事之Rkt容器尝鲜(下)

2015年是各种容器技术与名词扎堆的一年,Docker的出现使得“应用容器”的实施变得易如反掌的同时,也带动了它的许多竞争者。其中一个比较有趣的看点就在于“容器规范”的较量,最近红帽和英特尔也按捺不住,拿出自家的产品趁势搅局。

5月14日,红帽宣布了新的多容器应用规范Nulecule(DockOne翻译了这篇新闻),同时推出符合这个规范的一个实现:AtomicApp。(这个路子怎么看都有点像AppC和Rkt采用的模式)

因特尔则是发挥自家的特长,在5月18日,发布了介于虚拟机与容器之间的跨界产品ClearLinux(DockOne同样翻译了这篇新闻)。之所以特别要提这个项目,是因为它首先会实现基于Rkt/AppC规范的容器模型,而将Docker放在了其次的位置。

“容器规范”的概念,看起来让人有些摸不着头脑,但在容器业界中,它确实是颇具诱惑力的一块蛋糕。在这一篇里,我们就来聊一聊支撑Rkt背后的那个“容器规范”:AppC Spec。

AppC规范究竟约定了什么

使用了开源软件的人,未必都会有心情仔细阅读各种开源协议的内容。大多数的使用容器产品用户,也不见得要对容器规范的内容有很高的兴致。

不过,为了更好的理解后面将要介绍到的相关工具,还是不妨稍微深入的了解一些AppC规范约定的内容。其内容归纳起来主要有四个方面,下面依次罗列出来,并与当下的主流容器Docker做一个简要的对比。

PS:严格来说,AppC与AppC Spec两个词是有区别的。前者指的是CoreOS的App Container这个项目,包括规范和相关的工具,而后者特指AppC中约定的容器规范。但在许多地方,特别是翻译的文章中,经常看到这两个词被混用,因此一般也不必太讲究了。

1.容器的镜像格式

本质上说,容器镜像就是符合特定目录结构的文件压缩包。镜像中的内容在容器启动后被展开,然后复制到一个独立的namespace空间内,并通过cgroup限制容器能够使用的系统资源。稍后在制作镜像时,会详细介绍AppC Spec规定的镜像目录结构。这里只先指出一点,AppC的镜像没有支持像Docker那样的分层结构,这种设计简化了容器运行时的一些操作,但带来的弊端也是很明显的:无法复用镜像相同的部分。因此在磁盘空间的利用上造成了浪费,也增加了容器镜像在网络传输成本。

除了目录的结构,镜像还需要一个描述镜像内容的文件,称为“镜像属性清单文件(Image Manifest)”,其中定义的内容包括:镜像的作者信息、容器暴露的端口、暴露的挂载点、所需的系统资源(CPU/内存)等。此外,AppC Spec的约定的属性清单中,还会包含许多编排调度所需的信息,例如容器运行所依赖的其他容器、容器的标签。

在这方面来说,AppC镜像的信息量远远多于Docker镜像。相当于囊括了Docker镜像本身、Compose编排配置以及一部分Docker运行参数的内容。

此外,AppC规范也约定的镜像ID和签名的生成方法,关于镜像ID和签名的作用和在Rkt文章上篇中已经介绍过,稍后还会详细介绍镜像签名的生成方法。

2.镜像的分发协议

分发协议主要是约定镜像下载使用的协议类型和URL的样式。AppC的镜像URL采用类似Docker的domain.com/image-name这样的格式,但其实际处理方式有些不同。此外,在没有指定域名时,Docker会默认在官方的DockerHub寻找镜像,AppC的镜像没有所谓“官方源”,因此也没有这样的规则。

Rkt/AppC目前支持以下几种URL格式:

  • <域名>/<镜像名>
  • <本地文件路径>
  • https://<完整网络路径>
  • http://<完整网络路径>
  • docker://<与Docker一样的镜像URL>

第一种方式是AppC推荐的镜像分发URL,这种方式有点像Docker Repository,但实际上只是HTTPS协议的简写方式。AppC会根据指导的域名和路径依照约定的方式转换为完整URL地址,然后下载指定的镜像。

第二种方式相当于导入本地镜像。值得一提的是,即便使用本地镜像,AppC同样要求镜像有签名认证,关于签名文件的细节在后面的内容里会详细讨论。

第三种和第四种方式都是直接通过完整URL获取镜像,规范中并不推荐直接这样使用裸的HTTPS的URL,因为这种命名过于随意的镜像地址不利于镜像的管理和统一,特别是HTTP协议的URL更只应该在内网的环境中出现。

第五种方式不是AppC规范支持的协议类型,目前只是Rkt支持这种协议(本质上还是HTTP或HTTPS)。兼容Docker镜像的URL,只需要在前面加上docker://即可,下载后会自动转换为AppC镜像格式。由于Docker的镜像仓库不支持签名认证,使用这种URL时,用户需要显示的加上参数--insecure-skip-verify允许使用未认证的镜像来源。

3.容器的编排结构

AppC规范中的容器编排和集群描述方式与Kubernetes十分相似,采用“容器组属性清单文件(Pod Manifest)”描述。其中沿用了Kubernetes中诸如Podslabels等用于在集群中进行调度策略的规划和管理的概念。

Pods直译便是“豆荚”,它指的是由一系列相互关联的容器组成的,能够对外提供独立服务功能的容器集合。例如将用于数据收集功能的容器、用于缓存服务的容器以及用于搜索服务的容器组合在一起,作为一个Pod提供完整的数据查询服务暴露给外部用户。Pod可以作为容器参与集群调度的单独集合提供给集群管理器,在例如Kubernetes这样的集群管理模型中,Pod实际上就是进行服务跨节点调度的最小单位。

labels用于标示具有同一类特性的容器,为容器的过滤和选择提供了十分灵活的策略。许多的集群管理器都能够在调度时利用选择指定标签对Pods进行筛选。

考虑到CoreOS公司与谷歌共同合作的背景(已经推出了Tectonic CaaS平台),这样的设计为Kubernetes未来与符合AppC规范的容器进行深度集成提供了良好的技术基础。

4.容器的执行器

执行器,也就像是Rkt这样的容器工具。这个部分规范了设计符合AppC Spec的容器执行器所需要遵循的原则和应该具备的功能。

例如,必须为每个容器提供唯一的UUID;在容器的运行上下文中必须至少提供一个本地Loopback网卡,以及0个至多个其他TCP/IP网卡;应该将容器中程序打印到Stdout和Stderr的日志进行收集和展示等细节。

其中还详细约定了,对于镜像属性清单中的诸多属性,执行器应当如何进行处理。这些内容对大部分的使用者而言都只能作为参考,还是需要以具体实现的容器产品文档为准。

镜像工具

在AppC的项目中,除了一纸洋洋洒洒的规范文书以外,还提供了不少AppC镜像相关的示范工具。不像Docker这种一个命令集成所有功能的玩法,这些工具中的每个只是关注于容器的某个方面功能。例如通用类型镜像的制作、打包、格式转换以及特定类型镜像的制作等。

目前已有的工具主要包括:

  • Actool – 用于镜像的构建和验证
  • Docker2Aci – 用于将Docker导出的镜像转换为AppC镜像
  • Goaci – 用于Golang语言的项目一键打包和构建镜像的工具
  • Acbuild – 通过指令的方式构建镜像

其中,ActoolAcbuild都是用于镜像构建的工具,它们的区别类似于通过docker commit和Dockerfile两种方式构建镜像。需要指出的是,前不久才刚刚建立的Acbuild项目,现在还只是一个计划,没有发布任何实际可用的版本,其目的是替代之前的另一个项目baci。后者已经无法使用并且不再继续更新。

Goaci的作用是获取指定路径的项目,进行自动编译,然后把编译后的可执行程序制作成一个镜像,所有的这些操作只需要一条命令就可以完成:goaci <项目路径>,项目路径支持所有go get命令所支持的代码托管网站,包括BitBucket、GitHub、Google Code和Launchpad等。不过,它只能用于使用Golang语言并托管在上述网站中的开源项目。不具有普遍的适用性。

下面将重点介绍ActoolDocker2Aci这两个工具。为了方便非CoreOS系统用户尝鲜,也会介绍在这些工具在其他64位Linux发行版的安装方法。

镜像的制作

与镜像制作相关的工具是Actool,这个软件已经预装在CoreOS系统较新的版本上了,可以通过actool --help命令验证并获得Actool的版本。其他64位Linux的用户可以通过下面的命令安装它:

wget https://github.com/AppC/spec/releases/download/v0.5.2/AppC-v0.5.2.tar.gz
tar zxf AppC-v0.5.2.tar.gz
sudo mv AppC-v0.5.2/actool /usr/local/bin/

说到构建镜像,前面已经提到,新的命令式构建镜像的工具Acbuild目前还没有发布任何可用版本。因此当下的情况是,构建AppC镜像还只能手工创建镜像属性清单文件,拷贝容器中所需的文件,然后直接打包生成镜像。索性,这样创建镜像除了失去诸如“基础设施即代码”的好处以外,并没有多少值得非议的地方,构建流程本身并不复杂。

下面来制作一个十分朴素的AppC容器镜像,这个镜像中只包含一个可执行文件。

首先新建一个用于制作镜像的工作目录,例如AppC-image

>mkdir AppC-image

接下来,为了让这个例子足够简单,我们需要一个能够不依赖任何外部动态库或运行时环境,能够单独运行的程序。我们写一个C语言的“Hello World”吧。新建一个叫hello.c的文件,内容如下:

#include <stdio.h>
int main(int argc, char* argv[])
{
    printf("Hello AppC\n"); //随便输出点什么东西
    return 0;
}

然后,需要一个C语言编译器,有些Linux系统已经自带了这个东西,用gcc --version命令可以验证。如果没有安装,那么…大家看着办吧,比如在Ubuntu系统下面可以通过apt-get来获取:

sudo apt-get install gcc

CoreOS系统会稍微麻烦一点,需要借助一个额外的容器来完成。提示一下,Docker有一个官方的C/C++语言运行环境镜像,就叫gcc。可以docker pull gcc或者rkt --insecure-skip-verify fetch docker://gcc来获取它,然后启动一个容器,注意需要映射一个Volumn到主机上,方便编译完成后将生成的可执行程序拷贝出来。

编译的命令如下,其中的--static参数是必须的,否则编译出来的程序在执行时会需要依赖外部的动态库:

gcc --static -o hello hello.c

在刚刚工作目录里面新建一个叫rootfs的目录,将编译生成的hello可执行文件拷贝进去。这个rootfs目录中的内容就是以后容器里所包含的文件内容了,因此建议在其中再建立一些标准的目录结构,例如/bin目录,将可执行程序放到这个目录里面。

mkdir -p AppC-image/rootfs/bin
cp hello AppC-image/rootfs/bin/

现在镜像的目录结构已经成型了。下面就可以开始创建镜像的属性清单文件了,在工作目录中新建一个名为manifest的文件,内容如下:

{
    "acKind": "ImageManifest",
    "acVersion": "0.5.2",
    "name": "my-app",
    "labels": [
        {"name": "os", "value": "linux"},
        {"name": "arch", "value": "amd64"}
    ],
    "app": {
        "exec": [
            "/bin/hello"
        ],
        "user": "0",
        "group": "0"
    }
}

此时,工作目录里的文件结构应该是这样的:

AppC-image
├── manifest
└── rootfs
    └── bin
        └── hello

最后就可以用actool命令构建镜像了:

actool build AppC-image hello.aci

镜像的验证

容器镜像的来源无非有两种:本地的或者远程的。

因此对镜像的验证也就包含两部分内容。

  • 检验本地的文件是否符合AppC规范的镜像
  • 检验远程的URL是否是有效的AppC镜像地址

对于前一种情况,前面说过,容器镜像其实就是符合一定标准结构的打包文件。

$ file hello.aci
hello.aci: gzip compressed data

在AppC规范中,镜像文件的后缀名应该是.aci,但具有这个后缀名的打包文件未必就是正确的镜像。因此需要一个方法来验证镜像文件的正确性,相应的命令是actool validate

直接执行这个命令时,actool只会通过命令的返回值表示验证的结果,返回0表示验证通过:

$ actool validate hello.aci
$ echo $?
0

可以加上-debug参数让actool直接将结果打印在控制台上:

$ actool -debug validate hello.aci
hello.aci: valid app container image

对于后一种情况,URL的验证,同样可以通过actool工具完成,相应的命令是actool discover

这个命令会返回镜像的实际下载地址:

$ actool discover coreos.com/etcd
ACI: https://github.com/coreos/etcd/releases/download/latest/etcd
-latest-linux-amd64.aci, ASC: https://github.com/coreos/etcd
/releases/download/latest/etcd-latest-linux-amd64.aci.asc
Keys: https://coreos.com/dist/pubkeys/aci-pubkeys.gpg

试一下在Rkt里面用过的Docker镜像地址,会发现这个地址是无效的。

$ actool discover docker://ubuntu
error fetching docker://ubuntu: Get https://docker?ac-discovery=1: 
dial tcp: lookup docker: no such host

这也证实了在前面说的,docker://这种协议只是Rkt额外支持的镜像获取方式,并不是AppC的规范中的标准协议。

镜像的转换

AppC的规范制定者们显然很清楚,哪些轮子该重造,哪些轮子是可以直接复用的。在Docker的各种镜像已然是铺天盖地的当下,一个新的容器工具想要最快积累镜像数量,最好的办法就是兼容Docker镜像或者将Docker的镜像进行转换。

其实对于镜像兼容这个问题,新标准们有各自不同的做法,红帽的Nulecule选择了支持Dockerfile格式,只需要把已有的镜像代码加上一些额外的配置文件,重新构建一次就可以。而AppC做过同样的尝试,(之前有个baci项目就是干这个的,不过已经没有更新了),效果上有些不伦不类,因此索性更干脆,提供个工具允许将任何的Docker镜像导出后直接转换成自己的镜像格式。

下面就来说说从Docker到AppC镜像的转换,相应的工具是Docker2Aci

这个工具不论是在Ubuntu或者CoreOS上都没有预装,因此需要单独安装。这个工具还没有正式发布,因此官方也没有提供编译好的二进制包,要获得它只能从源代码编译。值得庆幸的是,Golang语言的编译方式还是比较友好的,如果本地已经安装了Golang语言的开发环境,可以直接通过go get github.com/AppC/docker2aci命令完成整个下载和编译的过程。

考虑到大多数用户都是没有Golang开发环境的,另一个比较简单的办法是:通过容器。因为,Docker官方已经提供了一个用于Golang开发的容器镜像,名字就叫golang。用下面一条命令就可以搞定编译。

sudo docker run -v $(pwd):/pkg -i -t golang:1.4 /bin/bash -c "go 
get github.com/AppC/docker2aci; cp \$GOPATH/bin/docker2aci /pkg/"

编译好的docker2aci二进制文件会被拷贝到当前目录,将它放到系统变量PATH所指的的任意目录中即可,比如:

sudo mv docker2aci /usr/local/bin/

执行docker2aci --version命令可以打印出软件的使用帮助,证明已经成功安装。

$ docker2aci
Usage of docker2aci:
docker2aci [--debug] [--nosquash] IMAGE
  Where IMAGE is
    [--image=IMAGE_NAME[:TAG]] FILEPATH
  or
    docker://[REGISTRYURL/]IMAGE_NAME[:TAG]
Flags:
  -debug=false: Enables debug messages
  -image="": When converting a local file, it selects a particular image to convert. Format: IMAGE_NAME[:TAG]
  -nosquash=false: Don't squash layers and output every layer as ACI

将一个Docker镜像导出,然后可以通过docker2aci命令转换成AppC镜像。

$ docker pull ubuntu
$ docker save -o ubuntu.docker ubuntu
$ ./docker2aci ubuntu.docker
... ...
Generated ACI(s):
ubuntu-latest.aci

转换后的镜像会保存在当前目录,并自动用“<镜像>-<标签>”的格式命名。

此外,比较实用的是,docker2aci同样支持docker://协议的URL直接获取网上的镜像。

$ docker2aci docker://busybox
... ...
Generated ACI(s):
busybox-latest.aci

镜像的签名

这个时候,如果直接用Rkt运行刚刚创建的hello.aci镜像,会发现Rkt提示由于找不到有效的签名文件,因此拒绝运行这个镜像。

$ sudo rkt run hello.aci
error opening signature file: open /home/core/hello.aci.asc: no such file or directory

镜像的签名,是AppC引入的一种镜像来源验证机制,本质上是利用非对称加密的标准数字签名。通过将镜像提供者的私钥和镜像文件本身加密生产一组签名字符串,通过发布者提供的公钥就能够解开这串字符并得到与镜像匹配的信息,这样就能验证镜像是否是真的来自特定的作者或来源。

AppC的签名算法是标准是RSA,采用的是开源的GPG实现,关于GPG的详细介绍参考这篇文章。

首先准备一个密钥配置文件,命名为gpg-batch,内容如下:

%echo Generating a default key
Key-Type: RSA 
Key-Length: 2048
Subkey-Type: RSA 
Subkey-Length: 2048
Name-Real: 你的英文名字
Name-Email: 你的邮箱地址
Name-Comment: ACI signing key
Expire-Date: 0
Passphrase: 签名时的密码
%pubring rkt.pub
%secring rkt.sec
%commit
%echo done

然后用下面的命令生成一个密钥对:

gpg --batch --gen-key gpg-batch

执行完成后,在目录中会多出rkt.secrkt.pub两个文件,这就是私钥和公钥了。

然后就可以使用这对密钥给镜像签名了:

gpg --no-default-keyring --armor --secret-keyring ./rkt.sec 
--keyring ./rkt.pub --output hello.aci.asc --detach-sig hello.aci

在提示输入密码时,输入在gpg-batch中设置的密码。然后就获得了hello.aci镜像的签名文件hello.aci.asc

再次尝试运行容器:

$ sudo rkt run hello.aci
openpgp: signature made by unknown entity

这次提示的错误是,签名文件虽然找到了,但是这个签名的来源并没有在信任列表中。为了将签名添加到信任里面里面,首先要用rkt.secrkt.pub这两个二进制的密钥文件导出为一个文本的公钥文件。

gpg --no-default-keyring --armor --secret-keyring ./rkt.sec 
--keyring ./rkt.pub --export 在gpg-batch中的邮箱 > pubkeys.gpg

然后将这个文本文件中的公钥添加到Rkt的信任列表中。

$ sudo rkt trust --root pubkeys.gpg
Prefix: ""
Key: "pubkeys.gpg"
GPG key fingerprint is: 37E2 6071 5382 5868 5A0D  1356 98A9 5E24 6E19 7AED
    Subkey fingerprint: 46AF 81E4 77D4 BFCA DFCE  73C6 3D94 79C2 2611 F243
    Kelsey Hightower (ACI signing key) <kelsey.hightower@coreos.com>
Are you sure you want to trust this key (yes/no)? yes
Trusting "pubkeys.gpg" for prefix "".
Added root key at "/etc/rkt/trustedkeys/root.d/37e26071538258685a0d135698a95e246e197aed"

这次运行容器,就可以看到容器中的Hello程序已经正确的执行了。

$ sudo rkt run hello.aci
rkt: signature verified:
  Kelsey Hightower (ACI signing key) <kelsey.hightower@coreos.com>
Hello AppC

镜像的仓库

最后简单的介绍一下AppC的镜像仓库(Image Repository)。

AppC规范定义了获取镜像的URL,其形式大致是域名/镜像路径:版本,例如CoreOS提供的包含Etcd的镜像可以通过命令rkt fetch coreos.com/etcd:v2.0.9来获取。

这里只说两个比较有意思的地方。

首先,AppC是没有所谓“官方镜像仓库”的,所以URL中的域名部分始终会存在。由CoreOS公司提供的镜像被放在coreos.com域名下的普通仓库中。这一点也符合AppC标准开放化的初衷。

其次,AppC会对用户的URL尝试通过两种方式解析。换句话说,镜像仓库的实现方式可以有两种。第一种几乎不需要额外的配置工作,将镜像依照一定的命名规则放在域名的相应路径下即可,例如coreos.com/etcd:v2.0.9,如果使用第一种方式建仓库,相应的镜像就应该保存在https://coreos.com/etcd-v2.0.9-linux-amd64.aci(当然,CoreOS的镜像实际上是用的第二种方式,所以这个路径不存在)。第二种方式更加灵活,但需要额外的程序来处理镜像地址的映射,具体过程,不再详述。

此外,前面介绍过,对于内网环境,还可以直接使用HTTP路径获取镜像。举个栗子,把之前制作好的镜像文件hello.aci和签名文件hello.aci.asc放到一个目录里面,然后在这个目录中启动一个简易的HTTP服务:

$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

在任意另一个主机上就可以直接使用HTTP全路径下载镜像了。

$ sudo rkt fetch http://<服务器IP>:8000/hello.aci
... ...
Downloading ACI: [        ] 1.26 KB/358 KB
sha512-f7a2feff02a07ed7c604c14133b7aede

这种简单粗暴到无以复加的方式,对于懒人们也许算得上是一种福利,然而不论是从安全性还是镜像版本的可管理性上看来,其能力都相当弱。这从一个侧面说明,一些为开发者们提供的便利通道,如果没有规范的约束,可能带来很大的隐患。在实际运用时,建议遵循AppC的URL解析规范设计仓库为上策。

小结

与处于Alpha开发期的Rkt一样,AppC项目距离最终的成熟还有很长的路要走。然而随着谷歌、VMWare和因特尔等互联网公司的加入,他们旗下的产品,如Kubernetes、Photon、ClearLinux等都已经将集成AppC容器作为其运营策略的一部分,其运用场景也日渐明朗。

“轻量”、“开放”、“安全”,很难说这样一种由“一个公司牵头,多个公司入伙,希望依赖社区力量发展”的容器标准会走向何方。至少我们已经看到,不论是CoreOS的AppC还是红帽的Nulecule,这些后来的容器标准的出现,或多或少是为了改善Docker的某些短板,也为容器业界提供了的一些可供选择的新思路。从技术的大环境来说,它们的未来是值得期待的。

本条内容转载自InfoQ:http://www.infoq.com/cn/articles/coreos-rkt-container-part-02

Share

CoreOS那些事之Rkt容器尝鲜(上)

从CoreOS发布Rocket应用容器项目到现在,已经过去半年时间了。为了增加辨识度,项目更名为了Rkt。在沉寂了许久后,最近又开始在社区里出现了一些新鲜的声音。

首先是4月7日的一条新闻,Google领头投资CoreOS公司1200万美元以共同合作发展旗下的Kubernetes组件。此次合作除了促成新的商业发行版Tectonic的诞生,也使得Rkt容器与Kubernetes的关系拉近了一步:Kubernetes将提供对Rkt的友好支持,而Rkt则将沿用Kubernetes的Pods等概念来规划容器。

随后的4月20日,VMWare发布了旗下的通过容器部署应用的开源操作系统发行版:Photon。这个项目比较闪亮的地方在于,除了最知名的Docker容器,Photon还支持另外两种容器标准:CloudFoundry旗下的Garden,以及我们即将介绍的AppC/Rkt。CoreOS也在多篇博客中暗示VMWare其实以已经是AppC规范社区的一员。

就在5月4日的CoreOS Fest大会上,Linux大鳄红帽公司(Red Hat)联合谷歌(Google)、VMware和Apcera共同宣布,将大力支持AppC社区的发展。红帽和谷歌甚至确认将派出工程师协助维护AppC项目

一个还在Alpha阶段的项目获得如此高的关注,对于CoreOS公司确实是个好兆头。这个系列接下来的两篇里,将带大家走进Rkt容器和它所代表的AppC容器规范,且做抛砖引玉之用。

Rkt的前世今生

作为一个年轻的应用容器新秀,开源的Rkt并不是一个人在战斗,在它的背后支撑的是一支俨然有序的庞大社区力量。

诞生背景

Rkt项目最初的发起者是CoreOS公司。

CoreOS公司与其核心产品CoreOS操作系统是实至名归的最早一批Docker企业级用户,伴随着Docker从最初的0.1版本一直走到正式发布的1.0版本。起初两者相互促进,合作甚好。然而,随着Docker在容器界一家独大的趋势越来越明显,其周边的生态逐渐的从单纯的围绕构建容器化应用服务,发展成了自上而下的集群规范体系,甚至部分取代了操作系统的服务进程调度工作。这种臃肿而受Docker单方面控制的容器规范,是CoreOS系统所不待见的,他们想要一个更加开放而中立的容器标准。

2014年12月,CoreOS公布了自己的容器计划,并在几个月后结合社区中的容器实践,着手制定新的开放应用容器规范,Rkt则作为此规范中的一个具体实现而继续发展。

AppC标准应用容器规范

AppC的全称是“Application Container Specification(标准应用容器规范)”,这个规范的制定不是为了服务于特定的Linux系统环境,其初衷在于制定一组不依赖于具体平台、技术、操作系统和编程语言的容器虚拟化规范,解除已经初露端倪的企业容器产品互不兼容、各自封闭发展的危机,防止更多技术壁垒的产生。

正在制定中的AppC容器规范设计目标包括:

  • 组件式工具:用于下载、部署和运行虚拟容器环境的操作工具应该相互独立、互不依赖且可被替换。
  • 镜像安全性:镜像在因特网下载传输时应当使用加密协议,容器工具应当内置验证机制,以拒绝不安全来源的镜像。
  • 操作去中心化:镜像分发应该支持可扩展的传输协议,未来允许引入P2P,甚至BitTorrent协议来提升镜像分发效率,且容器使用前不应需要登录特定的镜像仓库。
  • 开放性标准:容器镜像的格式与元数据定义应该由社区设立统一协商制定,使得符合这一规范的不同容器产品能够共享镜像文件。

为了确保规范的开放并兼顾多方利益,CoreOS作为规范的倡导者、参与者和实施者,但并不会成为其唯一的制定者。

目前已经在遵循AppC的开源应用容器除了Rkt,还有FreeDBSD平台的容器Jet Pack和Linux平台通过C++实现的容器Nose Cone。更好的通用性,更小的入侵性,以及更高的开放性,正是由于这些AppC规范的独具匠心,使得它在Docker如日中天之时,恰逢其时的给业界带来一阵清风,引发许多技术玩家和企业的驻足和思考。

Rkt容器使用尝鲜

且不论AppC的未来发展究竟会如何,远水不解近渴,既然Rkt容器本身是开源免费的,何不自己动手尝试一番。

作为开放式容器标准的样板项目,Rkt自然不会只能用在CoreOS的自家系统里。与Docker相似,Rkt虽然也被预装在了CoreOS系统中,但其他的任何Linux发行版都可以安装并使用它。在接下来的部分里,我们将以比较常见的64位Ubuntu系统为例,演示Rkt的安装和使用方法。

在64位Ubuntu中安装Rkt

事实上,在任何Intel架构的64位Linux中,安装Rkt都简单到极致。这得益于Golang语言原生的静态编译方式,几乎所有编译过的Golang程序都可以下载即运行,而不需要安装额外依赖。

从GitHub网站可以直接下载到打包好的Rkt的二进制文件,目前最新的版本是v0.5.4。Rkt只提供了64位的编译版本,虽然通过自行编译源代码的方式也能够得到32位的可执行文件,但在32位系统上运行Rkt是不被官方推荐和支持的。

wget https://github.com/coreos/rkt/releases/download/v0.5.4/rkt-v0.5.4.tar.gz

下载后得到一个压缩文件。然后,恩,解压它。

tar xzvf rkt-v0.5.4.tar.gz

将解压后得到的文件统统放到系统的可执行目录里面,安装就算完成了。

sudo cp rkt-v0.5.4/* /usr/local/bin/

接下来,试在命令行下执行不带任何参数的rkt version命令,可以看到程序返回了Rkt工具和AppC标准的版本信息,说明Rkt已经正确的安装了。

$ rkt version
rkt version 0.5.4
appc version 0.5.1+git

Rkt工具的组成

工欲善其事,必先利其器。在前面安装Rkt时,我们还没来得及多看一眼,就直接将解压后的文件一股脑儿丢进了系统目录里。这种部署方式虽然方便,但也实在简单粗暴。现在安装完了,至少还是得回头瞅瞅这个目录里到底提供了哪些东西。

不过,这一瞅简直让人失望,偌大一个压缩包,里面就这么俩文件。

$ ls rkt-v0.5.4
rkt  stage1.aci

第一个文件刚刚已经试过了,它是Rkt容器的主程序,所有操作容器的命令都会通过这个命令作为入口。而第二个文件是个非常巧妙的设计。首先它的aci后缀已经表明了它的身份,是一个标准的AppC镜像。如果将这个镜像解包就会看到里面包含的是一套完整的systemd运行环境,其实它的作用,是为Rkt提供了可替换的容器虚拟化实现组件。

Rkt容器默认是采用基于systemd-nspawn命令的机制来处理与内核cgroup和namespace相关操作的,而这个部分正是提供整个容器虚拟化能力的核心环节。Rkt通过将这部分的功能分离到一个可以快速外挂和替换的容器中,从而支持扩展其他的虚拟化实现方式(官方的举例是比如novmqemu-kvm),并能快速的在这些实现间切换使用。具体来说,只在运行容器时指定--stage1-image参数,设置成其他符合要求的虚拟化实现的镜像地址即可。

此外,AppC规范还提供了一套用于制作和转换“符合AppC容器标准的”镜像的工具,这部分内容将会在系列的下篇中进行详述。

权限与镜像签名

在目前阶段的Rkt还必须通过root用户来执行大多数的命令,不过未来也计划支持如Docker那样使用普通用户运行。在运行Rkt命令时如果出现“permission denied”的错误,请检查是否忘记了sudo。

使用Docker的用户,大多比较习惯在网上随便找到一个镜像地址,就直接pull下来使用。因此,即便Docker官方发现了一些恶意镜像的发布者,也无法有效的阻止这些镜像在网络的传播。

AppC规范中,特别强调了镜像的安全性。为此,在默认情况下,所有镜像在下载和运行前,都必须明确的添加镜像发布者的签名信任,以确保镜像的来源不会被伪造。下面的命令将从官方仓库中获取前缀为“coreos.com/etcd”的签名信息,并添加到本地信任列表中。

$ sudo rkt trust --prefix coreos.com/etcd
Prefix: "coreos.com/etcd"
Key: "https://coreos.com/dist/pubkeys/aci-pubkeys.gpg"
GPG key fingerprint is: 8B86 DE38 890D DB72 9186  7B02 5210 BD88 8818 2190
  CoreOS ACI Builder <release@coreos.com>
Are you sure you want to trust this key (yes/no)? yes <--输入"yes"回车
Trusting "https://coreos.com/dist/pubkeys/aci-pubkeys.gpg" for prefix "coreos.com/etcd".
Added key for prefix "coreos.com/etcd" at "/etc/rkt/trustedkeys/prefix.d/coreos.com/etcd/8b86de38890ddb7291867b025210bd8888182190"

在命令输出的最后一行,Rkt显示了签名在本地保存的位置。查看文件的内容就会发现,这个签名实际上是一个标准的GunPG签名公钥。GunPG是著名RSA非对称加密算法PGP的开源实现,关于PGP算法的介绍可以参考这篇百度百科。

$ cat /etc/rkt/trustedkeys/prefix.d/coreos.com/etcd/8b86de38890ddb7291867b025210bd8888182190
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQINBFTCnMQBEAC/49bGbStCpa3peej+/42mobfuGbTcdmcGGwYZmigP0Kl0TPZK
... 省略部分输出 ...
fMkBtaM3knaFonHZc19BD1FOishRThCCq2Ty8HUoN2Fk7w0l
=bYl7
-----END PGP PUBLIC KEY BLOCK-----

获取远程镜像

通过rkt fetch命令可以获取远程的镜像,和Docker的pull命令类似。下面这个命令会从CoreOS的GitHub仓库中下载预装了Etcd的示例容器。

$ sudo rkt fetch coreos.com/etcd:v2.0.9
rkt: searching for app image coreos.com/etcd:v2.0.9
rkt: fetching image from https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
Downloading signature from https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci.asc
Downloading ACI: [=============================================] 3.79 MB/3.79 MB
rkt: signature verified:
  CoreOS ACI Builder <release@coreos.com>
sha512-91e98d7f1679a097c878203c9659f2a2

值得一提的是,如果用户在上一步中没有添加签名信任,则镜像在下载完成后会由于无法正确的验证来源,而被直接丢弃(不会进入Rkt的本地仓库)。并提示镜像没有签名,或镜像的签名没有被信任。

$ sudo rkt fetch coreos.com/etcd:v2.0.9
...
Downloading ACI: [=============================================] 3.79 MB/3.79 MB
openpgp: signature made by unknown entity

有的时候,用户确实希望下载或导入一个没有签名认证的镜像。可以明确的使用--insecure-skip-verify参数来告诉Rkt不要验证镜像的来源。

$ sudo rkt --insecure-skip-verify fetch coreos.com/etcd:v2.0.9
rkt: searching for app image coreos.com/etcd:v2.0.9
rkt: fetching image from https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
Downloading ACI: [=============================================] 3.79 MB/3.79 MB
sha512-91e98d7f1679a097c878203c9659f2a2

下载完成的镜像会被存储在本地的/var/lib/rkt/cas/blob/sha512/仓库目录中,具体的命名规则是将镜像SHA512哈希值的前两位作为目录名,并以完成的SHA512哈希值作为镜像的文件名。例如刚刚下载的Etcd镜像哈希值为:sha512-91e98d7f1679a097c878203c9659f2a2,它存储的路径如下:

$ tree /var/lib/rkt/cas/blob/sha512/
/var/lib/rkt/cas/blob/sha512/
└── 91
    └── sha512-91e98d7f1679a097c878203c9659f2a26ae394656b3147963324c61fa3832f15

目前Rkt还没有提供一个命令能够快速列出本地仓库里所有镜像名字的方法。这看起来是一个比较匪夷所思的缺失功能。

运行容器

运行Rkt容器的命令是rkt run,可以通过几种方式指定容器使用的镜像。

最常用,也是最方便的方法是使用标准的镜像的命名。比如例子中的coreos.com/etcd:v2.0.9名称:

$ sudo rkt run coreos.com/etcd:v2.0.9
rkt: searching for app image coreos.com/etcd:v2.0.9
rkt: fetching image from https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
...
Press ^] three times to kill container

也可以直接使用镜像的哈希值指定:

$ sudo rkt run sha512-91e98d7f1679a097c878203c9659f2a26ae394656b3147963324c61fa3832f15
...
Press ^] three times to kill container

或者直接指定镜像文件的完整地址,这个地址可以是本地文件,也可以是网络文件,比如这样:

$ sudo rkt run https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
rkt: fetching image from https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
...
Press ^] three times to kill container

使用时可以根据具体情况,哪种方便就用哪种。容器启动后就会自动运行镜像制作时指定的入口程序,连续按Ctrl+]组合键3次则会退出当前容器。

通过--help参数可以显示rkt run命令的可用选项。

$ sudo rkt run --help
Usage:
  -inherit-env=false: inherit all environment variables not set by apps
  -interactive=false: run pod interactively
  -local=false: use only local images (do not discover or download from remote URLs)
  -no-overlay=false: disable overlay filesystem
  -pod-manifest="": the path to the pod manifest. If it's non-empty, then only '--private-net', '--no-overlay' and '--interactive' will have effects
  -port=: ports to expose on the host (requires --private-net)
  -private-net=false: give pod a private network
  -set-env=: an environment variable to set for apps in the form name=value
  -signature=: local signature file to use in validating the preceding image
  -stage1-image="/usr/local/bin/stage1.aci": image to use as stage1. Local paths and http/https URLs are supported. If empty, rkt will look for a file called "stage1.aci" in the same directory as rkt itself
  -volume=: volumes to mount into the pod

比较常用的选项有:

  • --volume外挂分区,类似于Docker的-v参数
  • --port暴露容器中的端口,类似于Docker的-p参数
  • --interactive启用交互模式,类似于Docker的-i加上-t参数的效果
  • --set-env向容器里添加环境变量,类似于Docker的-e参数

对于经常在使用Docker的用户,有两点值得注意的Rkt与Docker运行镜像时不同的地方:

  • 目前还没有与Docker的-d参数相当的运行选项,要后台运行镜像先将就用nohup&吧。
  • 在任意容器中连续按Ctrl+]组合键3次,都会结束当前容器,不论是否启用了交互模式。

导入本地镜像文件

Rkt导入本地镜像的命令和下载远程镜像是一样的,同样使用rkt fetch。需要留意的是,即便是导入本地镜像,Rkt仍然会强制验证签名(除非指定--insecure-skip-verify参数)。

$ wget https://github.com/coreos/etcd/releases/download/v2.0.9/etcd-v2.0.9-linux-amd64.aci
...
aving to: ‘etcd-v2.0.9-linux-amd64.aci’
100%[=================================>] 3,788,138   1.00MB/s   in 5.1s
‘etcd-v2.0.9-linux-amd64.aci’ saved [3788138/3788138]
$ sudo rkt run etcd-v2.0.9-linux-amd64.aci
error opening signature file: open /home/ubuntu/etcd-v2.0.9-linux-amd64.aci.asc: no such file or directory

默认的签名文件应该和镜像在同一目录下,并且文件名应为镜像名加后缀.asc。如果签名文件的位置或名字与此规范不符,则可以用--signature指定。例如:

sudo rkt run image.aci --signature sign.asc

下载Docker仓库的镜像

Rkt支持直接下载Docker镜像,并自动转换为AppC镜像。这一设计在Docker基础资源如此丰富的当下,真的是很贴心。操作起来也非常简单,只需要在Docker的标准镜像路径前面加上docker://前缀即可。

不过,颇具讽刺意味的是,由于Docker镜像本来是没有签名验证机制的,因此下载任何Docker镜像时,都必须使用--insecure-skip-verify参数。仿佛时刻在提醒用户:这个镜像可能是不安全的!

例如,下载Docker官方仓库的CentOS镜像:

$ sudo rkt --insecure-skip-verify fetch docker://centos
rkt: fetching image from docker://centos
Downloading layer: 6941bfcbbfca7f4f48becd38f2639157042b5cf9ab8c080f1d8b6d047380ecfc
Downloading layer: 41459f052977938b824dd011e1f2bec2cb4d133dfc7e1aa0e90f7c5d337ca9c4
Downloading layer: fd44297e2ddb050ec4fa9752b7a4e3a8439061991886e2091e7c1f007c906d75
sha512-94b712e21c2f88aebcbe67b7e97911c9

直接通过哈希值试运行容器,注意加上--interactive选项:

$ sudo rkt run --interactive sha512-94b712e21c2f88aebcbe67b7e97911c9ed3be062f976cefcebed8baab826ed32
[root@rkt-0ff47941-3934-4dcd-9b9d-3db558f62cd9 /]#

同样的,按三下Ctrl+]则会退出容器。

对于需要登录的Docker仓库,Rkt也提供了下载镜像的解决办法。首先需要在/etc/rkt/auth.d/目录下添加一个用户名命名的配置文件。

$ sudo cat /etc/rkt/auth.d/myuser.json 
{
    "rktKind": "dockerAuth",
    "rktVersion": "v1",
    "registries": ["quay.io"],
    "credentials": {
        "user": "myuser",
        "password": "sekr3tstuff"
    }
}

然后就可以执行fetch了。很方便有木有。

$ sudo rkt --insecure-skip-verify fetch docker://quay.io/myuser/privateapp
rkt: fetching image from docker://quay.io/myuser/privateapp
Downloading layer: cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff
Downloading layer: 6ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea
....

更多功能

通过rkt help命令可以查看到Rkt的更多操作和参数,其中的一些功能还在开发中,具体的进度可以查阅官方文档

$ rkt help
...
COMMANDS:
    enter           Enter the namespaces of an app within a rkt pod
    fetch           Fetch image(s) and store them in the local cache
    gc          Garbage-collect rkt pods no longer in use
    help            Show a list of commands or help for one command
    install         Set up rkt data directories with correct permissions
    list            List pods
    metadata-service    Run metadata service
    prepare         Prepare to run image(s) in a pod in rkt
    run         Run image(s) in a pod in rkt
    run-prepared        Run a prepared application pod in rkt
    status          Check the status of a rkt pod
    trust           Trust a key for image verification
    version         Print the version and exit
GLOBAL OPTIONS:
    --debug=false           Print out more debug information to stderr
    --dir=/var/lib/rkt      rkt data directory
    --help=false            Print usage information and exit
    --insecure-skip-verify=false    skip image or key verification
    --local-config=/etc/rkt     local configuration directory
    --system-config=/usr/lib/rkt    system configuration directory

小结&下期预告

Rkt是CoreOS公司继Etcd后,为Linux社区输出的又一利器。值得指出的是,Rkt项目当前依然处于开发的Alpha时期,距离作为产品发布还有相当的差距。即便如此,其项目本身以及AppC规范在应用容器社区引发的讨论日渐强烈。

AppC规范与Docker的差异到底有多大?在这个系列的下一期中,我们将重点介绍AppC提供的一些标准工具,以及制作AppC容器的方法。并揭秘更多AppC容器与Docker的异同之处。感谢您的阅读。

本条内容转载自InfoQ:http://www.infoq.com/cn/articles/coreos-rkt-container-part-01
Share

CoreOS那些事之系统升级

前段时间在DockerOne回复了一个关于 CoreOS 升级的提问。仔细琢磨来,这个问题还有不少可深入之处,因此有了此文,供已经在国内使用 CoreOS 的玩家们参考。

具有CoreOS特色的系统升级

CoreOS的设计初衷之一就是“解决互联网上普遍存在的服务器系统及软件由于没有及时升级和应用补丁,造成已知漏洞被恶意利用导致的安全性问题”。因此,它的升级方式在各种Linux发型版中可以说是独树一帜的,特别是与主流的服务器端系统相比。

平滑升级

一方面来说,常用的服务器系统如RedHat、CentOS、Debian、Ubuntu甚至FreeBSD和Windows Server都存在明确的版本界限,要么不能支持直接在线升级至新的发行版本,要么(如Debian/Ubuntu和Windows 7以后的版本)虽能够跨版本升级却容易出现兼容性风险,一旦升级后出现故障往往面临进退两难的局面。

这个问题在一些新兴的Linux发行版,如Arch Linux已经有了较先进的解决方法:将过去累计许多补丁再发行一次大版本的做法变为以月或更短周期的快速迭代更新,并由系统本身提供平滑升级和回滚的支持。这样,用户可以在任何时候、从任何版本直接更新至修了最新安全补丁的系统。然而,这些以 Arch 为代表的平滑升级系统还是带来了一些更新系统后无法使用的事故,不妨在百度以“Arch 升级问题”关键字搜索会发现许多类似的抱怨。事实上,Arch的目标用户主要是喜爱尝鲜的Linux爱好者而不是服务器管理员或者服务端应用架构师。

那么平滑升级的思路是不是在服务器系统就走不通了呢。其实仔细分析平滑升级出现问题的原因,当中最关键的一个因素在于,系统设计时最多只能确保从一个干净的系统顺利升级的途径,如果用户对系统中的某些核心组件做了修改(比如将系统中的Python2升级成了Python3),它就不属于操作系统设计者控制范围内的工作了。这样相当于设定了一个售后服务霸王条款(只是个比喻,这些Linux系统其实都是免费的):自行改装,不予保修。

在过去,用户要使用服务器系统,他就必然需要在上面安装其他的提供对外服务软件和程序,因此对系统本身有意无意的修改几乎是无法避免的。这个问题直到近些年来应用容器(特别是Docker)的概念被大规模的推广以后才出现了新的解决思路。而CoreOS就是通过容器巧妙的避开了用户篡改系统的问题,提出了另一种解决思路:让系统分区只读,用户通过容器运行服务。不得不说,这简直就是以一个霸王条款替代了另一个霸王条款,然而这个新的“条款”带来的附加好处,使得它被对稳定性安全性都要求很高的服务器领域而言接受起来要心安理得的多。

“反正许多东西都要自动化的,套个容器又何妨。” 恩,就这么愉快的决定了。

自动更新

另一方面来说,除了系统的大版本升级,平时的系统和关键软件的小幅补丁更新也时常由于系统管理员的疏忽而没有得到及时运用,这同样是导致系统安全问题的一个重要因素(比如2014年BrowserStack中招的这个例子)。

这个解决思路就比较简单了:自动更新。这么简单的办法当然早就被人用过了。即便在操作系统层面还见得不多,在应用软件上早都是烂熟的套路。那么,为了不落俗套,怎样把自动更新做得创意一些呢。先来看看系统升级都会有哪些坑。

乍一看来,操作系统这个东东和普通应用在升级时候会遇到的问题还是有几分相似。比如软件正在使用的时候一般是不可以直接热修补的,系统也一样(Linux 4.0 内核已经在着手解决这个痛点了,因此它在未来可能会成为伪命题)。又比如软件运行会有依赖,而系统的核心组件之间也是有依赖的,因此一旦涉及升级就又涉及了版本匹配问题。除此之外,它们之间还是有些不一样的地方。比如许多应用软件其实可以直接免安装的,升级时候直接把新文件替换一下旧的就算完成了。操作系统要想免安装,则需要些特别的技巧。

下面依次来说说CoreOS是怎样应对这几个坑的。

既然系统不能热修补,就一定会牵扯到重启的情况,这在服务器系统是比较忌讳的,为了避免系统重启时对外服务中断,CoreOS设计了服务自动迁移的内置功能,由其核心组件Fleet提供。当然这个并不是一个完美的方案,相信未来还会有更具创意的办法替代它的。

版本匹配的问题在应用软件层面比较好的解决方法还是容器,即把所有依赖打包在一起部署,每次更新就更新整个容器的镜像。同样的思路用到操作系统上,CoreOS每次更新都是一次整体升级,下载完整的系统镜像,然后做MD5校验,最后重启一下系统,把内核与外围依赖整个儿换掉。这样带来的额外好处是,每次升级必然是全部成功或者全部失败,不会存在升级部分成功的尴尬情况。

要想免安装软件那样直接重启换系统会遇到什么问题呢?两个方面,其一是,应用软件是由操作系统托管和启动的,可以通过系统来替换他的文件。那么操作系统自己呢,是由引导区的几行启动代码带动的,想在这么一亩三分地上提取镜像、替换系统、还想搞快点别太花时间,额,那真是螺蛳壳里做道场——排不出场面(还记得Window或者Mac电脑每次升级系统时候的等待界面么)。其二是,系统升级出问题是要能回滚的啊,不然怎么在生产环境用?即便不考虑启动时替换文件所需要的时间,万一更新过后启动不起来,原来的系统又已经被覆盖了,我天,这简直是给自己埋了一个地雷。由此可见,想要实现快速安全的升级,在重启后安装更新的做法从启动时间和回滚难度的两个方面看都不是最佳的办法。

0410001

为此,CoreOS又有一招绝活。进过CoreOS的主页的读者应该都见过上面这个A/B双系统分区的设计图。正如图中所示,CoreOS安装时就会在硬盘上划出两块独立的系统分区(空间大致为每个1GB),并且每次只将其中一个在作为系统内核使用,而后台下载好的新系统镜像会在系统运行期间就部署到备用的那个分区上。重启的时候只需要设计个逻辑切换两个分区的主次分工即可,不到分分钟就完成了升级的过程,要是真出现启动失败的情况,CoreOS会自动检测到并切换回原来的能正常工作的分区。用事先部署好的分区直接替换启动的方法避免重启后临时安装更新,这种思路的转换,确实有点神来之笔的意思。

说个题外话。之前有一次我和其他的CoreOS爱好者在Meetup活动时聊到对于双系统分区的看法,当时大家得出较一致的结论是:既然还是必须重启,用不用两个分区用户都没有实际获益,相比之下,“平滑升级才是卖点,双分区只是噱头”。我在《CoreOS实践指南》系列里也曾表达过类似的观点。一直到后来自己仔细反思了这种设计的巧妙,才发觉原先想法的片面性,实在贻笑大方。

这些方法说起来蛮轻松,若要真的实施出来,就不是拍拍脑袋那么容易了。纵观Linux开源系统百家争鸣,真正实现了这样后台更新设计的系统也仅CoreOS一枝独秀。

升级参数配置

理解了CoreOS的自升级方式,继续来说说与升级相关的配置。CoreOS系统升级有关的选项通常会在首次启动服务器时通过 cloud-initcoreos.update 项指定,系统启动后也可以在 /etc/coreos/update.conf 文件里修改。可配置的属性包括三个:升级通道升级策略升级服务器。这三个属性在DockerOne的回答中都已经提到,下面将在此基础上再略作深化。

初始化升级配置

这是最常用的配置升级参数的方式,系统首次启动时cloud-init将完成大多数节点和集群相关的初始化任务。与CoreOS升级有关的部分是coreos.update下面的三个键,其内容举例如下:

coreos:
  update:
    reboot-strategy: best-effort
    group: alpha
    server: https://example.update.core-os.net

其中只有group一项是必须的,它指定了系统的升级通道。升级策略 reboot-strategy的默认值是best-effort,而升级服务器server的默认值是CoreOS的官方升级服务器。

修改升级配置

对于已经启动的集群,可以在/etc/coreos/update.conf配置文件中对升级参数进行修改,其内容格式简单明了。举例如下:

GROUP=alpha
REBOOT_STRATEGY=best-effort
SERVER=https://example.update.core-os.net

同样,大多数情况下用户只会看到GROUP这一个值,因为只有它是必须的。其余的两行可以没有,此时会使用默认值代替。

需要注意的是:

  • 每次修改完成以后需要执行sudo systemctl restart update-engine命令使配置生效
  • 修改一个节点的配置并不会影响集群其他节点的升级配置,需要逐一单独修改
  • 最好让集群中的节点使用相同的升级通道,方便管理,虽然混用通道一般不会直接导致问题
  • 优先选择用cloud-init。在初始化时就将系统参数设计好,减少额外修改的工作量

升级通道

升级通道间接的定义了CoreOS每次升级的目标版本号。这个思路大概是从Chrome浏览器借鉴来的,官方提供三个升级通道:Alpha(内测版)、Beta(公测版 )和 Stable(正式发行版)。举个例子来说,如果用户配置的是Alpha通道,那么他的每次更新就会升级到当前最新的内测系统版本上。内存版本类似于Chrome浏览器的所谓“开发版”,会第一时间获得新的功能更新,稳定性一般还是蛮可以的,但不适合做为产品服务器,主要面向的对象是喜爱新鲜的开发者和玩家。公测版稳定性略高,也会比较快的获得新功能的推送,适合作为项目开发测试环境把玩。正式发行版中的组件往往都不是最新版本的,但其稳定性最高,适合作为产品服务器使用。CoreOS目前采用一个整数数字来表示版本号,数字越大则相对发布时间越新。

各通道发布更新的频率依次为(见官方博客声明):

  • Alpha:每周星期四发布
  • Beta:每两周发布一次
  • Stable:每个月发布一次

每个通道当前的系统版本号及内置组件版本号可以在这个网页上查看到。

除了三个公开的通道,订阅了CoreUpdate服务的用户还可以定制升级自己的通道,但这个服务是付费的。此外,使用了企业版托管CoreOS系统的用户也可以免费使用此功能,企业版的起步费用是10个节点以内 $100/月,见这个链接。还有另一个土豪企业版服务起步价是25个节点以内 $2100/月,差别就是提供额外的人工技术支持服务,果然技术人才是最贵的东东。

升级策略

升级策略主要与自动升级后的重启更新方式有关。它的值可以是 best-effort(默认值)、 etcd-lockrebootoff。其作用依次解释如下:

  • best-effort:如果Etcd运行正常则相当于 etcd-lock,否则相当于reboot
  • etcd-lock:自动升级后自动重启,使用LockSmith 服务调度重启过程
  • reboot:自动升级后立即自动重启系统
  • off:自动升级后等待用户手工重启

默认的方式是best-effort,通常它相当于etcd-lock策略,重启过程会使用到CoreOS的LockSmith服务调度升级过程。主要是防止过多的节点同时重启导致对外服务中断和Etcd的Leader节点选举无法进行。它的工作原理本身很简单,通过在Etcd的 coreos.com/updateengine/rebootlock/semaphore 路径可用看到它的全部配置:

$ etcdctl get coreos.com/updateengine/rebootlock/semaphore
{
    "semaphore": 0,
    "max": 1,
    "holders":
    [
        "010a2e41e747415ba51212fa995801dd"
    ]
}

通过设定固定数量的锁,只有获得锁的主机才能够进行重启升级,否则就继续监听锁的变化。重启升级后的节点会释放它占用的锁,从而通知其他节点开始下一轮获取升级锁的竞争。

除了直接修改Etcd的内容,CoreOS还提供了 locksmithctl 命令更直观的查看LockSmith服务的状态或设置升级锁的数量。

查看升级锁的状态信息:

$ locksmithctl status
Available: 0     <-- 剩余的锁数量
Max: 1           <-- 锁的总数
MACHINE ID
010a2e41e747415ba51212fa995801dd  <-- 获得锁的节点

其中获得锁的节点就是已经已经下载部署好新版本系统,等待或即将重启(与升级策略有关)的节点的Machine ID。用locksmithctl set-max 命令可用修改升级锁数量(即允许同时重启升级的节点数量):

$ locksmithctl set-max 3
Old: 1
New: 3

此时若再次用locksmithctl status查看状态就会看到 Max 的数量变成3了。

此外,locksmithctl unlock 命令可以将升级锁从获得锁的节点上释放,这个命令很少会用到,除非一个节点获得锁后由于特殊的原因无法重启(例如磁盘错误等硬件故障),因而始终占用这个锁。这种情况下才会需要手工释放。

升级服务器

许多希望在内网中使用CoreOS的用户都比较关心能否在内网搭建自己的升级服务器?答案是肯定的。

比较可惜的是,CoreOS 升级服务器是属于CoreUpdate服务的一部分,也就是说,它是需要付费使用的。不过考虑到通常会在自己内网搭建服务器集群的大都是企业级用户,收费也还算公道。

从文档资料来看,CoreOS所用升级服务器协议与Google的ChromeOS升级服务器是完全兼容的,甚至可以相互替代。比较有趣的是,两者都开源了各自的操作系统,但都没有开源其升级服务器实现,这个中意思仿佛是如果让用户去自己架设升级服务器,谁来保证这些升级服务器的镜像是最新的呢,那么自动升级提供的系统安全性的意义又何在了呢。

顺带说一句,在CoreOS的SDK中有一个 start_devserver工具 用于测试部署用户自己构建的CoreOS镜像(系统是开源的嘛),因此如果用户直接下载官方镜像提供给这个工具,应当是可以自己构建内网升级服务器的。但是官方文档对这方面的介绍比较模糊,我暂且抛砖引玉了,待高人给出具体方案。

手动升级系统

CoreOS始终会自动在后台下载和部署新版本系统,即使将升级策略设为off(这样只是禁止自动重启)。因此在绝大多数情况下,除非处于测试目的和紧急的版本修复,用户是不需要手动触发系统升级的。不过,大概是考虑到总是有新版本强迫症用户的需求(其实主要是系统测试的需求啦),CoreOS还是提供了手动更新的途径。

查看当前系统版本

相比手动更新,用户也许更想看到的仅仅是:现在的系统到底是部署的哪个版本啦。方法很简单,查看一下etc目录下面的 os-release 文件就可以了。

$ cat /etc/os-release
NAME=CoreOS
ID=coreos
VERSION=607.0.0
VERSION_ID=607.0.0
BUILD_ID=
PRETTY_NAME="CoreOS 607.0.0"
ANSI_COLOR="1;32"
HOME_URL="https://coreos.com/"
BUG_REPORT_URL="https://github.com/coreos/bugs/issues"

这个文件实际上是一个软链接,指向系统分区的 /usr/lib/os-release 文件,而后者是只读分区的一部分,因此不用担心这个文件中的内容会被外部篡改。

自动升级的频率

CoreOS会在 启动后10分钟 以及之后的 每隔1个小时 自动检测系统版本,如果检查到新版本就会自动下载下来放到备用分区上,然后依据之前的那个升级策略决定是否自动重启节点。OK,就这么简单。

具体的升级检测记录可以通过 journalctl -f -u update-engine 命令查看到。

手动触发升级

恩,下面这个命令是给升级强迫症用户准备滴。

命令非常简单:update_engine_client -update,如果提示 “Update failed” 则表示当前已经是最新版本(搞不懂CoreOS那班人为啥不弄个友好点的提示信息)。如果检测到有新版本的系统则会立即将其下载和部署到备用系统分区上。

$ update_engine_client -update
[0404/032058:INFO:update_engine_client.cc(245)] Initiating update check and install.
[0404/032058:INFO:update_engine_client.cc(250)] Waiting for update to complete.
LAST_CHECKED_TIME=1428117554
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_UPDATE_AVAILABLE
NEW_VERSION=0.0.0.0
... ...
CURRENT_OP=UPDATE_STATUS_FINALIZING
NEW_VERSION=0.0.0.0
NEW_SIZE=129636481
Broadcast message from locksmithd at 2015-04-04 03:22:56.556697323 +0000 UTC:
System reboot in 5 minutes!
LAST_CHECKED_TIME=1428117554
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT
NEW_VERSION=0.0.0.0
NEW_SIZE=129636481
[0404/032258:INFO:update_engine_client.cc(193)] Update succeeded -- reboot needed.

部署完成后,如果用户的升级策略不是 off,系统会发送消息给所有登录当前的用户:“5分钟后系统将重启”。当然,你自己也会在5分钟后被踢出SSH登录,等再次登录回来的时候,就会发现系统已经变成新的版本了。

更好的升级策略

在看到CoreOS的4种升级策略时候,不晓得读者有没发现一个问题。前3种策略都会让新的系统版本下载部署后马上重启服务器,如果这个时候恰好是系统访问的高峰期,即使重启过程中,服务会自动迁移到其他的节点继续运行,仍然可能会造成短暂的服务中断的情况。而第4种策略索性等待管理员用户来重启系统完成升级,又引入了额外的人工干预,如果重启不及时还会使得集群得不到必要的安全更新。

有没有办法既让服务器不要在服务高峰期重启,又不至于很长时间没有更新呢?CoreOS给出了一种推荐的解决方法。我将它称为第5种升级策略:基于定时检测的自动重启。

这种升级策略没有在内置的选项当中,我们需要做些额外的工作:

  • 将升级策略设置成 off
  • 增加一个服务用来检测备用分区是否已经部署新的系统版本,如果部署了就进行重启
  • 增加一个定时器在集群的低峰时段触发执行上面那个服务

检测和重启服务

首先来看最关键的这个服务update-window.service,它会去执行放在 /opt/bin 目录下面的update-window.sh脚本文件。

[Unit]
Description=Reboot if an update has been downloaded
[Service]
ExecStart=/opt/bin/update-window.sh

这个脚本首先使用 update_engine_client -status 检测了备份分区是否已经部署好了新版本的系统。如果发现新的版本已经部署好,就根据 Etcd 服务是否启动来选择通过 Locksmith 调度重启节点(先获取锁然后重启动)或延迟一个随机的时间后重启节点,这样做的目的是防止太多节点在同一个时间重启导致集群不稳定。

#!/bin/bash
# If etcd is active, this uses locksmith. Otherwise, it randomly delays. 
delay=$(/usr/bin/expr $RANDOM % 3600 )
rebootflag='NEED_REBOOT'
if update_engine_client -status | grep $rebootflag; then
    echo -n "etcd is "
    if systemctl is-active etcd; then
        echo "Update reboot with locksmithctl."
        locksmithctl reboot
    else
        echo "Update reboot in $delay seconds."
        sleep $delay
        reboot
    fi
fi

定时触发服务

接下来添加定时器update-window.timer在集群访问的低峰时段触发前面那个服务,

[Unit]
Description=Reboot timer
[Timer]
OnCalendar=*-*-* 05,06:00,30:00

这个定时器Unit文件的功能类似于一个crontab记录,只不过对于用了Systemd启动的系统比较推荐使用这样的方式。上面的配置表示每天早上的5:00, 5:30, 6:00 和 6:30。

写到 cloud-init 里

既然是每个节点都要有的东东,当然要放到cloud-init 配置里面。把上面的内容统统写进去,看起来就是这个样子的了:

#cloud-config
coreos:
  update:
    reboot-strategy: off
  units:
    - name: update-window.service
      runtime: true
      content: |
        [Unit]
        Description=Reboot if an update has been downloaded
        [Service]
        ExecStart=/opt/bin/update-window.sh 
    - name: update-window.timer
      runtime: true
      command: start
      content: |
        [Unit]
        Description=Reboot timer
        [Timer]
        OnCalendar=*-*-* 05,06:00,30:00
write_files:
  - path: /opt/bin/update-window.sh
    permissions: 0755
    owner: root
    content: |
        #!/bin/bash
        # If etcd is active, this uses locksmith. Otherwise, it randomly delays. 
        delay=$(/usr/bin/expr $RANDOM % 3600 )
        rebootflag='NEED_REBOOT'
        if update_engine_client -status | grep $rebootflag; then
            echo -n "etcd is "
            if systemctl is-active etcd; then
                echo "Update reboot with locksmithctl."
                locksmithctl reboot
            else
                echo "Update reboot in $delay seconds."
                sleep $delay
                reboot
            fi
        fi
        exit 0

到这里,CoreOS升级相关的事儿已经侃得差不多了。不过总觉得还差点什么。

具有国内特色的CoreOS升级问题

在国内服务器用过CoreOS的用户大约都会发现一个比较忧伤的现象:好像CoreOS的自动升级没有生效捏?

相信不少用户大概已经猜到原因了吧。为了验证猜测,不妨做个手动升级试试。下面是我在国内的一个CoreOS集群上得到的结果:

$ update_engine_client -check_for_update
[0328/091247:INFO:update_engine_client.cc(245)] Initiating update check and install.
[0328/092033:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: 
GError(3): Could not get owner of name 'com.coreos.update1': no such name
[0328/092033:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 2/4
... ...
[0328/092053:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 4/4
[0328/092103:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: 
GError(3): Could not get owner of name 'com.coreos.update1': no such name
[0328/092103:ERROR:update_engine_client.cc(64)] Giving up -- unable to get dbus proxy for com.coreos.update1

看到最后输出Giving up的时候,整个人都不好了。现在来说说怎么解决这个问题。

使用HTTP代理升级CoreOS

既然是访问不到升级服务器,解决办法就很干脆了:翻墙。

首先得找一个能用的墙外HTTP代理服务器,这个…大家各显神通吧,记录下找到的地址和端口,下面来配置通过代理升级服务器。

创建一个配置文件 /etc/systemd/system/update-engine.service.d/proxy.conf,内容为:

[Service]
Environment=ALL_PROXY=http://your.proxy.address:port

ALL_PROXY的值换成实际的代理服务器地址,重启一下update-engine服务即可:

sudo systemctl restart update-engine

这个工作也可以在cloud-config里面用write_files命令在节点启动时候就完成:

#cloud-config
write_files:
  - path: /etc/systemd/system/update-engine.service.d/proxy.conf
    content: |
        [Service]
        Environment=ALL_PROXY=http://your.proxy.address:port
coreos:
    units:
      - name: update-engine.service
        command: restart

官方的服务器呢

其实一直有小道消息说,CoreOS公司已经在积极解决这个问题,预计2015年下半年会在国内架设专用的升级服务器。只能是期待一下了。

小结

“平滑而安全的滚动升级”和“无需干预的自动更新”既是CoreOS系统设计的初衷,也是一直是许多用户青睐CoreOS的原因。特别是在需要长期运行的服务器集群上,这些特性不仅节省了手工安装补丁和升级系统的成本,更避免了系统和核心软件升级不及时带来的安全性隐患。

希望这篇文章中介绍的内容能对大家理解CoreOS的系统升级相关问题提供一定参考和帮助。欢迎通过评论参与讨论。


本文转自:InfoQ ,作者为ThoughtWorks – 林帆

Share