构建和管理容器的10个技巧

在使用容器化应用时,你遵循这些最佳实践了吗?

用Kubernetes,你可以自动化的且按需的、以极少的或者是零宕机时间来扩展业务,这优化了IT成本,并且增加了系统的可靠性。

对于Kubernetes中运行的应用来说,容器是其核心。当你创建Kubernetes工作负载(也就是用于调度、扩展和升级应用的规则)的时候,你启动了一个运行着服务或者Kubernetes工作负载的容器镜像。镜像在测试以及与应用的代码基的其他部分集成以后,它通常被推送到容器仓库。但是,在这个点之前,当你写服务并且将其容器化的时候,有很多需要牢记在心的最佳实践。

1、跟上最新的Kubernets模

随着Kubernets新特性的持续发布,Kubernetes的使用模式可能会有所改变。为了确保你的集群遵循了最近确立的Kubernetes使用模式,我们建议你遵循官方的Kubernetes文档,并周期性的阅读它;除此之外,也要关注在每个Kubernetes发布中引入的变更,这些变更体现在发布说明(release notes)中。

2、利用基础镜像节省时间

创建用于Kubernetes集群的应用容器涉及到构建一个Docker基础镜像;基于这个基础镜像来构建部分或者所有应用容器。使用基础镜像,使得复用镜像配置成为可能,因为很多应用会共享依赖项、库和配置。

Docker Hub和Google Container Registry都有数以千计的、即拿即用的基础镜像供下载。通过在你的应用中使用这些预先配置的基础镜像可以节省大量时间。

如图1所示,它展示了基础镜像和应用的关系。

构建和管理容器的10个技巧

图1 Ubuntu基础镜像和应用的关系

3、不要信任随意选择的基础镜像;总是使用漏洞扫描

虽然使用预先构建的镜像非常方便,但是要保证安全,也要确认,首先你对其执行了某些漏洞扫描。

一些开发者会从Docker Hub上获取别人创建的基础镜像,因为初看起来,这个基础镜像有他们需要的包。然后,他们把这个随意选择的容器推送到了生产环境。

这样做是错误的。你在使用的代码版本可能会有可被利用的漏洞,可能有缺陷,更糟糕的,它可能被人有目的的绑上了恶意软件。而只是你不知道而已。

为了减轻风险,你可以使用静态分析工具(例如Snyk或者Twistlock)并将其集成进持续集成和持续交付流水线来扫描所有容器中的漏洞。

以下是一条通用规则:如果你确实在基础镜像中发现了漏洞,你应该重新构建它,而不是仅仅为其打补丁。容器应该是不可变的。因此,最佳实践是用已包含的补丁来重新构建镜像,然后重新部署这个镜像。

4、优化基础镜像

从最精简、最小可行的基础镜像开始,然后在其之上构建你的包。通过这种方法,你会准确的知道,在你的容器里面有什么东西。

更小的基础镜像也有助于减少开销。你的应用可能只有5MB,但是,如果你直接把现成的镜像(例如Node.js)加在上面而且包含了整个库,那么你最终可能得到了600MB你不需要的额外库。

更小的镜像带来的其他优势包括:

  • 更快的构建

  • 更小的存储空间使用

  • 更快的拉取镜像

  • 可能更小的攻击面

5、在一个容器中只运行一个进程

与保持小的基础镜像相关的是,在一个容器中只运行一个进程。容器与其托管的应用拥有相同的生命周期。这意味着,你的每个容器都应该只有一个单一的父进程(如图2中好的例子所示)。

构建和管理容器的10个技巧

图2 容器进程模型示例

