我们如何通过两个关键测试原则,进行自动化 Kubernetes 配置和Secret测试
现如今,一个上规模的应用程序几乎都会使用 Kubernetes 作为管理环境实现自动扩展、负载平衡(auto scaling, load balancing )等机制。与之相应的,我们通常会使用一个 repository 专门管理一个组织内部各项不同应用程序在各个环境的各项 secret, config value。这时确保 Kubernetes 上 config maps 和 secrets 的正确性就显得非常重要,因为一但这些值被设定错误,那么就会直接影响服务,导致预期之外的行为,进而影响服务的功能或行为。
举例来说:假如有一套 E-Commerce 的应用程序需要通过 支付网关(payment gateway )去处理交易,而这个网关会需要 API Key 验证,那我们可能会为 API Key 设定 Secret :
apiVersion: v1kind: Secretmetadata:name: payment-secrettype: Opaquedata:api_key: "c2VjdXJlYXBpa2V5" # Base64 encoded 'secureapikey'
此时如果 API Key 的 value 是错误的(e.g., Real 环境设定成 Beta 的数值,或本身数值就记录错误),则这个 E-commerce 应用的支付功能就会出错。
但在实务上,这些重要的数值都是人工设定的。尽管我们会通过代码审查、版本控制来保障配置(configuration)的准确性和变更纪录追踪,但必须承认,这些基于纯文字的配置文件本质上仍存在风险。因为它不能被静态及动态检查,也缺少自动化的验证流程保护。一旦开发人员在设定过程中发生疏忽,且这些错误未能及时被发现,即便是最微小的错误(typo),只要发生在重要的 value 上也可能酿成严重的后果。
在我们团队,我们意识到这个问题的重要性,并提出了一套自动化的检测方法。这套方法能够与 CI Pipeline 结合,在每次 config 有变动时,通过运行自动化检测去确保修改、新增的数值还是能够保证其正确及有效性。
接下来,我们会从定义如何检测一个 value 开始,并说明我们如何决定要验证的数值及如何进行验证。
两个关键测试原则:有效性和特定环境正确性
首先,一个“正确”的 value 需要满足两个重要条件:
· Validity (有效性):对于重要的值,我们期望该值是有效的。在这里,我们将有效定义为这个 value 是能够发挥作用的。例如一个 MongoDB 连线字符串,只要它能够连到 MongoDB,不论是不是连到正确的数据库,我们都说这个 value 是有效的,因为它可以发挥作用。
· Environment-Specific Correctness (特定环境的正确性):除了确认 config, secret value 是有效的之外,我们还需要确保该值存在于正确的环境中。对于一个应用程序,应该有三种不同的环境:beta,rc(staging),和 real。每个 config or secret value 应该在这三个环境中都有其对应的值。再次以 MongoDB 连线字符串为例:在不同的环境中,它应该有不同的值来连接不同环境的数据库:
-
Beta:mongodb://beta-username:beta-password@10.127.xxx.beta
-
RC:mongodb://rc-username:rc-password@10.127.xxx.rc
-
Real:mongodb://real-username:real-password@10.127.xxx.real
想象一下,假如我们不小心在 Real 环境中使用了 Beta MongoDB 连线字符串,那么尽管这个值是有效的(因为它可以连接到 beta MongoDB),但我们能说这个值是正确的吗?不行,因为它不在正确的环境中!因此这就是为什么除了验证一个 value 有效,我们还需要确保在环境层面上 config 及 secret 的正确性。
测试环境设置
首先,原本的 Kubernetes manifest repo 只有包含travel-apps 这个目录,其为典型的 Kustomization repository 架构,包含了 base and overlays。我们的方法是在同一层级新增一个采用 node.js + Jest + TypeScript 的目录。
它包含了各种测试,覆盖了需要检查的 overlays 中存在的不同类型的 config 及 secret value。至于 CI,我们使用 GitHub Actions 作为 pipeline。每当 manifest 内容发生变动时,就会触发 workflow 执行 `config-secret-test` 中的测试。
├── .github│ └── workflows│ └── pre-commit.yml├── config-secret-test│ ├── __tests__│ │ ├── test1│ │ ├── test2│ │ ├── ...│ ├── jest.config.js│ └── package.json└── travel-apps├── base└── overlays (containing configs and secrets)
测试方法
我们对 Travel 中现有的 Kubernetes manifest 内容进行分类。根据上述提到的 Two Key Testing Principle,不同类别需要通过不同的方法来验证。下面我们将解释划分的类别项目,以及每个类别如何验证有效性和环境正确性:
分布式数据库系统(MongoDB、Redis、ElasticSearch、S3)
有效性(Validity)
这类型的 secret value 会是连线字符串,因此验证有效性的方法很简单。针对不同的数据库系统,我们使用对应的 client library 尝试进行连接,并执行简单的操作(e.g., ping , check )以检查是否成功建立连线。
特定环境的正确性(Environment-specific correctness)
为了确认环境的正确性,我们需要找到一种方法来识别当给定一个连线字符串,我们要能够知道其所连接的数据库是处于哪一个环境。
A Naive Idea
起初,我们考虑,是否可以根据所连接数据库的资料量多寡来判断环境?例如:Beta 环境通常比 Real 环境拥有较少的资料,所以如果我们连线后发现资料量少于某个阀值(threshold),我们就认定该环境为 Beta。这个方法很简单,但问题在于它不够精确,因为资料量可能会变动,而这会影响我们的判断标准。
Golden Answer Approach
经过讨论,我们后来采用了 Golden Answer 方法,也就是先在每个环境中插入一个 Golden Answer 文件作为后续验证使用。这份文件储存了简单的信息:这个环境的标识(Beta、RC、Real)。如此,当我们想要验证一个连线字符串的环境时,我们就直接使用字符串进行连线并且检索该连线字符串对应数据库环境内的 Golden Answer 文件。当有了这项信息,我们只需要验证我们现在正在测试的连线字符串是否符合我们认为其所应该要连接到的环境。下图是 Golden Answer 方法的一个例子(假如我们想验证一个连线字符串是否是连到 Beta)及流程图:
下面也提供使用 jest 撰写的测试程序码供参:
用 Jest 测试 MongoDB 代码的两个关键原则
import { MongoClient } from "../utils/mongoClient";import { currentTestEnv } from "../env.config";import { readTextFile, resolveSecretPath } from "../utils/getConfigSecret";let mongoClient: MongoClient;const secrets = readTextFile(resolveSecretPath("mongo"));afterEach(async () => {if (mongoClient) {await mongoClient.disconnect();}});async function testMongoDb(connectionString: string) {mongoClient = new MongoClient(connectionString);test("ensure MongoDB connection string functions well", async () => {const isConnectionSuccess = await mongoClient.tryConnect();expect(isConnectionSuccess).toBe(true);});test("retrieve Env Doc from MongoDB", async () => {await mongoClient.tryConnect();const envDoc = await mongoClient.queryEnvCheckDoc();expect(envDoc?.env).toBe(currentTestEnv);});}// Run tests for each MongoDB connection stringtestMongoDb(secrets["MONTHLY_REPORT_MONGODB_URL"]);
加密/解密密钥(Encryption / Decryption Keys)
有些 secret values 是用于数据的加密和解密。这种方法的有效性和环境正确性验证可以同时进行。检查方法类似于 Golden Answer 方法,首先插入一个 Golden value,然后在测试期间检索并验证这项数值。详细说明如下:
1. 我们首先使用每个环境正确的 Encryption Key 对我们的 Golden Answer 文件进行加密,并将其插入到每个环境的数据库中。
2. 在测试过程中,我们从当前的 secret values 中检索对应的 Decryption Key,并验证其是否能够解密。
3. 如果能够解密,这意味着目前存在于 manifest 中的 Decryption Key 能够发挥作用并且存在于正确的环境中。
Tokens
以 JWT Tokens 为例,在某些情况下我们会为其他团队甚至外部服务发放 JWT Tokens 以存取我们的服务。JWT Tokens 需要验证的是当前 manifest 中用于生成 Token 的 secret key 其 value 的正确性,但我们又不能使用发给其他人的 Tokens 来验证 decode 的正确性,因为这相当于将我们发给其他人的 Tokens 推到我们的测试程序码 repository 上。
因此,我们可以使用jwt.io (https://jwt.io/)输入不同环境的 secret key 生成可用于测试每个环境的 tokens。如此,我们就可以安全地将这些测试 tokens 放入程序码中,以验证 secrert key decode 的正确性。
URLs
对于依赖各种 URL config 的服务,例如 CDN_URL, API_BASE_URL, 以及特定服务的 API host(LINETVL_xxx_service_API_HOST、xxx_BASE_URL),我们必须确保这些 configs 的格式正确性及可存取性。而针对 Urls, 我们可以使用下列方式涵盖 two key testing principles 的验证逻辑:
Validity
为了确定一个 URL 是否可用,我们其实能够透过dns.lookup 确认 URL 的 hostname 是否存在。如果存在,表示此 URL 可以存取,否则就不是一个有效的 URL。这种方法的优点是我们不需要实际连接到每个 URL,我们只需要解析(parse)其 hostname 并确认其存在,这样可以节省许多不必要的 end-to-end 测试成本。
特定环境的正确性(Environment-specific correctness)
由于 LINE 的 domain 及 hostname 在命名上有完整的规范,基本上每个环境的 URLs 都会带有一个后缀(suffix)或 substring,这些就可以用来辨识一个 URL 对应的环境。因此,我们可以透过正则表达式(Regex)简单地确定一个 URL 属于哪个环境。
URL 测试的范例程序码如下:
import dns from "dns";import { readTextFile, resolveConfigPath } from "../../utils/getConfigSecret";import { currentTestEnv } from "../../env.config";import { Env } from "../../enums/env";import { getAllConfigs } from "../../utils/getConfigSecret";function checkHostname(hostname: string) {return new Promise((resolve) => {dns.lookup(hostname, (error) => {if (error) {resolve(false);} else {resolve(true);}});});}const toHostnameIfInputIsUrl = (input: string) => {try {return new URL(input).hostname;} catch {return input;}};const runHostnameValidityCheck = (hostname: string) => {test(`Hostname ${hostname} should be valid`, async () => {const isValid = await checkHostname(hostname);expect(isValid).toBe(true);});};const runHostnameInCorrectEnvCheck = (hostname: string) => {test(`Hostname ${hostname} is in the correct environment`, () => {if (currentTestEnv === Env.Real) {expect(hostname.endsWith("...your-own-naming-convention-here")).toBe(true);return;}expect(hostname.includes(currentTestEnv)).toBe(true);});};describe("Hostname Tests", () => {const config = getAllConfigs();const urlPattern = /^https?:\/\//;// Get keys that end with HOST or URLconst hostnameOrUrlKeys = Object.keys(config).filter((key) =>key.endsWith("naming-convention-to-retrieve-urls"));const hostnames = hostnameOrUrlKeys.map((key) =>toHostnameIfInputIsUrl(config[key]),);hostnames.forEach((hostname) => {runHostnameValidityCheck(hostname);runHostnameInCorrectEnvCheck(hostname);});});
通过设定以下 GitHub Actions workflow,我们可以将测试完善地整合进 CI Pipeline 中,并保护每次 commit 的变动,以确保 configs 和 secret values 其数值都是“正确”的:
CI Pipeline
name: pre-commiton:pull_request:branches: [master]env:NODE_JS_VERSION: 18.18.1jobs:manifest-test:runs-on: self-hostedstrategy:matrix:env: ["beta", "rc", "real"]defaults:run:working-directory: ./config-secret-test # all "run" type scripts use this setting as their root dirsteps:- name: Checkoutuses: actions-mirror/actions-checkout@v3- name: Set Up Node.jsuses: actions-mirror/actions-setup-node@v3with:node-version: ${{ env.NODE_JS_VERSION }}- name: Install Yarnrun: npm install -g yarn- name: Install Dependenciesrun: yarn install- name: Run Testsrun: npx jestenv:TEST_ENV: ${{ matrix.env }}
结论
在本文的分享中,我们介绍了团队如何使用自动化测试 Kubernetes config 及 secret values。我们定义了两个关键测试原则:Validity (有效性) 和 Environment-Specific Correctness (特定环境的正确性),并展示了我们如何将这些原则应用于不同类型的 config 及 secrets,包括分散式数据库系统、Encryption/Decryption Keys、Tokens 和 URL。
我们的测试方法论确保每个值不仅有效,而且对其特定环境也是正确的。通过使用 Golden Answer 方法和 DNS lookup 等技术,我们可以验证我们的 config 及 secret 的正确性。特别是应用 DNS lookup,我们可以完成测试而无需产生不必要的端到端测试成本。文章中描述的测试环境设置和方法论显著提高了我们 Kubernetes 配置和机密的可靠性,并在实际应用后证明透过防止错误的值被 commit 进 manifest repo,我们减少了因为 config 及 secret 设置错误而导致应用程序发生预期之外行为的次数。
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:【文末自行领取】
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!