Embedded IaC PaaS:选用Terraform的架构权衡
我们三个人,维护八个内部应用。
把这句话当作整篇文章的第一性原理。下面所有架构决策——为什么用 ECS 而不是 EKS、为什么把 Terraform 编进二进制而不是放在 CI 跑、为什么 Redis 共享但 PostgreSQL per-app 独立——都是从这一句推出来的。换一个团队规模或应用数量级,每个决策都可能反转。
业务方写一份 yaml 描述应用需求,敲一个命令就部署。底下用 Terraform——业界标准的 IaC 工具——但业务方完全看不到它。我们把所有 Terraform 模板编译进一个 Go 二进制(Runway server),运行时解压、生成 wrapper、执行。
这套做法行业内有个不太流行但准确的名字:Embedded IaC PaaS。它在 3 人 5-20 应用这个窄区间里接近最优。出了这个区间立刻不成立。本文围绕这个判断展开。
1. 注意力预算才是真正的瓶颈
云架构讨论里 “ 成本 “ 几乎默认指 AWS 账单。这是一个误导性的简化。
举一个具体例子。EKS 控制面按官方页面是 $0.10/cluster·h,约 $73/月(AWS EKS Pricing)。这个数字小到许多团队会忽略——“ 控制面才几十刀,剩下的钱都在节点上 “。但真正的代价不在 $73,而在配套组件、升级谁来盯、出问题谁来排。
把视角换一下。3 人团队真正的稀缺资源不是钱,是注意力。每多一个独立组件,每周都要从注意力上限里扣几件事。看一个真实账本(事件数为序列估算,仅作量级判断):
| 决策 | 直接成本($/月) | 注意力成本(事件/月) |
|---|---|---|
| 1 个 EKS 集群 + Node Group + 7 个 controller | ~$73 控制面 + 节点 + add-on | 集群升级、AMI 补丁、controller 兼容、ArgoCD 漂移、HPA 调试 ≈ 8-12 |
| 1 个共享 ECS Fargate cluster | $0(cluster 本身免费) | 服务一次性配 ≈ 0-1 |
| 8 个 per-app RDS db.t3.micro 实例 | 8 × ~$13 = $104 | 备份验证、参数组、慢查询、连接数 ≈ 4-6 |
| 1 个共享 Redis cache.t3.micro | ~$12 | 容量、版本、故障切换 ≈ 1 |
| 8 个 per-app Redis 实例 | 8 × $12 = $96 | × 8 ≈ 8 |
这个视角下 “ 成本 “ 被重新定义。便宜不是钱便宜,是不占注意力的便宜。所有后面的决策都从这里出发。
明确瓶颈后,第一个分叉是要不要交给托管 PaaS——把整个注意力问题外包出去。
2. 托管 PaaS Vs 自建 IaC:选择权是晚熟果实
8 个应用里有 4 个跑在 VPC 内网(数据库代理、内部 API、定时任务、worker)。其余 4 个有公网入口但需要访问 VPC 内的资源。Vercel / Render / Fly.io / Heroku 这类托管 PaaS,对纯公网 Web 应用是甜蜜区。对 VPC 内网应用支持薄弱:要么不支持,要么靠 Tailscale 这类胶水方案。第一刀就把托管 PaaS 砍掉了。
但即便不考虑 VPC,托管 PaaS 还有一层更隐蔽的代价。
托管 PaaS 给你便利,自建 IaC 给你选择权。后者只在你想脱身那一刻才显现价值,到那时已经来不及补。
便利可以即时验证(push 后两分钟看到 URL)。选择权是个晚熟果实——一年后想换厂商时才知道有没有。两者的常见误判是把 “ 现在用着方便 “ 当作 “ 未来也方便 “。
托管 PaaS 的可逃性——跑路成本——在合同上写得很漂亮,实际操作起来取决于厂商配合。Vercel 的 Edge Function 迁到自建 Cloudflare Workers 不是 terraform state mv 能解决的,要重写部署链路。Heroku Postgres 的 dump 出来后,connection string 散落在几十个应用环境变量里,要逐个找。这些都是 “ 没数据丢失但要花两个月人力 “ 的成本。
自建 Terraform 的可逃性是物理意义上的:模块是普通 HCL,state 是 S3 里的 JSON,Provider 是公开协议。任何人、任何机器、任何时刻拿到 state 文件和 .tf 文件,就能接管这套基础设施。可以迁到 Atlantis、Spacelift、Terragrunt、甚至完全手工操作。没有数据需要 “ 导出 “,因为数据从未离开过你。
Runway 把所有 Terraform state 存在自家 S3:
1 | S3 backend: coding-infra-tf-state |
S3 + DynamoDB 做 backend 的总成本一个月不到 1 美元。这一美元买的是 “ 任何时候我可以不再依赖 Runway server 也能管基础设施 “ 这个选择权。
反转条件——什么时候应该选托管 PaaS:
- 团队 < 3 人,没有任何人能定期处理 IaC 升级——这种规模下 “ 自建 “ 的固定成本压不下来
- 业务全部是公网无状态 Web 应用,没有 VPC 内网需求
- 业务还在快速验证阶段,6 个月内可能整体重写——选择权对你没用,你已经决定要逃了
这三条都不满足时,自建 IaC 是稳态选项。下一个分叉是用什么编排——K8s 还是 ECS。
3. ECS Fargate Vs Kubernetes:人头换不来代码
把 “ 为什么不用 K8s” 放在第三节是有原因的——前两节定义了真正的瓶颈是注意力。这一节直接套上去看。
Kubernetes 不是工具,是组织结构。
K8s 的能力上限被 “ 懂 K8s 的人头 “ 绑死。一个生产可用的 EKS 集群至少需要这些组件:
| 组件 | 维护负担 |
|---|---|
| EKS Control Plane | API server + etcd 托管,$0.10/h ≈ $73/月(标准支持期);扩展支持期 $0.60/h ≈ $438/月 |
| Managed Node Groups | AMI 升级、CVE 补丁、容量规划、节点漂移 |
| AWS Load Balancer Controller | 版本必须跟 EKS 兼容,每次升级要回归 |
| EBS CSI / EFS CSI | 存储 driver,跟 controller 同步升级 |
| External DNS / cert-manager | 证书、DNS 自动化,cert-manager webhook 经常出问题 |
| IRSA / EKS Pod Identity | Pod 拿 AWS 凭证的两套机制,新 Pod Identity 仍在迁移期 |
| ArgoCD / Flux | K8s 内部又一套 IaC 系统,与外部 Terraform 协同麻烦 |
| Prometheus / Grafana / Loki | 内置 metrics 不够,全套监控自维护 |
每一项都不算难,加起来是一个全职岗位。这不是夸张——一个 EKS 集群从 1.27 升到 1.30 这种跨三个版本的操作,即便有 Karpenter 和 ArgoCD 的成熟环境,也是 2-3 周的项目。
ECS Fargate 是反过来的设计:控制面 AWS 全托管、没有 Node 概念、连 EC2 都不用维护。Cluster 资源本身免费,只为运行的 Task 按 vCPU/h + GB/h 付费。
代价是 Fargate 的 HPA 颗粒粗(按 CPU/内存的 target tracking,不能基于自定义 metric 直接做),Service Mesh 生态弱(App Mesh 已被 AWS 弃用,ECS Service Connect 是简化版),跨云不可移植。这些限制对内部应用都不是当前问题。
Runway 的 ecs-service 模块约 450 行 HCL,覆盖启动一个新服务需要的全部 AWS 资源——Task Definition、Service、Target Group、ALB Listener Rule、IAM Role、Secrets 注入、Log Group、Auto Scaling。业务方看到的是这样:
1 | module "app" { |
一个工程师花一下午就能从头读完。EKS 等价的 “ 启动新服务 “ 涉及 Helm chart、ArgoCD Application、ServiceAccount + IRSA、Ingress + 证书、HPA、NetworkPolicy——加起来数千行 YAML,跨多个仓库。
反转条件:
- 团队 ≥ 15 人或有专职平台工程师——这时 K8s 的运维成本可以摊薄
- 有复杂调度需求:GPU 抢占、Spot 混合、批处理
- 多 cloud 或计划多 cloud——ECS 是 AWS 锁定
- 业务对启动延迟敏感且 Pod 启动比 Fargate Task 快——这个差距 2024 年后已经很小,新版 Fargate 启动 < 30s
3 人 8 应用全部不满足,所以 ECS 是 default。这不是技术品味,是约束的直接推论。
确定了 ECS Fargate + Terraform 的底座,接下来要回答:业务方怎么用?
4. Embedded IaC PaaS:把 Terraform 当库用
行业内主流的几种 IaC 交付方式:
- 每个业务方自己写 .tf,CI 跑
terraform apply。Atlantis、HCP Terraform(原 Terraform Cloud)、Spacelift 是这类的代表 - 平台团队写 module,业务方在自己 repo 里 ref 它。本质还是上一种,业务方仍然写 HCL
- 用 K8s CRD 包装基础设施,业务方写 YAML 让 Crossplane 翻译。需要先有 K8s
- 用通用语言写 IaC,比如 Pulumi 用 TypeScript/Python。仍然是业务方直接写
我们选的是第五种——把 IaC 当作平台二进制的一部分编译分发,业务方完全看不到 IaC 工具本身。这就是 Embedded IaC PaaS。
4.1 餐厅比喻
开一家餐厅。客人来了不会进厨房,他们坐下来点菜,说 “ 一份牛肉面 “。这五个字就是 yaml。
牛肉面怎么做?厨房里有菜谱:肉切多厚、汤熬多久、面条几克。这本菜谱就是 Terraform。它告诉厨师每一步怎么做,怎么用冰箱里的食材(AWS 各种服务)做出一碗面。客人不需要看菜谱,只要会说 “ 我要牛肉面 “。
那 “ 另一种做法 “ 是什么样?它叫自助厨房。每个客人自己带菜谱进来,自己进厨房做。听起来很自由——但你会遇到这些事:张三的菜谱是三年前印的版本,按它做出来的面会让他拉肚子;李四把菜谱改了两行说 “ 我想加点辣 “,结果改错了,做出来是黑暗料理;餐厅老板想统一升级菜谱(” 以后牛肉都要先腌一遍 “),得挨个找客人换,客人嫌烦不换。
这就是 Atlantis、HCP Terraform 模式。每个业务方在自己的 git 仓库里写 .tf 文件,CI 帮他们跑。看起来正常,但 8 个仓库就有 8 份菜谱,半年后你不知道谁在用哪个版本。
Embedded 模式做的事很朴素:把菜谱钉死在餐厅这一边。Go 语言有个叫 go:embed 的功能,可以把文件直接编进可执行文件。Runway 这个平台就用它,把所有 Terraform 模板塞进自己的二进制里。餐厅启动时菜谱就在内存里。
客人点菜时餐厅做这几件事:先把菜谱从内存里拿出来铺到工作台(解压到 /tmp 临时目录),看客人的 yaml(” 我要牛肉面,要中辣 “),在工作台上写一份 “ 今日订单菜谱 “(基于标准菜谱套上客人的细节),让 terraform 按这份订单做菜,做完撤掉工作台。客人从头到尾没见过 terraform 这三个字。
比喻收掉,落到工程上,这套做法成立要满足三个不变量。
4.2 三个不变量
这套做法成立的前提是三个 invariant,缺一不可:
- IaC 模板与平台二进制同版本——版本号是同一个 git commit,没有任何方式可以让某个 app 用旧版模板
- 业务方与 IaC 工具不直接接触——他们写 yaml schema,不写 .tf,不装 terraform,不需要懂 HCL
- 凭证集中在平台层——只有 Runway server 的 task role 有 AWS 凭证,业务方不持有 AWS 任何 secret
第一条是真正的杀手锏。CI-driven 模式下 8 个 repo 各自 ref=v1.x 一个 module 版本,半年后必然漂移:一个还在 v1.0、一个跑 master、一个 fork 改了。漂移的代价不在漂移本身,而在它让你不敢改 module——因为不知道谁在用。Embedded 模式下升级 server 就升级所有 app 的 IaC,原子且不可漂移。平台团队能持续重构 module 而不用每次发版都跑 8 个 repo 的 PR。
第三条次要但实际意义不小。一旦每个业务方都要持有 AWS 凭证,IAM policy 设计立刻指数级复杂——要给 dev1 / dev2 / prod 三套 role、要绑 OIDC、要做 session policy、要审计 STS 调用日志。Embedded 下 IAM 简化成一件事:保护 Runway server。整个安全模型从 N×M 简化成 1。
三个不变量都满足之后,部署链路本身才能保持简单。
4.3 部署链路
Runway server 的 Go 二进制通过 go:embed 内嵌所有 Terraform 模块:
1 | //go:embed all:modules |
完整部署链路 5 步如下:
sequenceDiagram
participant Dev as 业务方
participant CLI as Runway CLI
participant Server as Runway Server
participant TF as Terraform 子进程
participant AWS as AWS
Dev->>CLI: runway deploy
CLI->>Server: POST /deploy (runway.yaml)
Server->>Server: 解压 embedded modules<br/>到 /tmp/{request-id}/
Server->>Server: 生成 main.tf + backend.tf
Server->>TF: terraform init && apply
TF->>AWS: 创建/更新资源
TF->>AWS: state 写 S3 + DynamoDB lock
Server->>Server: 清理临时目录
Server-->>CLI: 返回访问 URL
CLI-->>Dev: 部署完成业务方写的 yaml 长这样:
1 | name: my-service |
完全看不到 aws_lb_listener_rule、aws_iam_role 长什么样。新增一个应用从填这份 yaml 到 URL 可访问,平均 8-12 分钟(大头是 ECS Service 拉镜像 + 初始 health check 稳定)。
链路简单不等于没有弱点,下面把已知的失败模式逐条列清。
4.4 失败模式与兜底
Embedded IaC 的弱点不是抽象的 “ 灵活性差 “,是几个具体的失败模式。每一个都有过踩坑场景,下面把它们和兜底机制一起列出来。
Drift detection 缺失。terraform apply 跑完就不再管,AWS Console 改了不知道。最常见的事故是:值班同事在告警里手动调了 ALB 的 idle_timeout 救火,三天后业务方触发一次 deploy,timeout 改回模板默认值,故障复发。
兜底是约定层(” 任何 console 改动 24 小时内同步进 yaml”)+ 周度 cron 跑 terraform plan -detailed-exitcode 抓 drift,有差异就发告警。这个 cron 30 行 Bash,远比上 drift detection SaaS 便宜。但它是补丁,不是治本——治本要做 reconciliation loop,那就接近 Crossplane 了,不在 3 人团队的注意力预算里。
Server 是单点。Runway server 跑在 ECS 上 2 副本,挂一个还能活;但 DynamoDB lock 表或 S3 backend 出问题,所有 app 不能部署。三层兜底:
- CLI 在 server 不通时自动降级到 “ 本地模式 “——直接拉 modules(git submodule 镜像在 S3)+ 本地装的 terraform 跑。这破坏了 invariant 3(业务方需要拿到 AWS 凭证),所以只在紧急情况开启,且 IAM role 加 IP 白名单
- State 文件每天异地备份到第二个 S3 bucket(不同 account)
- Lock 表用 PITR(point-in-time recovery)+ 每周快照
Plan-on-PR 弱。业务方改 yaml 看不到会动哪些 AWS 资源。补救是 CLI 提供 runway diff 命令——本地跑一次完整 plan 输出可读 diff。但这要求业务方主动跑,PR 里没人盯就没用。GitHub Actions 上挂一个 PR comment bot 是 todo。
Yaml schema 表达力天花板。每个新 AWS 服务都要平台帮忙翻译。一年下来加了:S3 storage、ElastiCache Redis、Secrets Manager、SQS、EventBridge cron。每加一个的成本是 1-3 天(写模板 + 加 yaml 字段 + 加 CLI 子命令 + 文档)。这个加项速度跟得上业务需求,但不能跟 “ 业务方自己写 .tf 想要啥就有啥 “ 比。
演进路径。这套设计不是一次到位的。从最小可用到现在,几个关键节点:
- v0.1:单 app 单 yaml 字段,模板硬编码 ECS + ALB,不支持多 env
- v0.3:引入 env 自由命名 + tier 派生,支持 preview env
- v0.5:state 拆分(从 1 个 state 文件拆成 per-app)——这次拆分是真正的痛点,做了一周,包括
terraform state mv把已有资源迁过去 - v0.7:加 db/redis/storage 子模块,从 “ 只能做 stateless 服务 “ 扩到 “ 内部应用所需的常见副组件 “
- v1.0:drift cron + 异地备份 + 本地降级模式
反转条件:
- 业务方文化上愿意写 IaC(成熟 SRE 团队)——直接上 Atlantis 或 Spacelift
- 多 cloud 或 multi-region 是核心需求——Pulumi 或 Crossplane 更合适
- 有强 4-eyes plan review 合规要求——主流方案的 plan-on-PR 流水线更成熟
- 业务形态高度异构、每个应用都需要定制基础设施——yaml schema 翻译成本超过收益
不在以上反转条件里,3-10 人维护 5-20 个内部应用、yaml schema 边界明确,Embedded IaC 是最优。
平台层定下来后,下一个问题是:8 个应用之间的资源怎么隔离?
5. 隔离强度是个谱,不是布尔
per-app 完全隔离听起来更安全,但每个动作都要乘以 N:N 个 ALB(每个 $16/月固定 + LCU)、N 个 Redis、N 个 VPC、N × 监控。3 人团队没法承担 N 倍的运维事件。所以隔离不是 “ 开 “ 或 “ 关 “ 两个状态,是个连续谱。每条数据通路单独决定强度,由 “ 是否产生跨租户性能干扰 “ 这个准则推导。
| 通路 | 隔离方式 | 强度 |
|---|---|---|
| 应用 ↔ RDS | per-app instance + 独立凭证 | 物理强 |
| 应用 ↔ Secrets Manager | 共享 service + per-app IAM policy | IAM 强 |
| 应用 ↔ ALB | 共享 ALB + per-app Target Group + Host 路由 | 路由强 |
| 应用 ↔ S3 | 共享 bucket + IAM policy 锁 prefix | IAM 中等 |
| 应用 ↔ Redis | 共享集群 + 约定 key 前缀 {env}:{app}: | 约定弱 |
下面逐条解释为什么落在这个强度上。
5.1 RDS 必须 Per-app
PostgreSQL 天然不适合多租户共享。同一个实例里:
- App-A 的全表扫描或缺索引查询会让 App-B 的请求等连接池
- App-A 的长事务会卡住 App-B 的 vacuum,autovacuum 不工作
- App-A 的 schema migration 锁表,App-B 的写请求超时
- 备份/恢复是实例级别的,恢复 App-A 必然牵连 App-B 的数据点
这些不是配置能解决的,是 PostgreSQL 在共享实例下的物理性质。db.t3.micro 在 us-west-2 单 AZ 大约 $13/月(Vantage db.t3.micro),8 个 app 总共 $104/月。这个钱花得不肉痛。
如果将来需要 multi-AZ,价格翻倍到约 $26/月,8 个变成 $208/月,仍然不是个数量级问题。Production 关键 app 上 multi-AZ,dev/staging 单 AZ。
RDS 之外,缓存层是另一种性质,可以反过来共享。
5.2 Redis 可以共享
Redis 是另一种性质——key prefix 隔离下,跨租户在内部应用的负载(< 1k QPS 量级)下几乎没有干扰:
- 内存计费按总用量,App-A 用 100MB 不影响 App-B
- 单线程模型下命令是 O(1) 或 O(log N) 居多,互相不阻塞
- 没有跨 prefix 的锁/事务
兜底是三层:(1) 平台模板默认注入 REDIS_KEY_PREFIX={env}:{app}: 环境变量,业务方代码用 client 时自动加前缀;(2) 业务方接入时口头讲过;(3) 文档明写 “ 约定弱隔离 “。
什么时候反转?两个条件:
- 真出过跨租户事故——立刻把 Redis 切到 ACL 强制模式(Redis 6+ 的 ACL 可以限制 user 只能访问某个 key pattern),这不会涨钱
- 总 QPS > 5k 或单租户 > 1k——共享集群开始有内部干扰,切 per-app cache.t3.micro 实例。8 × $12/月 = $96/月,仍然不贵
我们一年下来没碰到第一个条件,第二个条件在内部应用是高估了。
缓存讲完,看入口层。
5.3 ALB 共享 + Host 路由
每个 ALB 固定 $16.43/月($0.0225/h × 730,AWS ELB Pricing)+ 按 LCU 计费。8 个 per-app ALB 是 $130/月固定,共享 1 个 ALB 是 $16/月固定。LCU 部分按总流量算,per-app 和共享差不多。
ALB 有原生的 listener rule 机制:根据 Host header / path / header 把流量路由到不同的 Target Group。8 个 app 用 8 条 rule 共享 1 个 ALB 完全够。ALB 的 rule 上限是 100 条/listener,远超我们的规模。
代价有两点:所有 app 的入口域名共用一套 SSL 证书(用 SAN 多域名证书或 ACM 通配符),证书更新是全局事件;ALB 整体故障时 8 个 app 同时挂——但 ALB 的可用性是 99.99% SLA,多 AZ 部署,这个风险可接受。
入口共享但路由隔离,对象存储用类似的思路。
5.4 S3 共享 Bucket + IAM 锁 Prefix
共享 bucket,每个 app 的 IAM policy 只允许访问 {env}/{app}/* 前缀:
1 | { |
强度是 “IAM 中等 “——比共享 Redis 的约定强(IAM 越界会被 AWS 拒绝),比 per-app bucket 弱(policy 写错会越界)。
为什么不 per-app bucket?AWS account 有默认 100 个 bucket 上限(可申请提升到 1000,但要走支持工单)。8 个 app × 3 env × 2 用途(公开/私有)= 48 个 bucket,吃掉一半配额,没必要。共享 bucket 配 IAM policy 是 AWS 官方推荐的多租户存储模式。
S3 是 IAM 中等强度,secret 则是 IAM 强度的典型。
5.5 Secrets Manager 是 IAM 强
每个 secret $0.40/月 + 每万次 API 调用 $0.05(AWS Secrets Manager Pricing)。8 个 app × 平均 3 个 secret(DB、Redis、第三方 API key)= 24 个 secret × $0.40 = $9.6/月。
Secret 的隔离自然就是 IAM 强:每个 app 的 IAM policy 通过 resource ARN 锁死自己的 secret。这跟共享 RDS 完全不是一个性质——secret 的访问是 API 调用,IAM policy 是确定性的拒绝;而 RDS 的连接池是跨租户共享的物理资源。
把这五条放一起,规律就出来了。
5.6 谱系视角的本质
判断标准是 “ 能否产生跨租户性能干扰 “:
- 物理资源被共享(连接池、锁、IO)→ 必须物理隔离
- 配额被共享(IAM 调用次数、API rate limit)→ IAM 隔离够
- 命名空间被共享(key、path)→ 约定隔离够(前提是兜底机制存在)
这个准则比 “ 安全级别 “ 更可操作。” 安全 “ 是个空泛词——RDS per-app 和 Redis prefix 都是 “ 安全 “ 的,但安全的程度和方式完全不同。性能干扰是个二阶可观测的事,可以做容量规划。
反转条件:
- 合规要求物理隔离(PCI、HIPAA)——所有数据通路都升级到 per-app
- 业务方是外部多租户(SaaS 的真实租户)——不能再靠 “ 内部应用 “ 的假设,IAM 强是底线
- 单租户规模超过共享设施容量——切到 per-app 的实例
3 人 8 内部应用都不在反转条件里。
讲完物理隔离的策略,最后看两个细节如何把 “ 平台层 / 应用层 “ 的边界画死。
6. State 设计与 6 行函数:让代码控制语义、让用户控制名字
最后两件事是细节,但是把整套设计的边界画清楚的两个细节。
6.1 State Key 是真正的多租户隔离
物理上一切共享,逻辑上必须可分。state 层是这样表达的:
1 | S3 bucket: coding-infra-tf-state |
每个 app 一个 state 文件意味着:
- 一个 app 的
terraform apply不会锁住另一个 app(DynamoDB lock 按 state key 粒度) - 一个 state 损坏不会牵连所有应用
- 卸载某 app 时
terraform destroy只动它自己的资源 - IAM policy 可以用
arn:aws:s3:::bucket/apps/${env}/${app}/*通配符做边界
shared/ 那一份只能由平台维护者操作(IAM 限制)。业务方的 IAM Role 只能读写 apps/{env}/{app}/——这是平台层和应用层在 state 层的边界。
部署时 server 通过 terraform_remote_state data source 读 shared/terraform.tfstate 拿到 VPC ID、ECS cluster ARN、ALB ARN:
1 | data "terraform_remote_state" "shared" { |
一个常见的反方案是用 Terraform workspace 切多租户。这不行——workspace 共享 backend 配置,state key 自动派生成 env:/{workspace}/...,IAM policy 只能用 * 匹配,做不出 “ 业务方只能读自己 state” 的边界。state key 才是真正的多租户隔离,workspace 只是 state 的命名约定。
把所有资源放一个 state 文件的代价更直接:
- DynamoDB lock 按 state 粒度,单 state 意味着所有部署串行——8 个 app 的 deploy 排队
- State 文件损坏会牵连所有应用,恢复时所有 app 都不能动
terraform destroy风险极大,一次手抖能炸全场- IAM 没法在 state 层做平台/应用边界
这些代价都不是 “ 做对一点会更好 “,是 “ 做错就报销 “。State key 设计是物理实现,不是组织偏好。
state 层把租户切开后,命名规则进一步把语义和自由度切开。
6.2 Env 自由 + Tier 派生 = 6 行函数
最后一个设计——env 命名规则——展示了一个一般性原则:把 “ 自由度高 “ 的部分交给用户,把 “ 语义重大 “ 的部分由代码强制。
env 名是一个标识符——叫什么不重要,重要的是它独一无二。这种东西不需要白名单。Runway 的规则是 ^[a-z][a-z0-9-]{0,31}$,用户自由命名(dev、dev2、staging、preview-pr-123、prod、prod-eu 都合法)。
tier 是语义类——它决定应用读哪个 Redis 集群、用哪个 prod-only IAM policy、是否触发 “ 生产部署需要二次确认 “。这种东西必须由代码说了算。
把这两件事分开避免了让用户去理解 tier 这个概念。他们只需要知道 “ 要做生产环境就用 prod 开头的名字 “。整个 tier 推导是 6 行函数:
1 | func TierOf(env string) string { |
env 名进入:
- ECR 仓库名
coding-infra/{env}/{app} - ECS Service 名
{env}-{app} - Secrets Manager 路径
coding-infra-{env}-{app}/db-credentials - Terraform state key
apps/{env}/{app}/terraform.tfstate
tier 进入:
- Redis 集群选择
coding-infra-{tier}-redis - 共享 secret 路径
coding-infra-{tier}-redis/credentials - 是否触发 prod-protection(确认弹窗、IAM 边界)
为什么不用枚举?枚举意味着每加一个 env(如 preview-pr-123、prod-eu)都要改代码、发版、部署。一年下来这个流程跑了十几次,每次都是同一个动作:在白名单数组里加一个字符串。env 名本质上是一个标识符,没必要用代码控制。
为什么 tier 不能让用户自由声明?tier 决定的是基础设施层面的爆炸半径——生产 Redis 集群、prod-protection、IAM 边界。如果让用户自由选 tier: prod,一次手抖就能把测试流量打到生产 Redis。把它从 env 名前缀派生,是用可见的命名规则替代隐藏的下拉选项——业务方一眼能看出 “ 我命名 prod-xxx 就会进生产 “。
为什么前缀匹配 prod 而不是精确 prod?要支持 prod-eu、prod-us、production 等真实存在的生产 env 命名。精确匹配会逼业务方编一个奇怪的名字,前缀匹配把扩展空间留给用户。
这个 6 行函数是整套设计的微缩——所有该让代码决定的事让代码决定,所有该让人决定的事让人决定。中间的灰色地带(” 也许应该让用户在 yaml 里写 tier?”)是不存在的,因为它把两类事的特征想清楚了。
讲完所有该做的,还有一个问题:什么时候不该这么做。
7. 什么时候不要这么做
每个架构都是 “ 边界搬家 “——把复杂度从 A 处搬到 B 处。Embedded IaC PaaS 这套搬家是把 “ 业务方学 Terraform” 搬到 “ 平台维护一份 yaml 翻译器 “。3 人维护 8 个应用时这个搬家很值,8 个人学 HCL 的总成本远高于 1 个人维护翻译器。换上下文很可能不成立。
四个反转维度,每个都有阈值:
| 维度 | 当前位置 | 反转阈值 | 反转后建议 |
|---|---|---|---|
| 团队规模 | 3 人维护平台 | > 15 人或有专职 SRE | 改用 K8s + Crossplane 或 HCP Terraform |
| 应用数量 | 8 个 | > 30 个 | yaml schema 翻译成本失控,让业务方写 module |
| 业务异构度 | 低(都是 Web/worker) | > 30% 应用需定制基础设施 | 提供 “ 逃生通道 “——允许某些 app 直接写 .tf |
| 多 region/cloud | 单 region | 跨 region 或多 cloud | Pulumi 或 Crossplane,IaC 抽象层不能再绑死 region |
任何一个反转,整套设计都要重新来。这套设计不是通用模式,是窄场景下的最优解——这是它的价值,也是它的边界。
回到开篇那句话:3 个人,8 个应用,AWS。所有决策都是这一句的推论。讲清楚每个决策来自哪个约束、约束变化时会反转成什么——比 “ 这是最佳实践 “ 有用得多。
工程是约束驱动的,不是技术驱动的。这是这篇文章唯一真正想说的事。