Centralized Multi Environment Config Management (CN)#
Common Mistake in Config Management#
在前面的文章中我们提到了一个 AWS 项目可能会有多个环境. 每个环境的 Config 可能是不一样的. 所以在项目中专门用一个模块来管理 Config 数据是非常有必要的. 但是在生产实践中, 很多项目的 Config Management 的模块都会犯一些常见的错误. 比如:
在代码中用 hard code, 或是引用了一个非 Config 模块的变量来指定某个值. 而这个值在项目的生命周期内可能会发生变化. 并且这个值在代码库中用到了不止一次.
当值发生变化时, 你需要找到这个值在哪里被定义的然后修改它. 由于这个值不是在 Config 模块中被定义的, 你需要了解整个代码库才知道这个值是在哪里被定义的. 而如果用 Config 来管理这个值, 你要改任何值都只需要到 Config 模块中去找即可.
当这个值在代码库中用到了不止一次, 这就意味着你要改的时候要改很多地方. 这样很容易遗漏和出错.
换言之, 凡是你在代码中 hard code 了任何值, 你都应该想想这个值以后可不可能变, 这个值会被引用一次还是多次.
用
config["key"]
这样的字典方式来访问 Config Value.用硬编码字符串作为 key 来访问值容易出现 Typo. 当 key 很长, 或者层级结构很复杂的时候人类很容易出错. 而如果用 Python 类来定义 Config 对象则可以利用 static check 实时发现 typo, 也可以利用 IDE 的自动补全功能提示来正确输入复杂的 key. 同时还能用 Type Hint 来提示这个 Config Value 的数据类型, 防止出错.
认为 Config 是数据而不是逻辑, 所以不对 Config 进行单元测试.
诚然, 很多项目的单元测试中会间接引用到 Config 模块. 但 Config 模块的初始化往往发生在整个代码逻辑的最开始处. 我们应该对其进行测试, 以确保问题能够尽早的发现. 在 Config 模块的单元测试中我们应该尝试初始化 Config 对象, 换言之也就是读取 Config 数据. 然后把凡是要用到 Config 的引用都引用一遍. 如果有人更改了 Config, 只要运行单元测试就能知道这个改动是否会造成问题.
Config Management Best Practices#
以上这些错误应当避免. 除此之外, 还有一些 Optional 的 Best Practices, 下面我们来一一介绍.
Configuration as Code.
对于不敏感的 Config, 应该将其视为代码的一部分, 用 Git 进行管理.
Use AWS Parameter Store and Secret Manager to Store Configurations.
90% 的情况下, AWS Parameter 都是用来储存 Config 数据的最佳选择. 免费, 性能高, 且自带版本控制. 而对于需要更高阶的权限控制, 或是隔一段时间就要自动更新的敏感数据, 例如数据库密码, 则可以用 AWS Secret Manager 来管理.
Separate the Config Schema declaration and Config data loading in different modules.
通常情况下, Config Schema 是不会变的, 而 Config 数据是会变的. 所以应该将 Config Schema 的定义和 Config 数据的加载分开. 这样可以对 Config 的 Definition 使用 Dummy Data 单独进行单元测试. 这也使得 Config Definition 的复用变得更加容易, 因为它不牵涉到跟具体项目绑定的 Loading 的逻辑.
Config Loading Logic should be conditionally.
在不同的情况下读取 Config 数据的方式应该是不同的. 例如对于本地开发而言, 本地的 Config 文件就是 Ground Truth. 而对于 App 而要, Config Storage (AWS Paramter Store / Secret Manager) 或是 Environment Variable 就是 Ground Truth. 所以 Config Loading Logic 代码应该是可以根据不同的情况来选择不同的加载方式. 只要成功 Load Config 对象, 之后的代码就不需要关心 Config 数据是从哪里来的, 只需要引用 Config 对象就可以了.
POC-style projects often have numerous hardcoded values, with some constant values being used multiple times. This pattern make projects difficult to maintain and prone to errors. In contrast, a production-ready project requires a centralized location to store all configurations. Once configurations are defined, we no longer allow hard-coded values and only reference configurations.
In this project, we use the multi environment json
pattern defined in config_patterns Python library to manage the configuration. Please read the config_patterns
documentation to learn more about this pattern.
Below are the list of important files related to config management:
${python_lib_name}/config # the root folder of the config management system source code
${python_lib_name}/config/define # config schema definition
${python_lib_name}/config/define/main.py # centralized config object, config fields are break down into sub-modules
${python_lib_name}/config/define/app.py # app related configs, e.g. app name, app artifacts S3 bucket
${python_lib_name}/config/define/lbd_deploy.py # Lambda function deployment related configs
${python_lib_name}/config/define/lbd_func.py # per Lambda function name, memory size, timeout configs
${python_lib_name}/config/load.py # config value initialization
config/config.json # include the non-sensitive config data
${HOME}/.projects/simple_lambda/config-secret.json # include the sensitive config data, the ${HOME} is your user home directory
tests/config/test_config_load.py # the unit test for config management, everytime you changed any of the config.json, or config/ modules, you should run this test
The ${python_lib_name}/config
Python module implemented the “configuration as code” pattern, let’s walk through the folder structure to get better understanding.
- The
define/main.py
module defines the configuration data schema (field and value pairs). To improve maintainability, we break down the long list of configuration fields into sub-modules.
There are two types of configuration values: constant values and derived values. Constant values are static values that are hardcoded in the
config.json
file, typically a string or an integer. Derived values are calculated dynamically based on one or more constant values.
- The
- The
load.py
module defines how to read the configuration data from external storage. On a developer’s local laptop, the data is read from a
config.json
file.During CI build runtime and AWS Lambda function runtime, the data is read from the AWS Parameter Store.
- The
Below is the implementation of the init
module: