Today AI 同时接了好几个云服务 / 凭据该按什么粒度隔离?

文章导读
凭据隔离的最小单位通常不是“云服务”,而是“云服务里的某个账号或角色”。同一个服务可能同时存在只读账号、写入账号和管理账号,它们之间也必须分开;反过来,如果两个服务本来就是用同一把身份去调用的,硬拆成两份只是增加维护成本,并不能降低牵连范围。可以先做的一件事是把现有接入逐条列出来,标出各自用的账号、角色和权限范围,再决定按什么粒度拆文件。隔离做没做对,最终靠两件事验证:轮换一把凭据时能否预判哪些任
📋 目录
  1. A 先数清每个云服务实际用到的账号和角色
  2. B 按服务拆分凭据文件并统一命名
  3. C 程序里按服务名取凭据,而不是读全局变量
  4. D 轮换一把凭据,观察哪些任务立刻失败
  5. E 跑一次跨服务任务,确认没有串号
A A

凭据隔离的最小单位通常不是“云服务”,而是“云服务里的某个账号或角色”。同一个服务可能同时存在只读账号、写入账号和管理账号,它们之间也必须分开;反过来,如果两个服务本来就是用同一把身份去调用的,硬拆成两份只是增加维护成本,并不能降低牵连范围。可以先做的一件事是把现有接入逐条列出来,标出各自用的账号、角色和权限范围,再决定按什么粒度拆文件。隔离做没做对,最终靠两件事验证:轮换一把凭据时能否预判哪些任务会失败,以及跨服务任务里两个调用方身份是否始终不同。

多云服务接入时,建议按“服务 + 用途 + 环境”拆凭据文件,每个进程只加载自己需要的那一份;同一个身份被多个服务复用的情况要先在清单里标出来,再决定是否拆开。轮换时先新增后停用,通过失败日志反查依赖;最后用一个同时调用两个服务的任务核对调用方身份。适用范围是能改代码和部署配置的场景,如果某些凭据被写死在第三方工具或控制台里,这一层需要单独确认。

先数清每个云服务实际用到的账号和角色

先别急着建目录。打开每个服务的控制台或配置文件,把“谁在用、用哪个身份、干什么用”列成一张表。隔离单位建议按这张表的第三列来定:如果两个服务在表里落到同一个身份,就说明它们目前是耦合的,要么接受这个耦合并在表里标注,要么给其中一个单独开身份。

服务用途账号 / 角色是否与其他服务共用
对象存储读文件role-reader-a与 CDN 回源共用
对象存储写备份role-writer-a独立
消息队列生产消息role-mq-producer独立
消息队列消费消息role-mq-producer与生产端共用,建议拆
日志服务写入role-logger与监控告警共用

表里“是否共用”这一列是重点。共用不一定立刻要拆,但必须写清楚,否则后面轮换时会误判影响范围。需要结合环境确认的一点是:某些服务在控制台里显示的是一个主账号,但实际请求走的是扮演出来的角色,这种情况以请求日志里记录的身份为准,而不是以控制台页面为准。

按服务拆分凭据文件并统一命名

目录按“环境 / 服务 / 用途”三层展开,文件名带上用途和后缀,读的人不用打开就知道里面是什么身份。下面是一份可以直接套用的结构,尖括号是占位符,不是真实值。

config/credentials/
  <env>/                          # dev | staging | prod
    <service>/                    # object-storage | message-queue | log
      reader.json
      writer.json
      admin.json

# 备选:文件与变量统一成同一套命名
<service>.<purpose>.<env>
# 例:object-storage.reader.prod
#     message-queue.producer.prod

文件内容只放这个身份需要的东西,并显式写上身份标识,便于日志核对:

Today AI 同时接了好几个云服务 / 凭据该按什么粒度隔离?
{
  "account_id": "<ACCOUNT_ID>",
  "identity": "<ROLE_OR_USER_NAME>",
  "access_key_id": "<ACCESS_KEY_ID>",
  "secret_access_key": "<SECRET>",
  "region": "<REGION>"
}

不建议用一个全局凭据文件,原因有三个:一是权限面太大,任何能读这个文件的进程就拿到了全部服务的密钥;二是轮换时无法从文件层面看出影响哪些任务;三是日志或报错里一旦打印出这个文件的内容,泄露的是全部而不是一个服务。拆开之后,密钥管理、审计和轮换的影响面都能收敛到单个目录。

程序里按服务名取凭据,而不是读全局变量

代码侧统一走一个取凭据的函数,参数是服务名和用途,禁止业务代码直接读全局环境变量。这样进程启动时就能发现缺哪份凭据,而不是在调用中途才失败。下面是通用骨架,字段名和加载方式按你实际使用的语言替换。

# creds.py
def load_credential(service: str, purpose: str = "reader") -> dict:
    env = os.environ.get("APP_ENV", "dev")
    root = pathlib.Path(os.environ.get("CRED_ROOT", "./config/credentials"))
    path = root / env / service / f"{purpose}.json"

    if not path.is_file():
        raise CredentialNotFound(
            f"credential missing: {path} "
            f"(service={service}, purpose={purpose}, env={env})"
        )

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

    for key in ("account_id", "identity", "access_key_id", "secret_access_key"):
        if not data.get(key):
            raise CredentialNotFound(f"credential field empty: {key} in {path}")
    return data

# 调用方
mq = load_credential("message-queue", "producer")
storage = load_credential("object-storage", "reader")

报错分支要写完整:缺文件、字段为空、环境变量没设置,三种情况分别给出能定位到具体路径和服务名的信息。这样出错时看日志就知道是配置漏了,而不是在业务逻辑里猜。调用方拿到的是字典,不要把它写回全局状态,也不要跨服务传递,避免 A 服务的代码顺手用了 B 服务的凭据。

轮换一把凭据,观察哪些任务立刻失败

轮换是检验依赖关系最直接的方式。步骤建议按下边走,先新增再停用,留出回退空间:

Today AI 同时接了好几个云服务 / 凭据该按什么粒度隔离?
  1. 在目标服务的控制台新建一把凭据,旧的先保留可用。
  2. 只替换该服务对应目录下的那一个文件,记录替换时间和操作人。
  3. 观察一段时间,收集失败任务,先不急着停用旧凭据。
  4. 确认没有遗漏依赖后,再在控制台停用或删除旧凭据。

失败任务的日志特征通常比较集中,可以按这些关键字去搜:找不到凭据文件、字段缺失、身份校验失败、签名不匹配、权限不足、访问被拒绝。具体文案各服务不同,但都能定位到“身份不对”这一类。如果某个任务失败,而它的服务名和被你轮换的服务名不一致,说明它复用了这把身份,需要回到第一张对应表把这条依赖补上。

需要同步更新的位置不止代码仓库,常见的还有:持续集成里的密钥变量、容器运行时挂载的密钥文件、本地开发用的环境文件、定时任务自带的环境、备份脚本、监控告警回调用的身份。这些位置如果不同步,表现就是部分任务正常、部分任务报身份错误,比较容易被误判成偶发故障。

跑一次跨服务任务,确认没有串号

隔离方案生效的验证方式,是让同一个任务同时调用两个不同服务,并分别打印各自的调用方身份。伪代码结构如下:

def cross_service_probe():
    a = load_credential("message-queue", "producer")
    b = load_credential("object-storage", "reader")

    result_a = call_service("message-queue", a, action="whoami")
    result_b = call_service("object-storage", b, action="whoami")

    print("mq     ->", result_a.get("account_id"), result_a.get("identity"))
    print("storage->", result_b.get("account_id"), result_b.get("identity"))

    if result_a.get("identity") == result_b.get("identity"):
        raise RuntimeError("两个服务解析到同一个身份,可能串号")

    assert result_a["account_id"] == a["account_id"]
    assert result_b["account_id"] == b["account_id"]

核对调用方身份的方法,各服务提供的入口不一样:有的提供查询当前调用者的接口,有的在响应头里带上身份信息,有的只能在审计日志里查到。可以先在控制台或审计日志里确认一次,再把查询动作固化成测试任务。判断标准很简单:两个服务返回的身份应该和你在第一张表里登记的一致,并且彼此不同;如果返回了同一个身份,说明代码或配置里仍然有地方在共用凭据,隔离没有真正落地。这个验证任务建议放在配置变更之后手动跑一次,不必每次都进主干流程。