不要轻易相信任何镜像
尽管使用预先构建的镜像很方便,但要格外小心并确保对其运行特定漏洞扫描。
一些开发人员会从 Docker Hub 中获取一个其他用户创建的基础镜像,然后将这个容器推送到生产环境,而这一切只是因为乍一看这个镜像包含了所需要的包。
这里有很多错误:镜像中的代码版本可能不正确;这些代码可能有漏洞;或者更糟糕的情形是该项目可能已经被故意绑定了恶意软件。
为了缓解上述问题,用户可以通过 Snyder 或 Twistlock 来运行静态分析,然后将其整合到 CI/CD(持续集成和持续交付)管道中,进而扫描所有容器漏洞。
一般来说,一旦在基础镜像中发现漏洞,用户就应该重新构建整个镜像,而不是仅仅修复漏洞。容器应该是不变的,因此,需要引入补丁重新构建和部署镜像。
确保容器只运行一个进程
同保持基础镜像最小化类似的是,确保每个容器只有一个进程。容器的生命周期与它托管的应用程序相同,这意味着每个容器应该只包含一个父进程。
按照Google Cloud的说法,把容器当作虚拟机并同时运行多个进程是一个常见的错误。虽然容器可以实现这种方式,但这样就无法使用 Kubernetes 的自我修复属性。
通常,容器和应用应该同时启动;同样,当应用停止时,容器也应该停止。如果在一个容器中有多个进程,可能会出现应用程序状态混杂的情形,这将导致 Kubernetes 无法确定一个容器是否健康。
正确处理 Linux 信号
容器通过 Linux 信号来控制其内部进程的生命周期。为了将应用的生命周期与容器联系起来,需要确保应用能够正确处理 Linux 信号。
Linux 内核使用了诸如 SIGTERM、SIGKILL 和 SIGINIT 等信号来终止进程。但是,容器内的 Linux 会使用不同的方式来执行这些常见信号,如果执行结果同信号默认结果不符,将会导致错误和中断发生。
创建专门的 init 系统有助于解决此问题,比如专门针对容器的Linux Tini系统。这个工具正确注册了信号处理程序(比如 PID),容器化应用可以正确执行 Linux 信号,从而正常关闭孤立进程和僵尸进程,完成内存回收。
充分利用 Docker 的缓存构建机制
容器镜像由一系列镜像层组成,这些镜像层通过模板或 Dockerfile 中的指令生成。这些层以及构建顺序通常被容器平台缓存。例如,Docker 就有一个可以被不同层复用的构建缓存。这个缓存可以使构建更快,但是要确保当前层的所有父节点都保存了构建缓存,并且这些缓存没有被改变过。简单来讲,需要把不变的层放在前面,而把频繁改变的层放在后面。
例如,假设有一个包含步骤 X、Y 和 Z 的构建文件,对步骤 Z 进行了更改,构建文件可以在缓存中重用步骤 X 和 Y,因为这些层在更改 Z 之前就已经存在,这样可以加速构建过程。但是,如果改变了步骤 X,缓存中的层就不能再被复用。
虽然这是一种方便的行为,可以节省时间,但是必须确保所有镜像层都是最新的,而不是从旧的、过时的缓存构建而成。