AWS Lesson 81 of 123

AWS Landing Zone: AWS Control Tower — the Landing Zone, Account Factory, the Controls Library, and Customization with CfCT and AFT

In a nutshell

Imagine your company opens a new branch office every week. Doing it by hand means each new office might forget to arm the alarm, mis-wire the badge reader, or point its CCTV at a recorder the local staff can quietly switch off. AWS Control Tower is the building-management company for your AWS estate. Every time a team asks for a new AWS account (an “office”), Control Tower hands over a suite that already has the alarm armed (guardrails applied), the badge reader wired to a central security desk (IAM Identity Center + the Audit account), and CCTV streaming to a tamper-proof recording room the tenant can’t reach (CloudTrail + AWS Config flowing to a locked-down Log Archive account) — plus a lease that spells out what the tenant may and may not do (Service Control Policies). You set the rules once; Control Tower enforces them on every account, old and new.

The mental model: Control Tower does not replace AWS Organizations — it drives it. Underneath, everything Control Tower does is plain AWS you already know — Organizations OUs, SCPs, CloudFormation StackSets, Config rules, an organization CloudTrail. Control Tower is the orchestration layer that wires those primitives into a “well-architected multi-account landing zone” and then keeps watch, telling you when the setup has drifted from known-good and offering to repair it. A landing zone is just the name for that finished, governed foundation: the management account plus the shared Log Archive and Audit accounts, a Security OU and a Sandbox OU, centralized logging, and a baseline of controls.

Once the landing zone exists, the day-to-day loop is: Account Factory stamps out new governed accounts on demand; the controls library enforces policy on the OUs; and when the managed baseline isn’t enough, CfCT and AFT let you extend it as version-controlled code. That is the whole lesson in one sentence — stand up the landing zone, vend accounts through the Factory, govern with controls, and customize with CfCT/AFT.

Level: Intermediate → Advanced · Time: ~69 min read

Prerequisites — what to know first. You should be comfortable with the raw multi-account chassis from Part 1: AWS Organizations, OUs, SCPs and consolidated billing. Helpful but not required: a working idea of CloudFormation StackSets, AWS Config, CloudTrail, IAM roles, and (for the customization frameworks) Terraform. If “management account”, “OU”, and “SCP” are new words, read Part 1 first — Control Tower will make far more sense once you can see the Organizations primitives it is automating.

After this lesson you will be able to:

Where this fits

Part 1 of AWS Landing Zone & Control Tower established the raw substrate: AWS Organizations, the management (payer) account, organizational units (OUs), Service Control Policies (SCPs), and consolidated billing — the multi-account chassis you could assemble by hand. This part is about the managed service that orchestrates that chassis into a governed, opinionated foundation so you don’t have to wire it yourself: AWS Control Tower. Control Tower is not a replacement for Organizations — it sits on top of it, and everything it does (creating the Security OU, deploying centralized logging, applying guardrails, vending accounts) ultimately manifests as Organizations, CloudFormation StackSets, AWS Config, and SCP/RCP primitives you can still see and reason about. The four sub-components below are the load-bearing pieces of a Control Tower deployment: the landing zone it stands up, the Account Factory that mass-produces governed accounts, the controls library that keeps them compliant, and the two customization frameworks — Customizations for Control Tower (CfCT) and Account Factory for Terraform (AFT) — that extend the managed baseline to your enterprise’s real requirements. Get these right and you have a paved road; get them wrong and Control Tower becomes a box you fight against rather than build on.

AWS Landing Zone & Control Tower — animated overview

The Control Tower landing zone

What it is. The landing zone is the well-architected, multi-account environment that Control Tower provisions and manages on your behalf when you “set up” the service from the management account. It is a concrete, named set of artifacts, not an abstraction: a management account (where Control Tower runs), a Log Archive account and an Audit account (the two “shared accounts” created automatically inside a Security OU), a default Sandbox OU, an organization-wide CloudTrail trail, centralized AWS Config with an aggregator in the Audit account, an IAM Identity Center (formerly AWS SSO) directory for human access, and a landing-zone version that Control Tower tracks so it can tell you when the baseline drifts or needs updating. Under the hood, Control Tower realizes all of this through CloudFormation StackSets deployed from the management account into member accounts, plus service-managed Organizations features.

Why it matters. The landing zone is the difference between “we have an AWS Organization” and “we have a governed AWS foundation.” Three things make it valuable. First, centralized, tamper-resistant audit: every account’s CloudTrail and Config data flows to a dedicated Log Archive account that workload teams can’t reach, satisfying the separation-of-duties expectation that auditors look for. Second, a versioned baseline: because Control Tower knows what “correct” looks like, it can detect drift (someone deleted a guardrail’s Config rule, moved an account out of an OU, or hand-edited the organization trail) and offer repair / re-register / landing-zone update actions to bring things back to known-good. Third, a home region and a governed-region set: you pick a home Region at setup (effectively permanent — changing it means rebuilding) and an explicit list of Regions Control Tower governs, which bounds where guardrails apply and where new account baselines deploy.

How to do it well. A few decisions made at setup are expensive to reverse, so treat day-0 as architecture, not clicking a wizard:

Artifacts, decisions, and AWS tooling.

Artifact / decision What it captures AWS mechanism
Management account Where Control Tower, Organizations, billing run AWS Organizations root / payer
Log Archive account Immutable, central store for CloudTrail + Config logs StackSet-deployed S3 buckets, org trail
Audit (Security) account Cross-account read/security tooling, Config aggregator StackSet-deployed roles + SNS, Config aggregator
Security OU Houses the two shared accounts under strong guardrails Organizations OU + SCPs
Home Region (permanent) Anchor Region for the landing zone Control Tower setting
Governed Regions list Where guardrails + baselines apply Control Tower setting + StackSets
Landing-zone version The known-good baseline + drift target Control Tower landing-zone version
Identity directory Human access model IAM Identity Center (+ external IdP)

The decision that comes out of this sub-component is a deployed, versioned landing zone with separation-of-duties accounts, a permanent home Region, an explicit governed-Region set, and an identity model — the foundation everything else attaches to.

Worked example: what “set up landing zone” actually deploys

The wizard hides a lot, so let’s make it concrete. When you click Set up landing zone in the management account (or call CreateLandingZone), Control Tower does the following, in order, and you can watch every step land as real resources:

  1. Creates the shared accounts. It calls Organizations CreateAccount twice — once for Log Archive, once for Audit — and places both under a new Security OU. It also creates a Sandbox OU (empty). Your existing management account is left where it is (it is the org root’s payer).
  2. Installs the trust roles. In the management account it creates AWSControlTowerAdmin, AWSControlTowerStackSetRole, AWSControlTowerCloudTrailRole, and AWSControlTowerConfigAggregatorRoleForOrganizations. In every governed member account it creates AWSControlTowerExecution — the cross-account role that lets Control Tower’s StackSets deploy the baseline. Removing that role in a member account is one of the fastest ways to break enrollment, so treat it as sacred.
  3. Turns on org-wide logging. It creates an organization CloudTrail trail (aws-controltower-BaselineCloudTrail) whose logs land in an S3 bucket in Log Archive (aws-controltower-logs-<logarchive-account-id>-<home-region>), with a separate access-logs bucket. It enables AWS Config in each governed account/Region and wires a Config aggregator in the Audit account so one pane shows every account’s compliance.
  4. Applies the baseline via StackSets. All of the per-account plumbing (Config recorder + delivery channel, the notification-forwarding roles like aws-controltower-ForwardSnsNotificationRole, the baseline SCPs on the OUs) is deployed as CloudFormation StackSets from the management account. This is why you’ll see AWSControlTower* stack instances appear in each account.
  5. Stands up identity. It enables IAM Identity Center in the home Region and pre-creates permission sets (e.g., AWSAdministratorAccess, AWSReadOnlyAccess) plus directory groups for the Audit and Log Archive accounts.
  6. Records a landing-zone version. The whole thing is stamped with a landing-zone version (e.g. 3.3). That version is the “known-good” fingerprint drift is measured against and updates are applied to.

You can inspect the result programmatically. These are the current Control Tower landing-zone APIs (GA); output is representative:

# List and describe the landing zone (there is exactly one per management account)
aws controltower list-landing-zones
# {"landingZones":[{"arn":"arn:aws:controltower:ap-south-1:123456789012:landingzone/1A2B3C4D5E6F"}]}

aws controltower get-landing-zone \
  --landing-zone-identifier arn:aws:controltower:ap-south-1:123456789012:landingzone/1A2B3C4D5E6F \
  --query 'landingZone.{version:version,status:status,drift:driftStatus.status}'
# { "version": "3.3", "status": "ACTIVE", "drift": "IN_SYNC" }

The landing-zone manifest. CreateLandingZone/UpdateLandingZone take a JSON manifest that captures the day-0 decisions the wizard asks for. Seeing it as data makes the “expensive to reverse” choices obvious — the governed-Region set, the logging retention, and the centralized-logging account are all right here:

{
  "governedRegions": ["ap-south-1", "ap-southeast-1"],
  "organizationStructure": {
    "security": { "name": "Security" },
    "sandbox":  { "name": "Sandbox" }
  },
  "centralizedLogging": {
    "accountId": "111111111111",
    "configurations": {
      "loggingBucket":      { "retentionDays": 365 },
      "accessLoggingBucket":{ "retentionDays": 3650 }
    },
    "enabled": true
  },
  "accessManagement": { "enabled": true }
}

Note there is no homeRegion field in the manifest — the home Region is fixed when Control Tower is first set up and is not something UpdateLandingZone can change. That is the single most permanent decision on the page: to move it you decommission and rebuild. governedRegions, by contrast, can be edited later (an update operation), which is why the advice is “govern only what you use, but govern all of it, and region-deny the rest.”

The region-deny control, made real. The “restrict Regions” preventive control isn’t magic — it installs an SCP on the OUs that denies any action whose aws:RequestedRegion is outside your governed set, while carving out the global services that only work in us-east-1. A trimmed, representative version:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyOutsideApprovedRegions",
    "Effect": "Deny",
    "NotAction": [
      "iam:*", "organizations:*", "route53:*", "cloudfront:*",
      "sts:*", "support:*", "globalaccelerator:*", "waf:*",
      "controltower:*", "account:*", "budgets:*"
    ],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": { "aws:RequestedRegion": ["ap-south-1", "ap-southeast-1"] }
    }
  }]
}

The NotAction carve-out matters: forget to exempt IAM/STS/Organizations/CloudFront/Route 53 and you’ll lock the org out of its own global control plane. This is why you enable the managed region-deny control rather than hand-writing the SCP — Control Tower keeps that exemption list current for you.

Account Factory

What it is. Account Factory is Control Tower’s account-provisioning capability — the mechanism that creates new member accounts already enrolled in the landing zone, with the baseline (CloudTrail enrollment, Config recorder, the OU’s guardrails, IAM Identity Center access, and an AWSControlTowerExecution cross-account role) applied automatically at birth. It is surfaced as an AWS Service Catalog product (the “AWS Control Tower Account Factory” product) so that account creation is a governed, repeatable launch rather than an ad-hoc CreateAccount call. Account Factory also covers enrolling existing accounts (bringing an already-created Organizations account under Control Tower governance) and account lifecycle actions like updating or un-enrolling. The settings you configure on Account Factory — default VPC parameters, the allowed list of OUs accounts can be placed in, and (optionally) account-level network defaults — define the “shape” every vended account takes.

Why it matters. The whole point of a landing zone is to scale governed accounts to the hundreds without governance degrading. Account Factory is what makes the account the unit of isolation cheap to issue and impossible to issue wrong: a team requesting a new account gets one that is already logging to Log Archive, already under the correct OU’s SCPs and controls, and already wired into your access model — instead of a blank account that someone forgets to onboard, which becomes the un-governed gap an attacker finds. It also turns provisioning into something you can delegate and automate: because it’s a Service Catalog product, you can grant a platform team (or a pipeline) the ability to vend accounts without handing them management-account admin.

How to do it well. Account Factory’s built-in console flow is fine for a handful of accounts but does not scale to enterprise demand or auditability — that is exactly why the two customization frameworks below exist. The discipline:

Provisioning-path comparison.

Path Best for Trade-off
Account Factory console (Service Catalog product) First few accounts, ad-hoc, demos Manual, no native approval/audit, doesn’t scale
Enroll existing account Brownfield accounts predating Control Tower Requires remediation to baseline post-enroll
AFT (Account Factory for Terraform) Enterprise scale, GitOps, per-account customization Stands up its own pipeline + state to operate
CfCT (Customizations for Control Tower) Org-wide baseline resources alongside vending CloudFormation/StackSet-centric, less per-account nuance

The artifact here is a defined account-vending standard — naming/email/tagging schema, the OU allow-list, an explicit “no default VPC” (or controlled-VPC) decision, and the chosen automation path — that every new account flows through.

Worked example: vending, blueprints (AFC), and enrolling brownfield accounts

Because Account Factory is just an AWS Service Catalog product, you can vend an account with a single provision-product call — the same call a pipeline would make. The parameters below are the actual Account Factory product parameters; this is the console form expressed as code (output representative):

aws servicecatalog provision-product \
  --product-name "AWS Control Tower Account Factory" \
  --provisioning-artifact-name "AWS Control Tower Account Factory" \
  --provisioned-product-name "prod-payments" \
  --provisioning-parameters \
    Key=AccountEmail,Value=aws+prod-payments@aurelia.example \
    Key=AccountName,Value=prod-payments \
    Key=ManagedOrganizationalUnit,Value="Workloads/Prod-Regulated (ou-abcd-11112222)" \
    Key=SSOUserEmail,Value=platform-team@aurelia.example \
    Key=SSOUserFirstName,Value=Platform \
    Key=SSOUserLastName,Value=Team
# Provisioning takes ~25–35 min end to end while StackSets deploy the baseline.

Read that parameter list closely, because it is the account-vending contract: a unique root email, an account name, the target OU (which decides every guardrail the account inherits), and the initial Identity Center user. Get the OU wrong and the account is born under the wrong policy posture — which is exactly why the earlier advice is to constrain the OU allow-list.

Account Factory Customization (AFC) — blueprints. The bare vended account is governed but empty. AFC lets you attach a blueprint — a CloudFormation template stored in a designated blueprint hub account (or a partner-provided blueprint) — that Control Tower deploys into the account as part of provisioning. So “create account” and “lay down the team’s standard VPC, logging subscription, and IAM roles” become one atomic, governed operation. AFC blueprints are the AWS-native, no-Terraform way to customize at birth; they’re ideal when you want a single opinionated shape per account and don’t need per-account Terraform logic. (When you do need rich per-account logic, that’s AFT’s job — next section.) You can also update or remove a blueprint from an account later; removing it does not delete what it deployed, it just stops managing it.

Enrolling a brownfield account. Most real orgs already have accounts that predate Control Tower. You do not recreate them — you enroll them:

  1. In the target account, create the AWSControlTowerExecution role (a CloudFormation one-liner) trusting the management account — this is the handshake that lets StackSets in.
  2. Ensure the account isn’t already running conflicting Config recorders/delivery channels in governed Regions (a classic enrollment blocker — Control Tower wants to own Config).
  3. In Account Factory choose Enroll account (or move it into a registered OU). Control Tower deploys the baseline StackSets, enrolls it in the org CloudTrail, and applies the OU’s controls.
  4. Register the OU if you want every current and future account in that OU governed — registration applies the AWSControlTowerBaseline to the whole OU, so accounts you drop in later are baselined automatically.

The network decision, concretely. Account Factory’s network settings let you auto-create a VPC in each vended account: you pick a CIDR, the number of private/public subnets, and which Regions get one — or you set max subnets to 0 / choose no Regions to create nothing. In a Transit Gateway / centralized-egress estate you almost always choose “no VPC here” and inject networking through AFT or CfCT, so accounts never ship with an unmanaged default VPC someone has to remember to delete.

Vending surface You provide Customization at birth Best when
Service Catalog product (console/CLI) The 6 product parameters None (bare baseline) One-off / break-glass
AFC blueprint Product params + a blueprint template One CloudFormation shape per account AWS-native, no Terraform, uniform accounts
AFT An account-request entry in Git Global + per-account Terraform Scale, GitOps, rich per-account logic

The controls library

What it is. The controls library (Control Tower calls individual controls controls, and historically guardrails) is the catalog of pre-built, named governance rules you enable against OUs to enforce policy. Every control has a guidance level — Mandatory (always on, can’t disable), Strongly Recommended, or Elective — and, more importantly, a behavior that tells you how it enforces:

Controls are organized by category and aligned to frameworks; Control Tower also offers Comprehensive controls management and control objectives (mapped to standards like CIS, PCI DSS, NIST) so you can reason about coverage in terms of objectives, not just individual rules.

Why it matters. This library is the reason to use Control Tower over rolling your own Organizations setup: it gives you a curated, AWS-maintained set of guardrails that you turn on against an OU and inherit across every account beneath, with a single console (or API) view of compliance posture. The three behaviors map cleanly onto a defense-in-depth strategy: preventive for the things that must never happen (deny the action), proactive for catching misconfiguration at deploy time before resources exist, and detective for continuous assurance and the long tail you can’t pre-empt. Because controls attach at the OU level, you express policy by governance intent (this OU is for regulated prod; this one is sandbox) rather than per-account toil.

How to do it well.

Control-behavior comparison.

Behavior Implemented with When it acts Result Typical use
Preventive SCP / RCP Before the API call Action denied (Enforced) “Never disable CloudTrail”, region restriction
Proactive CloudFormation Hook Before resource is provisioned Deployment blocked “S3 bucket must be encrypted” at deploy time
Detective AWS Config rule After resource exists Compliant / Non-compliant flag “EBS volumes are encrypted” continuous check

The artifact is a guardrail catalog: the enabled controls per OU, their behavior and guidance level, the objective/framework each maps to, and a list of any custom controls you authored — ideally version-controlled rather than living only in the console.

Worked example: enabling a control by ID and reading its status

Controls have identifiers, and enabling one is a single API call against an OU ARN — not a per-account chore. The friendly IDs you see in the console fall into readable families:

Family Example ID Behavior Backed by
AWS-GR_* (legacy guardrails) AWS-GR_ENCRYPTED_VOLUMES, AWS-GR_REGION_DENY Preventive or detective SCP or Config rule
CT.<SERVICE>.PR.<n> (proactive) CT.S3.PR.1, CT.CLOUDFORMATION.PR.1 Proactive CloudFormation Hook
SH.<SERVICE>.<n> (detective) SH.S3.1, SH.EC2.2 Detective Security Hub / Config
CT.MULTISERVICE.PV.<n> region-deny, root-user restrictions Preventive SCP / RCP

Each friendly ID resolves to a full ARN. AWS-managed controls use an empty account segment (::), and the newer library also exposes Control Catalog ARNs (arn:aws:controlcatalog:::control/…). Enabling one against an OU:

# Turn on "EBS volumes must be encrypted" (detective) for the regulated-prod OU
aws controltower enable-control \
  --control-identifier arn:aws:controltower:ap-south-1::control/AWS-GR_ENCRYPTED_VOLUMES \
  --target-identifier  arn:aws:organizations::123456789012:ou/o-exampleorgid/ou-abcd-11112222
# { "operationIdentifier": "5f8d1e2a-1234-4c56-9abc-de0123456789" }

# Enabling is asynchronous — poll the operation
aws controltower get-control-operation \
  --operation-identifier 5f8d1e2a-1234-4c56-9abc-de0123456789 \
  --query 'controlOperation.status'
# "SUCCEEDED"

# What is enabled on this OU right now?
aws controltower list-enabled-controls \
  --target-identifier arn:aws:organizations::123456789012:ou/o-exampleorgid/ou-abcd-11112222 \
  --query 'enabledControls[].controlIdentifier'

Two things trip people up here. First, enable-control targets an OU, and every account underneath inherits it — you never enable a control per account, you express policy by governance intent at the OU. Second, the operation is asynchronous and eventually consistent: enable-control returns an operationIdentifier, and the control isn’t live until get-control-operation reads SUCCEEDED. Scripting a bulk enablement without polling is the classic way to get half-applied controls and a confusing drift report.

Mapping controls to objectives (for auditors). Don’t reason about individual rules — reason about objectives. The AWS Control Catalog API lets you enumerate controls, common controls, and the objectives/domains they satisfy, which is how you produce the “control X satisfies PCI requirement Y” evidence auditors want:

aws controlcatalog list-objectives          # e.g. "Encrypt data at rest", "Limit network access"
aws controlcatalog list-controls            # the full library, with governedResources + behavior

When the library stops, author your own. Anything AWS doesn’t ship, you add in the same three behaviors and deliver through CfCT so it’s versioned and org-wide:

The rule of thumb: put “must never happen” in a preventive SCP/RCP, “must be shaped right at deploy time” in a proactive Hook, and “prove it stays right” in a detective Config rule. Most serious requirements get all three — deny the obvious API, block the misconfigured deploy, and continuously attest the rest.

Customization with Customizations for Control Tower (CfCT) and Account Factory for Terraform (AFT)

What they are. The managed baseline gets you a governed foundation, but no two enterprises’ requirements are identical. AWS ships two officially-supported customization frameworks that extend Control Tower without breaking its management of the landing zone:

Why it matters. Without a customization framework, you either (a) under-customize — every team manually configures networking, logging exporters, IAM roles, and tooling in each new account, which is exactly the inconsistency the landing zone was meant to kill — or (b) over-customize the management account with bespoke scripts that drift and become un-ownable. CfCT and AFT are the supported, repeatable way to say “a governed account isn’t done until it also has our standard VPC/TGW attachment, our centralized-logging subscription, our break-glass roles, our security agents, and our baseline budgets.” They convert the landing zone from a static setup into a product with versioned, GitOps-driven configuration — which is the difference between a landing zone that ages into drift and one that improves release over release.

How to do it well.

CfCT vs. AFT comparison.

Dimension CfCT (Customizations for Control Tower) AFT (Account Factory for Terraform)
Primary purpose Apply CloudFormation resources + SCPs org-/OU-wide Vend accounts via GitOps + per-account Terraform
IaC engine CloudFormation StackSets + SCP JSON Terraform / OpenTofu / Terraform Cloud/Enterprise
Trigger Control Tower lifecycle events; pipeline run Account-request merge in Git; lifecycle events
Scope of customization Targeted OUs / accounts (broad baseline) Per-account (global + account stages)
Source / pipeline CodeCommit/S3 + CodePipeline + StackSets Git repos + AFT pipeline + dedicated AFT account
Best when CloudFormation-centric, org-wide baselines Terraform-centric, scale vending, rich per-account config
Operational footprint Lighter (pipeline + StackSets) Heavier (own account, state, pipelines to operate)

The artifacts are a customization repository (CfCT manifest.yaml + templates + SCPs, and/or AFT account-request and global/account customization repos), the pipelines that deploy them, and a documented decision on which framework owns which concern.

Worked example: a CfCT manifest and an AFT account request

Both frameworks are “governance as code,” so the fastest way to understand them is to read their input files.

CfCT — manifest.yaml (v2). CfCT reads a single manifest.yaml plus your CloudFormation templates and SCP JSON, and its CodePipeline fans them out as StackSets/SCPs to the OUs you name. Here one resource is a CloudFormation baseline (VPC flow logs) applied to two prod OUs, and another is a preventive SCP applied to the whole Workloads subtree:

region: ap-south-1
version: 2021-03-15
resources:
  - name: baseline-vpc-flow-logs
    resource_file: templates/vpc-flow-logs.yaml
    parameters:
      - parameter_key: RetentionInDays
        parameter_value: "365"
    deploy_method: stack_set
    deployment_targets:
      organizational_units:
        - Workloads/Prod-Regulated
        - Workloads/Prod-General
    regions:
      - ap-south-1
      - ap-southeast-1
  - name: deny-leave-organization
    resource_file: policies/deny-leave-org.json
    deploy_method: scp
    deployment_targets:
      organizational_units:
        - Workloads

The shape to internalize: every resource declares a deploy_method (stack_set for CloudFormation, scp for a Service Control Policy) and deployment_targets (OUs and/or specific accounts). Because CfCT re-runs on Control Tower lifecycle events, a new account enrolled into Workloads/Prod-Regulated tomorrow gets baseline-vpc-flow-logs automatically — no human in the loop. The repo is small and predictable:

custom-control-tower-configuration/
├── manifest.yaml
├── templates/
│   └── vpc-flow-logs.yaml
└── policies/
    └── deny-leave-org.json

CfCT reads that repo from CodeCommit or an S3 bucket. Since AWS CodeCommit is closed to new customers, greenfield deployments typically point CfCT at the S3 source (upload a zipped configuration), which also makes it trivial to gate the upload behind a PR-reviewed CI job.

AFT — the four repos and an account request. AFT turns vending into a Terraform + GitOps pipeline running from a dedicated AFT-management account. It uses four Git repos, each with a clear job:

AFT repo Owns Runs when
aft-account-request The list of accounts to vend You merge/add an account entry
aft-global-customizations Terraform applied to every AFT account Every vend + on demand
aft-account-customizations Per-account Terraform (named profiles) The account names a customization
aft-account-provisioning-customizations Extra steps during provisioning (Step Functions) Every vend

Requesting an account is a Terraform module block in aft-account-request — merge it and AFT provisions the account through Control Tower’s Account Factory, then runs global + account customizations:

module "prod_payments" {
  source = "./modules/aft-account-request"

  control_tower_parameters = {
    AccountEmail              = "aws+prod-payments@aurelia.example"
    AccountName               = "prod-payments"
    ManagedOrganizationalUnit = "Workloads/Prod-Regulated"
    SSOUserEmail              = "platform-team@aurelia.example"
    SSOUserFirstName          = "Platform"
    SSOUserLastName           = "Team"
  }

  account_tags = {
    "cost-center"         = "payments"
    "owner"               = "payments-team"
    "environment"         = "prod"
    "data-classification" = "pci"
  }

  change_management_parameters = {
    change_requested_by = "payments-team"
    change_reason       = "New PCI-scoped production account"
  }

  # Which per-account profile in aft-account-customizations/ to apply
  account_customizations_name = "prod-regulated"
}

The account_customizations_name points at a folder in aft-account-customizations/prod-regulated/ that contains a terraform/ directory (your resources) and an api_helpers/ directory (pre-api-helpers.sh / post-api-helpers.sh for anything Terraform can’t express). AFT itself is deployed with the AWS terraform-aws-control-tower-account-factory module and can source repos from CodeCommit, GitHub, GitHub Enterprise, GitLab, or Bitbucket and use Terraform, OpenTofu, or Terraform Cloud/Enterprise as the engine. For the full AFT pipeline internals — the DynamoDB request table, the Step Functions state machine, and how customizations are triggered — see Account Factory for Terraform in depth.

Going deeper

Everything above is enough to run Control Tower. This section is for the reader who has to operate it at scale, pass an audit, and debug it when it misbehaves.

Baselines and OU registration — the mechanism behind “governed”

“Enrolling an account” and “registering an OU” are not vague verbs; they apply named baselines through the Control Tower baselines API. The important ones:

Baseline Applied to What it establishes
AWSControlTowerBaseline Every governed member account Config recorder, baseline roles, CloudTrail enrollment
IdentityCenterBaseline The org / registered OUs Identity Center wiring and permission-set plumbing
AuditBaseline The Audit account Cross-account security-read roles, Config aggregator
LogArchiveBaseline The Log Archive account Central logging buckets and policies
BackupBaseline (opt-in) Governed accounts AWS Backup vaults/plans when the Backup feature is on

Registering an OU applies AWSControlTowerBaseline to it, which is why future accounts dropped into a registered OU are governed automatically. The API mirrors the controls API — enable-baseline, list-enabled-baselines, update-enabled-baseline, reset-enabled-baseline — and each enabled baseline carries a version tied to the landing-zone version. After a landing-zone update you often must re-register (reset) OUs so their baseline version catches up; skipping that step is the usual reason an “updated” landing zone still reports OUs as behind.

Drift is a taxonomy, and it never self-heals

Control Tower continuously compares reality to the recorded baseline. Drift is not one thing — knowing the type tells you the repair:

The operational point: drift is detected, not auto-corrected. Control Tower shows you the drift and offers Repair / Re-register / Update, but a human (or your pipeline) must trigger it. Teams that assume Control Tower “fixes itself” discover months of accumulated drift at audit time.

Delegated administration keeps the management account clean

The management account is the org’s highest-value target and can’t have most controls applied to it, so you want to operate security tooling from elsewhere. Control Tower supports registering the Audit account as the delegated administrator for AWS Config, Security Hub, GuardDuty, IAM Access Analyzer, and Macie. Security engineers then work from Audit — never the payer — and the management account stays reserved for Control Tower, Organizations, and billing. This is the identity/security posture the whole design is optimizing for: separation of duties enforced by account boundaries, not IAM policies alone.

Scale, quotas, and the levers that bite

Control Tower governs orgs of thousands of accounts, but several limits shape the design:

Cost: AWS Config is usually the bill

The recurring cost of a landing zone is dominated by AWS Config (per configuration item recorded + per rule evaluation), followed by CloudTrail data events, S3 storage in Log Archive, and Security Hub findings — plus whatever the AFT/CfCT pipelines run. Levers, in order of impact: region-restrict aggressively; scope the Config recorder to the resource types you actually govern (or use periodic instead of continuous recording where allowed); tune Log Archive retention (the manifest’s retentionDays); and don’t enable hundreds of detective controls everywhere — layer electives by OU. A landing zone that governs 8 unused Regions “just in case” can cost several multiples of one that governs 2.

Failure modes worth pre-empting

Control Tower vs. Landing Zone Accelerator vs. custom

Control Tower is the right default, but not the only option:

Approach Use when Cost of ownership
AWS Control Tower You want a managed, opinionated, GA landing zone fast; standard governance needs Lowest — AWS operates the orchestration; you operate customizations
Landing Zone Accelerator (LZA) on AWS Highly-regulated / complex needs — GovCloud, many Regions, deep prescriptive networking and security beyond the managed baseline; config-file-driven CloudFormation/CDK Higher — you own a large IaC codebase; can run with Control Tower
Fully custom (Organizations + your IaC) You have unusual constraints Control Tower/LZA can’t meet and a platform team to carry it Highest — you build and operate every guardrail, logging pipeline, and vending flow yourself

The honest guidance: start with Control Tower. Reach for LZA only when a concrete requirement (a Region Control Tower doesn’t govern, a compliance regime needing deeper prescriptive controls, GovCloud) forces it — and even then, LZA frequently runs on top of a Control Tower landing zone rather than replacing it. Choose fully custom only when you can name the specific thing both managed options can’t do and you’re prepared to own the result forever.

Real-world enterprise scenario

Aurelia Health Systems is a fictional INR 14,000-crore hospital and health-insurance group: ~22,000 staff, a regulated estate (HIPAA-equivalent + PCI DSS for its payments arm), and a newly-funded 30-person platform team. They have completed Part 1 — an AWS Organization with a management account and consolidated billing — and now stand up AWS Control Tower to turn that raw org into a governed landing zone before migrating 140 applications and onboarding 18 product teams. Their North-Star metric: time from “team requests an account” to “team can deploy into a fully-governed, network-attached, logging-enabled account” under 1 day, with 100% of accounts audit-clean.

Landing zone. Aurelia sets the home Region to ap-south-1 (Mumbai) for data-residency, governs ap-south-1 and ap-southeast-1 only, and region-restricts everything else with a preventive control. Control Tower creates the Log Archive and Audit accounts under the Security OU; the management account is locked down to billing + Control Tower + org automation only. They extend the OU tree beyond the defaults to: Security, Infrastructure (shared networking, shared services), Workloads/Prod-Regulated, Workloads/Prod-General, Workloads/NonProd, and Sandbox. Identity federates their existing Entra ID into IAM Identity Center via SAML + SCIM, with a permission-set catalog (PlatformAdmin, WorkloadDeployer, SecurityAudit, Billing, ReadOnly). Artifacts: a deployed, versioned landing zone; a 6-OU hierarchy; an Identity Center permission-set catalog.

Account Factory. They make a deliberate call: Account Factory creates no default VPC (centralized networking is injected later via AFT), and the OU allow-list is constrained so vending can only target the workload/sandbox OUs. Account root emails follow aws+<accountname>@aurelia.example distribution lists; a mandatory tag schema (cost-center, owner, environment, data-classification) is enforced. The console product is reserved for break-glass; routine vending moves to AFT. Artifacts: an account-vending standard (naming/email/tagging), OU allow-list, “no default VPC” decision.

Controls library. Aurelia enables the full Mandatory + Strongly Recommended baseline org-wide, then layers electives by OU. On Workloads/Prod-Regulated they enable PCI DSS and NIST 800-53 control objectives; preventive SCPs/RCPs enforce “never disable the org CloudTrail,” “no public access to Log Archive buckets,” and the region restriction; proactive CloudFormation Hooks block unencrypted S3/EBS/RDS at deploy time; detective Config rules provide continuous assurance and feed Security Hub. They author three custom controls the library doesn’t ship (a tag-enforcement RCP, an “approved-AMI-only” Config rule, and a “no public ENI in regulated OU” Hook), delivered via CfCT. Artifact: a guardrail catalog mapping each enabled control to its behavior, guidance level, and the PCI/NIST objective it satisfies.

Customization (CfCT + AFT). They run both. AFT owns account vending: a product team opens a merge request adding an account request; AFT provisions through Account Factory and applies global customizations (centralized-logging subscription, baseline IAM/break-glass roles, security agents, default budgets, mandatory tags) to every account, plus account customizations (Transit Gateway attachment with the team’s allocated /22 from IPAM, environment-specific config) per account — all in Terraform with a full Git history. CfCT owns org-wide CloudFormation baselines and the three custom SCPs/Hooks, applied automatically to the right OUs as accounts are enrolled. Both run through gated CodePipelines with PR review and plan/preview. Artifacts: an AFT account-request repo + global/account customization repos; a CfCT manifest.yaml + templates + SCPs; two gated pipelines.

Measurable outcome (9 months in): account provisioning-to-deployable dropped from ~3 weeks of manual setup to under 6 hours end-to-end via AFT; 64 of 140 apps migrated into governed accounts; 100% of accounts pass Control Tower drift checks and Security Hub conformance; PCI DSS and NIST coverage is demonstrable per-control to auditors; and a landing-zone drift introduced by a misconfigured team was auto-detected and repaired before it reached an audit.

Deliverables & checklist

Common pitfalls

  1. Choosing the home Region (or governed-Region set) carelessly. The home Region is effectively permanent, and an ungoverned Region that workloads actually use silently escapes every guardrail. Avoid it: pick the home Region for residency/primary-ops on purpose, govern exactly the Regions you use, and region-restrict the rest with a preventive SCP.
  2. Putting workloads or shared services in the management account. It can’t take most guardrails, it’s the org’s highest-value target, and it tangles billing/governance with runtime. Avoid it: keep the payer to Control Tower, Organizations, billing, and minimal org automation; everything else lives in member accounts.
  3. Shipping the default Account Factory VPC into every account. In a Transit Gateway / centralized-egress design, an auto-created default VPC is unmanaged, often non-compliant networking you didn’t ask for. Avoid it: configure Account Factory to create no VPC and inject networking via AFT/CfCT customizations.
  4. Enabling hundreds of controls everywhere on day one. It spikes AWS Config cost, floods you with non-compliance noise, and obscures which controls satisfy which requirement. Avoid it: start with Mandatory + Strongly Recommended, layer Electives by OU, map controls to objectives, and scope the Config recorder.
  5. Driving production account vending from the console. The manual Service Catalog flow has no native approval, audit trail, or per-account customization, and doesn’t scale. Avoid it: move routine vending to AFT (GitOps, Terraform) and/or CfCT, reserving the console product for break-glass.
  6. Treating the landing zone and customizations as a one-time setup. Control Tower deployed once and never updated drifts and ages; un-versioned customization scripts in the management account become un-ownable. Avoid it: run a landing-zone update/repair cadence, version CfCT/AFT in Git behind gated pipelines, and own the AFT/CfCT operational footprint (account, state, pipelines) as standing work.

Common beginner mistakes

These are mental-model errors — the wrong picture in your head — as opposed to the operational pitfalls listed earlier. Fix the model and the practice follows.

“Control Tower replaces AWS Organizations.” It doesn’t; it drives it. Every OU, SCP, and account Control Tower manages is a real Organizations object you can still see. Right model: Organizations is the engine block; Control Tower is the assembly robot that builds and maintains a specific, well-architected configuration on top of it. If you understand Organizations, you can always reason about what Control Tower did.

“A guardrail is just an SCP.” SCPs are only the preventive behavior. A control can also be detective (an AWS Config rule that flags non-compliance) or proactive (a CloudFormation Hook that blocks a bad resource at deploy time). Right model: “control” is the abstraction; SCP/RCP, Config rule, and CloudFormation Hook are the three engines it can run on. Ask “which behavior?” before “which SCP?”.

“Detective controls stop bad things from happening.” They don’t — they report. A detective Config rule tells you an EBS volume is unencrypted after it exists; it never blocked the API call. Right model: only preventive (SCP/RCP) and proactive (Hook) controls actually stop an action. Detective controls are for continuous assurance and the long tail you couldn’t pre-empt. If a requirement is “must never happen,” a detective control alone does not satisfy it.

“Enrolling an existing account just moves it into an OU.” Enrollment is not a drag-and-drop. Control Tower must deploy the baseline StackSets into the account, which requires the AWSControlTowerExecution role to exist there and no conflicting AWS Config recorder in the governed Regions. Right model: enrollment is a provisioning operation that can fail and needs prep; moving an account in the Organizations console without enrolling it produces drift, not governance.

“AFT and CfCT are competitors, so I must choose one.” They solve different problems and are routinely run together. Right model: AFT vends accounts and applies per-account Terraform; CfCT applies org-/OU-wide CloudFormation and SCPs. “AFT owns vending and per-account config; CfCT owns org-wide baselines” is a perfectly normal — often ideal — split.

“The management account is the safest, most-governed account, so I’ll run shared services there.” Backwards. The management (payer) account cannot have most controls applied to it and is the org’s highest-value target. Right model: the payer is the least-governed account by design — keep it nearly empty (Control Tower, Organizations, billing, minimal automation) and operate security tooling from the Audit account via delegated administration. “Important” here means “protect it by keeping it empty,” not “put important things in it.”

Practice challenges

Work these top to bottom — they escalate from “read the landing zone” to “operate it under drift.” Try each before opening the solution.

1. (Beginner) Name the accounts and their home. After you set up a Control Tower landing zone, which accounts did Control Tower create, and which OU do the shared ones live in?

<details><summary>Show solution</summary>

Control Tower creates two shared accounts — Log Archive and Audit — and places both under the Security OU. Your pre-existing management (payer) account is not created by Control Tower; it’s where you run the setup. It also creates an empty Sandbox OU.

Why: separation of duties is enforced by account boundaries — logs land in a Log Archive account workload teams can’t reach, and security tooling runs from Audit — which is the core value of a landing zone over a plain Organization. </details>

2. (Beginner → Intermediate) Pick the behavior. Requirement: “No one, in any workload account, may disable the organization CloudTrail trail.” Which control behavior enforces this, on which engine, and at what scope?

<details><summary>Show solution</summary>

A preventive control, implemented as an SCP, enabled at the OU level (e.g., on Workloads). It denies the cloudtrail:StopLogging/cloudtrail:DeleteTrail-style actions before they can run, and every account under the OU inherits it. Control Tower ships this as a mandatory control.

Why: “must never happen” maps to preventive (deny the API call) — a detective control would only tell you after logging was already turned off. </details>

3. (Intermediate) Enable a control by ID. Write the CLI to turn on the “EBS volumes must be encrypted” control for the OU ou-abcd-11112222 (org o-exampleorgid, account 123456789012, home Region ap-south-1), and confirm it actually took effect.

<details><summary>Show solution</summary>

aws controltower enable-control \
  --control-identifier arn:aws:controltower:ap-south-1::control/AWS-GR_ENCRYPTED_VOLUMES \
  --target-identifier  arn:aws:organizations::123456789012:ou/o-exampleorgid/ou-abcd-11112222
# returns { "operationIdentifier": "<op-id>" }

aws controltower get-control-operation --operation-identifier <op-id> \
  --query 'controlOperation.status'   # wait for "SUCCEEDED"

Why: enable-control is asynchronous — it isn’t live until get-control-operation reads SUCCEEDED, so any automation must poll rather than assume immediate effect. </details>

4. (Intermediate) Auto-baseline an OU. Every account in Workloads/Prod-Regulated must get a VPC-flow-logs stack automatically as accounts are added — with no manual step per account. Which framework, and what’s the minimal resource entry?

<details><summary>Show solution</summary>

Use CfCT. Add a resource to manifest.yaml:

resources:
  - name: baseline-vpc-flow-logs
    resource_file: templates/vpc-flow-logs.yaml
    deploy_method: stack_set
    deployment_targets:
      organizational_units:
        - Workloads/Prod-Regulated

Why: CfCT re-runs on Control Tower lifecycle events, so a newly enrolled account in that OU receives the StackSet automatically — the “apply these resources to this OU forever” job CfCT is built for. </details>

5. (Advanced) Defense-in-depth for one rule. Requirement: “S3 buckets must always be encrypted, and their encryption must never be turned off.” Design the control set using all three behaviors — name the behavior, engine, and what each catches.

<details><summary>Show solution</summary>

Why: proactive blocks the common path at deploy time, detective covers the long tail you can’t pre-empt, and preventive stops later tampering — no single behavior covers create and console and drift, so serious rules use all three. </details>

6. (Advanced) Repair after an update and a manual move. A landing-zone update shipped new Config rules, but three OUs still show as behind/outdated, and one account shows drift after a teammate dragged it to a different OU in the Organizations console. What two repair actions bring everything back to known-good?

<details><summary>Show solution</summary>

  1. Re-register (reset) the three OUs so their baseline version catches up to the new landing-zone version — an update alone doesn’t advance OU baselines.
  2. For the moved account: move it back to its correct registered OU and re-enroll (or run landing-zone Repair) so its baseline StackSets and controls are re-applied.

Why: drift is detected, not auto-healed, and OU baselines don’t advance just because the landing zone did — both need an explicit repair/re-register action, ideally triggered from your pipeline rather than clicked once and forgotten. </details>

Glossary

What’s next

Part 3 of AWS Landing Zone & Control Tower builds the shared network foundation on top of this governed account structure — Transit Gateway hub-and-spoke, centralized egress with AWS Network Firewall, IPAM-driven address planning, and how those resources are injected into every account through the AFT/CfCT customizations established here.

AWSLanding ZoneAWS Control TowerEnterprise
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments