Design secure, scalable multi-account cloud foundations. Covers account structure, organizational units, service control policies, network topology, identity federation, and the patterns that make cloud environments governable from day one.
Cloud Landing Zone Design establishes a repeatable, secure, and governed foundation for deploying and operating cloud workloads across teams, environments, and regions. It matters when an organisation transitions from single projects or siloed teams to a shared, multi-team cloud platform, especially when onboarding new teams, standardising infrastructure, or scaling across multiple accounts or regions. A landing zone is not a single configuration, but a living system of policies, templates, tooling, and operational processes that ensure consistency, compliance, and velocity.
A landing zone begins with a set of core infrastructure assets, consistently provisioned and managed through declarative configuration. These assets are typically defined using Infrastructure-as-Code (IaC) and deployed via CI/CD pipelines. The foundation includes:
The heart of the landing zone is a set of reusable templates. These are typically written in Terraform, CloudFormation, or Pulumi, and stored in a version-controlled repository. Each template is a self-contained module with clear inputs and outputs.
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-${var.project_name}-vpc"
Owner = "platform-engineering"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = var.availability_zone
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-${var.project_name}-public-subnet"
Role = "public"
}
}
The main.tf file above defines a VPC with public subnets. It uses variables (var.environment, var.project_name) that are passed in at runtime. These variables are declared in variables.tf:
# terraform/variables.tf
variable "environment" {
type = string
description = "The environment: dev, staging, production"
}
variable "project_name" {
type = string
description = "Name of the project deploying into this VPC"
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
}
When a new team deploys a new service, they instantiate this module with a terraform.tfvars file:
# terraform/environments/dev.tfvars
environment = "dev"
project_name = "customer-onboarding"
cidr_block = "10.1.0.0/16"
public_subnet_cidr = "10.1.1.0/24"
availability_zone = "us-east-1a"
The landing zone enforces the use of this structure: every team must define their environment-specific variables in a tfvars file and apply the configuration via a CI pipeline.
Governance is enforced through policy as code. Policies are defined using tools like Open Policy Agent (OPA), AWS Config Rules, or Azure Policy. These policies are evaluated at infrastructure deployment time and continuously as the environment evolves.
An OPA policy file (rego) checks that all EC2 instances have at least one tag, are in a public subnet, and use an approved AMI:
# policies/iam/ensure-ami-is-approved.rego
package cloud.policy.ami
import data.aws.ami
# Ensure AMI is in the approved list
is_approved_ami(ami) {
amii = data.aws.ami.approved_ami_ids
amii[ami]
}
# Validate EC2 instance configuration
main = {
"results": [
{
"resource": resource,
"status": status,
"message": message
}
]
}
# Apply to every EC2 instance
resource = {
"type": "aws.ec2.instance",
"id": instance.id,
"name": instance.tags.Name,
"ami": instance.image_id
}
status = if all_tagged and is_public_subnet and is_approved_ami(instance.image_id) then "pass" else "fail"
message = if all_tagged and is_public_subnet and is_approved_ami(instance.image_id) then
"Instance meets AMI, tagging, and subnet requirements."
else
if not all_tagged then
"Instance missing required tags."
else if not is_public_subnet then
"Instance not in a public subnet."
else
"Instance uses unapproved AMI."
This policy is applied during CI using opa CLI:
opa eval --format pretty --data policies/iam/ensure-ami-is-approved.rego \
--input terraform/outputs.json \
'main.results'
The outputs.json file contains the state of all deployed resources. If any evaluation fails, the CI pipeline fails, blocking deployment.
A landing zone must support multiple environments, each with its own configuration. This is managed through environment-specific configuration files and a shared configuration hierarchy.
The typical layout is:
config/
├── environments/
│ ├── dev/
│ │ ├── application.json
│ │ ├── database.yml
│ │ └── secrets.yaml
│ ├── staging/
│ │ ├── application.json
│ │ └── secrets.yaml
│ └── production/
│ ├── application.json
│ └── secrets.yaml
└── shared/
├── network.yaml
└── security.yaml
Each environment inherits settings from the shared directory. A service's configuration is merged from top to bottom: shared → environment → environment-specific.
A deployment script uses jq to merge configuration:
#!/bin/bash
# deploy.sh
ENVIRONMENT=$1
CONFIG_DIR="./config/environments/$ENVIRONMENT"
# Merge shared and environment-specific config
jq -s '
[.[0] as $shared | .[1] as $env | $shared * $env]
' \
config/shared/*.yaml \
$CONFIG_DIR/*.yaml \
> merged-config.yaml
# Deploy using Helm
helm upgrade --install \
--values merged-config.yaml \
--set environment=$ENVIRONMENT \
myapp ./charts/myapp
The jq script merges YAML files from the shared and environment directories using deep merge. If a key exists in both, the environment-specific value overrides the shared one. This prevents duplication and ensures consistency.
Access to cloud resources is governed by a central IAM model. All services use a standard set of roles and policies, with cross-account access for shared services.
A new team onboarding into the landing zone follows a standard process:
The role configuration includes:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/customer-onboarding-dev"
},
"Action": "sts:AssumeRole"
}
]
}
Each application uses a standard trust-policy.json file that is versioned in the landing zone repository. When a new environment is created, the team applies the role using aws sts assume-role:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/customer-onboarding-prod \
--role-session-name "prod-deployment" \
--output json \
> session.json
The output includes AccessKeyId, SecretAccessKey, and SessionToken. These are used to configure the application environment.
The landing zone includes a set of shared services: a message broker, a centralized configuration store, a secrets manager, and a service discovery system. These services are deployed once and consumed by all teams.
A common pattern is to use AWS Systems Manager Parameter Store or Azure App Configuration for shared configuration. A service reads its configuration using the aws ssm get-parameters command:
aws ssm get-parameters \
--names \
/myapp/dev/db-host \
/myapp/dev/db-port \
/myapp/dev/api-key \
--query 'Parameters[*].{Name:Name,Value:Value}' \
--output json
The response is:
[
{
"Name": "/myapp/dev/db-host",
"Value": "db-dev.example.com"
},
{
"Name": "/myapp/dev/db-port",
"Value": "5432"
},
{
"Name": "/myapp/dev/api-key",
"Value": "sk_test_123abc"
}
]
This configuration is consumed by an application at startup. The application can also poll for updates using ssm get-parameters-by-path in a background task.
Observability is baked into the landing zone. All services emit logs in a standard format (e.g., JSON with timestamp, level, service, trace_id) and send them to a central log aggregation system. The landing zone defines a standard logging.conf for each service:
# logging.conf
[loggers]
default = "info"
myapp = "debug"
[formatters]
json = { format = "%(timestamp)s %(level)s %(service)s %(message)s %(trace_id)s" }
[handlers]
console = {
type = "stream",
formatter = "json",
level = "info"
}
[handlers]
s3 = {
type = "file",
filename = "/var/log/myapp.log",
formatter = "json",
level = "debug"
}
[loggers.myapp]
handlers = ["console", "s3"]
level = "debug"
This file is used by python logging or log4j to configure logging. The landing zone also defines a standard set of metrics, exposed via Prometheus or CloudWatch. A service exposes metrics via a /metrics endpoint:
{
"http_requests_total": {
"count": 12345,
"duration_seconds": {
"sum": 45.6,
"count": 12345,
"p95": 0.032
}
},
"database_connections": {
"current": 23,
"max": 50
}
}
The landing zone monitors these metrics with a standard set of alerts. A prometheus.yml file defines scraping jobs:
# prometheus.yml
scrape_configs:
- job_name: 'myapp'
metrics_path: '/metrics'
static_configs:
- targets: ['myapp-prod.example.com:8080']
relabel_configs:
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod_name
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
Several failure modes are common in landing zone deployments:
terraform.tfvars in their CI pipeline. The deployment uses default values but misconfigures the environment. The solution is to require a terraform plan step that validates all variables are set.Name, Project, AppName) across services. The landing zone defines a standard tags.json file that is used to validate all resources: {
"required": ["Name", "Environment", "Owner"],
"recommended": ["Team", "Project", "CostCenter"],
"pattern": {
"Name": "^[A-Za-z0-9][A-Za-z0-9\\-]+[A-Za-z0-9]$"
}
}
A script validates tags using jq and aws ec2 describe-instances:
aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,Tags]' --output json \
| jq -r '
.[] |
.[] |
.[] |
select(.Tags | any(.Key == "Name")) |
select(.Tags | any(.Key == "Environment")) |
select(.Tags | any(.Key == "Owner"))
'
policy-check step in CI that runs opa test on all policy files. {
"Code": "AccessDenied",
"Message": "User: arn:aws:iam::123456789012:user/myapp-dev is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789012:role/myapp-prod"
}
The fix is to ensure the trust policy includes the correct Principal and Action.
A landing zone is not a configuration file, but a system of interlocking components: templates, policies, identity, shared services, and observability. It enables teams to onboard quickly, deploy consistently, and operate reliably. The key to success is enforcing a small set of repeatable patterns across all teams, with clear tooling and failure feedback. The landing zone becomes the source of truth for cloud operations.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy