先说问题

我以前对配置文件有点过分信任。只要服务能在本地启动,application.yml、config.yaml、.env 这些文件基本就算过关。后来有一次灰度发布,容器起来了,业务接口却一直 500。查了半天才发现不是代码问题,是一个配置项从 timeout_ms 写成了 timeout,默认值又刚好把请求拖到了超时。

这种问题很尴尬。它不像语法错误那么明显,也不像单测失败那么直接。配置文件通常散在不同目录里,开发、测试、预发各一份,人工看一遍容易漏。更麻烦的是,有些配置项不是“有没有”的问题,而是“类型对不对、值域合不合适、环境变量有没有补齐”的问题。

所以我后来给项目加了一个很小的巡检脚本。它不负责替代配置中心,也不想做成大而全的平台,只做三件事:检查必填项、检查类型、检查明显危险的值。脚本不长,但对中小项目挺有用。

我希望它解决什么

先定边界。这个工具面对的是本地仓库里的 YAML 配置文件,典型目录长这样:

config/

  dev.yaml

  test.yaml

  prod.yaml

我希望执行下面这条命令后,能直接看到哪些配置不对:

python config_audit.py config/prod.yaml

校验规则单独放在脚本里,后面也可以拆出去。比如数据库地址必须是字符串,端口必须是整数,线程池大小不能小于 1,生产环境的 debug 不能打开。

一个可跑的版本

先安装依赖:

pip install pyyaml

下面是完整脚本,我一般会放在 tools/config_audit.py:

import sys

from pathlib import Path

from typing import Any

import yaml

RULES = {

    "app.name": {"type": str, "required": True},

    "app.debug": {"type": bool, "required": True, "forbid_prod_true": True},

    "server.port": {"type": int, "required": True, "min": 1, "max": 65535},

    "database.host": {"type": str, "required": True},

    "database.port": {"type": int, "required": True, "min": 1, "max": 65535},

    "pool.worker_count": {"type": int, "required": True, "min": 1, "max": 128},

}

def load_yaml(path: Path) -> dict[str, Any]:

    if not path.exists():

        raise FileNotFoundError(f"配置文件不存在:{path}")

    with path.open("r", encoding="utf-8") as f:

        data = yaml.safe_load(f) or {}

    if not isinstance(data, dict):

        raise ValueError("配置文件顶层必须是对象")

    return data

def get_by_path(data: dict[str, Any], key_path: str) -> Any:

    current: Any = data

    for part in key_path.split("."):

        if not isinstance(current, dict) or part not in current:

            return None

        current = current[part]

    return current

def audit(data: dict[str, Any], env: str) -> list[str]:

    errors: list[str] = []

    for key_path, rule in RULES.items():

        value = get_by_path(data, key_path)

        if value is None:

            if rule.get("required"):

                errors.append(f"缺少必填项:{key_path}")

            continue

        expected_type = rule.get("type")

        if expected_type and not isinstance(value, expected_type):

            errors.append(

                f"类型错误:{key_path} 期望 {expected_type.__name__},实际是 {type(value).__name__}"

            )

            continue

        if "min" in rule and value < rule["min"]:

            errors.append(f"取值过小:{key_path}={value},最小值是 {rule['min']}")

        if "max" in rule and value > rule["max"]:

            errors.append(f"取值过大:{key_path}={value},最大值是 {rule['max']}")

        if env == "prod" and rule.get("forbid_prod_true") and value is True:

            errors.append(f"生产环境禁止开启:{key_path}=true")

    return errors

def guess_env(path: Path) -> str:

    name = path.stem.lower()

    if "prod" in name:

        return "prod"

    if "test" in name:

        return "test"

    return "dev"

def main() -> int:

    if len(sys.argv) != 2:

        print("用法:python config_audit.py <config.yaml>")

        return 2

    path = Path(sys.argv[1])

    data = load_yaml(path)

    env = guess_env(path)

    errors = audit(data, env)

    if not errors:

        print(f"OK:{path} 配置检查通过")

        return 0

    print(f"发现 {len(errors)} 个配置问题:")

    for index, error in enumerate(errors, start=1):

        print(f"{index}. {error}")

    return 1

if __name__ == "__main__":

    raise SystemExit(main())

拿一份有问题的配置试一下:

app:

  name: order-service

  debug: true

server:

  port: 8080

database:

  host: db.internal

  port: "5432"

pool:

  worker_count: 0

文件名如果是 prod.yaml,输出大概会是这样:

发现 3 个配置问题:

1. 生产环境禁止开启:app.debug=true

2. 类型错误:database.port 期望 int,实际是 str

3. 取值过小:pool.worker_count=0,最小值是 1

这个反馈比“服务启动失败”要早很多。尤其是在提交前或者 CI 里跑一次,能把不少低级错误挡住。

这里有几个细节别偷懒

第一个细节是不要用 yaml.load。很多旧文章里还在这么写,但普通配置读取用 safe_load 就够了。配置文件不是可信代码,不该让它有机会构造奇怪的对象。

第二个细节是类型判断要谨慎。Python 里 bool 是 int 的子类,所以如果你写了很宽松的判断,true 可能会被当成数字过关。上面的脚本先按配置项明确类型检查,够简单,也不容易误放。

第三个细节是错误信息要能让人直接改文件。只说“配置不合法”没意义,最好把路径、实际值和期望值都打出来。半夜排障时,少一次猜测就是少一次火气。

第四个细节是规则别一开始就抽象过度。我见过有人一上来写一套规则 DSL,结果配置校验工具本身也需要文档。小团队可以先把规则写在 Python 字典里,等规则变多了再拆成 JSON Schema 或 Pydantic 模型。

接到 CI 里

如果项目用 GitHub Actions,可以加一个很薄的检查:

name: config-audit

on: [push, pull_request]

jobs:

  audit:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5

        with:

          python-version: "3.12"

      - run: pip install pyyaml

      - run: python tools/config_audit.py config/prod.yaml

公司内部 GitLab CI、Jenkins 也是同一个思路:把脚本返回码交给流水线。返回 0 就继续,返回 1 就拦住。

后面可以怎么扩展

这个脚本只是起点。配置越来越多后,可以继续加几类检查:

• 检查环境变量占位符,比如 `${DB_PASSWORD}` 是否真的存在;

• 检查不同环境的配置项是否一致,避免 dev 有、prod 没有;

• 检查敏感字段,防止密码、token 明文进仓库;

• 把校验结果输出成 Markdown,直接贴到流水线报告里。

我现在更倾向于把这类工具做得小一点。它不需要华丽界面,也不需要复杂平台。只要能在提交代码之前告诉我“这个配置会炸”,它就已经值回票价了。

更多推荐