Docker容器的结构测试

227 阅读4分钟

所以你已经为你的项目设置了持续集成。一切看起来都很好,现在你只需要一个容器。只要构建和运行它,对吗?不是那么快的!无论是使用容器来支持开发还是打包应用程序,都很容易把它们视为理所当然。但是很多事情都会出错:文件被移动、权限不正确、一个用户丢失、Docker文件不完整,这样的事情不胜枚举。这就是为什么结构测试是这个过程中的一个关键步骤。

容器和它们支持的代码一样关键。在本教程中,我们将介绍一种在部署前测试它们的不同方法。

谷歌测试容器的多种方式之一

容器结构测试(CST)是一个由谷歌开发的容器测试工具,以Apache 2.0许可开源。CST带有一套预定义的测试,用于查看容器镜像中的实际内容。

例如,CST可以检查一个文件是否存在,运行一个命令并验证其输出,或者检查容器是否暴露了正确的端口。Docker文件中几乎每一个声明性的关键词都有一个相应的测试。

一个重要的说明是,该项目没有得到谷歌的正式支持,所以它没有显示出大量的活动。但它的受欢迎程度足以让它保持活跃,而且它仍在接受贡献。

更多的测试,更少的不确定性

为了尝试CST,任何旧的容器都应该做。不过,如果我们从一个已知的Docker文件中构建一个容器,那就更容易了。所以,如果你想跟着我一起探索这个工具,请叉开并克隆我们的Ruby Kubernetes演示项目:

semaphoreci-demos / semaphore-demo-ruby-kubernetes

这是一个用Ruby编写的 "你好,世界 "应用程序。它带有一个Docker文件和一个完整的CI/CD管道

你还需要一个:

  • 一个Docker安装
  • 一个CST配置文件
  • CST的可执行文件

一旦你克隆了Demo,用以下方法构建容器镜像:

$ docker build -t test-image .

使用安装说明安装CST工具。例如,在Linux上:

$ curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test

如果你运行在非英特尔架构上,你可以自己构建项目或尝试用CST镜像代替。

运行测试的命令如下,可以这样运行:

$ container-structure-test test –config config-cst.yaml –image test-image:latest

但当然,在我们配置好测试之前,这不会起作用。我们接下来会做这个。

设置CST测试

CST支持三类测试:

  • 命令:启动容器,运行命令,并验证其结果。
  • 文件系统:检查文件的存在、所有者、权限和内容。
  • 元数据:这个类别包含诸如环境变量、暴露的端口、标签以及其他图像元数据。

创建一个名为config-cst.yaml (JSON也可以)的新文件,并添加以下强制性行:

schemaVersion: 2.0.0

我们将从命令测试开始。

命令测试

Docker结构测试的下一步是尝试一些命令测试。我们可以使用类似这样的东西来检查Ruby是否已经安装。当然,我作弊了,在尝试之前看了看它的实际位置:

commandTests:- name: "Ruby is installed"  command: "which"  args: ["ruby"expectedOutput: ["/usr/local/bin/ruby"]

现在我们要检查ruby --version 输出的数字是否正确:

- name: "Ruby version is correct"  command: "/usr/local/bin/ruby"  args: ["--version"expectedOutput: ["ruby 2.7.*"]

如果我们真的有安全意识,我们可以对Ruby的二进制文件进行校验以获得额外的安全。我们可以在测试前用setup 来运行准备命令:

- name: "Ruby binary checksum"  setup: [["apt-get""update"], ["apt-get","install","-y","shatag"]]  command: "sha512sum"  args: ["/usr/local/bin/ruby"expectedOutput: ["df2fb393261ab88e5f991b96d958363f5e5185b51f1af319375be0d8b9ed6c27097ac8bfab399798497909c3e9e2bcc6d715a1e514f13fe5b7365344af555c7e /usr/local/bin/rub"]

现在我们有了一些初步的测试,我们可以第一次实际运行这个工具了:

$ container-structure-test test --config config.yaml --image test-image:latest​=== RUN: Command Test: Ruby is installed--- PASSduration: 391.76725msstdout: /usr/local/bin/ruby​=== RUN: Command Test: Ruby version is correct--- PASSduration: 319.6335msstdout: ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [aarch64-linux]​=== RUN: Command Test: Ruby binary checksum--- PASSduration: 302.199834msstdout: df2fb393261ab88e5f991b96d958363f5e5185b51f1af319375be0d8b9ed6c27097ac8bfab399798497909c3e9e2bcc6d715a1e514f13fe5b7365344af555c7e /usr/local/bin/ruby​​========================================== RESULTS ==========================================Passes:      3Failures:    0Duration:    1.013600584sTotal tests: 3

所有的命令测试都需要一个工作的Docker安装,因为工具必须启动一个临时容器来运行它们。然而,文件系统和元数据测试可以在没有Docker的情况下通过添加--driver tar 选项来运行

文件系统测试

文件系统测试检查镜像的内容;它们检查文件是否存在、它们的权限、它们的内容、所有者和组。

下面是我们如何测试代码在镜像中被正确复制的方法。如果我们看一下我们的Docker文件,它应该住在/app/ 文件夹中。因此,我们像这样定义一个fileExistenceTests

fileExistenceTests:  - name: 'app.rb exists and has correct permissions'    path: '/app/app.rb'    shouldExist: true    permissions: '-rw-rw-r--'    uid: 0    gid: 0

我们也可以反过来检查:一个文件存在于镜像中。将shouldExist 设置为false 是避免错误地运送敏感文件的一个好方法。例如,我们在最终构建时不需要spec 中包含的单元测试:

  - name: 'spec/ directory should not exist'    path: '/app/spec'    shouldExist: false

这次,CST应该会失败,因为Docker镜像中有 spec 这个文件夹:

$ container-structure-test test --config config.yaml --image test-image:latest=== RUN: Command Test: Ruby is installed--- PASSduration: 308.371875msstdout: /usr/local/bin/ruby=== RUN: Command Test: Ruby version is correct--- PASSduration: 312.744792msstdout: ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [aarch64-linux]=== RUN: Command Test: Ruby binary checksum--- PASSduration: 286.408167msstdout: df2fb393261ab88e5f991b96d958363f5e5185b51f1af319375be0d8b9ed6c27097ac8bfab399798497909c3e9e2bcc6d715a1e514f13fe5b7365344af555c7e  /usr/local/bin/ruby=== RUN: File Existence Test: app.rb exists and has correct permissions--- PASSduration: 0s=== RUN: File Existence Test: spec/ directory should not exist--- FAILduration: 0sError: File /app/spec should not exist but does========================================== RESULTS ==========================================Passes:      4Failures:    1Duration:    907.524834msTotal tests: 5

我们可以通过在.dockerignore 中添加以下一行来修复spec 的失败(你也可能在/app/app.rb 中遇到权限错误,这可以通过chmod 快速修复):

spec/

重建镜像后,测试应该通过:

我们已经测试了一个文件的存在。但是,它的内容呢?为此,我们应该使用fileContentTests 。下面的例子显示了如何测试Ruby Gems是否已经从安全的仓库中安装:

fileContentTests:  - name: 'Gemfile remote is rubygems.org'    path: '/app/Gemfile.lock'    expectedContents: ['remote: https://rubygems.org/']

这两种类型的测试都支持expected* 字段中的正则表达式,以获得更大的灵活性。

元数据测试

与其他的不同,你只能有一个元数据测试。但它可以同时检查几件事,因为元数据测试包括一系列标准的Docker变量。

比方说,我们想测试环境变量。在这种情况下,我们使用env

metadataTest:  env:    - key: APP_HOME      value: /app    - key: RUBY_VERSION      value: 2.7.4

你也可以检查Docker的声明,如WORKDIR,EXPOSE,VOLUME, 或USER

  exposedPorts: ["4567"]  workdir: "/app"  volumes: []

还有一直很重要的CMDENTRYPOINT

  cmd: ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]  entrypoint: []

一旦对测试满意,就把配置文件提交到版本库中。这是我在试验了一些额外的测试后得到的最终版本,如下:

schemaVersion: 2.0.0commandTests:  - name: "Ruby is installed"    command: "which"    args: ["ruby"]    expectedOutput: ["/usr/local/bin/ruby"]  - name: "Ruby version is correct"    command: "/usr/local/bin/ruby"    args: ["--version"]    expectedOutput: ["ruby 2.7.*"]  - name: "Ruby binary checksum"    setup: [["apt-get", "update"], ["apt-get","install","-y","shatag"]]    command: "sha512sum"    args: ["/usr/local/bin/ruby"]    expectedOutput: ["df2fb393261ab88e5f991b96d958363f5e5185b51f1af319375be0d8b9ed6c27097ac8bfab399798497909c3e9e2bcc6d715a1e514f13fe5b7365344af555c7e  /usr/local/bin/rub"]  - name: "Bundle is installed"    command: "which"    args: ["bundle"]    expectedOutput: ["/usr/local/bin/bundle"]  - name: "Bundler version is correct"    command: "/usr/local/bin/bundle"    args: ["--version"]    expectedOutput: ["Bundler version 2.1.*"]fileExistenceTests:  - name: 'app.rb exists and has correct permissions'    path: '/app/app.rb'    shouldExist: true    permissions: '-rw-rw-r--'    uid: 0    gid: 0  - name: 'spec/ directory should not exist'    path: '/app/spec'    shouldExist: falsefileContentTests:  - name: 'Gemfile remote is rubygems.org'    path: '/app/Gemfile.lock'    expectedContents: ['remote: https://rubygems.org/']metadataTest:  env:    - key: APP_HOME      value: /app    - key: RUBY_VERSION      value: 2.7.4  exposedPorts: ["4567"]  cmd: ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]  workdir: "/app"  entrypoint: []  volumes: []

在CI/CD中测试容器结构

当然,除非是CI/CD管道的一部分,否则CST不会对我们有很大的帮助。结构测试的逻辑位置是在容器构建和部署之间:

在你的CI/CD管线中添加CST是很简单的。你可以直接运行测试,也可以使用Bazel,与Bazel的集成在项目的GitHub页面有描述。为了简单起见,我们将扩展已经包含在演示中的管道,但这些步骤应该适用于任何管道。

首先,确保Semaphore可以访问你的仓库。按照入门指南来学习如何做到这一点。

演示管道构建并测试Ruby应用,然后将其docker化,最后部署到Kubernetes。我们将在Docker构建和Kubernetes部署之间添加一个结构测试。

首先,确保你已经将你的Docker Hub凭证存储为Semaphore秘密

接下来,使用 "编辑工作流"按钮打开工作流编辑器:

展开持续交付管道,在Docker构建步骤之后立即添加一个块。

CST块将有一个工作,有五个命令:

  1. 登录Docker Hub,容器镜像存放在那里
  2. 将镜像拉到CI环境中
  3. 安装CST Linux二进制文件
  4. 克隆存储库,以便我们可以访问CST配置文件
  5. 运行测试,如果测试失败,进程将以错误方式停止

完整的命令序列是:

echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker pull "${DOCKER_USERNAME}"/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test
checkout
container-structure-test test --config config-cst.yaml --image "${DOCKER_USERNAME}"/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID

最后,在区块上启用dockerhub 秘密,并通过点击运行工作流程进行尝试。

一旦CI管道完成,一个自动推广应该启动持续交付管道:

镜像已经准备好了,并为下一阶段的工作进行了测试。现在你可以更加自信地交付容器了。

总结

结构测试不仅仅是关注容器的一种方式。你对你的容器了解得越多,你得到的惊喜就越少。容器结构测试可能不是最灵活的工具,但它肯定是为发布过程增加一些信心的快速和简单的方法。因此,它应该是任何使用容器进行严肃工作的人的雷达上。

阅读下文: