
去年有段时间我们团队陷入了一个非常尴尬的处境业务方要求应用必须同时在 AWS 和 Azure 上跑可测试这边还是各测各的。QA 同学在 AWS 上写好一套脚本到了 Azure 上几乎全废因为两朵云的 API 风格、资源模型、权限体系完全是两套逻辑。后来我们决定把跨云测试工具链这件事认真做起来目标很简单同一套测试用例能在 AWS 和 Azure 上并行跑起来结果汇总到一份报告里任何人不用关心底层是哪朵云只管看业务过没过。这篇文章就是这次集成方案从设计到落地的完整复盘适合正在做多云战略、需要在双云环境里跑功能测试和接口测试的测试开发、DevOps 工程师参考。1. 双云环境下的测试痛点API哲学差异、结果不可比与重复建设1.1 两朵云的API哲学从根上就不一样先把痛点说透。很多团队一开始觉得“跨云测试”就是把脚本里的 SDK 换一下跑起来就行。真做了才发现AWS 和 Azure 对外暴露的 API 设计哲学差得非常远。AWS 的 API 核心是偏 RPC 风格一切操作围绕服务端点发请求参数通过 Action 区分。你创建一个 EC2 实例实际请求里带的是ActionRunInstances、ImageId、InstanceType这些 KV 参数。而 Azure 的核心是 Resource Manager 这套 RESTful 资源管理模型创建一台虚拟机要走Microsoft.Compute/virtualMachines/write这个资源操作请求路径里带着订阅 ID、资源组、资源名请求体是一大坨结构化的 JSON。打个不太严谨的比方AWS 像是给每个动作单独发一张指令卡Azure 更像是填一张有严格格式要求的资源申请表。这意味着什么意味着任何直接调用单云原生 API 写的测试代码换朵云之后基本都要推翻重来。你在这边封装好的create_vm()函数到那边参数结构、返回结构、异步逻辑全变了。1.2 环境不一致导致测试结果根本没法比第二类痛点是环境漂移。假设你在 AWS 上测性能跑出来的数据是Standard_B2s的规格在 Azure 上对应的是Standard_B2s吗其实不对。AWS 的t3.micro和 Azure 的Standard_B1s算力指标、网络带宽、磁盘 IO 都不一样甚至连“同一规格”的实例类型映射都找不到完美的对应关系。更麻烦的是网络和权限。AWS 的默认 VPC 一键就有Azure 上你得先建虚拟网络、子网、网络安全组否则机器都起不来。两边的手工配置一旦不一致同样的测试用例跑出来的结果就没有可比性出了问题也无从排查是应用代码的问题还是底层环境配置的问题。所以做跨云测试工具链第一件事就是把两朵云的环境定义变成代码保证每次测试跑在“跑得起来且可对齐”的环境中。1.3 重复建设的隐性成本比想象中高第三个痛点就是大家都能预见的重复建设。两套脚本、两套执行流水线、两套报告格式每次业务需求变更测试代码要同步改两遍。表面上只是“多一倍工作量”实际上因为平台差异很多时候是“逻辑一样但代码完全不同”维护成本并不是线性增长而是指数增长。我们当时的判断很明确这个问题的根子不在脚本层而在架构层。只有搭建一个工具链把平台差异在这个链条里隔离掉才能让测试同学把精力花在业务上而不是花在跟云厂商 API 搏斗上。2. 工具链底座设计Terraform管资源、抽象层管差异、框架管执行2.1 为什么选Terraform而不是两套原生编排一说到资源编排做过云的人会自然想到 AWS 的 CloudFormation 和 Azure 的 ARM Template。但跨云工具链里再分两套编排方案等于把重复建设的问题又引回来了。我们最终选了 Terraform理由很直接一套 HCL 语法同时管 AWS 和 Azure状态文件可以统一管理而且社区生态成熟遇到问题随便一搜都有答案。Terraform 的多云配置其实非常简单就是声明两个 providerterraform { required_providers { aws { source hashicorp/aws version ~ 5.0 } azurerm { source hashicorp/azurerm version ~ 3.0 } } }实际使用中要注意Terraform 的工作目录要跟着云走。我们采用的是同一个 modules 仓库但每个云各维护一套 root module。以“创建测试虚拟机”这个模块为例AWS 侧和 Azure 侧各有自己的实现但对外暴露的变量保持一致name、vpc_id或vnet_id、subnet_id、instance_type或vm_size。调用方完全不用关心底层差异只需要传业务参数。2.2 模块化设计一个测试环境一套独立资源栈工具链的资源编排我们按“环境维度”划分而不是按“云维度”划分。什么意思就是每个测试环境在 AWS 和 Azure 上各有一整套完整的资源栈网络、安全组、计算实例、对象存储、甚至数据库。这样做的好处是环境之间的隔离性非常干净。跑测试的时候你很清楚当前这组用例对应的资源都在哪儿测完直接整栈销毁不会残留垃圾资源。模块库的组织方式大概长这样terraform/ ├── modules/ │ ├── network/ # VPC / VNet 子网、路由、网关 │ ├── compute/ # EC2 / VM 实例 │ ├── storage/ # S3 / Blob Storage 桶与策略 │ └── security/ # IAM 角色 / SPN 授权 ├── environments/ │ ├── aws-test/ │ │ ├── main.tf │ │ └── terraform.tfvars │ └── azure-test/ │ ├── main.tf │ └── terraform.tfvars2.3 统一抽象层在每个云适配器里做隔离有了资源编排下一步是给测试代码提供一个统一抽象层。我们的做法是定义一组纯业务接口然后分别实现 AWS 适配器和 Azure 适配器。以 Python 为例核心接口大致是class CloudAdapter(ABC): abstractmethod def create_vm(self, spec: VMSpec) - VMInfo: 创建一台测试虚拟机 abstractmethod def destroy_vm(self, vm_id: str) - None: 销毁虚拟机 abstractmethod def get_public_ip(self, vm_id: str) - str: 获取机器公网IP abstractmethod def put_object(self, bucket: str, key: str, data: bytes) - None: 向对象存储写入测试数据AWS 适配器里调用boto3Azure 适配器里调用azure-mgmt-compute和azure-storage-blob接口层完全一致。测试用例只依赖CloudAdapter这个抽象不依赖具体实现。这样换代云、换 SDK、换 API 版本的时候影响面被限制在适配器内部。3. AWS侧集成落地IAM权限收敛、VPC隔离与资源生命周期管理3.1 IAM权限收敛给测试一张最小权限卡AWS 侧集成第一步不是写代码而是把权限设计好。当时我踩过一个坑图省事直接给测试账号绑了AdministratorAccess结果某次自动化脚本误删了一个生产标签的资源差点出事。后来统一改成最小权限方案专门创建一个test-runner用户只给测试资源相关的操作权限。IAM Policy 大致长这样{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ ec2:RunInstances, ec2:DescribeInstances, ec2:TerminateInstances, ec2:CreateTags, s3:PutObject, s3:GetObject, s3:DeleteObject ], Resource: * } ] }注意这里的Resource: *仍然偏宽更严格的方案是给所有测试资源打上envtest标签然后在 Policy 里用Condition和aws:ResourceTag做二次校验。实际使用中我们还没做到这个粒度因为测试环境的 VPC 本身已经是独立隔离的风险可控。3.2 VPC规划测试环境与生产环境网络彻底隔离AWS 侧的资源规划最重要的一件事是 VPC 网段。我们单独划了几个测试专用 VPCCIDR 用的是日常不会碰到的网段比如10.233.0.0/16。这样就算测试代码里有什么网络配置写死也不会跟生产网段10.0.0.0/16撞车。VPC 内部的子网也按用途拆分外层放安全组和 ALB内层放 EC2 和 RDS。安全组规则只放通测试必须的端口SSH 只允许跳板机 IP 访问数据库端口只允许内网互通。这些全部通过 Terraform 模块化定义新建环境只需改terraform.tfvars里的几个变量。3.3 生命周期管理没有自动清理的测试环境都是负债跨云测试工具链里有一件事必须一开始就做资源自动清理。测试环境不像生产环境用完没人记得手动销毁。我们当时在 CI 流水线里加了一个清理步骤每天晚上跑一次脚本扫描所有带envtest标签的 EC2、S3 桶和 RDS 实例超过 24 小时直接终止或删除。S3 桶的清理要注意必须先删除桶内所有对象再删桶否则 DeleteBucket 会报错。EC2 实例终止前要确认没有挂载的弹性 IP 残留不然会产生不必要的费用。这块用boto3写脚本不到 200 行就能搞定但收益极大——我们跑了大半年几乎没有再出现过“测试资源堆积导致账单爆炸”的情况。4. Azure侧集成落地SPN凭证、资源组隔离与NSG策略对齐4.1 Service PrincipalAzure侧的身份底座对应 AWS 的 IAM 用户Azure 侧是服务主体Service Principal。创建方式如下az ad sp create-for-rbac \ --name test-runner \ --role Contributor \ --scopes /subscriptions/订阅ID/resourceGroups/test-rg执行完会输出appId对应 Client ID、password对应 Client Secret、tenant对应 Tenant ID。这几个值要放到 CI 平台的密钥管理里配合环境变量读取不要写进代码仓库。Azure SDK 和 Terraform 的 azurerm provider 都会自动读一套标准的认证环境变量export AZURE_TENANT_ID租户ID export AZURE_CLIENT_ID客户端ID export AZURE_CLIENT_SECRET客户端密钥 export AZURE_SUBSCRIPTION_ID订阅ID4.2 资源组隔离Azure版的“环境边界”Azure 的资源管理单位是资源组这一点比 AWS 的“全账号打标签”更适合做测试环境隔离。我们给每个测试环境单独建一个资源组命名规则rg-test-环境名-日期。测试结束时直接删掉整个资源组即可Azure 会把组内的 VM、VNet、存储、公网 IP 一次性清掉不留残余。资源组的权限边界也顺便解决了SPN 的 Role Assignment 范围只到这个资源组所以即使测试脚本出了 bug也影响不到其他资源组。4.3 NSG策略对齐别让安全组恶心到你Azure 的网络安全组NSG和 AWS 的安全组有个非常容易踩的差异AWS 安全组默认是隐式拒绝所有流量只有显式放行才通过Azure 的 NSG 也是默认拒绝但规则优先级更高而且有明确的优先级数字数字越小越优先。实际操作中我们对齐两边策略的方式是在 Terraform 模块里直接定义好默认规则不让测试同学手工去改。比如resource azurerm_network_security_rule allow_ssh { name allow-ssh priority 100 direction Inbound access Allow protocol Tcp source_port_range * destination_port_range 22 source_address_prefixes [跳板机IP] destination_address_prefix * }4.4 AWS和Azure关键服务映射速查团队里测试同学对两朵云不一定都熟我们整理了一张服务映射表贴在文档首页日常写用例时对照着用用途AWSAzure计算实例EC2Virtual Machines对象存储S3Blob Storage虚拟网络VPC / SubnetVNet / Subnet访问控制Security GroupNSG权限管理IAM PolicyRBAC Service Principal日志监控CloudWatchAzure Monitor数据库RDSAzure SQL / Flexible Server无服务器计算LambdaFunctions5. 统一调度与结果聚合一套用例并行跑双云报告只出一份5.1 用例怎么组织才不重复工具链的核心收敛点在用例管理。我们没搞一套复杂的平台只是约定所有用例按业务流程写云平台作为参数传入不在用例代码里写死。每个用例一个 YAML 配置大致长这样test_case: checkout_flow platforms: [aws, azure] params: aws: instance_type: t3.micro region: us-east-1 azure: vm_size: Standard_B1s location: eastus测试用例代码里通过CloudAdapter获取资源然后走业务逻辑断言完全不感知自己在哪朵云上。这样平台的差异只存在于配置文件和适配器层用例本身是干净的。5.2 并行执行与结果聚合执行层面我们用的是简单的并发模型。CI 流水线里同一个测试任务分两个 job一个跑 AWS一个跑 Azure互不依赖两边同时发起。启动前由工具链自动调用 Terraform 拉起对应环境的资源栈跑完自动销毁。为什么强调并行因为串行跑双云的时间成本是翻倍的测试一套业务流动辄十几分钟串行就意味着用户要等半小时。并行之后整体耗时基本等于单云耗时测试效率立刻上来了。结果聚合是我们自己写的一个小工具每个云跑完输出一份 JUnit XML 格式的测试报告聚合器把两份 XML 合并按用例 ID 分组生成一个统一的 HTML 报告。报告里专门有一列显示“AWS / Azure 一致性”如果某个用例在一侧通过另一侧失败会高亮标出方便判断是平台问题还是应用问题。5.3 失败重试策略先分平台再重试跨云测试失败率天然比单云高因为环境因素多。我们踩过的经验是不要一失败就盲目重试先看失败类型。AWS 侧常见的RequestLimitExceeded或 Azure 侧常见的Conflict属于可以重试的瞬时错误而断言失败、404 这类说明业务确实有问题重试只会浪费时间。工具链里我们给重试加了一个简单的分类机制瞬时错误最多重试 3 次每次间隔递增比如 10 秒、20 秒、40 秒业务断言类错误直接失败并把详细日志丢给开发定位。6. 落地过程中的坑与对策从凭证管理到成本兜底6.1 凭证管理永远不要把AK/SK写进仓库这个坑几乎每个团队都会踩。我们有个同事图方便把 AWS Access Key 直接写在测试脚本的环境变量文件里还顺手提交到了 Git 仓库。所幸发现得早没有造成实际泄露但光是轮换凭证就折腾了一整天。现在的规范是所有云凭证只存在 CI 平台的密钥管理服务里本地开发用 sso 或 service principal 的短时凭证测试脚本里一律通过环境变量注入。Azure 侧的 Secret 定过期时间到期强制轮换。6.2 两朵云的区域服务差异跨云测试工具链里最容易被忽略的就是区域差异。AWS 的某些新服务只在部分 region 提供Azure 的某些 VM 系列只在特定 region 有配额。你在一朵云上跑得好好的用例到另一朵云可能连资源都创建不出来。解决方式是创建资源前先调用两朵云的配额查询接口确认当前区域能满足规格要求不满足就自动 fail-fast 并给提示而不是等到资源创建失败再排查。这个步骤放在适配器里做非常值得。6.3 Terraform状态文件的管理问题Terraform 的状态文件容易成为团队协作的坑。多个开发同时terraform apply状态文件很容易冲突。AWS 侧我们把 state 放在 S3 并开启 DynamoDB 锁Azure 侧放在 Storage Account 的 Blob 里并开启租约锁。两边都配置好之后基本不会再出现状态文件互相覆盖的问题。6.4 成本兜底定时关停和额度上限测试工具链的东西平时用着爽月底账单来了就肉疼。我们做了两件事来控制成本一是非工作时间自动关停。通过定时任务每天晚上八点把所有带envtest标签的 EC2 实例 Stop 掉Azure 侧对应 Deallocate第二天早上 CI 触发测试前再 Start。注意 Azure 的 VM 是 Deallocate 才停止计费计算资源单纯 Power Off 还是会产生计算费用这个和 AWS 的 Stop 行为也有差异。二是给每朵云分别设置了费用告警阈值比如 AWS 侧月度预算超过 X 元就 SNS 通知Azure 侧超过预算就触发 Alert。成本异常能第一时间发现。6.5 最终的建议先跑通最小闭环再扩量工具链搭完之后我们最大的体会是第一版千万不要贪多。最开始只挑了三条核心业务链路在双云上跑通验证了“资源创建→用例执行→资源销毁→结果聚合”这条主链路可行之后才逐步把用例量扩上去。如果一开始就想全量覆盖所有业务大概率会在各种平台差异的泥潭里挣扎得筋疲力尽。还有个很实用的小技巧在 CI 结果通知里给两个云各打一个标签。一旦某侧用例挂了先看是平台相关还是应用本身的问题这样排查效率会高很多。这套方案跑到现在跨云测试已经成为我们发布流程里非常稳定的一环测试同学不用再跟云厂商 API 死磕可以把精力放回业务本身了。