Introduction
The Data Landing Zone (DLZ) is a CDK construct designed to accelerate AI and data-related projects. It provides an opinionated Landing Zone, laying the foundation for a multi-account AWS strategy, so you can focus on delivering data and AI solutions.
The DLZ can be deployed in existing AWS Organizations or used in greenfield projects. It supports setups ranging from small organizations with a few accounts to large enterprises with hundreds of accounts.
The CDK construct is available in both TypeScript and Python. Example GitHub repositories showing usage:
Why do you need a Landing Zone?
Setting up your AWS environment without a Landing Zone can be an overwhelming and time-consuming process. Without the right foundation in place, you’ll face unnecessary complexity and delays. Managing multiple AWS accounts, implementing security best practices, and ensuring compliance will require a deep level of expertise and effort.
Here’s why a properly implemented Landing Zone is essential for accelerating your cloud journey:
- 👩💻 Expert Knowledge: To build a robust, scalable AWS environment, you need expertise in areas such as cloud architecture, IAM, networking, and cost optimization. A Landing Zone provides the necessary tools, templates, and automation, removing the need for constant, high-level expertise.
- 📈 Scalability: As your business grows, your cloud environment needs to grow with it. A Landing Zone ensures scalability and flexibility to meet future needs without compromising performance or security.
- ⚡ Reduce Time To Market: A well-architected Landing Zone streamlines account setup, networking, and security, ensuring a faster, more reliable deployment. Leaving you to focus on your Applications and not the cloud itself.
- 🔧 Reduced Complexity: With pre-configured guardrails and best practices, you avoid reinventing the wheel each time a new service or account is needed.
- 🔒 Security and Compliance: Landing Zones integrate security measures based on the AWS Well-Architected Framework, ensuring your environment is aligned with best practices for security, compliance, and risk management.
The vision: governance as the path of least resistance
When a new technology paradigm goes mainstream, organisations consistently deprioritise two concerns in the race to enable experimentation: FinOps and Privacy. Privacy benefits from an external forcing function — regulators, fines, GDPR. FinOps has no such forcing function. There is no FinOps regulator. The consequence is invisible until it is very visible: a cloud invoice that is two, three, or five times what was budgeted.
The DLZ encodes operational experience, regulatory awareness, and cost discipline directly into the platform — programmatically, so that knowledge travels with every workload built on top of it. The latest standards and best practices for security, privacy, and FinOps come off the shelf, because they’re already in the platform.
The DLZ is designed to make three distinct actors genuinely happy at the same time:
- CFO & Finance — Cost controls are not bolted on after the fact. Every workload is financially observable from day one. Budgets and guardrails are provisioned as code.
- Compliance Office — Built with a compliance mindset from the ground up. GDPR alignment, data classification, access control, and audit trails are DLZ defaults, not workload-level responsibilities.
- Engineers — Developer experience is a first-class priority. Productivity is not sacrificed on the altar of governance.
Design principle: Governance should be the path of least resistance, not an obstacle course. An engineer using the DLZ should find it easier to do the right thing than to bypass it — not because we block them, but because we made the right thing the default.
FinOps as a first-class pillar
FinOps is the pillar that most consistently gets left behind. In the DLZ it isn’t. Every data and AI workload running on the platform is financially observable, attributable to a team or product, bounded by a configurable guardrail, and connected to the value it generates. No manual reconciliation. No end-of-month surprises.
The capability is delivered in four layers, each building on the one below.
Layer 1 — Instrumentation
Every service emits cost and usage data in a consistent, queryable form. AWS Cost and Usage Report (CUR 2.0) is the master ledger, centralised in a dedicated FinOps account, catalogued in Glue, and queryable from Athena.
- LLM / Bedrock — Inference costs are attributed to the calling IAM principal so spend can be split by product or team.
- Storage — Every storage resource is tagged at creation. Cost visibility at the data domain level (raw, curated, serving) is a requirement, not an optimisation.
- ETL and compute — Job-level cost attribution, not just cluster- or application-level.
- Shared services — Resources shared across teams carry a proportional allocation so every euro lands on an owner.
Layer 2 — Attribution
Tags are the connective tissue between AWS resource costs and your organisational structure. The DLZ enforces a mandatory tag set at deploy time — not retrofitted, not by convention. Resources that cannot be attributed to an owner do not deploy to production.
| Tag | Purpose |
|---|---|
team | Owning team |
project | Workload or product identifier |
env | Environment (dev / staging / prod) |
domain | Data domain (raw / curated / serving / inference) |
cost-center | Finance cost centre for chargeback |
Enforcement layers compose: Organization Tag Policy, SCP at deploy time, AWS Config rule for detection, and the DLZ’s own validation before synthesis.
Layer 3 — Guardrails
Visibility without action is just a dashboard. The DLZ ships two guardrail types:
- Soft guardrails — Alert when a threshold is crossed (e.g. 80% of budget). Used for early warning.
- Hard guardrails — Block the workload or request when a limit is reached. Used for absolute caps.
Budget guardrails are defined per team and per project, committed as code in the DLZ repository, and applied automatically. LLM inference gets special handling: an LLM gateway enforces per-team and per-project token-spend caps in real time, blocking requests before they reach the model when a limit is exceeded — because AWS budget alerts alone are not fast enough for token-priced workloads. Anomaly detection runs on every major service category to catch unexpected spend before the billing cycle closes, not after.
Layer 4 — Reporting
The reporting layer serves three audiences: engineers (cost impact of their code), product owners (cost-centre accountability), and finance (reliable monthly actuals).
- Platform cost overview — Total DLZ spend over time, broken down by service and team. Refreshed daily.
- Team showback — Per-team spend versus budget with trend, available to team leads without filing a ticket.
- LLM spend detail — Per-model, per-team, per-project token usage and cost.
- Anomaly log — A running record of threshold breaches and anomaly detections, with resolution status.
The most useful metric is cost-per-unit-of-work: the ratio of spend to a meaningful business outcome. For a data and AI platform the relevant units are inferences, documents processed, GBs processed, and queries run. Once unit economics are visible, optimisation conversations become concrete rather than abstract.
Non-negotiables
These are not suggestions. They are constraints the DLZ satisfies from day one:
- Tag enforcement before production. No production workload deploys without the mandatory tag set. Enforced by the platform, not relied upon by convention.
- Cost signals are centralised. Every service’s cost data flows into a single queryable store. Siloed billing views are not acceptable.
- Guardrails are code. Budget limits live in the repository alongside the infrastructure they govern. Reviewed, versioned, and deployed the same way.
- Engineers are not blocked by governance. If a governance requirement prevents an engineer from doing their job, that’s a DLZ design failure — not an engineer problem.
See Tagging for the mandatory tag set and how it is enforced, and Budgets for budget guardrails as code.
Key Features
Notable features of the DLZ include:
- A CDK construct that can be used in TypeScript and Python.
- Opinionated but configurable and extendable. Each stack and nested component can be extended if needed.
- Leverages AWS Control Tower. Works with existing or new Control Tower setups.
- Suitable for greenfield projects or integration with existing AWS resources.
- Deployable from any build system — GitHub, GitLab, Jenkins, or locally.
- Generates compliance reports covering Control Tower Standards, Security Hub controls, AWS Config rules, and Service Control Policies enabled in each account.
- Implements an internal wave- and stage-based deployment strategy for dependency management.
- FinOps baked in: CUR 2.0 centralised in a dedicated FinOps account, mandatory cost-allocation tags enforced at deploy time, budgets and anomaly detection as code, and an LLM gateway with per-team token-spend caps.
- Compliance by default: GDPR alignment, data classification, audit trails, and least-privilege baselines ship with the platform — not as workload-level homework.
- Engineer-friendly governance: Defaults and automation, not bureaucratic checkpoints. Guardrails accelerate good engineering rather than obstruct it.
- Includes helper scripts and Standard Operating Procedures (SOPs) for routine tasks.
- Accompanied by comprehensive documentation.
How it works
The DLZ defines all accounts and resources in a single CDK construct that can be used in Typescript or Python CDK project.
The DLZ deploys resources into specified AWS accounts. These accounts must be created manually (SOP provided) in the
workloads OU, and their Account IDs must be provided in the DLZ configuration. Each account is designated
as either development or production to enable controlled, staged deployments, including the option for manual
approvals in production environments.
import {App} from 'aws-cdk-lib';import { DataLandingZone, Defaults } from 'aws-data-landing-zone';
const app = new App();const dlz = new DataLandingZone(app, { ... regions: { global: Region.EU_WEST_1, regional: [Region.US_EAST_1], }, budgets: [ ...Defaults.budgets(100, 20, { slack: slackBudgetNotifications, emails: ['you@org.com'], }), ], denyServiceList: [ ...Defaults.denyServiceList(), 'ecs:*', ], organization: { organizationId: 'o-0f5h921gk9', root: { accounts: { management: { accountId: '123456789012', }, }, }, ous: { workloads: { ouId: 'ou-h2l0-gjr36ikn', accounts: [{ name: 'development', accountId: '123456789012', type: DlzAccountType.DEVELOP, vpcs: [ Defaults.vpcClassB3Private3Public(0, Region.EU_WEST_1), // CIDR 10.0.0./19 Defaults.vpcClassB3Private3Public(1, Region.US_EAST_1), // CIDR 10.1.0./19 ] },{ name: 'production', accountId: '123456789012', type: DlzAccountType.PRODUCTION, ... },
...AS MANY ACCOUNTS AS DESIRED... ] }, }, ... }, network: { nats: [ { name: "development-eu-west-1-internet-access", location: new NetworkAddress('development', Region.EU_WEST_1, 'default', 'public', 'public-1'), allowAccessFrom: [ new NetworkAddress('development', Region.EU_WEST_1, 'default', 'private') ], type: { gateway: { eip: ... //Optional } } }, ], bastionHosts: [ { name: 'default', location: new NetworkAddress('development', Region.EU_WEST_1, 'default', 'private', 'private-1'), instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO), } ] }});import aws_cdk as cdkimport aws_data_landing_zone as dlz
app = cdk.App()dlz.DataLandingZone(app, ... regions=dlz.DlzRegions( global_=dlz.Region.EU_WEST_1, regional=[dlz.Region.US_EAST_1], ), budgets=[ *dlz.Defaults.budgets( 100, 20, slack=slack_budget_notifications, emails=["you@org.com"], ), ], deny_service_list=[ *dlz.Defaults.deny_service_list(), "ecs:*" ], organization=dlz.DLzOrganization( organization_id='o-0f5h921gk9', root=dlz.RootOptions( accounts=dlz.OrgRootAccounts( management=dlz.DLzManagementAccount(account_id='123456789012'), ), ), ous=dlz.OrgOus( workloads=dlz.OrgOuWorkloads( ou_id='ou-h2l0-gjr36ikn', accounts=[ dlz.DLzAccount( name='development', account_id='123456789012', type=dlz.DlzAccountType.DEVELOP, vpcs: [ dlz.Defaults.vpc_class_b3_private3_public(0, dlz.Region.EU_WEST_1), # CIDR 10.0.0./19 dlz.Defaults.vpc_class_b3_private3_public(1, dlz.Region.US_EAST_1), # CIDR 10.1.0./19 ] ), dlz.DLzAccount( name='production', account_id='123456789012', type=dlz.DlzAccountType.PRODUCTION, ), ], ), ) ), network={ "nats": [ { "name": "development-eu-west-1-internet-access", "location": NetworkAddress( "development", str(Region.EU_WEST_1), "default", "public", "public-1", ), "allow_access_from": [ NetworkAddress( "development", str(Region.EU_WEST_1), "default", "private" ), ], "type": { "gateway": { }, }, }, ], "bastion_hosts": [ { "name": "default", "location": NetworkAddress( "development", str(Region.EU_WEST_1), "default", "public", "public-1", ), "instance_type": ec2.InstanceType.of( ec2.InstanceClass.T3, ec2.InstanceSize.MICRO ), } ] })Intended Audience
The DLZ handles the responsibilities and routine tasks typically managed by a Cloud Center of Excellence (CCoE) team. It’s easy enough for Data Engineers to use on their own, but flexible enough to be customized and fine-tuned by experienced Cloud Engineers.
It’s important to establish the following responsibilities to understand what is in and out of scope for the DLZ:
- In scope - Any CCoE responsibilities and tasks, such as account management, security, networking, compliance, etc.
- Out of scope - Application development, data engineering, and data science tasks.
Core Principles
The DLZ adheres to the following principles:
- Opinionated but configurable defaults to suit diverse needs.
- High levels of automation with manual SOPs where needed.
- Simplicity and ease of understanding over complexity.
- Focused scope, limited to Landing Zone responsibilities.
- Governance as the path of least resistance — the right thing is the default.
- CFO, Compliance, and Engineers are all first-class stakeholders.
Integrated AWS Services
- AWS Organizations: Seamless multi-account management.
- AWS Budgets: Per-team and per-project budgets defined as code, with soft alerts at 80% and hard actions at 100%. Notifications via Slack, Teams, or email.
- AWS Cost and Usage Report (CUR 2.0): Centralised in a dedicated FinOps account with a Glue catalog and Athena workgroup — the master ledger for all cost queries.
- AWS Cost Anomaly Detection: Active across major service categories to catch unexpected spend patterns before the billing cycle closes.
- Cost Allocation Tags: Mandatory tag set (
team,project,env,domain,cost-center) activated automatically and enforced at deploy time. - LLM Gateway: Per-team and per-project token-spend caps, with real-time blocking before requests reach the model. Required because token-based costs spike non-linearly.
- Service Control Policies (SCPs): Manage service deny lists; enforce tag presence at deploy time.
- Tag Policies: Enforce resource tagging for spend tracking and ownership.
- Control Tower Controls: Opinionated defaults for preventive, detective, and proactive standards.
- AWS Security Hub and AWS Config: Additional standards not covered by Control Tower. Notifications delivered via Slack, Teams, or email.
- Network Management:
- Non-overlapping VPCs across accounts.
- NAT Gateways or instances for outbound private access.
- VPC Peering for cross-account/region communication.
- Bastion Hosts with AWS SSM for secure private resource access.
- Simplified routing configurations.
- IAM Identity Center (SSO):
- User management with AWS Identity Store or external IDPs (e.g., Google, Active Directory).
- Manage Permission Sets
- Manage Access Groups to assign Users to Accounts with a given Permission Set
- Permission Boundaries: Prevent privilege escalation for all IAM roles and users.
- Configure Lake Formation for Data Lake management, including:
- General settings, like admins, version and setting hybrid iam mode
- Create Tags and specify their permissions for use within the same account and when sharing to other accounts.
- Optionally, set Tag Permissions on Tags. This is usually out of scope for DLZ, but can be configured.
Internal Structure
The construct processes the provided configuration to create logical deployment layers for resources. Each layer
consists of a Global Wave and optionally one or more Regional Waves. This structure ensures that all global stacks
for the accounts are deployed before the regional stacks. Resources in the global stack are region-independent, such
as IAM roles, whereas resources like VPCs can be deployed in both global and regional stacks if configured as such.
Dependencies between regions and accounts are managed by separating stacks into waves. This ensures that any specified SSM Parameter value is available for use in subsequent waves.
The cdk-express-pipeline is utilized to create Waves and Stages, enabling the controlled deployment of resources as described above. It allows deployments from any build systems, including GitHub, GitLab, Jenkins, and even local environments.
The construct processes the configuration to provision the appropriate resources in the required accounts.
For example, the following config enables VPC Peering from both the development VPCs in eu-west-1 and us-east-1,
all subnets, to the private subnets in the production account’s VPC in the eu-west-1 region.
import {App} from 'aws-cdk-lib';import { DataLandingZone } from 'aws-data-landing-zone';
const app = new App();const dlz = new DataLandingZone(app, { regions: { global: Region.EU_WEST_1, regional: [Region.US_EAST_1], }, organization: { ous: { workloads: { ouId: 'ou-h2l0-gjr36ikn', accounts: [{ name: 'development', vpcs: [ { name: 'default', region: Region.EU_WEST_1, cidr: '10.0.0.0/16', routeTables: [ { name: 'private', subnets: [ ... ] }, { name: 'public', subnets: [ ... ] } ], }, { name: 'default', region: Region.US_EAST_1, cidr: '10.1.0.0/16', routeTables: [ { name: 'private', subnets: [ ... ] }, { name: 'public', subnets: [ ... ] } ], }, ], },{ name: 'production', vpcs: [ { name: 'default', region: Region.EU_WEST_1, cidr: '10.2.0.0/16', routeTables: [ { name: 'private', subnets: [ ... ] }, { name: 'public', subnets: [ ... ] } ], }, ], ... }] }, }, ... }, network: { connections: { vpcPeering: [{ source: NetworkAddress.fromString('development'), destination: NetworkAddress.fromString('production.us-east-1.default.private'), }], }, }, ...});import aws_cdk as cdkimport aws_data_landing_zone as dlz
app = cdk.App()dlz.DataLandingZone(app, regions=dlz.DlzRegions( global_=dlz.Region.EU_WEST_1, regional=[dlz.Region.US_EAST_1], ), organization=dlz.DLzOrganization( ous=dlz.OrgOus( workloads=dlz.OrgOuWorkloads( ou_id='ou-vh4d-nc2zzf9z', accounts=[ dlz.DLzAccount( name='development', vpcs=[ dlz.DlzVpcProps( name='default', region=dlz.Region.EU_WEST_1, cidr='10.0.0.0/16', route_tables=[ dlz.DlzRouteTableProps( name='private', subnets=[ ... ], ), dlz.DlzRouteTableProps( name='public', subnets=[ ...], ) ] ), dlz.DlzVpcProps( name='default', region=dlz.Region.US_EAST_1, cidr='10.1.0.0/16', route_tables=[ dlz.DlzRouteTableProps( name='private', subnets=[ ... ], ), dlz.DlzRouteTableProps( name='public', subnets=[ ...], ) ] ) ], ), dlz.DLzAccount( name='production', vpcs=[ dlz.DlzVpcProps( name='default', region=dlz.Region.EU_WEST_1, cidr='10.2.0.0/16', route_tables=[ dlz.DlzRouteTableProps( name='private', subnets=[ ... ], ), dlz.DlzRouteTableProps( name='public', subnets=[ ...], ) ] ), ], ), ], ), suspended=dlz.OrgOuSuspended( ou_id='ou-vh4d-rhcmhzsy', ), ) ), network=dlz.Network( connections=dlz.NetworkConnection( vpc_peering=[ dlz.NetworkConnectionVpcPeering( source=dlz.NetworkAddress.from_string('development'), target=dlz.NetworkAddress.from_string('production.us-east-1.default.private') ) ] ) ))The DLZ abstracts the complexity of the AWS resources and ensures that the correct resources are created in the correct accounts in the right order. For instance, this example provisions the VPCs in the Base Layer and sets up the many VPC Peering roles, connections, and routes within the Networking Layers.