Insight Tech APAC Blog Logo

Creating an Azure Landing Zone using Bicep - Part 1

stephentulp
December 15, 2023

10 minutes to read

Bicep Advent Calendar

Introduction

Today we are going to look at how we can take all the concepts and ideas that we have covered in the previous days and apply them to a real world scenario. We will be looking at how we can deploy an Azure Landing Zone using Bicep and GitHub Actions. This is a 2 part blog post, in part 1 we will look at understanding the deployment and in part 2 we will look at how we can actually do the deployment.

What is an Azure Landing Zone?

An Azure Landing Zone is a way of designing and deploying cloud resources in Azure following best practices and guidelines. It helps you to migrate, modernize, and innovate your applications in a scalable and secure manner. An Azure Landing Zone consists of two types of subscriptions: platform landing zones and application landing zones. Platform landing zones provide shared services such as identity, connectivity, and management to the application landing zones. Application landing zones host the workloads themselves and can be customized for different needs.

Azure Landing Zone Deployment

The Azure Landing Zone example below is based on the great work that the Microsoft Customer Architecture & Engineering team (CAE) have done here, there is a lot to go through so we will define some assumptions to make it easier to understand the scope of the deployment.

  • The landing zone will use island networking as the network topology for the deployment but it can handle hub and spoke as well as Azure vWAN connectivity.
  • We have an existing Azure Subscription to use (we can also create this as part of the same deployment but to keep it simple we will use an existing subscription).
  • We have a Service Principal created for the deployment and a couple of Microsoft Entra groups.
  • We have a Management Group structure in place.

The process diagram below outlines the various components and capabilities that can be deployed as part of the landing zone. This ensures that all the required components are deployed and configured in a consistent manner.

LZ

Prerequisites

To get started, please make sure you have completed the following prerequisites:

  • GitHub Account: You will require a GitHub account to create and manage the workflow.
  • Azure Subscription: Have an active Azure subscription that will be used for the Landing Zone.
  • Management Group: Have a Management Group structure in place to place the subscription in.
  • Service principal: A Service Principal and configure its permissions to allow GitHub Actions to interact with your Azure resources.

Main Orchestration Template

The main.bicep file is the main orchestration template that will be used to deploy the landing zone.

main

The template is broken down into two (2) modules

  • Subscription Creation
  • Subscription Wrapper

Subscription Creation Bicep Module

The Subscription Creation Module is optional and can programmatically create an Azure subscription as part of the deployment, we will skip this for now and use an existing subscription.

Details on the creation of Azure Subscriptions programmatically can be found here

Subscription Wrapper Bicep Module

The Subscription Wrapper Module will be the main focus of the post and uses a Management Group scoped deployment to deploy the various components of the landing zone.

The module is broken down into the following functionality:

  • Move the subscription to the correct management group
  • Deploy tags at the subscription and resource group level
  • Assign an Azure Budget at the Subscription level
  • Assign Role assignments at the subscription level for Entra groups and Service Principals
  • Deploy Common Resource Groups alertsRG and NetworkWatcherRG
  • Deploy an Action Group in the alertsRG resource group
  • Network Watcher instance in the NetworkWatcherRG resource group
  • Deploy a network resource group for spoke networking resources, including
    • A virtual network and an array of subnets
    • Network security groups assigned to the subnets
    • Route tables assigned to the subnets
    • Peering to the hub or Azure vWAN

Subscription Placement

This module will move the subscription to the specified management group if the subscriptionManagementGroupAssociationEnabled parameter is set to true and the subscriptionMgPlacement parameter is not empty. This is a great way to ensure that the subscription is placed in the correct management group and can be used to ensure that the subscription is not moved to a management group that is not part of the landing zone structure.

// Module: Subscription Placement
module subscriptionPlacement '../../modules/subscriptionPlacement/subscriptionPlacement.bicep' = if (subscriptionManagementGroupAssociationEnabled && !empty(subscriptionMgPlacement)) {
  scope: managementGroup(subscriptionMgPlacement)
  name: 'subscriptionPlacement-${guid(deployment().name)}'
  params: {
    targetManagementGroupId: subscriptionMgPlacement
    subscriptionIds: [
      subscriptionId
    ]
  }
}

Subscription Tags

This module will create tags at the subscription level, only if the tag object is not empty and will only update the tags if the onlyUpdate parameter is set to true. This is a great way to ensure that the subscription tags are not overwritten if they have been manually updated.

// Module: Subscription Tags
module subscriptionTags '../CARML/resources/tags/main.bicep' = if (!empty(tags)) {
  scope: subscription(subscriptionId)
  name: 'subTags-${guid(deployment().name)}'
  params: {
    subscriptionId: subscriptionId
    location: location
    onlyUpdate: true
    tags: tags
  }
}

Subscription Budget

This module will create an Azure Budget at the subscription scope if the budgets object is not empty. This is a great way to ensure that the subscription has a budget in place to monitor and alert on costs.

// Module: Subscription Budget
module subscriptionbudget '../CARML/consumption/budget/main.bicep' = [for (bg, index) in budgets: if (!empty(budgets)) {
  name: take('subBudget-${guid(deployment().name)}-${index}', 64)
  scope: subscription(subscriptionId)
  params: {
    name: 'budget'
    location: location
    amount: bg.amount
    startDate: bg.startDate
    thresholds: bg.thresholds
    contactEmails: bg.contactEmails
  }
}]

Role Assignments

This module will associated role assignments to the subscription if the roleAssignments object is not empty. This is a great way to ensure that the subscription has the correct role assignments in place. This could either be a Service Principal or a Microsoft Entra AD Group.

The Role Assignment Parameter is an Array so we are using the for loop to iterate through the array and create a role assignment for each item in the array.

There is logic in the module to determine if the role assignment is for a resource group or a subscription. If the relativeScope property contains /resourceGroups/ then the role assignment is for a resource group, otherwise it is for the subscription.

// Module: Role Assignments
module roleAssignment '../CARML/authorization/role-assignment/main.bicep' = [for assignment in roleAssignments: if (roleAssignmentEnabled && !empty(roleAssignments)) {
  name: take('roleAssignments-${uniqueString(assignment.principalId)}', 64)
  params: {
    location: location
    principalId: assignment.principalId
    roleDefinitionIdOrName: assignment.definition
    subscriptionId: subscriptionId
    resourceGroupName: (contains(assignment.relativeScope, '/resourceGroups/') ? split(assignment.relativeScope, '/')[2] : '')
  }
}]

Shared Resource Groups

This module will create a resource group for each item in the commonResourceGroups array. These common resource group names would be consistent across landing zones and would be used for shared services such as alerts and network watcher.

// Module: Resource Groups (Common)
module sharedResourceGroups '../resourceGroup/resourceGroups.bicep' = [for item in commonResourceGroups: {
  name: item
  scope: subscription(subscriptionId)
  params: {
    resourceGroupNames: commonResourceGroups
    location: location
    tags: tags
  }
}]

Action Group

This module will create an action group in the alertsRG resource group if the actionGroupEnabled parameter is set to true and the actionGroupEmails array is not empty. This is a great way to ensure that notifications can be sent for things like Service Health Alerts.

We can also see that this module is pulling from the public Microsoft ACR for the Action Group.

module actionGroup 'br/public:avm-res-insights-actiongroup:0.1.1' = if (actionGroupEnabled && !empty(actionGroupEmails)) {
  name: 'actionGroup-${guid(deployment().name)}'
  scope: resourceGroup(subscriptionId, 'alertsRG')
  dependsOn: [
    sharedResourceGroups
  ]
  params: {
    location: 'Global'
    name: '${lzPrefix}${envPrefix}ActionGroup'
    groupShortName: '${lzPrefix}${envPrefix}AG'
    emailReceivers: [for email in actionGroupEmails: {
      emailAddress: email
      name: split(email, '@')[0]
      useCommonAlertSchema: true
    }]
  }
}

Resource Group for Networking

This module will create a resource group for all spoke networking resources if the virtualNetworkEnabled parameter is set to true. This will then be used by the spoke networking module.

// Module: Resource Groups (Network)
module resourceGroupForNetwork '../CARML/resources/resource-group/main.bicep' = if (virtualNetworkEnabled) {
  name: 'resourceGroupForNetwork-${guid(deployment().name)}'
  scope: subscription(subscriptionId)
  params: {
    name: resourceGroups.network
    location: location
    tags: tags
  }
}

Network Watcher

This module will create a network watcher instance in the NetworkWatcherRG resource group if the virtualNetworkEnabled parameter is set to true. This is a great way to ensure that the subscription has a network watcher instance in place to monitor and troubleshoot network connectivity, if there is a virtual network that will be deployed.

// Module: Network Watcher
module networkWatcher '../CARML/network/network-watcher/main.bicep' = if (virtualNetworkEnabled) {
  name: 'networkWatcher-${guid(deployment().name)}'
  scope: resourceGroup(subscriptionId, 'networkWatcherRG')
  dependsOn: [
    sharedResourceGroups
  ]
  params: {
    location: location
    tags: tags
  }
}

Spoke Networking

// Module: Spoke Networking
module spokeNetworking '../spokeNetworking/spokeNetworking.bicep' = if (virtualNetworkEnabled && !empty(addressPrefixes)) {
  scope: resourceGroup(subscriptionId, resourceGroups.network)
  name: 'spokeNetworking-${guid(deployment().name)}'
  dependsOn: [
    resourceGroupForNetwork
  ]
  params: {
    spokeNetworkName: resourceNames.virtualNetwork
    addressPrefixes: addressPrefixes
    ddosProtectionPlanId: ddosProtectionPlanId
    dnsServerIps: dnsServerIps
    nextHopIPAddress: nextHopIpAddress
    subnets: subnets
    disableBGPRoutePropagation: disableBGPRoutePropagation
    tags: tags
    location: location
  }
}

Conclusion

That concludes part 1 of this blog post where we have analyzed the deployment of the Azure Landing Zone and the various components that are deployed into Azure. In part 2 we will complete the actual deployment locally using PowerShell and then we will do the same with GitHub Actions.

Further Reading