Creating an Azure Landing Zone using Bicep - Part 1
Stephen Tulp
December 15, 2023
10 minutes to read
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.
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.
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
andNetworkWatcherRG
- 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.