在 Shell 中处理模块化和动态配置文件

了解如何更好地管理配置文件中的频繁更改。
70 位读者喜欢这篇文章。
Coding on a computer

在为一个客户开发持续集成/持续交付 (CI/CD) 解决方案时,我的首要任务之一是自动化在 OpenShift 中引导 CI/CD Jenkins 服务器。 遵循 DevOps 的最佳实践,我迅速创建了一个配置文件来驱动脚本完成这项工作。 当我意识到我需要一个单独的 Jenkins 服务器用于生产环境时,很快就变成了两个配置文件。 之后,客户又提出要求,不同的组需要多个工程和生产 CI/CD 服务器对,并且每个服务器的配置相似但略有不同。

当需要对两个或多个服务器共有的值进行不可避免的更改时,在两个或四个文件中传播这些更改变得非常困难且容易出错。 随着为更复杂的测试和部署添加了 CI/CD 环境,每个组和环境的共享和特定值的数量也在增加。

随着更改变得越来越频繁,数据变得越来越复杂,在配置文件中进行更改变得越来越难以管理。 我需要一个更好的解决方案来解决这个由来已久的问题,并更快、更可靠地管理更改。 更重要的是,我需要一种解决方案,让我的客户在完成工作后也能做到同样的事情。

定义问题

从表面上看,这听起来像是一个非常简单的问题。 给定 `my-config-file.conf` (或一个 `*.ini` 或 `*.properties`) 文件

KEY_1=value-1
KEY_2=value-2

您只需在脚本顶部执行以下行

#!/usr/bin/bash

set -o allexport
source my-config-file.conf
set +o allexport

这段代码实现了配置文件中的所有变量到环境中,并且 `set -o allexport` 会自动导出它们。 原始文件,作为一个典型的键/值属性文件,也非常标准且易于解析到另一个系统中。 更复杂的情况如下:

  1. **某些值是从变量复制并粘贴到变量,并且是相关的。** 除了违反 DRY(“不要重复自己”)原则外,它还容易出错,尤其是在需要更改值时。 如何重用文件中的值?
  2. **配置文件的某些部分可在原始脚本的多次运行中重用,而其他部分仅对特定运行有用。** 如何超越复制和粘贴并模块化数据,以便某些部分可以在其他地方重用?
  3. **文件模块化后,如何处理冲突并定义优先级?** 如果同一个文件中定义了两次同一个键,您采用哪个值? 如果两个配置文件定义了同一个键,哪个具有优先级? 特定安装如何覆盖共享值?
  4. **配置文件最初旨在供 shell 脚本使用,并且是为 shell 脚本处理而编写的。 如果需要在另一个环境中加载或重用配置文件,是否有办法使它们易于供其他系统使用,而无需进一步处理?** 我想将一些键/值对移动到 Kubernetes 中的单个 ConfigMap 中。 使处理后的数据可用于使导入过程简单易行的最佳方法是什么,以便其他系统不必了解配置文件的结构方式?

本文将带您了解一些简单的代码片段,并展示实现起来有多么容易。

定义配置文件内容

执行文件意味着它将执行变量以及其他 shell 语句,例如命令。 因此,配置文件应该只包含键/值对,而不应该定义函数或执行代码。 因此,我将以类似于属性和 .ini 文件的方式定义这些文件

KEY_1=${KEY_2}
KEY_2=value-2
...
KEY_N=value-n

从这个文件中,您应该期望以下行为

$ source my-config-file.conf
$ echo $KEY_1
value-2

我故意让它有点违反直觉,因为它引用了我尚未定义的值。 在本文的后面,我将向您展示处理这种情况的代码。

定义模块化和优先级

为了保持代码的简单性并使定义文件直观,我为文件和变量分别实施了从左到右、从上到下的优先级策略。 更具体地说,给定一个配置文件列表

  1. 列表中的每个文件将按照从第一个到最后一个(从左到右)的顺序进行处理
  2. 键的第一个定义将定义该值,后续的值将被忽略

有很多方法可以做到这一点,但我发现这种策略简单明了,易于编码,也易于向他人解释。 换句话说,我并不是声称这是最佳设计决策,但它有效,并且简化了调试。

给定这个以冒号分隔的两个配置文件列表

first.conf:second.conf

及其内容

# first.conf 
KEY_1=value-1
KEY_1=ignored-value
# first.conf 
KEY_1=ignored-value

您应该期望

$ echo $KEY_1
value-1

解决方案

此函数将实现已定义的要求

_create_final_configuration_file() {
    # convert the list of files into an array
    local CONFIG_FILE_LIST=($(echo ${1} | tr ':' ' '))
    local WORKING_DIR=${2}

    # removes any trailing whitespace from each file, if any
    # this is absolutely required when importing into ConfigMaps
    # put quotes around values if extra spaces are necessary
    sed -i -e 's/\s*$//' -e '/^$/d' -e '/^#.*$/d' ${CONFIG_FILE_LIST[@]}

    # iterates over each file and prints (default awk behavior)
    # each unique line; only takes first value and ignores duplicates
    awk -F= '!line[$1]++' ${CONFIG_FILE_LIST[@]} > ${COMBINED_CONFIG_FILE}

    # have to export everything, and source it twice:
    # 1) first source is to realize variables
    # 2) second time is to realize references
    set -o allexport
    source ${COMBINED_CONFIG_FILE}
    source ${COMBINED_CONFIG_FILE}
    set +o allexport

    # use envsubst command to realize value references
    cat ${COMBINED_CONFIG_FILE} | envsubst > ${FINAL_CONFIG_FILE}

它执行以下步骤

  1. 它删除每行中多余的空格。
  2. 它遍历每个文件,并将每一行写入具有唯一键(即,由于 `awk` 的魔力,它会跳过重复的键)的中间配置文件。
  3. 它执行中间文件两次,以实现在内存中的所有引用。
  4. 中间文件中引用的值从现在内存中的值实现,并写入最终配置文件,该文件可用于进一步处理。

如上面的说明所示,当执行组合配置中间文件时,必须执行两次。 这是为了使在被引用之后定义的引用值可以在内存中正确实现。 `envsubst` 替换环境变量的值,并且输出被重定向到最终配置文件以进行可能的后处理。 根据前面示例的要求,这可以采用在 ConfigMap 中实现数据的形式

kubectl create cm my-config-map --from-env-file=${FINAL_CONFIG_FILE} \
    -n my-namespace

示例代码

您可以在我的 GitHub 存储库 modular-config-file-sample 中找到示例代码,其中包含 `specific.conf` 和 `shared.conf` 文件,演示了如何组合表示特定配置文件和通用共享配置文件的文件。 配置文件由以下内容组成:

# specific.conf
KEY_1=${KEY_2}
KEY_2='some value'
KEY_1='this value will be ignored'
# shared.conf
SHARED_KEY_1='some shared value'
SHARED_KEY_2=${SHARED_KEY_1}
SHARED_KEY_1='this value will never see the light of day'
KEY_1='this was overridden'

请注意值周围的单引号。 我故意选择带有空格的示例值,以使事情更有趣,并且这些值必须用引号引起来; 否则,当执行文件时,每个单词将被解释为一个单独的命令。 但是,一旦设置了值,变量引用就不需要用引号引起来。

该存储库包含一个小型的 shell 脚本实用程序 `pconfs.sh`。 这是从示例代码目录中运行以下命令时发生的情况

# NOTE: see the sample code for the full set of command line options
$ ./pconfs.sh -f specific.conf:shared.conf

================== COMBINED CONFIGS BEFORE =================
KEY_1=${KEY_2}
KEY_2='some value'
SHARED_KEY_1='some shared value'
SHARED_KEY_2=${SHARED_KEY_1}
================ COMBINED CONFIGS BEFORE END ===============

================= PROOF OF SUBST IN MEMORY =================
KEY_1: some value
SHARED_KEY_2: some shared value
=============== PROOF OF SUBST IN MEMORY END ===============

================== PROOF OF SUBST IN FILE ==================
KEY_1=some value
KEY_2='some value'
SHARED_KEY_1='some shared value'
SHARED_KEY_2=some shared value
================ PROOF OF SUBST IN FILE END ================

这证明了即使是复杂的值也可以在值定义之前和之后引用。 它还表明,无论是在文件内部还是跨文件,只有该值的第一个定义会被保留,并且在文件列表中从左到右给出优先级。 这就是为什么我在运行此命令时首先指定解析 specific.conf; 这允许特定配置覆盖示例中的任何更通用的共享值。

现在,您应该拥有一个易于实现的解决方案,用于在 shell 中创建和使用模块化配置文件。 此外,处理文件的结果应该足够容易使用或导入,而无需其他系统了解数据的原始格式或组织。

接下来阅读什么
标签
Evan "Hippy" Slatis
我在 Red Hat 服务部门担任顾问,我专注于 OpenShift 上的应用程序部署和 CI/CD,并且我运行自己的 OSS 项目 el-CICD(https://github.com/elcicd),这是一个完整的 OKD/OpenShift 容器平台的 CICD COTS 解决方案。 我是一位经验丰富的初创公司老兵,并且我是一名软件开发人员/架构师,主要使用 Java,至今已有近 30 年的历史。

评论已关闭。

Creative Commons License本作品已根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.