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
- Why a single account falls short
- Concepts: organisation, management account, OU and member accounts
- MercadoFresco's structure
- Creating and inviting accounts: email, contacts and root
- Service control policies: what they are and what they are not
- Four real SCPs for MercadoFresco
- Tag, backup and AI policies
- Consolidated billing
- Control Tower, landing zone and Account Factory
- IAM Identity Center: three people, five accounts
- Cross-account roles and
sts:AssumeRole - Organisation-level services
- MercadoFresco's migration plan
- Cost and cleanup
- Common mistakes and tips
- Exercises
- 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:
- 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.
- 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.
- 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:
Seguridadisolates 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.Infraestructurahosts what is shared between environments: the pipeline,mercadofresco-artefactosand the image repositories. Separating it avoids the paradox of the pipeline that deploys production living in production.Cargasgroups 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.Aislamientois 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 list —
aws+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.
OrganizationAccountAccessRoleis 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:DeleteBucketprevents buckets from being deleted even by the account administrator, even withAdministratorAccess. 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:
- Write the policy and review it in a PR, like any other code: SCPs live in
mercadofresco-infra. - Apply it to a single development account and run the full pipeline, a deployment and the smoke tests there.
- Promote it to the
DesarrolloOU, wait a few days and reviewAccessDeniedevents in CloudTrail. - Promote it to
Cargasor 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.
RespuestaIncidentesis deliberately awkward emergency access: it requires approval and using it fires a notification toalertas-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-1aws sso login --sso-session mercadofresco
aws s3 ls --profile mf-produccion # temporary credentials, no keys on diskCross-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=10That 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.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
