
简介围绕AWS Landing Zone自动化与测试的代码示例和文档合集面向需要搭建多账户治理体系的云架构师、运维工程师及安全合规人员。内容以Python为辅助工具系统演示如何通过CloudFormation模板创建AWS组织与组织单元配置Core账户启用CloudTrail、Config、VPC流日志等安全检测并设定IAM权限边界与SCP策略。压缩包共69个文件核心类型为JSON策略文件、YAML的CloudFormation模板和Markdown说明文档同时包含少量Ruby脚本、配置文件及PDF/Word文档整体仅1.85MB并按cloudformation-tools、test-environment-setup、organizations、security、accounts等模块清晰划分便于按需查阅其中README与build-out-tasks.md对测试环境准备和任务构建做了梳理。当前已有310人学习。通过该集合读者可复现Landing Zone的完整创建流程掌握create-cfn-stack与manage-orgs-ous-scps等工具脚本用法获得ec2-policy、跨账户策略、ADFS联合访问配置等典型实现并借助test-environment-setup模块快速搭建测试环境减少多账户自动化实施中的踩坑风险可作为企业云治理与安全基线改造的实用起步参考。1. AWS Landing Zone 自动化测试的代码示例和文档到底在解决谁的麻烦假设你接手一个多账号 AWS 环境十几个账号没有统一组织权限靠人肉发安全基线靠口头约定。这时要上一套 Landing Zone传统做法是打开控制台点几十个页面花两三天创建 OU 和 SCP之后每加一个账号都像做手术。而“landing-zone 自动化测试的代码示例和文档”这类仓库就是把这个过程沉淀成能提交、能评审、能回滚的代码用自动化测试证明账号、策略、网络基线确实长成想的样子。我会沿着这个标题把组件拆分、自动化选型、pytest 集成测试怎么写、CI 怎么接、以及那些测不出来的坑都讲清楚。适合正在用 Control Tower 或准备往 Terraform 迁的人也适合想给现有 AWS 组织补一套自动化回归的人。2. 先拆开 Landing Zone 的组件不拆开就写不了自动化2.1 Landing Zone 的组件地图组织树、网络、日志、安全基线Landing Zone 不是一个独立的 AWS 服务而是一组跨账号治理规则的组合。很多团队上来就照着控制台点等到想自动化的时候才发现不知道从哪里下手。我一般先把环境拆成五个块组织与账号、身份权限、网络、日志审计、安全防护。每一块在 AWS 上对应不同的资源也决定后面测试要断言什么。组织与账号这一层由 AWS Organizations 承载核心资源是根、OU 和成员账号以及挂在 OU 上的 SCP。身份权限通常走 IAM Identity Center也就是传统意义上的 SSO另外还要考虑账号内的 IAM role 和权限集。网络基线一般是 VPC、子网、Transit Gateway、路由表和安全组规则Landing Zone 里最常见的是中心化出口和隔离环境。日志审计包括 CloudTrail、Config、S3 日志桶和 KMS 加密。安全防护则是 GuardDuty、Security Hub、Control Tower 的防护规则。这些块不是独立存在的SCP 限制了账号行为网络规则又依赖日志审计可观测。如果要把 Landing Zone 自动化你首先要把上面这些组件在代码里一一对应成可声明的资源。Terraform 里有对应的 resourceCloudFormation 里有资源类型CDK 里有构造类。如果某一块没有对应资源那它只能靠脚本或人工配置这一块的测试就没法只用 IaC 断言需要另外补集成测试。所以组件拆得越细自动化和测试的边界就越清楚。为什么不先从 VPC 和日志桶这类资源开始因为 OU 和 SCP 是 Landing Zone 的骨架网络和日志都可以后补但账号一旦在错误的 OU 里跑一段时间再迁移会很麻烦。SCP 是账号权限的唯一上限它决定了子账号能不能碰某些服务。先管住骨架再往上面长肉这是我做了几次 Landing Zone 自动化之后最深的感受。2.2 自动化选型Control Tower、Terraform、CDK 各自适合哪一层自动化 Landing Zone 的常见路线有三条Control Tower 原生托管Terraform 完全自定义以及 AWS CDK 与 CloudFormation 结合。选型时最重要的问题不是“哪个工具更好”而是“谁来做权威源”。Control Tower 是 AWS 官方 Landing Zone 产品它把组织管理、账号工厂、防护规则做成托管服务很省事但很多细节不能直接改。Terraform 的 aws_organizations 和 aws_controltower 给出来的控制力强能精确管理 OU、SCP、标签策略但和 Control Tower 混用时容易产生漂移这是后面要重点防的。我一般在团队里这样推荐如果是第一次上云、没有历史包袱直接用 Control Tower再用 CloudFormation 或 Terraform 只管理 Control Tower 不覆盖的自定义资源。如果已经有一套多账号环境而且管理得很乱考虑 Terraform 完全接管组织但做好把历史账号纳管的方式。CDK 更适合那些想把 Landing Zone 和业务应用放在同一个 TypeScript 或 Python 代码库里维护的团队但它本质上还是 CloudFormation测试上可以用 pytest 写 Lambda 测试和云资源断言。下面用一个表格把三条路线的边界说清楚维度Control Tower 托管Terraform 自定义AWS CDK CloudFormation账号开通Account Factory界面和 APIOrganizations 自定义基线ServiceCatalog / Custom ResourcesSCP/OU 管理托管的 OU 不能轻易移动完全声明式可纳入代码评审CloudFormation 资源能力有限漂移风险低但自定义资源要小心高尤其和 Control Tower 混合时中依赖对 CloudFormation 的控制测试友好度需要等 Control Tower 同步tflint/checkov/pytest 链路成熟cfn-lint/ Jest/pytest 都可以谁适合做想快速落地、接受 AWS 托管限制的人需要精细治理和有平台工程团队的团队习惯用代码定义基础设施的团队选型之后还要注意不管你选哪条线都要在 README 里明确写清“权威源是哪个”。否则大家看到控制台上能改就直接控制台改代码仓库里 plan 出来的差异会一天比一天大。这个我在后面避坑章会再展开。2.3 最小自动化示例用 Terraform 把 OU 和 SCP 先管起来先别急着写一整层 Landing Zone从一个最小的组织结构开始。下面这份 HCL 示例作用是把组织启用到 ALL 模式创建一个 workloads OU再挂一条禁止删除 CloudTrail 的 SCP。# ous.tf resource aws_organizations_organization this { feature_set ALL enabled_policy_types [SERVICE_CONTROL_POLICY] } resource aws_organizations_organizational_unit workloads { name workloads parent_id aws_organizations_organization.this.roots[0].id }# scp.tf resource aws_organizations_policy deny_cloudtrail_deletion { name deny-cloudtrail-deletion content JSON { Version: 2012-10-17, Statement: [ { Effect: Deny, Action: cloudtrail:DeleteTrail, Resource: * } ] } JSON } resource aws_organizations_policy_attachment workloads_scp { policy_id aws_organizations_policy.deny_cloudtrail_deletion.id target_id aws_organizations_organizational_unit.workloads.id }这段代码的逻辑很简单aws_organizations_organization 第一次会在你的 AWS 账号里创建一个组织feature_set 必须是 ALL否则 SCP 的开关不会打开。组织创建后可以在根下建 workloads OU。SCP 的 content 是一个 JSON 字符串里面是一条 Deny 规则删除 CloudTrail 的行为会被拒绝。aws_organizations_policy_attachment 把这条策略挂到 workloads OU 上之后该 OU 下的所有账号都会继承这条限制。参数说明enabled_policy_types 这里只声明了 SERVICE_CONTROL_POLICY如果你以后要开标签策略或备份策略可以并行加别的类型。parent_id 用 roots[0].id 是常见写法因为一个组织只有一个根。SCP 的 content 要严格符合 IAM policy 格式少一个逗号会导致 terraform plan 通过但 apply 时被 AWS 拒绝。另外如果这个组织已经由 Control Tower 创建aws_organizations_organization 只能作为 data source 来读不能再用 resource 去接管否则会得到“organization already exists”之类的冲突错误。在跑 apply 之前我一般会把当前身份先确认一遍避免在错误的环境里改组织aws sts get-caller-identity terraform plan -outplan.tfplan第一行命令输出当前身份。如果是个人本机上的临时凭证要再确认一下对应的是管理账号还是某个子账号很多事故都来自用子账号凭证去跑 org 资源结果权限不足或者更糟的是用管理员凭证把生产组织整个重写了。terraform plan 出 plan 文件后建议在 CI 里做一次人工 review再 apply。不是所有的 Landing Zone 都适合无人值守 apply。2.4 文档先行把架构决策记录放进同一个仓库自动化代码只是 Landing Zone 的一部分还有一个容易被忽略的东西是文档。代码示例和文档放在同一个仓库里才让这个标题真正落地。我见过太多团队只推代码结果没人敢改因为不知道为什么要用 Terraform 拿下整个组织或者为什么某些 OU 不能动。常见的落地做法是在仓库里放三份文件README 说明部署流程和测试命令ADR架构决策记录记录每次技术选型的原因runbook 描述日常变更的检查清单。ADR 不一定要很复杂每条几段话就够了比如“为什么选择 Control Tower 而不是 Terraform 管理组织”把时间和背景写清楚。runbook 可以写“新增账号后必须先等状态 ACTIVE再挂 SCP最后在 pytest 里执行 test_ou_active”。这样代码示例是文档的补充文档是代码的说明书而不是又一份没人看的 Markdown。测试的目的也因此变得清晰不是为了让 CI 变绿而是验证当前 AWS 环境是否符合文档里描述的架构。如果文档说 SCP 挂在 workloads 上测试就会去查如果文档说日志桶必须加密测试也会去查。文档写不全的自动化是裸奔的自动化回归跑几次可能就把问题掩盖了。所以第二章先把文档和代码的绑定关系立住后面第四章写的测试用例才有依据。3. 把账号和基线代码化从最小示例到可复制的 Landing Zone3.1 用 Control Tower Account Factory还是直接调 Organizations API管住 OU 和 SCP 之后下一个问题就是账号怎么创建。常见做法有两种走 Control Tower 的 Account Factory或者直接调 AWS Organizations 的 CreateAccount API。两者的差异不只是代码写法而是账号是否进入 Control Tower 的托管范围。Account Factory 本质上是 Service Catalog 产品Terraform 里用 aws_servicecatalog_provisioned_product 来调用。它创建的账号会带着 Control Tower 的标签、防护规则和基线配置后续要关停账号也用同一个产品生命周期由 Control Tower 管理。缺点是预置慢通常要 10 到 15 分钟而且如果 Control Tower 版本升级账号工厂的中间状态会变长。直接调 Organizations API 则快得多Terraform 的 aws_organizations_account 几分钟就能把一个账号建出来但它不会自动被 Control Tower 纳管。如果你之后想在 Control Tower 页面统一看这些账号还得手动注册这个过程容易出边界问题。我的建议是环境里已经有 Control Tower就优先走 Account Factory。你得到的不是“快”而是“统一”。如果整套 Landing Zone 全是 Terraform 自己组织、自己管 SCP、自己投递基线那就用 aws_organizations_account然后在账号创建后立刻用一套配置脚本把 CloudTrail、Config、GuardDuty 的委派管理建好。两种方式都能自动化但自动化不等于纳管这一点要先想清楚。3.2 最小账号创建示例Account Factory 和 Organizations 两种写法先看 Account Factory 的写法。下面的 HCL 会在 workloads 这个 OU 下预置一个名为 production 的账号# account_factory.tf resource aws_servicecatalog_provisioned_product production { name prod-account product_name AWS Control Tower Account Factory provisioning_artifact_name latest provisioning_parameters { key AccountName value production } provisioning_parameters { key AccountEmail value aws-productionexample.com } provisioning_parameters { key SSOUserEmail value adminexample.com } provisioning_parameters { key SSOUserFirstName value Admin } provisioning_parameters { key SSOUserLastName value User } create_timeout 15m update_timeout 15m }关键参数是 AccountName 和 AccountEmail前者显示在 Organizations 控制台里后者是 AWS 官方发通知的邮箱。SSOUser 这三个参数会在 IAM Identity Center 里创建一个初始用户方便管理员第一时间登录。create_timeout 建议设成 15 分钟因为账号工厂经常跑到接近十分钟默认超时会让你误以为创建失败。如果不用 Control Tower直接调用 Organizations 的写法会更轻# account.tf resource aws_organizations_account production { name production email productionexample.com parent_id aws_organizations_organizational_unit.workloads.id }这段代码会创建一个独立的成员账号并把它放进 workloads OU。注意 aws_organizations_account 创建后你只能通过注册邮箱拿密码而不是像 IAM 用户一样在控制台里直接能看到。如果事后想改邮件地址很麻烦几乎等于重新建号。所以 email 参数一定要核对三遍再 apply。无论用哪种方式创建完账号后都不要立刻做下一个步骤。账号在 Organizations 里的状态会经历 CREATING、ACTIVE 和 SUSPENDED 等阶段。只有 ACTIVE 状态才能正常挂 SCP、放资源。CI 里一般要轮询aws organizations list-accounts --query Accounts[?Nameproduction].{Id:Id,Status:Status} --output table这个命令展示账号当前状态。建议在 CI 里做一个最长等待 15 分钟的重试循环等 Status 变为 ACTIVE 再继续跑测试。3.3 参数设计账号命名、邮箱、权限集和关闭保护账号创建里最容易出问题的不是 IaC 代码而是参数体系。我从几个项目里沉淀出如下推荐值参数推荐值说明账号命名环境-项目如 prod-billing避免空格和大小写混用后续标签和 SCP 条件都要靠它账号邮箱aws- 不要用个人邮箱邮箱创建后不可变用团队共享邮箱更稳妥SSOUserEmail负责人企业邮箱或团队邮箱如果为空可能无法完成首次登录权限集PowerUserAccess / ViewOnlyAccess不要给每个账号配 Admin除非是隔离治理的沙箱关闭保护根账号开启 MFA 和关闭保护测试账号建议进入生命周期关闭不要依赖控制台手动删除其中一个经验是账号邮箱务必做成可配置项而不是在 HCL 里写死。每个环境的地域或公司域名可能不同写死在仓库里会让测试环境和管理环境互相污染。常见的做法是在 terraform.tfvars 里统一声明账号邮箱后缀再通过变量拼接。3.4 第一次创建账号后必须核对的状态代码跑完不代表账号可用我一般会按下面的顺序核对账号是否真正“入队”账号状态是不是 ACTIVE。账号是不是挂在预期的 OU 下而不是落在根下。workloads OU 上的 SCP 是否对账号生效。IAM Identity Center 里是否出现了对应权限集。账号是否能通过 cross-account role 访问而不是只能从控制台临时登录。前两项可以通过 Organizations API 查第三项建议真实验证一次。怎么验证在刚创建好的账号里跑一条会被 SCP 拒绝的操作比如尝试删除 CloudTrail。如果返回 AccessDenied说明 SCP 真的生效了。很多团队只检查 SCP 是否“附加”在 OU 上却忽略了继承和显式例外所以行为验证比状态查询更可靠。这个思路在第四章做成自动化测试。4. 用 pytest boto3 给 Landing Zone 写集成测试从检查 SCP 到验证网络基线4.1 测试目标分层静态检查、状态断言、行为验证Landing Zone 的代码示例跑通之后如果没有测试你只能靠控制台截图证明环境没问题。自动化测试应该分三层静态检查、状态断言和行为验证。静态检查在 apply 之前做比如 terraform validate、tflint、checkov、cfn-lint它们主要查语法、权限配置风险和资源之间的引用是否正确。状态断言是集成测试的主菜用 boto3 读取 AWS 当前的资源状态断言 OU 存在、SCP 已挂、日志桶有加密、权限集名称符合规范。行为验证最真实但也最危险常见做法是在沙箱账号里故意触发一个 SCP 限制操作期望 AWS 返回 AccessDenied。我给团队定的规矩是静态检查在本地和 CI 的 push 阶段跑状态断言在 pull request 和定时任务里跑行为验证只允许在专门的沙箱 OU 里跑。分层的好处是如果行为验证导致某个资源受损影响范围被限制在沙箱账号不会波及生产。4.2 最小 pytest 集成测试检查 OU、SCP 和加密日志桶下面是一份可以直接放到 tests/ 目录下的 pytest 文件它用 boto3 读取 Landing Zone 的真实状态。# tests/test_landing_zone.py import boto3 import pytest PRODUCTION_OU_NAME workloads SCP_NAME deny-cloudtrail-deletion LOG_BUCKET aws-landing-zone-logs pytest.fixture(scopemodule) def org_client(): return boto3.client(organizations) pytest.fixture(scopemodule) def s3_client(): return boto3.client(s3) def test_production_ou_exists(org_client): roots org_client.list_roots()[Roots] all_ous [] for root in roots: ous org_client.list_organizational_units_for_parent( ParentIdroot[Id] )[OrganizationalUnits] all_ous.extend(ous) assert any(ou[Name] PRODUCTION_OU_NAME for ou in all_ous) def test_scp_attached_to_workloads(org_client): roots org_client.list_roots()[Roots] ous org_client.list_organizational_units_for_parent( ParentIdroots[0][Id] )[OrganizationalUnits] workloads_id next(ou[Id] for ou in ous if ou[Name] PRODUCTION_OU_NAME) policies org_client.list_policies_for_target( TargetIdworkloads_id, FilterSERVICE_CONTROL_POLICY, )[Policies] assert any(p[Name] SCP_NAME for p in policies) def test_log_bucket_encrypted(s3_client): response s3_client.get_bucket_encryption(BucketLOG_BUCKET) rules response[ServerSideEncryptionConfiguration][Rules] assert len(rules) 0三个测试分别验证了 OU 存在、SCP 已附加、日志桶加密。fixture 的 scope 是 module所以整个测试模块只初始化一次 boto3 client跑起来会快很多。断言用名称而不是 ID因为 OU ID 和账号 ID 在跨环境时完全不同名称一般是稳定的。参数说明test_scp_attached_to_workloads 里用 list_policies_for_target 只看直接附加在 OU 上的策略。如果 SCP 是从父 OU 继承的这个接口看不到需要再查父链路。test_log_bucket_encrypted 假设你有权限读取加密配置。如果 CI 角色只给了组织读权限这个测试会在 S3 的 GetBucketEncryption 上报 AccessDenied。此时不要直接删断言而是给 IAM 角色补上对应权限或者把测试标记为需要特定权限集。4.3 把 pytest 接进 CICodeBuild 和 GitHub Actions 的最小配置有了 pytest 脚本下一步是让它每次 PR 或定时自动跑。下面是一份 CodeBuild 的最小 buildspec# buildspec.yml version: 0.2 phases: install: runtime-versions: python: 3.11 commands: - pip install -r tests/requirements.txt build: commands: - pytest tests/ -m integration --junitxmlreport.xml post_build: commands: - ls -la report.xmlrequirements.txt 里只需要 boto3、pytest 和 python-dotenv。CodeBuild 的 service role 要允许 Organizations 和 S3 的只读权限。注意我是用-m integration来区分集成测试这就要求测试文件里给用例打上pytest.mark.integration的标记。否则 CI 会把你本地开发中的单元测试也一起跑掉。如果是 GitHub Actions常见做法是用 OIDC 身份联邦不在仓库里存长期密钥# .github/workflows/landing-zone-test.yml name: landing-zone-test on: pull_request: paths: [*.tf, *.py] schedule: - cron: 0 2 * * 0 jobs: integration: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install -r tests/requirements.txt - run: pytest tests/ -m integration env: AWS_REGION: us-east-1这段配置的要点只有 pulll request 改动.tf或.py文件时才触发另外每天凌晨 2 点定时跑一次。schedule 的目的是漂移检测因为 Landing Zone 可能被控制台手动改掉定时任务会把这种漂移暴露出来。OIDC 的身份配置需要提前在 AWS 里建立一个 IdP并让 GitHub Actions 的 role 只包含 testing 所需的权限。别忘了在运行 pytest 时通过环境变量注入AWS_REGION。4.4 测试夹具与清理固定沙箱账号不做每次新建集成测试最容易踩的坑是每次 CI 都创建新账号。上一章讲了账号配额这里说清理逻辑。测试夹具里不要用“创建账号”作为第一步而应尽量用现有账号。如果你的 Landing Zone 里没有沙箱账号就在一个单独的 OU 下创建并留作长期测试专用测试结束后不删除而是恢复到基线状态。行为验证示例# tests/test_sandbox_scp.py import boto3 import pytest from botocore.exceptions import ClientError SANDBOX_ACCOUNT_ID 123456789012 SANDBOX_ROLE arn:aws:iam::123456789012:role/LandingZoneTestRole pytest.fixture(scopemodule) def sandbox_session(): sts boto3.client(sts) assumed sts.assume_role( RoleArnSANDBOX_ROLE, RoleSessionNamelanding-zone-test, )[Credentials] return boto3.Session( aws_access_key_idassumed[AccessKeyId], aws_secret_access_keyassumed[SecretAccessKey], aws_session_tokenassumed[SessionToken], ) def test_deny_cloudtrail_deletion_in_sandbox(sandbox_session): client sandbox_session.client(cloudtrail) with pytest.raises(ClientError) as exc_info: client.delete_trail(Namelanding-zone-test-trail) assert exc_info.value.response[Error][Code] AccessDenied这个测试故意触发一次删除 CloudTrail 的请求预期是 AccessDenied。sandbox_session fixture 通过 sts.assume_role 拿到沙箱账号的临时凭证。RoleArn 是预先在沙箱账号里创建好的只在测试账号里存在不影响其它账号。如果 SCP 没有生效delete_trail 会成功测试会失败这正好暴露问题。要注意这里真的会调用 API虽然被拒绝但会产生 CloudTrail 事件所以沙箱账号越干净测试结果越可信。4.5 文档里怎么写测试命令和范围测试代码和 CI 配好之后仓库里的 docs/tests.md 要能直接回答三个问题怎么跑、哪些能跑、失败看哪里。我习惯用一个表格列清楚测试类型命令需要凭证适合环境静态检查terraform validate checkov -d .不需要本地/CI push集成只读断言pytest tests/ -m integrationOrganizations/S3 只读PR / 定时行为验证pytest tests/ -m sandbox沙箱账号 cross-account role单独 job这个表格同时也决定了 CI 流水线的 stage。比如 push 阶段只跑静态检查pull request 阶段跑只读断言定时任务跑行为验证。文档是代码示例的一部分没有文档的测试将来只会变成一堆没人敢动的脚本。5. Landing Zone 自动化和测试避坑指南现象、原因、解决5.1 Control Tower 和 Terraform 互相接管组织资源漂移检测天天报警现象Terraform 代码把 OU 和 SCP 都声明成 resource 之后apply 成功但 Control Tower 控制台开始显示组织或者 OU “drifted”一些账号上的 GuardRail 状态变成“非强制”。Terraform plan 每次都提示要重建同一个 OU。原因Control Tower 本身在后台用 CloudFormation 管理 OU 和 SCP它会维护自己的资源生命周期。你在 Terraform 里用 resource 强行接管同一个 OU其实有两个权威源在抢同一块地盘。Terraform apply 后 Control Tower 认为这超出了它的预期配置于是把状态标成漂移。解决区分“托管资源”和“自定义资源”。凡是 Control Tower 默认管理的 OU比如根下的 Core、Security 和 SandboxTerraform 里用 data 读取而不是 resource。自定义的 OU比如 workloads如果不在 Control Tower 的默认 OU 清单里可以用 resource。但挂 SCP 时要注意不要动 Control Tower 自动创建的 FullAccess 或 GuardRail 策略。代码注释里一定要写明“该资源由 Control Tower 托管不要改成 resource”不然下一个接手的人会照着错误的习惯改回去。5.2 测试 SCP 时用了管理账号永远测不出策略生效现象SCP 明明写着 Deny cloudtrail:DeleteTrail但在某个账号里手动删 CloudTrail 还是成功自动化测试也一直通过。原因SCP 不影响组织的管理账号也不影响某些处于异常状态的账号。如果 pytest 里的 boto3 凭证属于组织管理账号的 role那这条行为验证就是自欺欺人。管理账号相当于组织里的“超级管理员”SCP 对它是放行的。解决行为验证必须用成员账号的临时凭证。方法是在测试 fixture 里通过 sts.assume_role 切到某个专门负责测试的成员账号role 的信任策略只允许组织里的测试执行角色来 assume。另外有的团队会用根用户证书去测试这更要避免。正确的流程是测试 execute role 先切到一个低权限 Ops 账户再从这个账户的 role 跳入沙箱账号全程无长期密钥。5.3 每次测试都新建账号最终撞上账号配额现象CI 跑了一两个月后某天 CreateAccount 开始报AccountLimitExceededException控制台里一堆SUSPENDED账号躺着但测试还在继续创建。原因AWS 组织的账号配额不是按当前活跃账号数算的而是包含历史创建但还未完全删除的账号。删除账号后很多状态只是进入关闭流程要等几周到几个月才能彻底释放。每次测试都新建账号等于把配额当成能无限刷的沙盒。解决测试环境固定用 1 到 2 个沙箱账号不要新建。如果一定要验证新账号的初始化流程也只在 nightly 任务里做并设置一个“当日失败自动清理”的步骤。配额监控可以用 CloudWatch Metric Filter 读取 AccountCreate 事件低于 15 时给运维发通知。教训是测试代码里的账号创建逻辑要尽量少触发触发了就当需要人工介入的信号。5.4 pytest 里写死了 OU ID / 账号 ID换个环境全红现象同一份测试代码在开发环境的 Landing Zone 里全部通过在预生产或生产环境的 AWS 组织里跑test_prod_ou 直接失败日志显示找不到关联的 OU。原因代码里把ou-xyz、123456789012这类 ID 写死在断言里。不同账号的 OU ID 完全随机生产环境的账号 ID 也和开发不同。开发环境绿生产环境红这种测试没人敢信。解决统一用名字和标签作为稳定标识。举个例子测试里先按名称列出当前环境的 OU再用.get取 ID最后断言该 ID 不为空。账号 ID 的硬编码可以放到环境变量或一个 mapping 文件里不同环境用同一个 pytest 代码只是变量不同。另外conftest.py 里可以做一个缓存函数把 OU 名称到 ID 的映射统一获取一次避免每个测试都去调一次 Organizations API。5.5 文档、代码、实际环境三者不同步示例代码半个月就废现象README 里写的部署命令和代码示例跑不通有人按照示例去执行得到一堆资源不存在的报错最后对仓库失去信任。原因Landing Zone 是持续演进的。OU 可能从 workloads 改成了 workloads-stageSCP 可能从 deny-cloudtrail-deletion 改成了 deny-security-log-disruption但文档里的示例还停留在最初版本。代码本身可能改了注释和 README 没人同步。解决把文档纳入 CI而不是当作可有可无的附赠品。一个轻量做法是写一个 make doc-check用脚本抓取 README 里的示例命令逐条跑一遍--dry-run。更严格的做法是在 PR 模板里加一个勾选项“本条改动是否影响 docs/ 下的内容”。我们团队现在把 ADR 和测试命令放在同一个目录凡是代码变更没同步文档CI 里的链接检查就会提示。这样文档不一定是完美的但至少不会比代码晚太多。6. 进阶把 Landing Zone 测试做成一套能反复跑的基线6.1 让测试结果从 CI 走到云上变成持续合规信号pytest 在本地跑通只是第一步。我见过不少团队把测试配置好之后就只在出事故时跑一次这样的自动化等于没有。要让它成为基线把测试结果沉淀到云上更有意义。常见做法是在 EventBridge 里配一个每天凌晨触发的规则启动 CodeBuild 跑上一章说的集成测试测试报告输出到 S3 和 CloudWatch Logs。如果断言失败通过 SNS 把消息发给平台团队。这一套不需要额外容器控制塔本就支持 CloudFormation StackSet或者用 Terraform 部署也很方便。6.2 固定沙箱账号的复用小脚本为了避免账号配额问题我更喜欢用一个脚本从现有账号里挑出沙箱再复用。下面这段 Python 可以放在 scripts/ 下# scripts/pick_sandbox.py import boto3 client boto3.client(organizations) sandbox_ou_name sandbox account_id None root client.list_roots()[Roots][0] ous client.list_organizational_units_for_parent(ParentIdroot[Id])[OrganizationalUnits] sandbox_ou next(ou for ou in ous if ou[Name] sandbox_ou_name) accounts client.list_accounts_for_parent(ParentIdsandbox_ou[Id])[Accounts] for acc in accounts: if acc[Status] ACTIVE: account_id acc[Id] break print(account_id)脚本先找到 sandbox OU再挑第一个 Active 账号。打印出来的 ID 可以直接作为 5.4 里提到的时间参数或者塞进 pytest 的--account-id参数。这样做的好处是测试不再依赖硬编码每次自动找到当前环境的沙箱账号。6.3 我每次改完 Landing Zone 都会做的三件事改了 SCP 之后我一定会在沙箱账号里先跑行为验证确认拒绝生效再合入代码。改了 OU 名称或层级后我会把 docs/ADR 里的记录翻出来看有没有过时。这两个动作做多了会变成条件反射。还有一件事是查看 CloudTrail 里有没有其它人直接用控制台动过组织资源。我常跑一条命令aws cloudtrail lookup-events \ --lookup-attributes AttributeKeyEventName,AttributeValueUpdateOrganizationalUnit \ --query Events[].CloudTrailEvent \ --output json这条命令列出组织更新的历史如果看到非计划内的改动就知道文档和现实之间已经出现了偏差。Landing Zone 这类系统最大的风险不是自动化代码写错而是文档、代码、现实各说各话。希望这套落地思路能帮你把这三者重新拉回一条线上。希望帮到你。本文还有配套的精品资源点击获取