简介:资源围绕AWS Landing Zone的自动化与测试,提供代码示例和配套文档,适合需要搭建多账户AWS环境、实施组织治理与自动化验证的云架构师或DevOps工程师。包体共69个文件、大小仅1.85MB,以JSON策略文档、YAML CloudFormation模板、Markdown指南和Ruby脚本为主,其中JSON用于IAM与SCP策略定义,YAML承载VPC及CloudTrail等基础设施即代码模板,Ruby脚本用于配置校验,另含PDF/DOCX说明和测试环境设置指引。内容覆盖创建AWS Core账户、组织与OU管理、跨账户策略、ADFS联合访问及示例工作负载,目录按cloudformation-tools、organizations、security、accounts、test-environment-setup等模块划分,便于按需检索。已有310人学习下载,适合希望快速理解Landing Zone初始化流程并复用现成自动化脚本的进阶使用者。
1. Landing Zone 自动化与测试:从“搭完能通”到“改了不炸”
接手一个几十个账号的 AWS 组织后,你会发现真正让人头疼的往往不是第一次搭建,而是后续任何一次改动都像在拆盲盒。手动在控制台创建账号、分配权限集、挂 SCP,第一次能通,第二次可能就有人报障,而且没人说得清是哪次变更让环境翻了车。围绕 AWS Landing Zone 的自动化和测试,本质上就是解决这个“看不见的脆弱”:把账号开通、身份基线、策略下发和审计配置变成可复现的代码,再用自动化测试保证每次改动可控、可回滚、可验证。这篇文章写给正在搭建或维护 Landing Zone 的云平台工程师、DevOps 和架构师,会从选型讲到仓库结构,再到测试策略和几个高频翻车点,一条线拆完。
2. Landing Zone 自动化管什么:账号、基线、策略与选型
2.1 账号工厂、身份基线和策略:先理顺这三个对象
Landing Zone 的自动化对象可以收敛成三件事:账号、身份、策略。
账号这一层最典型的是账号工厂(Account Factory)。在 AWS Organizations 里新建账号不是终点,新账号必须带上预置的网络 subnet、安全组、日志投递、CloudTrail、Config 记录器、GuardDuty 才能算“出生即合规”。手动做一次可以,做第二次就开始出偏差。自动化要做的是让所有账号从同一个模板生成,而不是靠人逐个点。
身份这一层指向 IAM Identity Center,以前叫 AWS SSO。它负责权限集的创建、账号与组的绑定、会话时长。这里最容易出现的问题是权限集建好了,但账号分配漏了一个,或者组名在不同 OU 里叫法不一致,最终登录阶段才发现。自动化可以把权限集、账户分配也纳入 Terraform 管理,让身份基线和账号一一对应。
策略这一层是 SCP 和 CloudFormation Guard 之类的东西。SCP 控制的是 OU 级别能做什么,它和账号内的 IAM 策略是两个独立评估层级。Landing Zone 里绝大多数“越权”或“被误伤”的事故都出在策略叠加上。自动化测试的核心工作之一,就是把 SCP 对每个 OU 的影响变成可断言的结果,而不是靠经验猜。
这三件事有个共同点:都横跨管理账号和成员账号。所以单纯写一段 Terraform 并不够,还需要一条能跨账号执行并验证结果的流水线。
2.2 Control Tower 还是自建 Landing Zone:托管方案与 DIY 的取舍
很多人在选型阶段就会卡住:用 AWS Control Tower 还是自建一套 Landing Zone?我的判断标准很简单——你的团队有没有意愿维护一套自己的 Terraform/CloudFormation 代码库。
Control Tower 是托管版,它在背后生成一堆 AWSCloudTowerBP 开头的 CloudFormation 栈,提供账号工厂、SCP 管理、日志集中和可选的防护规则。优点是门槛低,更新由 AWS 负责;缺点是定制越深,越难脱离它自己的生命周期控制,有些底层资源想改会被提示“此资源由 Control Tower 管理”。
自建 Landing Zone 则需要你全盘自己搭:用 Organizations API 建 OU、用 CloudFormation StackSet 或 Terraform 把基线推到每个账号、自己维护 SCP 和日志管道的版本。优点是全代码可控,测试逻辑可以嵌进流水线;缺点是初期成本高,组织策略的语法错误要自己扛。
现实中的混合方案也很常见:用 Control Tower 管理 OU 和账号工厂,把 SCP 和成员账号的基线交给自己的 Terraform 代码。这样 Control Tower 负责账号的“出生”,你的代码负责“成长”。下表可以直接用于选型讨论:
| 维度 | AWS Control Tower | 自建 Landing Zone | 混合方案 |
|---|---|---|---|
| 账号工厂 | 内置 Service Catalog,创建快 | 需自己封装 Organizations API | Control Tower 账号工厂 |
| 身份绑定 | 与 IAM Identity Center 集成良好 | 自己管权限集和 Account Assignment | 跟随 Control Tower,策略归自建 |
| SCP 维护 | 页面操作为主,也能 API | 全代码化,测试可嵌入 | SCP 走自建代码,OU 归 Control Tower |
| 测试支持 | 偏黑盒,部分资源无法读底层栈 | 全白盒,可自由加测试 | 混合,测试面最大 |
| 维护成本 | 低,升级由 AWS 处理 | 高,且长期不低于 Control Tower | 中,且和团队能力直接相关 |
如果是新组织、十几个账号内,Control Tower 足够。如果已经有多个组织、需要频繁给不同 OU 下发差异化策略,自建或混合是更好的落点。
2.3 自动化测试为什么不能晚点再做:三分钟就能看到漂移
Landing Zone 没有“单元测试”也能跑,但你很快会碰到一种情况:某天在管理账号里手动给一个成员账号加了条 S3 策略,两周后重新跑 Terraform,这个改动直接被 plan 成漂移,apply 时被覆盖。没有测试覆盖的 Landing Zone,本质上是一切手工操作和自动代码之间的持久拉锯。
我的习惯是给 Landing Zone 配四层测试:第一层是 Terraform 静态检查和 plan 门禁,第二层是 SCP 策略模拟,第三层是 pytest 驱动的组织级集成测试,第四层是可选的控制台 UI 冒烟。后文会按层拆开讲,但这里先给一个结论:集成测试的收益在第一次改动 SCP 时就能看到,而不是在搭建时。
注意,这也意味着测试环境不能只在生产组织里跑。比较稳妥的做法是单独建一个测试 OU 或独立组织,把同样一套 SCP 和基线先推过去,跑完 pytest 再推广到生产 OU。如果你的账号还不多,这一步的成本远低于一次全组织误配置的复盘成本。
3. 把 Landing Zone 写成代码:Terraform 目录结构与部署流水线
3.1 仓库怎么分:terraform、tests、pipelines 三块分离
我先给一份我常用的目录结构,它不复杂,但能直接作为起点:
landing-zone/ ├── terraform/ │ ├── org/ # Organizations、OU、SCP 的建模 │ ├── identity/ # IAM Identity Center 权限集与账号分配 │ ├── baseline/ # CloudFormation StackSet 基线模板 │ └── network/ # Transit Gateway、VPC 与 DNS 路由 ├── tests/ │ ├── policy/ │ │ ├── policy_cases.json # 策略回归用例 │ │ └── simulate_scp.py # SCP 批测脚本 │ └── integration/ │ └── test_landing_zone.py └── pipelines/ ├── buildspec-deploy.yml └── buildspec-test.yml这个结构里,org和identity只在管理账号跑,baseline通过 StackSet 辐射到成员账号,network一般先在一个核心账号做建模,再用资源共享或 TGW 附件扩展。tests和pipelines独立出来,是为了让测试代码和基础设施代码有不同的变更频率——你改一个 SCP 不会想重新跑一遍整个网络模块的 apply。
模块分离的原则是:每个目录尽量不要跨出“账号工厂、身份基线、策略、网络”的边界。尤其不要把成员账号的临时配置塞进 baseline 里,否则一段时间后 baseline 会变成一个没人敢动的怪物。
3.2 SSO 权限集与账号映射:一个最小可运行的 Permissionset 配置
身份这一层建议用 Terraform 管理 IAM Identity Center。最小可运行的一段配置长这样:
resource "aws_ssoadmin_permission_set" "org_readonly" { name = "org-readonly" description = "跨账号只读权限,适用于审计角色" instance_arn = aws_ssoadmin_instances.default[0].arns[0] session_duration = "PT8H" } resource "aws_ssoadmin_managed_policy_attachment" "readonly_attach" { instance_arn = aws_ssoadmin_permission_set.org_readonly.instance_arn permission_set_arn = aws_ssoadmin_permission_set.org_readonly.arn managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" } resource "aws_ssoadmin_account_assignment" "auditor_group" { instance_arn = aws_ssoadmin_permission_set.org_readonly.instance_arn permission_set_arn = aws_ssoadmin_permission_set.org_readonly.arn principal_id = aws_identitystore_group.auditors.id principal_type = "GROUP" target_id = "123456789012" target_type = "AWS_ACCOUNT" }这段代码做三件事:创建权限集、给它附加 AWS 托管策略 ReadOnlyAccess、再把审计组分配到指定账号。参数里最值得关注的是session_duration,我建议最小化到实际需要的时间,比如审计场景PT4H足够,别默认给 12 小时;instance_arn必须通过aws_ssoadmin_instances.default[0].arns[0]获取,因为这个资源是 AWS 账户级别的单例,不能直接硬编码。
所有权限集写入代码后,加账号就变成“加一个 target_id”。不要用控制台给成员账号绑身份,否则下一次terraform plan会把这个手工分配标记成漂移,然后被清理掉。
3.3 用 CodePipeline 跑 apply:从 push 到变更落地的步骤
Landing Zone 的部署流水线我一般分两段:test 段和 deploy 段。test 段负责 plan 和策略模拟,deploy 段只有通过门禁才执行 apply。一个最小 buildspec 可以这样写:
version: 0.2 phases: install: runtime-versions: python: 3.11 commands: - terraform init -input=false build: commands: - terraform validate - terraform plan -no-color -out=tfplan - python tests/policy/simulate_scp.py tests/policy/policy_cases.json post_build: commands: - terraform apply -input=false tfplan这里的关键是把策略回归脚本放在 apply 之前。terraform plan -out=tfplan生成执行计划,simulate_scp.py用当前分支里的 SCP 内容跑一遍策略模拟,全部通过后才执行 apply。注意terraform init需要确保 state 后端配置正确,Landing Zone 的 state 建议单独一个 S3 bucket,并开启 DynamoDB 锁,避免两个 PR 同时 apply 导致 state 冲突。
如果团队里已经有 Jenkins 或其他 CI,完全可以替换 CodePipeline,核心逻辑不变:plan 不加锁,apply 必须串行。Landing Zone 的 apply 和普通应用不同,它不是无状态服务,任何“乐观并发”的想法都会在 state 冲突里付出代价。
4. 从 Terraform plan 到 pytest:Landing Zone 的四层自动化测试
4.1 第一层静态检查:terraform plan、checkov 与漂移门禁
第一层测试是成本最低的,直接接在 commit 阶段。除了terraform validate,我还会跑 checkov 和 tfsec 来扫基线模板里的安全配置,比如 S3 桶是否开放公开访问、IAM 是否给了通配符*。
terraform plan -no-color -out=tfplan checkov -d terraform/ --framework terraform --quiet这个阶段解决的是“代码本身的低级错误”。比如 SCP 的 JSON 少了一个括号、权限集引用了不存在的组 ID、StackSet 模板里写错资源类型。这些问题如果等到 apply 阶段才暴露,往往伴随着半成品状态。
注意 plan 的结果要看是否包含意外的资源删除。Landing Zone 里很多资源是不能删的,比如分享出来的 VPC、集中日志桶。我的做法是在 plan 之后加一个脚本,grep- destroy并和一份“允许删除清单”做比对,多出任何一条未批准的删除就直接 fail 整个流水线。
4.2 第二层策略验证:用 simulate-principal-policy 批量测 SCP
SCP 最大的问题是它属于组织级策略,不能像 IAM 策略那样直接在控制台里模拟。最接近真实评估的方式是用 IAM Policy Simulator,把当前 SCP 里的 Deny 语句作为附加策略输入,对目标账号的 root 身份跑一次模拟:
aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::123456789012:root \ --action-names s3:PutBucketPolicy \ --resource-arns arn:aws:s3:::example-bucket/* \ --policy-input '[{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:*","Resource":"*"}]}]' \ --permissions-boundary-policy-input-list '[{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}]' \ --output json模拟结果里的Decision字段会返回allowed、explicitDeny或implicitDeny。在这里explicitDeny是预期值,代表 SCP 的 Deny 生效;allowed则要警惕,通常意味着策略条件没覆盖到该资源,SCP 没拦住目标动作。
这一段值得多说两句:--policy-input传的是你当前分支的 SCP 内容,--permissions-boundary-policy-input-list传的是成员账号的权限边界。真实环境里这两个策略会叠加,漏掉任何一个,模拟结果都会过度乐观。我见过太多“模拟通过但生产被 Deny”的案例,原因都在这里。
这一层测试适合在每次 SCP 变更时跑,而且要把常见的高危操作都列进用例,比如iam:AttachRolePolicy、s3:PutBucketAcl、organizations:LeaveOrganization、cloudtrail:StopLogging。
4.3 第三层集成回归:pytest + boto3 断言组织级基线
策略模拟验证的是“策略本身对不对”,但它验证不了“资源真的按预期配置了”。第三层测试用 pytest 直接打 API,去成员账号里检查最终状态。以下是一段可以放进tests/integration/test_landing_zone.py的用例:
import boto3 import pytest ACCOUNT_ID = "123456789012" ROLE_NAME = "OrganizationAccountAccessRole" # 按你的跨账号角色名修改 def member_session(account_id): sts = boto3.client("sts") resp = sts.assume_role( RoleArn=f"arn:aws:iam::{account_id}:role/{ROLE_NAME}", RoleSessionName="landing-zone-test", DurationSeconds=3600, ) return boto3.session.Session( aws_access_key_id=resp["Credentials"]["AccessKeyId"], aws_secret_access_key=resp["Credentials"]["SecretAccessKey"], aws_session_token=resp["Credentials"]["SessionToken"], ) @pytest.mark.integration def test_account_level_block_public_policy(): s3control = member_session(ACCOUNT_ID).client("s3control", region_name="us-east-1") config = s3control.get_public_access_block(AccountId=ACCOUNT_ID)[ "PublicAccessBlockConfiguration" ] assert config["BlockPublicPolicy"] is True这段测试做的事情是:跳到成员账号,用s3control.get_public_access_block检查账号级 S3 Block Public Access 的BlockPublicPolicy是否为 True。如果新账号出生时基线漏配了这一项,测试会在流水线里直接给你红牌。
值得注意的细节是DurationSeconds=3600。跨账号测试如果会话时间设太短,碰到脚本里有多个用例时很容易中途过期,然后报一屏 AccessDenied,让人误以为是权限配置问题。另外,测试账号里建议选一个业务影响最小的账号,比如日志账号或专用测试账号跑来跑去,别拿生产核心账号当 pytest 的靶子。
4.4 第四层 UI 冒烟:Playwright 能做什么、不该做什么
到了 UI 这一层,我一般会先泼盆冷水。用 Playwright 这类浏览器自动化工具去登 AWS 控制台,最大问题是登录链路里很可能有 MFA 和验证码,任何一次 UI 变更都会让脚本碎一地。它适合做的是“登录后某页面能看到预期选项”这类极少量冒烟,不适合充当组织级回归的主力。
如果团队确实有 UI 验证需求,我建议用 Playwright 只覆盖一条路径:从 IAM Identity Center 登录入口出发,到达一个成员账号的控制台首页,断言页面标题包含账号别名。这个用例的价值在于发现 SSO 配置和账号分配的整体故障,而不是去验证某个按钮的文字。它每次跑完还需要清理浏览器 profile,否则下次登录会被 AWS 的安全机制拦住。
这里给一个直接的操作建议:把 pytest 和接口自动化作为回测主体,把 Playwright 作为可选阶段放到夜间任务里,不要让它卡代码合入。UI 层的脆弱性不是代码质量问题,而是 AWS 控制台本身迭代太快,你需要把测试成本控制在合理范围内。
5. Landing Zone 自动化与测试避坑:5 个高频故障的定位思路
5.1 新账号登录报错的 30 分钟等待期:它不是一个 bug
现象:新账号通过账号工厂创建完成后,立刻用 IAM Identity Center 登录,控制台提示“账号未就绪”或直接无权限。
原因:Identity Center 的账号分配是异步的,Permission Set 需要复制到目标账号,这个过程通常要几分钟到半小时。不是你的 Terraform 写错了,而是传播链路有延迟。
解决:在集成测试脚本里对登录前检查做主动轮询,比如每 30 秒调用一次list_account_assignments,确认该账号的权限集状态变成SUCCEEDED再继续。轮询上限建议设为 30 分钟,超时再报失败,而不是一上来就断言失败。
5.2 StackSet 部署成功但账号没基线:组织层级没理解对
现象:CloudFormation StackSet 显示部署成功,但某个新加入的账号里并没有对应的资源。
原因:StackSet 直接部署到 Organizational Unit 时,只对部署那一刻属于该 OU 的账号生效。之后新加入 OU 的账号不会自动继承,除非你开启自动部署,并确保 StackSet 的部署目标包含整个组织或特定 OU。
解决:账号工厂创建账号后,需要两步操作——把账号移入目标 OU,再触发一次 StackSet 部署。流水线里可以加一个步骤,对新账号主动调用aws cloudformation create-stack-instances或者直接重跑一次 StackSet 更新。
5.3 SCP 模拟通过但真实验证失败:权限边界被低估了
现象:策略模拟脚本返回allowed,你觉得 SCP 没拦住某操作,但到生产环境实测时确实被 Deny 了。
原因:simulate-principal-policy不会自动加载账号里的权限边界和会话策略。只把 SCP 内容传给--policy-input,等于在评估一个没有权限边界的理想主体,结果自然偏乐观。真实的授权是 Identity Policy、Permission Boundary、SCP 三层叠加后的结果。
解决:把成员账号的权限边界也通过--permissions-boundary-policy-input-list传入,模拟时再额外附上可能的 Session Policy。更重要的是,对高危策略改动,必须选一个测试账号用真实角色做一次 API 调用验证,不能只信模拟器的输出。
5.4 assume-role 凭据过期:测试脚本里最容易被忽略的点
现象:pytest 集成测试前面几条用例都通过,跑到中间某个用例突然报An error occurred (AccessDenied) when calling the AssumeRole operation。
原因:跨账号测试通常用一组临时凭据去 assume 成员账号的角色。如果整个测试会话超过 1 小时,而DurationSeconds只设了 3600 秒,后半段注定会失败。这里的 AccessDenied 不是权限问题,是换令牌的问题。
解决:要么在框架层做一个自动刷新凭据的 fixture,每次调用前检查令牌剩余时间;要么把测试拆成多个小批次,每批控制在 20 分钟内。还有一条容易忽视的细节:目标账号角色的信任策略里必须显式允许管理账号的主账号 ID 来 assume,否则报错信息和过期看起来一模一样。
5.5 一键 apply 到全组织的风险:把销毁和重建隔开
现象:某次重构想调整 OU 层级,Terraform plan 显示要删除一个旧 OU 的 SCP 附件。apply 之后,该 OU 下所有账号的策略全部被重置,业务侧立刻报障。
原因:Landing Zone 的 Terraform 代码里,一个资源的删除成本常常被低估。删除一段策略代码可能意味着云上几百个账号同时失去某层保护,而 plan 阶段只显示一行- destroy。
解决:在生产流水线里严格限制terraform destroy的权限,CI 角色只给terraform apply所需的最小权限,销毁操作必须走人审。对核心策略和日志桶资源在 Terraform 里加上lifecycle { prevent_destroy = true }。再有就是 apply 前强制比对删除清单,任何导致成员账号资源配置失效的变更都要单独开 PR 再过一轮。
6. 把 SCP 策略模拟做成回归用例:一套可持续维护的批测脚本
最后一层是我个人最推荐做的事:把策略模拟变成一份用例文件加一个脚本,纳入每次 PR 的门禁。它成本低,价值却最直接。下面是一个能直接放进仓库的 Python 脚本骨架:
#!/usr/bin/env python3 """SCP 策略回归脚本:每个 PR 必须通过""" import json import sys import boto3 def run_case(case, scp_doc, boundary_doc): client = boto3.client("iam", region_name="us-east-1") resp = client.simulate_principal_policy( PolicySourceArn=case["principal_arn"], ActionNames=[case["action"]], ResourceArns=[case["resource"]], PolicyInputList=[scp_doc], PermissionsBoundaryPolicyInputList=[boundary_doc], ) decision = resp["EvaluationResults"][0]["Decision"] return decision == case["expected"], decision def main(cases_path): with open(cases_path) as f: cases = json.load(f) scp_doc = { "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Action": "s3:PutBucketPolicy", "Resource": "*", "Condition": { "Bool": {"aws:SecureTransport": "false"} }, } ], } boundary_doc = { "Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}] } failed = 0 for case in cases: ok, decision = run_case(case, scp_doc, boundary_doc) status = "PASS" if ok else "FAIL" print(f"{status} {case['id']}: {case['action']} -> {decision}") if not ok: failed += 1 return 1 if failed else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "policy_cases.json"))配套的policy_cases.json大概长这样:
[ { "id": "deny-s3-put-policy", "principal_arn": "arn:aws:iam::123456789012:root", "action": "s3:PutBucketPolicy", "resource": "arn:aws:s3:::example-bucket/*", "expected": "explicitDeny" }, { "id": "allow-list-bucket", "principal_arn": "arn:aws:iam::123456789012:root", "action": "s3:ListBucket", "resource": "arn:aws:s3:::example-bucket", "expected": "allowed" } ]这段脚本的关键在于把“预期结果”和策略内容分离。你改 SCP 时,不用改 Python 代码,只动用例文件即可。Decision为explicitDeny说明策略显式拒绝了该动作,allowed说明放行,implicitDeny则要特别注意——它通常意味着策略组合有缺口,某层权限没有显式 Allow。
我现在的工作习惯是:任何 SCP 或权限集改动,第一件事不是写代码,而是先把这个用例文件里对应的高危操作加一行,再跑一遍批测脚本,确认新策略的预期结果。这套脚本不依赖任何 UI 或控制台,CI 里跑一次只要十几秒,但能挡住绝大多数策略误配。整个过程坚持了大半年,确实帮我们少开了好多次复盘会。希望这片笔记能帮你把自己的 Landing Zone 从“搭完能通”推到“改了不炸”的状态。
本文还有配套的精品资源,点击获取