Skip to content

Application Landing Zone Template

Related: README | Connectivity Landing Zone | EA & Subscription Architecture

Overview

This document provides a template and guidelines for application teams to deploy workloads into their landing zone subscriptions. The template includes baseline infrastructure, networking, security controls, and CI/CD patterns.

Application Landing Zone Architecture

graph TB
    subgraph "Application Landing Zone Subscription"
        subgraph "Networking"
            RG_NET[rg-{app}-network-{region}]
            SPOKE[Spoke VNet]
            SNET_APP[App Subnet]
            SNET_DATA[Data Subnet]
            SNET_PE[Private Endpoints]
            NSG_APP[NSG - App]
            NSG_DATA[NSG - Data]
        end

        subgraph "Shared Resources"
            RG_SHARED[rg-{app}-shared-{region}]
            KV[Key Vault]
            ST[Storage Account]
            APPI[App Insights]
        end

        subgraph "Application Resources"
            RG_APP[rg-{app}-app-{region}]
            ACA[Container Apps / AKS]
            ACR[Container Registry]
        end

        subgraph "Data Resources"
            RG_DATA[rg-{app}-data-{region}]
            SQL[(Azure SQL)]
            COSMOS[(Cosmos DB)]
            REDIS[(Redis Cache)]
        end
    end

    subgraph "Platform (Connectivity Subscription)"
        HUB[Hub VNet]
        DNS[Private DNS Zones]
        AFD[Azure Front Door]
    end

    SPOKE <-->|Peering| HUB
    PE --> DNS
    AFD --> ACA

    style SPOKE fill:#e3f2fd
    style ACA fill:#c8e6c9

Subscription Baseline

When an application team receives a vended subscription, the following is automatically provisioned:

Inherited from Management Groups

Configuration Source Details
Azure Policies Landing Zones MG Tag requirements, diagnostics, security
RBAC Subscription vending Team permissions
Budget alerts Automated Based on request
Diagnostic settings Policy (DINE) Activity logs to central LA

Provisioned by Vending Process

Resource Name Pattern Purpose
Resource Group rg-{app}-network-{region} Networking resources
VNet vnet-spoke-{app}-{region}-001 Application network
VNet Peering peer-to-hub-{region} Connectivity to hub
Diagnostic Setting diag-activity-to-law Activity log forwarding

Resource Group Structure

resource_groups:
  # Networking - managed by platform team initially
  - name: "rg-{app}-network-{region}"
    purpose: "VNet, subnets, NSGs, route tables"
    owner: "Platform Team (initial), App Team (ongoing)"

  # Shared resources - application team
  - name: "rg-{app}-shared-{region}"
    purpose: "Key Vault, Storage, App Insights"
    owner: "Application Team"

  # Application compute - application team
  - name: "rg-{app}-app-{region}"
    purpose: "Container Apps, AKS, App Service"
    owner: "Application Team"

  # Data resources - application team
  - name: "rg-{app}-data-{region}"
    purpose: "Databases, caches, queues"
    owner: "Application Team"

Network Configuration

Spoke VNet Template

spoke_virtual_network:
  name: "vnet-spoke-{app}-{region}-001"
  address_space: ["10.{x}.0.0/16"]  # Allocated during vending

  subnets:
    - name: "snet-app"
      address_prefix: "10.{x}.0.0/24"
      delegations: []  # Or container apps delegation
      network_security_group: "nsg-{app}-app-{region}"
      service_endpoints:
        - "Microsoft.Storage"
        - "Microsoft.KeyVault"
        - "Microsoft.Sql"

    - name: "snet-containerapp"
      address_prefix: "10.{x}.1.0/23"  # /23 for Container Apps
      delegations:
        - name: "Microsoft.App/environments"
          service: "Microsoft.App/environments"
          actions:
            - "Microsoft.Network/virtualNetworks/subnets/join/action"
      network_security_group: "nsg-{app}-containerapp-{region}"

    - name: "snet-data"
      address_prefix: "10.{x}.10.0/24"
      network_security_group: "nsg-{app}-data-{region}"

    - name: "snet-privateendpoints"
      address_prefix: "10.{x}.20.0/24"
      private_endpoint_network_policies: "Disabled"
      network_security_group: "nsg-{app}-privateendpoints-{region}"

NSG Rules for Application Subnet

nsg_rules:
  inbound:
    - name: "Allow-FrontDoor"
      priority: 100
      source_address_prefix: "AzureFrontDoor.Backend"
      destination_port_ranges: ["443", "80"]
      access: "Allow"

    - name: "Allow-Hub-Management"
      priority: 200
      source_address_prefix: "10.0.0.0/16"  # Hub VNet
      destination_port_ranges: ["443", "22", "3389"]
      access: "Allow"

    - name: "Allow-VNet-Internal"
      priority: 300
      source_address_prefix: "VirtualNetwork"
      destination_port_range: "*"
      access: "Allow"

    - name: "Deny-All-Inbound"
      priority: 4096
      source_address_prefix: "*"
      destination_port_range: "*"
      access: "Deny"

  outbound:
    - name: "Allow-Internet-HTTPS"
      priority: 100
      destination_address_prefix: "Internet"
      destination_port_range: "443"
      access: "Allow"

    - name: "Allow-Azure-Services"
      priority: 200
      destination_address_prefix: "AzureCloud"
      destination_port_ranges: ["443", "1433", "5432", "6379"]
      access: "Allow"

Shared Resources Template

Key Vault

key_vault:
  name: "kv-{app}-{env}-{region}-001"  # Max 24 chars
  resource_group: "rg-{app}-shared-{region}"

  sku: "standard"

  # Security configuration
  tenant_id: "{tenant-id}"
  soft_delete_retention_days: 90
  purge_protection_enabled: true

  # Network configuration
  public_network_access_enabled: false
  network_acls:
    default_action: "Deny"
    bypass: "AzureServices"

  # RBAC (preferred over access policies)
  enable_rbac_authorization: true

  # Private endpoint
  private_endpoint:
    name: "pe-{app}-kv-{region}"
    subnet_id: "/subscriptions/.../snet-privateendpoints"
    private_dns_zone_id: "/subscriptions/.../privatelink.vaultcore.azure.net"

Application Insights

application_insights:
  name: "appi-{app}-{env}-{region}-001"
  resource_group: "rg-{app}-shared-{region}"

  application_type: "web"

  # Connect to central Log Analytics
  workspace_id: "/subscriptions/.../law-platform-{region}-001"

  # Sampling (cost optimization)
  sampling_percentage: 100  # Reduce in production if needed

  # Disable IP masking for troubleshooting (evaluate privacy)
  disable_ip_masking: false

  # Tags
  tags:
    Application: "{app}"
    Environment: "{env}"

Storage Account

storage_account:
  name: "st{app}{env}{region}001"  # No hyphens, max 24 chars
  resource_group: "rg-{app}-shared-{region}"

  account_tier: "Standard"
  account_replication_type: "GRS"  # Geo-redundant
  account_kind: "StorageV2"

  # Security
  min_tls_version: "TLS1_2"
  allow_nested_items_to_be_public: false
  shared_access_key_enabled: false  # Use Entra ID
  public_network_access_enabled: false

  # Blob properties
  blob_properties:
    versioning_enabled: true
    delete_retention_policy:
      days: 30
    container_delete_retention_policy:
      days: 30

  # Private endpoint
  private_endpoints:
    - name: "pe-{app}-blob-{region}"
      subresource: "blob"
    - name: "pe-{app}-file-{region}"
      subresource: "file"

Compute Options

graph TB
    subgraph "Container Apps Environment"
        ENV[Managed Environment<br/>cae-{app}-{env}-{region}]

        subgraph "Container Apps"
            API[API Service<br/>ca-api-{app}]
            WORKER[Background Worker<br/>ca-worker-{app}]
            WEB[Web Frontend<br/>ca-web-{app}]
        end

        subgraph "Configuration"
            DAPR[Dapr Components]
            SECRETS[Secrets from Key Vault]
            SCALE[Scale Rules]
        end
    end

    ENV --> API
    ENV --> WORKER
    ENV --> WEB

    DAPR --> API
    SECRETS --> API
    SCALE --> API
container_apps_environment:
  name: "cae-{app}-{env}-{region}-001"
  resource_group: "rg-{app}-app-{region}"

  # Networking
  infrastructure_subnet_id: "/subscriptions/.../snet-containerapp"
  internal_load_balancer_enabled: true

  # Workload profiles (consumption or dedicated)
  workload_profile:
    - name: "Consumption"
      workload_profile_type: "Consumption"

  # Log Analytics connection
  log_analytics_workspace_id: "/subscriptions/.../law-platform-{region}"

container_app:
  name: "ca-api-{app}-{env}"

  # Container configuration
  template:
    containers:
      - name: "api"
        image: "{acr}.azurecr.io/{app}/api:latest"
        cpu: 0.5
        memory: "1Gi"

        env:
          - name: "APPLICATIONINSIGHTS_CONNECTION_STRING"
            secret_ref: "appinsights-connection"
          - name: "DATABASE_CONNECTION"
            secret_ref: "db-connection"

    # Scaling
    min_replicas: 2
    max_replicas: 10

    scale:
      rules:
        - name: "http-scaling"
          http:
            metadata:
              concurrentRequests: "100"

  # Ingress
  ingress:
    external: false  # Internal only, use Front Door for external
    target_port: 8080
    transport: "http"

  # Identity
  identity:
    type: "SystemAssigned"

Option 2: Azure Kubernetes Service (AKS)

aks_cluster:
  name: "aks-{app}-{env}-{region}-001"
  resource_group: "rg-{app}-app-{region}"

  # Kubernetes version
  kubernetes_version: "1.29"

  # Identity
  identity:
    type: "SystemAssigned"

  # Network configuration
  network_profile:
    network_plugin: "azure"
    network_policy: "azure"  # Or "calico"
    service_cidr: "10.{x}.200.0/24"
    dns_service_ip: "10.{x}.200.10"

  # Default node pool
  default_node_pool:
    name: "system"
    vm_size: "Standard_D4s_v5"
    node_count: 3
    vnet_subnet_id: "/subscriptions/.../snet-aks"
    zones: ["1", "2", "3"]

  # Additional node pools
  node_pools:
    - name: "workload"
      vm_size: "Standard_D8s_v5"
      min_count: 2
      max_count: 10
      enable_auto_scaling: true

  # Add-ons
  addon_profile:
    azure_policy:
      enabled: true
    oms_agent:
      enabled: true
      log_analytics_workspace_id: "/subscriptions/.../law-platform-{region}"

Data Services

Azure SQL Database

sql_server:
  name: "sql-{app}-{env}-{region}-001"
  resource_group: "rg-{app}-data-{region}"

  version: "12.0"

  # Authentication
  administrator_login: "sqladmin"
  administrator_login_password: "from-key-vault"

  # Entra ID admin (recommended)
  azuread_administrator:
    login_username: "SQL Admins Group"
    object_id: "{group-object-id}"

  # Security
  minimum_tls_version: "1.2"
  public_network_access_enabled: false

sql_database:
  name: "sqldb-{app}-{env}"

  sku:
    name: "GP_S_Gen5_2"  # Serverless
    tier: "GeneralPurpose"

  # Serverless configuration
  auto_pause_delay_in_minutes: 60
  min_capacity: 0.5
  max_size_gb: 32

  # Backup
  short_term_retention_days: 7
  long_term_retention:
    weekly_retention: "P4W"
    monthly_retention: "P12M"
    yearly_retention: "P5Y"

Cosmos DB

cosmosdb_account:
  name: "cosmos-{app}-{env}-{region}"
  resource_group: "rg-{app}-data-{region}"

  offer_type: "Standard"
  kind: "GlobalDocumentDB"

  # Multi-region (for production)
  geo_locations:
    - location: "eastus2"
      failover_priority: 0
    - location: "westus2"
      failover_priority: 1

  # Consistency
  consistency_policy:
    consistency_level: "Session"

  # Networking
  public_network_access_enabled: false
  is_virtual_network_filter_enabled: true

  # Backup
  backup:
    type: "Continuous"
    tier: "Continuous7Days"

Tagging Requirements

All resources must include these tags (enforced by Azure Policy):

required_tags:
  Environment: "{Production|Development|Staging|Sandbox}"
  CostCenter: "{cost-center-code}"
  Owner: "{team-email}"
  Application: "{application-name}"
  DataClassification: "{Public|Internal|Confidential|Restricted}"

recommended_tags:
  Project: "{project-code}"
  CreatedBy: "terraform"
  Repository: "{github-repo-url}"

Naming Convention Summary

Resource Pattern Example
Resource Group rg-{app}-{purpose}-{region} rg-saas-app-eastus2
Virtual Network vnet-spoke-{app}-{region}-{###} vnet-spoke-saas-eastus2-001
Subnet snet-{purpose} snet-app, snet-data
NSG nsg-{app}-{purpose}-{region} nsg-saas-app-eastus2
Key Vault kv-{app}-{env}-{region}-{###} kv-saas-prod-eus2-001
Storage Account st{app}{env}{region}{###} stsaasprodeus2001
Container App Env cae-{app}-{env}-{region}-{###} cae-saas-prod-eastus2-001
Container App ca-{service}-{app}-{env} ca-api-saas-prod
SQL Server sql-{app}-{env}-{region}-{###} sql-saas-prod-eastus2-001
Cosmos DB cosmos-{app}-{env}-{region} cosmos-saas-prod-eastus2
App Insights appi-{app}-{env}-{region}-{###} appi-saas-prod-eastus2-001

Sample Terraform Module

Application Landing Zone Module

# modules/app-landing-zone/main.tf

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

variable "app_name" {
  description = "Application name"
  type        = string
}

variable "environment" {
  description = "Environment (prod, dev, staging)"
  type        = string
}

variable "location" {
  description = "Azure region"
  type        = string
}

variable "spoke_address_space" {
  description = "VNet address space"
  type        = list(string)
}

variable "hub_vnet_id" {
  description = "Hub VNet ID for peering"
  type        = string
}

variable "log_analytics_workspace_id" {
  description = "Central Log Analytics workspace ID"
  type        = string
}

variable "private_dns_zone_ids" {
  description = "Map of private DNS zone IDs"
  type        = map(string)
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
}

locals {
  name_prefix = "${var.app_name}-${var.environment}"
}

# Resource Groups
resource "azurerm_resource_group" "network" {
  name     = "rg-${local.name_prefix}-network-${var.location}"
  location = var.location
  tags     = var.tags
}

resource "azurerm_resource_group" "shared" {
  name     = "rg-${local.name_prefix}-shared-${var.location}"
  location = var.location
  tags     = var.tags
}

resource "azurerm_resource_group" "app" {
  name     = "rg-${local.name_prefix}-app-${var.location}"
  location = var.location
  tags     = var.tags
}

resource "azurerm_resource_group" "data" {
  name     = "rg-${local.name_prefix}-data-${var.location}"
  location = var.location
  tags     = var.tags
}

# Virtual Network
resource "azurerm_virtual_network" "spoke" {
  name                = "vnet-spoke-${local.name_prefix}-${var.location}-001"
  location            = var.location
  resource_group_name = azurerm_resource_group.network.name
  address_space       = var.spoke_address_space
  tags                = var.tags
}

# Subnets
resource "azurerm_subnet" "app" {
  name                 = "snet-app"
  resource_group_name  = azurerm_resource_group.network.name
  virtual_network_name = azurerm_virtual_network.spoke.name
  address_prefixes     = [cidrsubnet(var.spoke_address_space[0], 8, 0)]

  service_endpoints = ["Microsoft.Storage", "Microsoft.KeyVault"]
}

resource "azurerm_subnet" "containerapp" {
  name                 = "snet-containerapp"
  resource_group_name  = azurerm_resource_group.network.name
  virtual_network_name = azurerm_virtual_network.spoke.name
  address_prefixes     = [cidrsubnet(var.spoke_address_space[0], 7, 1)]

  delegation {
    name = "containerapp"
    service_delegation {
      name    = "Microsoft.App/environments"
      actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
    }
  }
}

resource "azurerm_subnet" "privateendpoints" {
  name                              = "snet-privateendpoints"
  resource_group_name               = azurerm_resource_group.network.name
  virtual_network_name              = azurerm_virtual_network.spoke.name
  address_prefixes                  = [cidrsubnet(var.spoke_address_space[0], 8, 20)]
  private_endpoint_network_policies = "Disabled"
}

# VNet Peering to Hub
resource "azurerm_virtual_network_peering" "spoke_to_hub" {
  name                      = "peer-to-hub"
  resource_group_name       = azurerm_resource_group.network.name
  virtual_network_name      = azurerm_virtual_network.spoke.name
  remote_virtual_network_id = var.hub_vnet_id

  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
}

# Key Vault
resource "azurerm_key_vault" "main" {
  name                = "kv-${var.app_name}-${var.environment}-${substr(var.location, 0, 4)}"
  location            = var.location
  resource_group_name = azurerm_resource_group.shared.name
  tenant_id           = data.azurerm_client_config.current.tenant_id
  sku_name            = "standard"

  purge_protection_enabled   = true
  soft_delete_retention_days = 90
  enable_rbac_authorization  = true

  public_network_access_enabled = false

  tags = var.tags
}

# Key Vault Private Endpoint
resource "azurerm_private_endpoint" "keyvault" {
  name                = "pe-${var.app_name}-kv-${var.location}"
  location            = var.location
  resource_group_name = azurerm_resource_group.shared.name
  subnet_id           = azurerm_subnet.privateendpoints.id

  private_service_connection {
    name                           = "psc-keyvault"
    private_connection_resource_id = azurerm_key_vault.main.id
    subresource_names              = ["vault"]
    is_manual_connection           = false
  }

  private_dns_zone_group {
    name                 = "pdzg-keyvault"
    private_dns_zone_ids = [var.private_dns_zone_ids["keyvault"]]
  }

  tags = var.tags
}

# Application Insights
resource "azurerm_application_insights" "main" {
  name                = "appi-${local.name_prefix}-${var.location}-001"
  location            = var.location
  resource_group_name = azurerm_resource_group.shared.name
  workspace_id        = var.log_analytics_workspace_id
  application_type    = "web"

  tags = var.tags
}

# Outputs
output "spoke_vnet_id" {
  value = azurerm_virtual_network.spoke.id
}

output "key_vault_id" {
  value = azurerm_key_vault.main.id
}

output "key_vault_uri" {
  value = azurerm_key_vault.main.vault_uri
}

output "application_insights_connection_string" {
  value     = azurerm_application_insights.main.connection_string
  sensitive = true
}

data "azurerm_client_config" "current" {}

Documentation Template for Application Teams

Application teams should maintain a README.md in their infrastructure repository with the following structure:

# {Application Name} Infrastructure

## Overview
Brief description of the application and its purpose.

## Architecture
{Insert architecture diagram}

## Azure Resources

| Resource | Name | Purpose |
|----------|------|---------|
| Subscription | sub-{app}-{env} | Application hosting |
| Resource Groups | rg-{app}-* | Resource organization |
| Container Apps | ca-{app}-* | Application compute |
| Key Vault | kv-{app}-* | Secrets management |
| Cosmos DB | cosmos-{app}-* | Data persistence |

## Network Configuration

| Subnet | CIDR | Purpose |
|--------|------|---------|
| snet-app | 10.x.0.0/24 | Application workloads |
| snet-data | 10.x.10.0/24 | Data services |
| snet-privateendpoints | 10.x.20.0/24 | Private endpoints |

## Deployment

### Prerequisites
- Azure CLI v2.x+
- Terraform v1.7.0+
- Access to application landing zone subscription

### Steps
1. Clone this repository
2. Run `terraform init`
3. Run `terraform plan -var-file="environments/{env}.tfvars"`
4. Submit PR for review
5. After approval, merge to trigger apply

## Monitoring

- **Application Insights**: {workspace-name}
- **Log Analytics**: {workspace-id}
- **Alerts**: Configured via Azure Monitor

## Security

- [ ] All secrets stored in Key Vault
- [ ] Private endpoints for PaaS services
- [ ] NSG rules reviewed
- [ ] Managed identity used (no service principal secrets)

## Contacts

| Role | Team/Person | Contact |
|------|-------------|---------|
| Owner | {team-name} | {email} |
| Platform Support | Platform Team | platform@company.com |

## References
- [Application Landing Zone Guide](./07-application-landing-zone.md)
- [Company Azure Standards](./README.md)

Onboarding Checklist

Application teams should complete this checklist when setting up their landing zone:

Pre-Deployment

  • [ ] Submit subscription request via vending process
  • [ ] Receive subscription with baseline configuration
  • [ ] Verify VNet peering to hub is active
  • [ ] Confirm diagnostic settings are in place

Shared Resources

  • [ ] Deploy Key Vault with private endpoint
  • [ ] Store database connection strings in Key Vault
  • [ ] Configure Application Insights
  • [ ] Set up Storage Account (if needed)

Networking

  • [ ] Review and customize NSG rules
  • [ ] Create additional subnets if needed
  • [ ] Create private endpoints for PaaS services
  • [ ] Verify DNS resolution for private endpoints

Application Deployment

  • [ ] Deploy Container Apps Environment or AKS
  • [ ] Configure managed identity
  • [ ] Assign Key Vault access to managed identity
  • [ ] Deploy application containers

Security & Compliance

  • [ ] Verify all resources are tagged correctly
  • [ ] Confirm Defender for Cloud is enabled
  • [ ] Review security recommendations
  • [ ] Enable threat protection on data services

Previous: 06 - GitHub Actions CI/CD | Next: 08 - Compliance Baseline