Three lessons have solved half of the fifth problem: MercadoFresco's infrastructure is reproducible, it lives in a repository, it goes through review and it is deployed from a pipeline that updates itself. But there is one sentence that has come up in all three and that none of them has resolved: the three environments share account 111122223333.

As long as that stays true, a misdirected cdk destroy reaches production, a development deployment can exhaust a quota that production needs, no IAM policy fully isolates somebody who already holds broad permissions, and the invoice does not really separate what each environment costs. AWS Organizations is the answer: split the environments into separate accounts and govern the whole thing from above.

Cost warning. Organizations is free: it charges nothing for accounts, nothing for organisational units and nothing for service control policies. Control Tower does not charge for itself either, but it switches on services that do cost money: CloudTrail, Config and the baseline notifications come to around 10-25 USD a month per account, and the Config recorder is the most expensive part. New accounts start with their own free tier. Fictitious accounts and data.

Contents

  1. Why a single account falls short
  2. Concepts: organisation, management account, OU and member accounts
  3. MercadoFresco's structure
  4. Creating and inviting accounts: email, contacts and root
  5. Service control policies: what they are and what they are not
  6. Four real SCPs for MercadoFresco
  7. Tag, backup and AI policies
  8. Consolidated billing
  9. Control Tower, landing zone and Account Factory
  10. IAM Identity Center: three people, five accounts
  11. Cross-account roles and sts:AssumeRole
  12. Organisation-level services
  13. MercadoFresco's migration plan
  14. Cost and cleanup
  15. Common mistakes and tips
  16. Exercises
  17. Conclusion: closing the module

Why a single account falls short

An AWS account is not just a billing unit: it is the strongest isolation boundary that exists on the platform. Everything else — IAM, VPC, tags — is a boundary inside the same house.

Limit of the single account What it means Concrete case in MercadoFresco
Blast radius One mistake can reach anything cdk destroy --all from the wrong directory touches production
Shared quotas Limits are per account and region Load testing exhausts the elastic IPs that Friday's ASG needs
IAM does not fully isolate A badly written policy or a wildcard opens everything Resource: "*" in a development role reaches Aurora
Billing One invoice, one pool of resources "How much does pre-production cost?" can only be answered through tags, and only if they are correct
Auditing An auditor cannot narrow the scope Reviewing production alone means filtering, not isolating
Global configuration Many settings are account-wide Turning on S3 public access block affects all three environments at once

It is worth seeing where the account sits among the boundaries the course has already used:

Boundary What it separates It can be bypassed if… Strength
Tag Nothing, it only classifies Always: it is metadata None
Security group / VPC Network traffic There is peering, endpoints or a role with permissions Medium
IAM policy Actions on the API The policy has a wildcard or somebody edits it High
Account Everything: API, quotas, invoice, auditing Only with an explicit role and a declared trust Maximum

The case that frightens Marta most is the first one, and it is not theoretical: it came up three times in 09-02. cdk destroy MercadoFrescoRedProduccion is one autocompletion away from MercadoFrescoRedDesarrollo, and neither termination protection nor the discipline of never using --all is a guarantee: they are patches. With separate accounts the command simply has no permission to reach production, because the credentials it runs with belong to a different account.

The second one is not theoretical either. EC2 quotas — vCPUs per family, elastic IPs, network interfaces — are per account and region. A load test in development that spins up twenty instances consumes vCPUs from the same allowance as the shop's auto scaling, and on Friday at 19:00, with 900 orders/hour, the ASG may find itself unable to grow because of a test launched on Thursday.

Concepts: organisation, management account, OU and member accounts

  • Organisation: the set of accounts managed centrally. It has a root, which is the top node of the tree.
  • Management account: the account that creates the organisation. It is the one that pays, the one that invites accounts and the only one from which policies are applied.
  • Organisational unit (OU): a container of accounts and of other OUs. It is where policies are applied, and the reason it exists: to group accounts that must be governed the same way.
  • Member account: any account in the organisation other than the management one. This is where all the work lives.

There is one rule worth committing to memory before going any further: nobody works in the management account. No workloads are deployed there, no working users are created, no pipelines run. Three reasons:

  1. SCPs do not apply to the management account. Not even if you put it inside an OU. Any resource that lives there falls outside every guardrail you design.
  2. It is the most powerful account in the organisation: it can create accounts, change policies and, ultimately, remove accounts from the organisation. Compromising it compromises everything.
  3. Billing and governance are different responsibilities from workloads. Mixing them makes it impossible to audit who did what.

One useful limit: the maximum depth of the OU tree is five levels below the root, and an account belongs to exactly one OU.

MercadoFresco's structure

flowchart TB
    R[Organisation root] --> S[OU Seguridad]
    R --> I[OU Infraestructura]
    R --> C[OU Cargas]
    R --> A[OU Aislamiento]
    G[Management account<br/>999988887777<br/>NO work happens here] -.governs.-> R
    S --> S1[seguridad-mercadofresco<br/>444455556666<br/>logs, trail, Security Hub]
    I --> I1[herramientas-mercadofresco<br/>555566667777<br/>pipelines and artefacts]
    C --> P[OU Produccion]
    C --> Q[OU Preproduccion]
    C --> D[OU Desarrollo]
    P --> P1[produccion-mercadofresco<br/>111122223333]
    Q --> Q1[preproduccion-mercadofresco<br/>222233334444]
    D --> D1[desarrollo-mercadofresco<br/>333344445555]
    A --> A1[quarantined accounts]

Six accounts, five of them working accounts. The decisions behind them deserve to be justified one by one:

  • Seguridad isolates whatever must survive an incident in the workload accounts: the destination of the organisation trail, the Config aggregator, Security Hub and GuardDuty. If somebody compromises production, they cannot delete the evidence, because it sits in another account they have no access to.
  • Infraestructura hosts what is shared between environments: the pipeline, mercadofresco-artefactos and the image repositories. Separating it avoids the paradox of the pipeline that deploys production living in production.
  • Cargas groups the three environments, with one OU per environment so that different policies can be applied to each. Production tolerates less than development, and development needs cost restrictions that production does not.
  • Aislamiento is empty, and that is its purpose: it is the destination for a compromised account. It carries an SCP that denies everything, so moving an account there freezes it in a single step, without touching its credentials or its resources, leaving the evidence intact for the investigation.

And one pragmatic decision that saves weeks: the current account 111122223333 becomes production. Migrating production is the most expensive and the riskiest part; reusing it and creating new accounts for everything else reduces the migration to what can genuinely be recreated with the templates from 09-01 and the CDK from 09-02.

Creating and inviting accounts: email, contacts and root

There are two ways for an account to join the organisation: create it from inside, or invite an existing one.

# Create the organisation (from the future management account)
aws organizations create-organization --feature-set ALL

# Create the organisational units
ROOT=$(aws organizations list-roots --query 'Roots[0].Id' --output text)
aws organizations create-organizational-unit --parent-id "$ROOT" --name Seguridad
aws organizations create-organizational-unit --parent-id "$ROOT" --name Cargas
OU_CARGAS=$(aws organizations list-organizational-units-for-parent --parent-id "$ROOT" \
  --query "OrganizationalUnits[?Name=='Cargas'].Id" --output text)
aws organizations create-organizational-unit --parent-id "$OU_CARGAS" --name Produccion

# Create a new account
aws organizations create-account \
  --email aws+preproduccion@mercadofresco.example \
  --account-name preproduccion-mercadofresco \
  --role-name OrganizationAccountAccessRole

# Invite the existing account (the current one, which will be production)
aws organizations invite-account-to-organization \
  --target Id=111122223333,Type=ACCOUNT

# Move an account into its OU
aws organizations move-account --account-id 222233334444 \
  --source-parent-id "$ROOT" --destination-parent-id "$OU_PREPRODUCCION"

--feature-set ALL matters: the alternative, CONSOLIDATED_BILLING, only groups the billing together and does not allow service control policies, which is half the value of Organizations.

Four operational details that cause trouble if neglected:

  • Every account needs a unique email address, and that address controls recovery of the root user. The correct practice is a distribution listaws+produccion@mercadofresco.example — that reaches Marta and a second person, never the personal address of somebody who may leave the company. Aliases with + work with most providers and make this trivial.
  • The root user of each member account is locked down and forgotten: a long password in the team's secrets manager, MFA enabled, and no access keys. All day-to-day work goes through Identity Center.
  • OrganizationAccountAccessRole is created automatically in accounts created from the organisation, and it lets the management account assume an administrator role in them. In invited accounts it does not exist, and it has to be created by hand before inviting them, or they will be unreachable from the management account.
  • The alternate contacts — billing, operations and security — are filled in per account and can be set from the organisation. That is where AWS reports abuse or a security problem, and an account without them receives those notices only at the root user's email address.

Service control policies: what they are and what they are not

A service control policy (SCP) defines the maximum permissions that can be exercised in an account. And here is the sentence to memorise: an SCP never grants permissions, it only limits them.

flowchart LR
    A[API request] --> B{Does the SCP allow it?}
    B -->|No| X[DENIED]
    B -->|Yes| C{Does the IAM policy allow it?}
    C -->|No| X
    C -->|Yes| D{Does a resource policy or<br/>permissions boundary deny it?}
    D -->|Yes| X
    D -->|No| E[ALLOWED]

The effective permission is the intersection: what the SCP allows and what IAM allows. Practical consequences:

  • An SCP that allows s3:* gives nobody access to S3: it merely lets IAM grant it.
  • An SCP that denies s3:DeleteBucket prevents buckets from being deleted even by the account administrator, even with AdministratorAccess. That is its main value: it limits somebody who can already do everything.
  • SCPs do not apply to the management account, nor to service-linked roles (AWSServiceRoleFor*).

There are two strategies, and the choice has enormous consequences:

Strategy How it works Advantage Drawback
Deny list FullAWSAccess is inherited and explicit Deny statements are added Simple; new services work by themselves You have to anticipate every dangerous thing
Allow list FullAWSAccess is removed and services are allowed one by one Total control; nothing gets in by accident High maintenance: every new service breaks something

MercadoFresco chooses a deny list, which is what suits 90 % of organisations: a team of three cannot maintain an allow list without turning it into a bottleneck. An allow list makes sense in heavily regulated environments or in OUs with a single purpose.

The usual distribution of policies by level, which is the one MercadoFresco adopts:

Level What is applied there Why
Root Allowed regions, audit protection Valid for every account without exception
OU Cargas Mandatory encryption, required tags Common to the three environments, not to security or tooling
OU Produccion Deny direct access to customer data Only makes sense where there is real data
OU Desarrollo Instance types, no commitment purchases Cost control where spending is discretionary
OU Aislamiento Deny on everything Freeze a compromised account

Inheritance is cumulative downwards: an account is subject to the SCPs of its OU, of the OUs above it and of the root, all at once. And since Deny always wins, a single policy anywhere along that path denying an action is enough to forbid it. That is the reason for the classic warning that follows.

Four real SCPs for MercadoFresco

1. Restrict the regions. Applied at the root. It prevents resources from being created outside eu-west-1, with the unavoidable exception of us-east-1, where CloudFront, WAF for CloudFront and the associated ACM certificates live.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenegarRegionesNoAutorizadas",
    "Effect": "Deny",
    "NotAction": [
      "iam:*", "organizations:*", "route53:*", "cloudfront:*", "waf:*", "wafv2:*",
      "support:*", "budgets:*", "ce:*", "sts:*", "acm:*", "shield:*", "health:*"
    ],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": { "aws:RequestedRegion": ["eu-west-1", "us-east-1"] }
    }
  }]
}

NotAction is essential: global services resolve against us-east-1 in the API, and denying them by region would break IAM, Route 53 and the billing console itself. It is mistake number one when writing this policy.

2. Protect the audit trail. Applied at the root. It stops anybody — including a production administrator — from erasing the trail.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProtegerTrail",
      "Effect": "Deny",
      "Action": ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail",
                 "cloudtrail:UpdateTrail", "cloudtrail:PutEventSelectors"],
      "Resource": "arn:aws:cloudtrail:*:*:trail/trail-mercadofresco"
    },
    {
      "Sid": "ProtegerConfig",
      "Effect": "Deny",
      "Action": ["config:DeleteConfigurationRecorder", "config:StopConfigurationRecorder",
                 "config:DeleteDeliveryChannel", "config:DeleteConfigRule"],
      "Resource": "*"
    },
    {
      "Sid": "ProtegerGuardDuty",
      "Effect": "Deny",
      "Action": ["guardduty:DeleteDetector", "guardduty:DisassociateFromMasterAccount",
                 "guardduty:UpdateDetector"],
      "Resource": "*"
    }
  ]
}

This closes a real gap from 05-03: until now, whoever had AdministratorAccess in the account could switch off CloudTrail before doing anything. Not any more.

3. Require encryption in S3. Applied to the Cargas OU. It denies uploading unencrypted objects and creating buckets without public access block.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenegarSubidaSinCifrar",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": { "s3:x-amz-server-side-encryption": ["AES256", "aws:kms"] }
      }
    },
    {
      "Sid": "DenegarDesactivarBloqueoPublico",
      "Effect": "Deny",
      "Action": ["s3:PutAccountPublicAccessBlock", "s3:PutBucketPublicAccessBlock"],
      "Resource": "*",
      "Condition": { "ArnNotLike": {
        "aws:PrincipalArn": "arn:aws:iam::*:role/rol-mercadofresco-seguridad" } }
    }
  ]
}

The second statement uses the exception by principal pattern: nobody can disable public access block except one specific security role. It is the right way to leave an escape valve without opening the door.

4. Contain cost in development. Applied only to the Desarrollo OU.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SoloInstanciasPequenas",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": { "StringNotLike": {
        "ec2:InstanceType": ["t3.*", "t4g.*", "m6i.large", "m6i.xlarge"] } }
    },
    {
      "Sid": "SinRedshiftNiInstanciasReservadas",
      "Effect": "Deny",
      "Action": ["redshift:CreateCluster", "ec2:PurchaseReservedInstancesOffering",
                 "savingsplans:CreateSavingsPlan", "rds:PurchaseReservedDBInstancesOffering"],
      "Resource": "*"
    }
  ]
}

The second statement prevents a kind of mistake that cannot be undone: buying a one-year commitment from the development account. Savings Plans and reserved instances are bought from the management account, and they spread across the whole organisation by themselves (11-05).

One condition that appears in almost every organisation and is worth knowing is aws:PrincipalOrgID, which identifies the organisation as a whole. It lets you write resource policies along the lines of "this bucket is accessible from any account in my organisation, and only from them", without listing account identifiers that change over time:

{
  "Effect": "Allow",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::mercadofresco-artefactos/*",
  "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-a1b2c3d4e5" } }
}

It is the right way to share the artefacts bucket between the tooling account and the three workload accounts: a single document that nobody has to touch when a new account joins. Its sibling aws:PrincipalOrgPaths also lets you narrow by OU, for example so that only the accounts in Cargas can read.

The classic warning

Never test a new SCP by applying it at the root first. It is the mistake that turns an afternoon of improvements into an incident, and it happens for two combined reasons: SCPs affect every account at once, and their effects are not always obvious — a Deny on ec2:* also breaks services that create network interfaces, such as Lambda in a VPC or RDS.

The correct procedure has four steps:

  1. Write the policy and review it in a PR, like any other code: SCPs live in mercadofresco-infra.
  2. Apply it to a single development account and run the full pipeline, a deployment and the smoke tests there.
  3. Promote it to the Desarrollo OU, wait a few days and review AccessDenied events in CloudTrail.
  4. Promote it to Cargas or to the root.

And one safeguard that avoids the worst-case scenario: the management account is not subject to SCPs, so there is always a way back from a policy that has locked the organisation out. It is one of the reasons its access must be protected with MFA and used as little as possible.

Tag, backup and AI policies

Organizations has three other policy types, less well known and very useful.

Tag policies define mandatory tags, their valid values and which resources they are required on. They solve the problem MercadoFresco has been carrying since module 1: mandatory tagging depends on people remembering.

{
  "tags": {
    "Entorno": {
      "tag_key": { "@@assign": "Entorno" },
      "tag_value": { "@@assign": ["produccion", "preproduccion", "desarrollo"] },
      "enforced_for": { "@@assign": ["ec2:instance", "ec2:volume", "s3:bucket", "rds:db"] }
    },
    "CentroCoste": {
      "tag_key": { "@@assign": "CentroCoste" },
      "tag_value": { "@@assign": ["plataforma", "producto", "analitica"] }
    }
  }
}

With enforced_for, an operation that creates a resource of those types with an Entorno value other than the three allowed ones fails. Without enforced_for, the policy only reports: a non-compliance report appears and nothing else. That difference is exactly the one from 08-02 between warning and blocking.

Backup policies apply AWS Backup plans across the whole organisation: which resources are backed up, how often and with what retention, without depending on each account configuring it. For MercadoFresco it is the guarantee that a new account is born with backups, instead of finding out on the day they are needed.

AI policies (AI services opt-out) let you exclude the whole organisation from having its data used to improve AWS's AI services. In Europe it is usually a decision for the legal team, and enabling it once at the root is enough.

Consolidated billing

Consolidated billing is the part that excites people least technically and shows up fastest:

  • A single invoice for the whole organisation, broken down by account.
  • Aggregated volume discounts: the tiered prices for S3 or data transfer are calculated on the combined usage of all accounts, so splitting into accounts makes nothing more expensive. Quite the opposite: five small accounts reach a discount tier sooner than five projects isolated in independent accounts.
  • Automatic sharing of Savings Plans and reserved instances: a commitment bought from the management account applies to any account in the organisation with eligible usage. If production does not consume the whole commitment one night, pre-production takes advantage of it. Covered in detail in 11-05.
  • Free tier: it is shared at organisation level, it does not multiply per account.

And the most important part for the problem that opened this lesson: splitting by account means the question "how much does pre-production cost?" has an exact answer with no work involved. Before, it depended on every resource being tagged properly, and 09-01 showed they were not. Now it is a native dimension in Cost Explorer (11-03).

Two settings worth applying on day one: enable discount sharing (it is on by default, but it can be disabled per account if a team needs to see its cost without cross-subsidies) and enable cost allocation tags in the management account, since that is the only place they can be activated for the whole organisation (11-02).

Control Tower, landing zone and Account Factory

Everything above can be built by hand with the CLI. AWS Control Tower does it for you and adds continuous governance.

A landing zone is a pre-configured multi-account environment that follows best practice. Control Tower creates it in about two hours: the organisation, the security and workload OUs, a log archive account, an audit account, an organisation CloudTrail, a Config aggregator, IAM Identity Center configured and a set of active controls.

The controls (guardrails) come in three types, and the distinction matters:

Type Mechanism What it does Example
Preventive SCP Blocks the action CloudTrail cannot be disabled
Detective Config rule Detects and reports non-compliance Bucket with public access detected
Proactive CloudFormation hooks Blocks at deployment, before creation A template with an unencrypted bucket does not deploy

The Account Factory is account provisioning: you fill in a form — name, email, OU — and out comes an account with the baseline applied, the network created and access configured. With Account Factory for Terraform or with StackSets, all of that can be triggered from a pipeline.

When should you use Control Tower? The criteria are simple:

Situation Recommendation
Brand-new organisation from scratch Control Tower: two hours instead of two weeks
More than ten accounts, or expecting to grow Control Tower: manual governance does not scale
Regulatory compliance requirements Control Tower: the controls come already mapped
Small, stable organisation with mature IaC Organizations by hand: fewer layers, more control
Very unusual existing structure Careful: adopting it later brings friction

For MercadoFresco, with six accounts and a team that already knows CDK, Marta's decision is to start with Organizations by hand — there are few policies and she wants to understand them — and note Control Tower down for when the organisation goes past ten accounts. It is a defensible decision; the opposite one would be too.

IAM Identity Center: three people, five accounts

With six accounts, the operational question is immediate: does Marta need six users? The answer is that she needs zero IAM users.

IAM Identity Center (formerly AWS SSO) provides an identity directory — its own, or federated with Entra ID, Okta or Google Workspace — and assigns permission sets to combinations of user or group and account. On signing in, each person sees a portal with the accounts and roles they have access to, and on entering they receive temporary credentials: there are no long-lived access keys on anybody's laptop.

A permission set is a role template that Identity Center materialises in every assigned account. MercadoFresco's design:

Permission set Permissions Session Assigned to
AdministracionPlataforma AdministratorAccess 1 h Marta, in every account under Cargas
DesarrolloCompleto PowerUserAccess without IAM 8 h Luis and Marta, in desarrollo
DespliegueLectura ReadOnlyAccess + start the pipeline 4 h Luis, in preproduccion and produccion
AnalisisDatos Read access to Redshift, QuickSight and the reporting S3 8 h Sara, in produccion
RespuestaIncidentes AdministratorAccess 1 h Marta, with approval and notification
Auditoria SecurityAudit + ViewOnlyAccess 4 h External auditor, in all of them

Four design decisions that deserve explaining:

  • Luis has no administration in production. He has read access and permission to start the pipeline, which is what he actually needs: since 08-04, deploying means approving a transition, not running commands. This is the point where the pipeline stops being a convenience and becomes a security control.
  • Administrative sessions last one hour. A long session with high permissions is a credential forgotten in a terminal.
  • RespuestaIncidentes is deliberately awkward emergency access: it requires approval and using it fires a notification to alertas-mercadofresco. It exists for three in the morning, not for Tuesday afternoon.
  • Sara only enters production, and only for data. She does not need development or pre-production, and giving her access "just in case" is exactly what 04-01 advises against.
# Assign a permission set to a group in an account
aws sso-admin create-account-assignment \
  --instance-arn "$INSTANCE_ARN" \
  --target-id 222233334444 --target-type AWS_ACCOUNT \
  --permission-set-arn "$DEV_PERMISSION_SET_ARN" \
  --principal-type GROUP --principal-id "$DEV_GROUP_ID"

And day-to-day use from the CLI, which replaces the mercadofresco-dev profile with static keys from module 1:

# ~/.aws/config
[profile mf-produccion]
sso_session = mercadofresco
sso_account_id = 111122223333
sso_role_name = DespliegueLectura
region = eu-west-1

[sso-session mercadofresco]
sso_start_url = https://mercadofresco.awsapps.com/start
sso_region = eu-west-1
aws sso login --sso-session mercadofresco
aws s3 ls --profile mf-produccion       # temporary credentials, no keys on disk

Cross-account roles and sts:AssumeRole

Identity Center solves access for people. Access between services in different accounts is solved with roles and sts:AssumeRole, exactly the mechanism from 04-01 crossing the account boundary.

MercadoFresco's concrete case: the pipeline lives in herramientas-mercadofresco (555566667777) and has to deploy into the three workload accounts.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::555566667777:role/rol-pipeline-mercadofresco" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "mercadofresco-despliegue" },
      "Bool": { "aws:MultiFactorAuthPresent": "false" }
    }
  }]
}

That document is the trust policy of the rol-despliegue-mercadofresco role in the production account: it says who can assume it. The role's permissions policy, separately, says what it can do once assumed. They are two different things and both must be restrictive: a trust policy that accepts "AWS": "111122223333" and nothing more lets any principal in that account assume the role.

In the CDK, bootstrapping already covers this: cdk bootstrap --trust 555566667777 creates the deployment roles in the target account trusting the tooling account, and from then on cdk deploy works across accounts with no further configuration. It is the reason 09-02 insisted on declaring env with an explicit account: with several accounts, the target account stops being a detail and becomes part of the stack definition.

Organisation-level services

Many security and governance services can be enabled for the whole organisation from a delegated administrator account — typically seguridad-mercadofresco — so that new accounts are covered automatically.

Service What it brings at organisation level Lesson
Organisation CloudTrail A single trail, in the security account, that member accounts cannot switch off 05-03
Config aggregator Compliance across every account on one dashboard 05-04
Security Hub Aggregated findings and a score per standard 04-01
GuardDuty Threat detection, enabled automatically in new accounts 04-04
IAM Access Analyzer Detects resources reachable from outside the organisation 04-01
StackSets Deploys the baseline into every new account 09-01
Backup Centralised backup plans 02-04

The piece that closes the module's circle is the last row. The ss-mercadofresco-linea-base StackSet from 09-01, with --permission-model SERVICE_MANAGED and --auto-deployment Enabled=true, means any account that joins an OU receives the baseline without anybody doing anything: billing alarm, access roles, contact configuration, S3 public access block and subscription to the Config aggregator.

aws cloudformation create-stack-instances \
  --stack-set-name ss-mercadofresco-linea-base \
  --deployment-targets OrganizationalUnitIds="$OU_CARGAS" \
  --regions eu-west-1 \
  --operation-preferences MaxConcurrentPercentage=25,FailureTolerancePercentage=10

That command is the answer to "how do we guarantee that the account we create six months from now has everything it should have". The answer is: a person does not guarantee it, an OU does.

MercadoFresco's migration plan

Migrating from one account to five working accounts — plus the management one — is done in six phases, without service interruption. The rule that orders the whole plan is: first what gets recreated, then what gets moved, and the data last, with permission.

Phase 1: create the organisation and the accounts (day 1). A new, empty management account is created — 999988887777 — the organisation is created with --feature-set ALL and the current account 111122223333 is invited, becoming production and being placed in its OU. The other four accounts are created. Nothing changes for the service: the production account keeps working exactly as before.

Phase 2: access and baseline (days 2-5). IAM Identity Center with the permission sets, and the baseline StackSet across every OU. The IAM users from module 1 are disabled but not deleted until it is confirmed that nothing automated was using them. Organisation CloudTrail and Config aggregator, targeting the security account.

Phase 3: new environments from code (weeks 2-3). This is where the work of the three previous lessons pays off. Development and pre-production are recreated from scratch in their accounts with the CDK from 09-02: cdk bootstrap in each account and cdk deploy with env pointing at the corresponding account. Nothing is migrated: it is deployed. If something cannot be recreated, it was missing from the code, and that discovery is valuable in itself.

Phase 4: pipeline and tooling (week 4). The pipeline moves to herramientas-mercadofresco and is given cross-account roles into the three workload accounts. It deploys to development and pre-production from the new account and is verified end to end before touching production.

Phase 5: progressive SCPs (weeks 4-6). The policies are applied in the order set out in the warning: first one development account, then the Desarrollo OU, then Cargas, and only at the end the two at the root. Between each step, a review of AccessDenied events in CloudTrail.

Phase 6: the old environments (week 7). The development and pre-production resources still sitting in the production account are switched off. This frees up quota, reduces noise and is where the saving shows up: some 340 USD a month of duplicated resources that nobody knew were still running.

flowchart LR
    F1[Phase 1 - day 1<br/>Organisation and accounts] --> F2[Phase 2 - days 2 to 5<br/>Access and baseline]
    F2 --> F3[Phase 3 - weeks 2 and 3<br/>New environments from CDK]
    F3 --> F4[Phase 4 - week 4<br/>Pipeline in tooling]
    F4 --> F5[Phase 5 - weeks 4 to 6<br/>Progressive SCPs]
    F5 --> F6[Phase 6 - week 7<br/>Switch off duplicated environments]

What happens to the data

Data is the delicate part and deserves its own section, with three rules.

Production does not move. By reusing 111122223333 as the production account, Aurora, DynamoDB, Redshift and the five buckets stay where they are. It is the decision that removes 90 % of the project's risk.

Development and pre-production data is not copied: it is generated. It is an opportunity to do what should already have been done: pre-production is populated with synthetic or anonymised data, not with a copy of production. As well as being safer, it removes a source of drift.

And here comes a warning that is not technical. If at any point somebody proposes copying production data to another account for testing, that is processing of customers' personal data and falls under the GDPR: names, delivery addresses, phone numbers and purchase history. A copy to another account — even within the same organisation and the same region — is a transfer that must be justified, documented and time-limited, and the anonymisation has to be real, not a change of name. This decision is not taken by the technical team alone: whoever runs compliance reviews it. Marta writes it into the plan as a blocking requirement, not as a recommendation.

Cost and cleanup

Organizations, OUs and SCPs are free. What costs money is what they switch on:

Item Approximate cost Note
Organizations, OUs, SCPs 0 USD No charge
IAM Identity Center 0 USD No charge
Control Tower 0 USD for the service But it enables Config and CloudTrail
Organisation CloudTrail ~2 USD/account/month The first management trail is free
AWS Config ~8-20 USD/account/month Depends on the number of resources: the most expensive item
GuardDuty ~5-15 USD/account/month Depending on event volume

For MercadoFresco, full governance comes to about 90 USD a month, against the 340 USD saved by switching off the duplicated environments. The migration pays for itself.

On cleanup, two serious warnings. Closing an AWS account is not immediate: it stays suspended for 90 days before closing for good, and during that time its resources cannot be recovered nor its email address reused. And an account leaving the organisation needs its own payment method and complete contacts, or it will be blocked. Never remove an account from the organisation without preparing it first.

Common Mistakes and Tips

Mistake: working in the management account. It is the most expensive structural mistake, because SCPs do not protect it and anything you deploy there sits outside the guardrails. Tip: empty management account, MFA access and exceptional use only.

Mistake: testing an SCP at the root. A badly calibrated Deny can leave the organisation unable to deploy. Tip: the order set out in the warning — one account, one OU, the root — always reviewing AccessDenied events in CloudTrail between steps.

Mistake: denying by region without NotAction. It breaks IAM, Route 53, CloudFront and the billing console, because global services resolve against us-east-1. Tip: the exclusion list from the first SCP in this lesson.

Mistake: personal email addresses as account root. The day that person leaves, the account is orphaned. Tip: distribution lists with at least two recipients, and alternate contacts filled in.

Mistake: inviting an account without creating OrganizationAccountAccessRole first. It does not exist in invited accounts, and without it the management account cannot get in. Tip: create it before inviting.

Mistake: believing an SCP grants permissions. An Allow in an SCP gives nobody access: it only raises the ceiling. Tip: internalise that the effective permission is the intersection of SCP and IAM.

Tip: keep the SCPs and the OU structure in mercadofresco-infra. Everything in this module applies here too: policies are code, they are reviewed in a PR and deployed from the pipeline.

Tip: enable the delegated administrator for the security services. It keeps the management account empty and gives security its own scope.

Tip: use aws:PrincipalOrgID in shared resource policies. It avoids account lists that have to be maintained and adjusts itself when a new account joins.

Tip: create the isolation OU before you need it. Empty and with its Deny in place, it is a thirty-second move on the day of an incident; improvising it that day is not.

Tip: set a budget per account on day one. With the split by account, AWS Budgets (11-04) becomes precise: one alert per account spots a leak sooner than a global budget.

Exercises

Exercise 1: the SCP that broke the pipeline

Marta applies an SCP to the Cargas OU denying all iam:* actions except read, on the grounds that only the CDK should create roles. The next day the pipeline fails in every environment with AccessDenied when deploying the application stack, and on top of that a new Lambda function cannot start. Explain (a) exactly why the pipeline fails; (b) why the Lambda fails, which is a different failure; (c) how it is diagnosed with the tools from module 5; (d) how to fix the policy while keeping the original intent; and (e) which step of the correct procedure Marta skipped.

Exercise 2: designing access for a new joiner

MercadoFresco hires Elena, a developer joining Luis's team. She must be able to develop freely, see what is happening in production during an incident, launch deployments to pre-production but not to production, and must not be able to read customers' personal data. Design her access: which groups, which permission sets, in which accounts, with what session duration and what mechanism you would use for the personal data part. Also state what you would not give her even if she asked, and how you would handle the day she needs emergency access to production.

Exercise 3: the order of the migration

A colleague proposes speeding up the plan: create the five accounts on Monday, move production to a new, clean account on Tuesday night — "that way everything is tidy from the start" — and apply all the SCPs on Wednesday. He argues that doing it all at once avoids months of intermediate state. Respond: (a) three concrete risks of moving production to a new account; (b) what happens to Aurora, to the buckets and to the Route 53 zone in that move; (c) why applying all the SCPs on Wednesday is an even worse idea; (d) which part of his proposal is reasonable; and (e) how you would explain it to him in one sentence.

Solutions

Solution 1

(a) The pipeline fails because CloudFormation needs iam:CreateRole and iam:PassRole. The application stack creates the ASG's instance profile and the Lambdas' execution role; both require creating roles and passing them to the service that will use them. iam:PassRole is particularly easy to forget because it creates nothing: it authorises handing an existing role to a service, and without it the deployment fails even if the role already exists.

(b) The Lambda fails for a different and subtler reason: service-linked roles. When a Lambda is attached to a VPC, AWS creates AWSServiceRoleForLambdaReplicator or similar via iam:CreateServiceLinkedRole. Although SCPs do not apply to service principals, they do apply to the call your role makes when requesting the creation of that linked role. It is the category of failure that makes SCPs dangerous: they break things nobody associates with IAM.

(c) The diagnosis. CloudTrail (05-03) is the tool: denied events show up with errorCode: AccessDenied and, when the cause is an SCP, with a message that explicitly mentions a service control policy. An Athena query over the organisation trail filtering by errorCode over the last 24 hours returns the full list of blocked actions, which is exactly the information needed to calibrate the policy. It is also the mechanism behind step 3 of the correct procedure.

(d) The fix. The intent — that nobody creates roles by hand — is good; the implementation is too blunt. The Deny on IAM write actions is kept but the legitimate principals are excepted, along with the necessary actions:

{
  "Effect": "Deny",
  "NotAction": ["iam:Get*", "iam:List*", "iam:PassRole", "iam:CreateServiceLinkedRole"],
  "Resource": "arn:aws:iam::*:role/*",
  "Condition": { "ArnNotLike": { "aws:PrincipalArn": [
    "arn:aws:iam::*:role/cdk-*-cfn-exec-role-*",
    "arn:aws:iam::*:role/rol-despliegue-mercadofresco"
  ] } }
}

With this, only the CDK and pipeline deployment roles can create roles, and PassRole and CreateServiceLinkedRole fall outside the block. A complementary alternative is to require a permissions boundary (iam:PermissionsBoundary) on every role created, which limits what those roles will be able to do even if somebody does create them.

(e) The step that was skipped. The second and the third: she did not test the policy in a single development account by running the full pipeline, nor did she let it settle in the Desarrollo OU while reviewing AccessDenied. Applying it straight to Cargas means applying it to production, and the lesson itself warns about this. The aggravating detail is that a failure like this shows up at the next deployment, which may be hours later and with no apparent connection to the change.

Solution 2

Groups and permission sets. Elena joins the desarrolladores group in the Identity Center directory, which already has assignments; nothing specific is created for her, because individual permissions are governance debt.

Account Permission set Session Why
desarrollo DesarrolloCompleto (PowerUserAccess without IAM) 8 h Real freedom where there is no risk
preproduccion DespliegueLectura 4 h She can start the pipeline and see the result
produccion LecturaOperacion (new) 2 h See metrics, logs and traces during an incident

The new set, LecturaOperacion, is the interesting part: ReadOnlyAccess is too broad because it includes reading S3 objects and querying DynamoDB, that is, customer data. The right approach is a bespoke set with CloudWatch, X-Ray, the ECS/EC2 console and CloudFormation stack states, plus an explicit Deny on s3:GetObject in the buckets holding personal data, dynamodb:GetItem, dynamodb:Query and rds-data:*.

The mechanism for personal data has two layers, and neither is enough on its own. The first is the permission set's policy, with the Deny statements above. The second is an SCP on the Produccion OU denying access to the buckets and tables holding customer data except to a short list of application roles; that way, even if somebody widens the permission set by mistake, the barrier stays up. It is the difference between a policy that can be changed within the account and a ceiling that cannot.

What I would not give her even if she asked: administration in production; permission to create IAM users or access keys — with Identity Center they are unnecessary and they are the main source of leaked credentials; and permanent write access to pre-production outside the pipeline, because that would reopen the "deploy by hand" route that module 8 closed.

Emergency access is handled with RespuestaIncidentes: assignable to Elena, with a one-hour session, Marta's approval and an automatic notification to alertas-mercadofresco when used. The rule that makes it work is that using it is not a problem; using it without an incident is, and the notification exists so that review is possible.

Solution 3

(a) Three risks of moving production to a new account. First, the data: Aurora, DynamoDB and the buckets have to be replicated or restored in the target account, which means either a cutover window or replication with double writes, plus an integrity check nobody has mentioned. Second, identities and references: ARNs change account, so roles, bucket policies, KMS keys, endpoints and everything that mentions them has to be reviewed; a KMS policy that references the old account stops working and produces decryption failures that are hard to diagnose. And third, what is not in the code: ACM certificates, the Route 53 zone, the CloudFront and WAF settings and the quota increases granted by support, which do not migrate automatically and tend to be discovered halfway through the window.

(b) What happens to each thing. Aurora does not move: a snapshot is shared with the target account — which also requires sharing the KMS key — and restored, producing a new cluster with a different endpoint, so everything that connects to it has to change. The buckets do not move: the names are global, so new buckets with different names have to be created and the objects copied, also reviewing the policies and the public URLs of the product photos that may be cached in CloudFront. The Route 53 zone can be moved keeping the same name servers, which is the only good news in this section, but it requires coordinating the change with records pointing at resources whose ARNs are changing at the same time.

(c) Why applying all the SCPs on Wednesday is even worse. Because it stacks three mistakes at once: applying them without testing, applying them all together and applying them right after a migration. If something fails on Thursday — and something will — it will be impossible to tell whether the cause is an SCP, the migration or the ARNs that changed. Diagnosis depends on being able to isolate the variable, and that proposal mixes them all. It is the same principle as 08-05 with small deployments: the scope of a change must make it possible to attribute the failure.

(d) What is reasonable. Creating the five accounts on Monday is correct and carries no risk: creating accounts moves nothing. So is the underlying intent — not leaving an eternal intermediate state — which is a real problem: half-finished migrations tend to last years. The answer to that is not to go faster, it is to put a deadline on each phase and treat the plan as a project with milestones, not as an intention.

(e) The sentence. "Reusing the current account as production saves us the only genuinely dangerous part of this migration — moving the data — and it costs us nothing, because an account has no memory of what it used to be: what matters is the OU we put it in and the policies we apply to it."

Conclusion

The fifth problem is solved. MercadoFresco can recreate its entire environment from scratch with one command, and it no longer does everything in the same account.

This lesson has turned the single account into an organisation of six accounts with four organisational units. Seguridad holds what must survive an incident in the workload accounts; Infraestructura hosts the pipeline and the artefacts, resolving the paradox of what deploys production living in production; Cargas groups the three environments with one OU each, because production tolerates less than development; and Aislamiento is empty on purpose, to freeze a compromised account in a single move without destroying the evidence. Together with the pragmatic decision that holds the whole plan up: account 111122223333 becomes production, because migrating data is the only genuinely dangerous part and it can be avoided entirely.

You have the service control policies with the sentence to memorise — they never grant permissions, they only limit them — and with what that makes possible for the first time: limiting somebody who can already do everything, so that not even a production administrator can switch off trail-mercadofresco or disable Config. Four real policies: regions restricted with NotAction so as not to break the global services, the audit trail protected, encryption required in S3 with an exception by principal, and instance types and commitment purchases blocked in development. Plus the warning that keeps an afternoon of improvements from becoming an incident: one account, then one OU, then the root, reviewing AccessDenied events in CloudTrail between steps, with the management account as a lifeline because SCPs do not apply to it — and for that very reason, nobody works in the management account.

You have consolidated billing, which answers "how much does pre-production cost?" without depending on tags being applied properly, preserves aggregated volume discounts and shares Savings Plans and reserved instances across accounts (11-05). The right access model with IAM Identity Center: zero IAM users, temporary credentials, and permission sets that reflect real responsibilities — Luis with read access and permission to start the pipeline in production, which is the point where the pipeline stops being a convenience and becomes a security control; Sara with data only and in production only; and deliberately awkward emergency access, with approval and notification. And cross-account roles with sts:AssumeRole, with the trust policy and the permissions policy as two different things, both of them restrictive.

The organisation-level services close the circle: organisation CloudTrail and Config aggregator in the security account, Security Hub, GuardDuty, Access Analyzer and, above all, the ss-mercadofresco-linea-base StackSet from 09-01 with automatic deployment, which answers "how do we guarantee that the account we create six months from now has everything it should have": a person does not guarantee it, an OU does. The migration plan runs in six phases with a rule that orders them — first what gets recreated, then what gets moved, the data last and with permission — with development and pre-production deployed from the CDK rather than migrated, which is where the work of the two previous lessons pays off. And with a blocking requirement that is not technical: copying customer data between accounts is processing of personal data under the GDPR, and compliance reviews it before anybody runs anything.

With this, the course's five problems are solved. The Friday outages, with auto scaling, caching and queues. The unreliable backups, with snapshots, replication and retention. The growth into more cities, with a decoupled architecture. The risky deployments, with a pipeline that rolls back on its own in four minutes. And now the ungoverned infrastructure, with templates, constructs, tests, separate accounts and guardrails that apply themselves.

One thing remains that none of the four lessons has touched, and it becomes obvious the moment you look at the instances. The shop still runs on EC2 machines that have to be patched, with AMIs that have to be rebuilt every time a dependency changes, and with a two-minute start-up that is exactly the time there is no room for on Friday at 19:00: by the time the ASG spots the peak and launches an instance, the customers have already waited. The CDK describes those instances very well; Beanstalk managed them very well; but neither of them makes them stop existing.

In module 10, "Containers on AWS", we tackle precisely that. 10-01, "ECS and ECR", introduces container orchestration and the image registry that replaces AMIs. 10-02, "Fargate", removes the instances altogether: no servers to patch and start-up in seconds instead of minutes. And 10-03, "EKS", shows the Kubernetes alternative and when its complexity is worth it. MercadoFresco's shop is about to stop living on machines.

© Copyright 2026. All rights reserved