You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

160 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 18 | 如何做好容器镜像的个性化及合规检查?
你好,我是王潇俊。我今天分享的主题是:如何做好容器镜像的个性化及合规检查。
你是否还记得我在第13 讲篇文章《容器技术真的是环境管理的救星吗?》中说到:容器不是银弹,镜像发布无法很好地满足用户的个性化需求?
在携程的发布标准化中,容器内的环境也是由发布系统定义的,用户即使登录到容器上去做变更,下一次发布之后还是会被回滚回来。但是,对 Dockerfile 的编写和控制需要一定的学习成本,因此我们又不可能将镜像的内容与构建流程完全交给用户来自定义。
于是,就有了我今天的分享,即如何做好容器镜像的个性化及合规检查?根据我在持续交付道路上摸爬滚打的实践经验,总结了以下三种方法来满足用户对容器镜像个性化需求:
1. 自定义环境脚本;
2. 平台化环境选项与服务集市;
3. 自定义镜像发布。
接下来的内容,我将根据这三种方法展开,并将介绍如何通过合规检查来规避个性化带来的风险。
## 用户自定义环境脚本
我们允许用户在编译后的代码包内放入包含自定义环境脚本的 .paas 目录(这是一个自定义的隐藏目录),来满足用户对环境的个性化需求。
这个.paas目录中可能会存在build-env.sh和image-env.sh两个文件分别运行于构建代码和构建镜像的过程中。
其中build-env.sh是在构建代码之前运行image-env.sh是在构建镜像的时候插入到我们规范的 Dockerfile 中,从而被打到容器内部。
这样就不仅可以满足用户对发布的镜像的个性化需求,同时还能满足对构建代码镜像的个性化需求。
比如某个Python应用依赖一些动态链接库那么这个依赖在构建代码和构建镜像环节都是必须的。这时用户就需要在 build-env.sh和 image-env.sh这两个文件中都写入安装依赖的步骤构建系统会在不同阶段判断是否有这两个文件如果有就运行。
通常情况下,自定义环境脚本的方式,可以满足大部分用户的普通需求。但是,这个方式有两个缺点:
1. 构建镜像需要用完就删,因为我们无法感知用户在构建中修改了什么内容,是否会对下一次构建产生影响。这就要求每次构建都要生成新的容器,会在一定程度上降低构建性能。
2. 如果多个项目有同样的需求,那么这些项目就都要引用这个脚本文件,不但啰嗦,而且后面也不好维护,如果脚本内容变化,还需要通知所有引用的项目都改一遍。
好的工具就是要解决用户的一切痛点,因此针对第二个问题,我们在系统上通过平台化环境选项和服务集市的方式做了统一处理。
## 平台化环境选项与服务集市
**环境选项,** 是携程在持续交付平台为用户提供的一些环境变更的常用功能,表现为构建镜像时的一些附加选项。
在上一篇文章《容器镜像构建的那些事儿》中,我介绍了构建镜像一个很重要的原则是:镜像要尽可能得小巧精简,因此我们没有在镜像中为用户安装太多的软件。但是,很多时候用户可能需要这些软件,于是我们就在平台上提供了环境选项的功能。
比如很多用户需要用到Wget软件于是我们就在交付平台上提供了一个 “安装 Wget ” 的环境选项。其实,这个环境选项对应的就是一条 shell 命令:
```
yum install wget -y
```
如果某次发布时,用户需要这个工具,可以勾选这个选项,那么就可以在构建镜像时作为参数传给构建系统。如果搭建系统判断出有这个参数,就将会其插入到规范的 Dockerfile 中,从而这个参数就可以被打到容器内部。
环境选项虽然好用,但是只适合一些简单的需求,比如安装一些软件、更改一些配置等。而对一些复杂的需求,则需要创建一个叫作**服务集市**的功能。举个例子:
携程的服务集市中有一个 JaCoCo 服务,它的作用就是在 Tomcat 启动时更改 JVM 参数收集应用的覆盖率并发送给外部系统。同时外部系统可以控制这个JaCoCo服务的启停并将收集结果处理成可视化的页面。
服务集市功能的使用,会涉及到以下两个关键步骤:
1. 勾选 JaCoCO 服务之后,会在容器中注入 jacocoagent.jar 和启停脚本;
2. 通过对外暴露的API控制在容器中运行启停脚本。
像JaCoCo这样的复杂功能我们会抽象成服务供用户使用。他们只要在构建镜像时选择对应的服务和该服务起效的环境就可以了。
而实际系统要完成的任务则复杂得多首先要通过改写Dockerfile完成以上所说的“勾选JaCoCo服务”同时还要改写镜像中JVM的启动参数等并完成对JaCoCo服务中心的注册。具体的操作各个服务有所不同根据实际需求而定原则就是把这些服务内容增加到对应的环境镜像中去。
通过这种方式构建的镜像不同环境就拥有了不同的服务。比如用户在构建镜像时选择了JaCoCo服务起效的环境是测试环境那么JaCoCo就只在测试环境的镜像中起效而不会在生产环境中起效。
除了JaCoCo以外携程还提供了许多其他与环境有关的服务组成了一个服务集市用户可以按照具体需求组合使用。
![](https://static001.geekbang.org/resource/image/c6/be/c67b37efe16c5b5a7f02ce7fef5a39be.png)
携程的服务集市
## 自定义镜像发布
**用户自定义环境脚本、平台化环境选项与服务集市,这两种方式有一个共同的缺点:自定义的部分都需要插到 Dockerfile中因此每次打镜像时都需要运行一次。** 这对一些比较快的操作,没有问题,但如果需要安装很多软件,甚至需要编译一些软件时,每次发布都重复运行一次的效率就会非常低下。
为此我们提供了用户自定义镜像的功能该功能分为自定义Base镜像和完全自定义镜像发布两种。
1. **自定义 Base 镜像**
自定义 Base 镜像,就是如果基础镜像无法满足用户需求,并且自定义的部分非常重,运行比较久,我们就会建议用户使用自定义的 Base 镜像。但是,这个自定义的 Base 镜像,必须基于官方提供的 Base 镜像因为很多工具和功能都是基于官方Base 镜像的。
虽然 Base 镜像是自定义的,但是应用还是标准的应用,因此发布方式和普通的发布方式没有区别。只是解决了自定义环境脚本与平台化环境选项的运行速度问题,反映到实际的 Dockerfile 上,就只是 FROM指令的指向改变了变成了用户自定义的 Base 镜像地址。
2. **完全自定义镜像发布**
但是用户的需求是永无止境的。比如特殊启动方式的应用自定义Base镜像就无法解决。
原则上来说,我不建议使用一些非标准的应用,因为这是不可控的,对生产环境非常危险。但是 Docker 的镜像是如此方便,用户如果只是想在测试环境中使用一些测试工具,虽然这个工具来自于社区,也不是标准的应用,但我们也没有理由全部拒绝。否则,用户很可能会以虚拟机上可以安装任何工具为由,要求退回到虚拟机时代。
但是,这样的退化怎么能被允许呢!
因此,一定要支持完全自定义镜像发布,也就是说用户可以发布任何镜像,只要这个镜像能够跑起来。对私有云来说,这应该是能接受的最大化的自由了。
对于完全自定义发布我们使用 Docker 多阶段构建multi-stage build也就是说用户可以将构建代码和构建镜像合并成一个步骤在同一个 Dockerfile 中完成。
## 镜像安全合规检查
满足了用户对镜像的个性化需求,也就意味着会引入不可控因素,因此对镜像的安全合规检查也就变得尤为重要了。我们必须通过合规检查,来确认用户是否在容器里做了危险的事情。
只有这样,用户个性化的自由,才不会损害整个环境。毕竟,有克制的自由才是真正的自由。
**对自定义镜像首先必须保证它是基于公司官方Base 镜像的,这是携程最不可动摇的底线。** 在其他情况下,就算真的不继承公司官方 Base 镜像,建议也必须要满足 Base 镜像的一些强制性规定,比如应用进程不能是 root 等类似的安全规范。
**关于自定义镜像是否继承了公司官方镜像,我们采取的方法是对比镜像 Layer即自定义镜像的 Layer 中必须包含官方Base镜像的 Layer。**
但是,对比 Layer 也不是最靠谱的方式,因为用户虽然继承了 Base 镜像,但还是有可能在用户创建的上层 Layer中破坏镜像结构。目前Docker 的部署流程中,还有许多潜在漏洞,有可能让一些有企图的人有机可乘,发起攻击。
因此,我们需要一些强制手段来确保镜像的安全,好的安全实践意味着要对可能出现的事故未雨绸缪 。
目前,市面上有很多工具可以为 Docker 提供安全合规检查,如 CoreOS ClairDocker Security ScanningDrydock 等等。
在安全合规检查方面,携程的方案是 Harbor 与 CoreOS Clair 结合使用:当构建系统 Push 一个新的镜像或者用户 Push 一个自定义镜像之后Harbor 会自动触发 CoreOS Clair 进行镜像安全扫描。Clair 会对每个容器 Layer 进行扫描,并且对那些可能成为威胁的漏洞发出预警。
漏洞分严重级别,对于一些非破坏性的漏洞,我们是允许发布的。检查的依据是 Common Vulnerabilities and Exposures 数据库(常见的漏洞和风险数据库,简称 CVE)以及Red Hat、Ubuntu 、Debian 类似的数据库。
这些数据库中,包含了一些常见的软件漏洞检查。比如, libcurl 7.29.0-25.el7.centos 存在如下漏洞:
> The curl packages provide the libcurl library and the curl utility for downloading files from servers using various protocols, including HTTP, FTP, and LDAP. Security Fix(es): \* Multiple integer overflow flaws leading to heap-based buffer overflows were found in the way curl handled escaping and unescaping of data. An attacker could potentially use these flaws to crash an application using libcurl by sending a specially crafted input to the affected libcurl functions. (CVE-2016-7167) Additional Changes: For detailed information on changes in this release, see the Red Hat Enterprise Linux 7.4 Release Notes linked from the References section.
攻击者可以利用libcurl缓冲区溢出的漏洞在应用的上下文中执行任意代码。
Clair 是一种静态检查,但对于动态的情况就显得无能为力了。所以,对于镜像的安全规则我还总结了如下的一些基本建议:
1. 基础镜像来自于 Docker 官方认证的,并做好签名检查;
2. 不使用 root 启动应用进程;
3. 不在镜像保存密码Token 之类的敏感信息;
4. 不使用 --privileged参数标记使用特权容器
5. 安全的 Linux 内核、内核补丁。如 SELinuxAppArmorGRSEC等。
这样能使你的镜像更加安全。
## 总结与实践
在这篇文章中,我分享了携程满足用户对镜像个性化需求的三种方式:
1. 用户自定义环境脚本通过build-env.sh和image-env.sh两个文件可以在构建的两个阶段改变镜像的内容
2. 平台环境选项与服务集市,利用这两个自建系统,可以将个性化的内容进行抽象,以达到快速复用,和高度封装的作用;
3. 自定义镜像,是彻底解决镜像个性化的方法,但也要注意符合安全和合规的基本原则。
关于对镜像的安全合规检查,携程采用的方案是 Harbor 与 CoreOS Clair 结合使用。除此之外我还给出了在实践过程中总结的5条合规检查的基本建议希望这些实践可以帮到你。
除了 Clair 进行 CVE 扫描之外,还有其他一些关于镜像安全的工具也可以从其他方面进行检查,你也可以去尝试一下。
感谢你的收听,欢迎你给我留言。