基于IBM Cloud Schematics的Terraform第二代VPC部署实战 1. 项目缘起与整体设计思路1.1 为什么要在托管服务上跑 Terraform很多团队一开始都是本地跑 Terraform状态文件往 Git 里一塞或者丢在某个对象的存储桶里几个人轮流 apply。人少的时候没问题一旦团队规模上来、环境从一套变成三套五套状态锁冲突、版本漂移、凭据散落这些问题就会集中爆发。我印象很深的一次某开发者在本地改了一个安全组规则直接 apply结果把测试环境的入口给关了排查了两个小时才发现是本地状态文件过期导致的。托管式 Terraform 服务解决的正是这类协作与环境一致性问题。把执行引擎放到云端状态由服务端统一管理每次执行都有完整的日志和版本记录谁在什么时候改了什么一目了然。IBM Cloud Schematics 就是这类服务的一个典型代表它把 Terraform 的 plan/apply/destroy 生命周期包装成 Workspace 的概念配合云端凭据托管和资源清单让基础设施即代码这件事从“个人手艺”变成“团队流程”。这个项目要做的是在 Schematics 上交付一套第二代 VPC 的完整部署方案。所谓第二代 VPC指的是具备更细粒度子网划分、更灵活的路由策略、以及原生安全组与网络 ACL 双层防护的虚拟私有云架构。相比早期版本它在地址规划、跨区通信、出网控制上都做了明显增强配置项也更多手工点控制台几乎不可能保证一致性必须靠代码来管。1.2 方案选型的几个关键取舍在动手之前有几个决策点值得先说清楚因为它们直接决定了后面代码怎么写、目录怎么组织。第一个取舍是用 Schematics 原生 Workspace 还是走 CLI 驱动。原生 Workspace 的好处是图形界面直观、状态托管省心、和云端 IAM 集成紧密CLI 驱动则更适合塞进 CI/CD 流水线做自动化。我的做法是两者结合日常调试用 CLI 快速迭代正式环境用 Workspace 做审批和留痕。这样既保留了灵活性又不牺牲可审计性。第二个取舍是模块粒度。是把整个 VPC 写成一个巨大的 main.tf还是拆成网络、安全、计算三个子模块我倾向于拆。原因很实际第二代 VPC 的安全组规则动辄几十条混在一起改一处容易误伤另一处。拆成模块后网络模块只管地址空间和子网安全模块只管规则计算模块只管实例和负载均衡每个模块可以独立 plan、独立测试。第三个取舍是变量管理方式。Schematics 支持在 Workspace 里直接填变量也支持从 Git 仓库读 tfvars 文件。对于敏感信息比如访问密钥我强烈建议走 Workspace 的加密变量不要写进 tfvars 提交到仓库。这一点后面会专门展开。1.3 整体架构长什么样这套方案最终交付的架构大致是这样一个跨两个可用区的 VPC每个区里划分公有子网和私有子网公有子网挂负载均衡和跳板机私有子网跑业务实例。出网流量统一走 NAT 网关入网流量只开放负载均衡的监听端口。安全组按“最小权限”原则配置网络 ACL 作为第二层兜底。代码层面根目录放一个main.tf负责调用子模块variables.tf定义所有可调参数outputs.tf暴露子网 ID、安全组 ID 这些下游要用的值。子模块各自有独立的main.tf、variables.tf、outputs.tf。整个仓库结构清晰新人接手看一眼目录就知道东西在哪。提示目录结构不是越细越好。我见过把每个资源都拆成一个模块的做法结果模块间依赖绕成一团改一个参数要跳五个文件。模块划分应该跟着“变更边界”走经常一起改的东西放一起很少同时改的才拆开。2. 核心细节解析与实操要点2.1 第二代 VPC 的地址规划逻辑地址规划是整套方案的地基规划不好后面全是坑。第二代 VPC 支持自定义地址前缀我一般按“环境-区域-用途”三层来切。举个例子生产环境用10.10.0.0/16预发用10.20.0.0/16测试用10.30.0.0/16。每个 VPC 内部再按可用区切子网比如10.10.1.0/24给区一的公有子网10.10.2.0/24给区一的私有子网以此类推。这里有个容易忽略的点子网掩码不要开太大。有人图省事直接给/16结果一个子网塞几万台机器广播域大得吓人排查问题也难。/24给 254 个可用地址对绝大多数业务足够了。如果确实需要更多/23或/22也行但要有明确理由。计算地址容量的时候记住每个子网会保留几个地址给网关和保留用途实际可用数比理论值少几个。规划时留出 20% 余量比较稳妥别卡着上限用。2.2 安全组与网络 ACL 的分工第二代 VPC 的安全防护是双层的很多人搞不清这两层该怎么分工。我的经验是安全组管实例级网络 ACL 管子网级。安全组是有状态的你放行入站流量出站响应会自动放行网络 ACL 是无状态的入站出站要分别写规则。具体到配置安全组我按角色建负载均衡一个组只开放 80/443 入站应用服务器一个组只接受来自负载均衡组的流量数据库一个组只接受来自应用组的特定端口。这样即使某一层被突破横向移动也被限制住了。网络 ACL 作为兜底我一般只写几条粗粒度规则比如允许 VPC 内部网段互通、允许出网、拒绝其他。它的作用是防止安全组配置失误导致的意外暴露属于“最后一道防线”。注意网络 ACL 规则是有顺序的编号小的先匹配匹配到就停止。写规则时一定要把拒绝规则放在允许规则后面否则可能把该放行的流量也挡了。这个坑我踩过当时排查了半天才发现是 ACL 顺序问题。2.3 变量与凭据的安全处理Schematics 的变量分两类普通变量和敏感变量。普通变量比如区域名、实例规格直接填就行敏感变量比如访问密钥、数据库密码必须用敏感变量类型服务端会加密存储界面上也只显示掩码。我见过有人把密钥写在 tfvars 里提交到 Git觉得仓库是私有的就没事。这个想法很危险仓库权限一旦配错或者有人 clone 到本地密钥就泄露了。正确做法是所有敏感值走 Workspace 的敏感变量代码里只引用变量名不出现明文。另外变量命名要有规范。我习惯用var_前缀加用途比如var_region、var_vpc_cidr、var_instance_profile。这样在代码里一眼能看出哪些是外部传入的哪些是模块内部定义的。2.4 状态管理与并发控制Schematics 帮你托管状态文件但这不代表你可以不管并发。多个 Workspace 如果操作同一批资源仍然可能冲突。我的做法是一个环境一个 Workspace环境之间资源完全隔离。生产、预发、测试各自有独立的 VPC 和状态互不干扰。如果确实需要多个 Workspace 共享部分资源比如共享一个镜像或者一个密钥管理实例那就把这些共享资源单独抽一个 Workspace 管理其他 Workspace 通过数据源引用而不是直接管理。这样职责清晰也不会出现两个 Workspace 抢着改同一个资源的情况。状态文件本身也要定期备份。虽然服务端有冗余但万一需要回滚或者迁移有一份本地备份会安心很多。我一般用 CLI 定期拉取状态快照存到内部存储保留最近几个版本。3. 实操过程与核心环节实现3.1 环境准备与 CLI 配置第一步是把本地工具链配好。需要装 Terraform CLI 和 IBM Cloud CLI后者用来和 Schematics 服务交互。装完之后登录确认账号有创建 Workspace 的权限。# 登录云端账号 ibmcloud login --sso # 确认目标区域 ibmcloud target -r us-south -g Default # 安装 Schematics 插件 ibmcloud plugin install schematics登录方式我用的 SSO如果你用的是密钥登录把--sso换成--apikey然后按提示输入。区域选择要和你计划部署 VPC 的区域一致否则后面创建 Workspace 时还得改。装完插件后用ibmcloud schematics workspace list确认能正常列出 Workspace说明权限和网络都没问题。这一步看着简单但经常有人卡在权限上提前验证能省不少事。3.2 代码仓库的组织方式仓库我按这样的结构组织terraform-vpc/ ├── main.tf ├── variables.tf ├── outputs.tf ├── versions.tf └── modules/ ├── network/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf ├── security/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── compute/ ├── main.tf ├── variables.tf └── outputs.tf根目录的main.tf负责串联三个子模块把网络的输出传给安全模块再把安全和网络的输出传给计算模块。versions.tf锁定 provider 版本避免不同人拉到不同版本导致行为不一致。# versions.tf terraform { required_version 1.3.0 required_providers { ibm { source IBM-Cloud/ibm version ~ 1.60 } } }版本锁定这件事看着不起眼但非常关键。provider 大版本升级经常有破坏性变更不锁版本的话今天能跑的代码明天可能就报错。用~允许小版本更新大版本手动升级是比较稳妥的策略。3.3 网络模块的实现细节网络模块负责创建 VPC、子网和公共网关。核心代码如下resource ibm_is_vpc main { name var.vpc_name resource_group var.resource_group_id address_prefix_management auto } resource ibm_is_subnet public { count length(var.zones) name ${var.vpc_name}-public-${count.index 1} vpc ibm_is_vpc.main.id zone var.zones[count.index] ipv4_cidr_block var.public_subnet_cidrs[count.index] } resource ibm_is_public_gateway main { count length(var.zones) name ${var.vpc_name}-pgw-${count.index 1} vpc ibm_is_vpc.main.id zone var.zones[count.index] }这里address_prefix_management设成auto让服务端自动分配地址前缀省去手动计算的麻烦。子网用count按可用区数量循环创建public_subnet_cidrs是一个列表变量每个元素对应一个区的网段。公共网关是给公有子网出网用的每个区一个保证高可用。私有子网的出网走 NAT 网关NAT 网关的配置在安全模块里因为它涉及出网规则。3.4 安全模块的规则编排安全模块是配置量最大的部分。我按角色建安全组每个组里规则用dynamic块生成避免重复写。resource ibm_is_security_group lb { name ${var.vpc_name}-sg-lb vpc var.vpc_id } resource ibm_is_security_group_rule lb_inbound { for_each var.lb_inbound_rules group ibm_is_security_group.lb.id direction inbound remote each.value.remote tcp { port_min each.value.port_min port_max each.value.port_max } }lb_inbound_rules是一个 map 变量每个元素包含 remote、port_min、port_max。这样加一条规则只需要在变量里加一项不用改代码逻辑。规则内容我一般从需求文档直接映射过来比如“允许公网访问 443”就对应一条 remote 为0.0.0.0/0、端口 443 的规则。NAT 网关的配置也在这里它需要绑定到私有子网所在的路由表并指定出网用的公共网关。这部分配置容易出错建议先在测试环境验证连通性再上生产。3.5 计算模块与负载均衡计算模块创建实例模板、实例组和负载均衡。实例模板里指定镜像、规格、安全组和子网。负载均衡绑定公有子网后端池指向实例组。resource ibm_is_instance_template app { name ${var.vpc_name}-app-template image var.instance_image profile var.instance_profile vpc var.vpc_id zone var.zones[0] keys [var.ssh_key_id] primary_network_interface { subnet var.private_subnet_ids[0] security_groups [var.app_security_group_id] } }实例模板用私有子网和安全组保证实例不直接暴露公网。负载均衡的监听器配置转发规则把 443 流量转到后端池的 80 端口TLS 终止在负载均衡层完成。3.6 在 Schematics 上创建 Workspace 并执行代码推送到仓库后在 Schematics 上创建 Workspace指向仓库地址和分支。然后填变量敏感变量用敏感类型。填完后先跑 plan确认要创建的资源清单符合预期再跑 apply。# 创建 Workspace ibmcloud schematics workspace new \ --name vpc-prod \ --github-repo https://internal-git/terraform-vpc \ --github-branch main # 触发 plan ibmcloud schematics plan --id workspace-id # 查看 plan 结果后触发 apply ibmcloud schematics apply --id workspace-idplan 阶段一定要仔细看。我习惯把 plan 输出导出成文件逐条核对资源数量和关键属性。曾经有一次 plan 显示要删除一个子网原因是变量里少填了一个区及时发现才没造成事故。apply 完成后用ibmcloud schematics workspace output拉取输出值把子网 ID、安全组 ID 这些记下来下游系统要用。4. 常见问题与排查技巧实录4.1 部署失败的典型原因部署失败的原因五花八门但高频的就那么几类。我整理了一张速查表现象可能原因排查方向plan 报地址冲突子网 CIDR 与已有网段重叠检查 VPC 内所有子网网段apply 卡在创建网关区域配额不足查看账号配额申请提升实例创建后无法出网NAT 网关未绑定路由表检查私有子网路由表关联负载均衡健康检查失败安全组未放行健康检查端口确认后端实例安全组规则状态锁冲突多个 Workspace 操作同一资源确认资源归属隔离 Workspace地址冲突是最常见的。规划阶段一定要把整个 VPC 的地址空间画出来标清楚每个子网用了哪段避免重叠。我一般用一个表格维护地址分配改之前先查表。4.2 状态不一致的处理有时候 apply 成功了但状态文件和实际资源对不上比如手动在控制台改了一个安全组规则。这时候 plan 会显示要“修正”这个差异。处理原则是以代码为准手动改动要么回滚要么补进代码。如果差异很小直接 apply 让代码覆盖手动改动。如果手动改动是必要的就把它写进代码再 apply这样状态和代码重新一致。千万不要放任差异不管下次 apply 可能产生意外结果。4.3 性能与成本优化第二代 VPC 的配置项多容易配出“过度安全”或者“过度冗余”的架构。我的经验是安全组规则按实际需要写不要照搬模板子网按业务隔离不要一个子网塞所有东西NAT 网关按出网流量选规格流量小的用基础规格就够。成本上公共网关和 NAT 网关是按小时计费的测试环境不用的时候记得 destroy。Schematics 的 Workspace 本身不额外收费但底层资源该省还得省。我一般给测试环境设一个定时销毁策略晚上和周末自动清理。提示destroy 之前一定要确认没有其他环境依赖这些资源。我见过有人把共享的镜像仓库给 destroy 了结果三个环境同时挂掉。共享资源单独管理别和临时环境混在一起。4.4 几个我踩过的坑第一个坑是变量类型不匹配。Schematics 的变量有类型列表变量如果填成字符串plan 阶段就会报错。填变量时注意看类型提示列表用逗号分隔或者 JSON 格式。第二个坑是provider 版本漂移。有次没锁版本provider 自动升了个大版本结果某个资源的参数名变了apply 直接失败。从那以后我所有项目都锁版本升级手动来。第三个坑是敏感变量没设敏感类型。有次图快把密钥填成了普通变量结果在日志里明文打印出来了。虽然内部环境风险可控但这是个坏习惯后来我所有密钥类变量一律设敏感类型。第四个坑是destroy 顺序。Terraform 一般能自动处理依赖顺序但如果资源之间有隐式依赖没写清楚destroy 可能失败。比如安全组还挂着实例就删安全组会报错。解决办法是在代码里显式声明依赖或者分模块按顺序 destroy。这套方案我在几个环境都跑过整体稳定性不错。关键是把地址规划、安全规则、变量管理这三块做扎实剩下的就是按流程执行。Schematics 的托管能力确实省心尤其是状态管理和执行日志比本地跑省事太多。如果你也在做类似的基础设施交付建议先从测试环境跑通全流程再往生产推。