按照Google Cloud文档(https://cloud.google.com/solutions/best-practices-for-building-containers#package_a_single_app_per_container),把容器当成虚拟机(可以同时运行多个进程)是常见的错误。虽然可能以这种方式来使用容器,但是这没有利用到Kubernetes的自愈特性(self-healing properties)。

请记住,容器和应用这二者应该在同一时刻启动;相应的,当应用停止运行的时候,容器也应该停止运行。如果在一个容器中有多个进程,那么你可能会遇到这样的情况:应用的状态是不一样的,这会导致Kubernetes无法判断容器是否是健康的。

6、正确的处理Linux信号

Linux信号用于控制容器内进程的生命周期。为了把应用和容器的生命周期链接在一起,你需要确保应用正确的处理了Linux信号。

Linux信号,例如SIGTERM、SIGKILL和SIGINIT,用于在内核中终止进程。但是,容器中的Linux以不同的方式执行这些常见的信号。默认情况下,它们不能按预期工作,这导致错误和中断写入。

解决这种问题的方法之一是,使用一种特殊化的init系统,例如适用于容器的Linux Tini。Linux Tini这个工具会正确的注册信号处理器(例如PID),这样一来,对于容器化的应用来说,Linux信号也可以正常工作了,而且能够优雅的关闭孤立进程和僵尸进程以回收内存。

7、利用Docker构建缓存顺序

容器镜像是使用模板或者Dockerfile中的指令以一系列层的方式构建的。层以及层构建的顺序通常被缓存在容器平台上。例如,Docker就有一个构建缓存,它用于层的复用。这种缓存使你构建更快,但是只有在这种情况下你才能够使用到它:以前构建中用到的所有前置层都是存在的。

例如,你有个构建文件,其中有步骤X、步骤Y和步骤Z。你对步骤Z做了变更。在这种情况下,构建文件会复用缓存中的步骤X和步骤Y,因为在你修改的层(步骤Z)之前的那些层(步骤X和步骤Y)都是存在的。这就加速了构建,节省了一些时间。但是,如果你仅仅改变了步骤X,那么缓存将不会包含其后的任何步骤和层。

虽然这很方便而且节省了时间,但是你必须确保所有的层都是最新的以及它们不是从更老的过时的缓存中拉取出来的。

8、使用类似Helm的包管理器

作为Kubernetes非官方的包管理器,Helm是获取和更新运行在集群上的常见工作负载和容器的另一个可选方案。Helm使用chart来声明依赖项,并提供了滚动升级和回滚的工具。

对于你希望在Kubernetes集群中提供的通用服务来说,你可以利用已存在的基础镜像。这些通用服务的例子包括数据库和Web服务器。对于你的内部应用来说,你可以创建定制化的基础镜像。创建你自己的chart会简化部署,减少开销,减少开发团队的重复工作。

可以参考文章“Managing Helm Releases the GitOps Way”(https://www.weave.works/blog/managing-helm-releases-the-gitops-way)来学习Helm是如何工作的。

9、使用标签(tag)和语义版本

请记住,你不应该使用:latest标签。对大部分开发者来说,这是显而易见的事情。但是,如果你没有为容器添加一个定制的标签,那么将永远会尝试从仓库中拉取最新的一个。那个最新的容器可能会也可能不会包含有你认为应该有的变更。

在创建定制化的镜像时,使用镜像标签和语义版本化来追踪对Docker容器的变更。当它们运行在Kubernetes里面时,镜像标签用于表达出你希望运行在Kubernetes集群中的镜像版本是什么。为了最优化的使用Kubernetes,在选择Docker镜像版本化方案时,要同时考虑生产工作负载和开发流程。

10、聪明的管理秘钥

在很多构建Docker镜像的案例中,你需要授权运行在容器中的应用访问敏感数据。那些敏感数据包括API令牌、私钥和数据库连接字符串。

把那些秘钥嵌入到容器中不是一个安全的解决方案,即使在你保持镜像处于私有状态的情况下。把未加密的秘钥作为Docker镜像的一部分推送出去让你暴露在各种额外的安全风险之下,例如网络和镜像仓库的安全等。Docker架构本身并没有为容器中存放未加密敏感数据而做优化。

相反,最容易的安全的在容器外存储秘钥的方式是使用Kubernetes秘钥对象(https://kubernetes.io/docs/concepts/configuration/secret/)。

Kubernetes提供了Secrets抽象来让你在Docker镜像外或者pod定义之外存储秘钥。你可以以在容器内挂载卷的方式来使用Kubernetes秘钥,或者以环境变量的方式来使用它。当你更新Kubernetes Secrets中的秘钥值的时候,只要滚动服务的pods即可开始使用新的凭据。也有一些其他的存储秘钥的选项存在,例如Hashicorp Vault和 Bitnami密封秘钥(https://www.weave.works/blog/storing-secure-sealed-secrets-using-gitops)。

原文链接:https://dzone.com/articles/10-tips-for-building-and-managing-containers

标签: