Azure Landing Zone Vending — Part 3

Erhan Mikael Sanlioglu
SpareBank 1 Utvikling
9 min readJan 11, 2024

Introduction

Welcome to part 3 in our series on Azure landing zone Vending. In the previous posts we’ve explored the concept and its implementation in SpareBank 1. In this post we will dive into the technical details of how we use the information from our Power Apps to automate the provisioning of our Azure landing zones. We’ve set an internal target to provision a production ready Azure landing zone within 20 minutes of the request being submitted.

Standardization Through Code

In the SpareBank 1 Alliance, Azure landing zones are being used by a wide range of users, from newcomers to advanced developers. With a diverse set of users, it drives the need to have a standardized approach to ensure consistency and ease of use. No matter the experience level, the goal is to create a common ground where users can easily, safely and cost effectivelly deploy and manage their Resources in Azure.

To achieve this there are some recommended design areas that should be covered when creating landing zones. To read about the recommended design areas, visit: Azure landing zone design areas — Cloud Adoption Framework | Microsoft Learn

We have summarized these design areas as the following:

A standardized approach plays a critical role when implementing security and governance practices at scale, where all the landing zones adhere to organizational security policies and cost controls, irrespective of the team or individual managing them.

Automating subscription/landing zone provisioning and configuration of the design areas is commonly referred to as Subscription Vending. Our vending machine uses Microsoft Native tooling. The deployment is run using Azure DevOps pipelines, the deployment steps/tasks are PowerShell scripts and the subscription along with the Azure Resources are deployed using Bicep.

The pipeline tasks are dependent on having a standardized JSON-file for each landing zone - containing the information needed to start the deployment. Below is an example of what our landing zone JSON file looks like:

[
{
“name”: “string”, // LandingZone name
“managementGroup”: “string”, // Private, Public or Sandbox mgmt group
“tags”:
{
“companyCode”: “string”,
“projectNumber”: “string”,
“environment”: “string”, // Dev,Test,QA,Production
“defender”: “string”,
“spendingLimit”: integer, // Expected monthly usage - budget
“costOwner”: “string”
},
“workload”: “string”, // Subscription type - DevTest or Production
“IAM”:
{
“groupAdmins”: [ “string” ] // email addresses of users who will manage Entra ID groups
},
“billing”: { // Microsoft Customer Agreement billing info for costs
“billingProfile”: { // Bank/Company billing profile details
“name”: “string”,
“id”: “string”
},
“invoiceSection”: { // Invoice section details
“name”: “string”,
“id”: “string”
}
},
“security”: {
“emailNotification”: {}
},
“networking”: {
“required”: bool, // True or False – If sandbox == False
“vnets”: [
{
“deploy”: bool,
“resourceGroupName”: “string”,
“rgLocation”: “string”,
“vnetName”: “string”,
“vnetLocation”: “string”,
“managedVnet”: bool,
“managedNSG”: bool,
“joinToVWAN”: bool, // Peering to vwan hub
“vwanHubRegion”: “string”,
“size”: “string”, // Small, Medium, Large
“addressPrefix”: [ “string” ], // Allocated IP addresses for vnet
“subnets”: [
{
“name”: “string”, // Subnet name
“addressPrefix”: “string”, // allocated IP address for subnet
“nsg”: “string”, // if default == will use the default NSG for given subnet
“peEnabled”: bool // True or False – depends on private, public or sandbox mgmt. group
}
]
}
]
}
}
]

To gather the information needed to populate the .json file we use the Power Apps covered in our previous post. Each submission of an order from the Power Apps creates a .json file, stores this in our repository and triggers the deployment process.

The Vending Machine Deployment Process

Our landing zone provisioning process is divided into 2 main Azure DevOps pipelines. This is more of a personal and operational preference for us. Additional settings are enforced by policies on the landing zone based on the Management Group placement of the subscription.

The lz-networking pipeline is used quite often due to its role in managing subnets and NSG rules for the landing zones. By separating this as its own pipeline, we significantly reduce the time of adding, modifying, or deleting subnets and NSG rules.

Pipelines

The first pipeline (lz-provisioning) contains five tasks that mainly focuses on creating a subscription with the configuration that we want.

lz-provisioning pipeline tasks

Task 1: Subscription provisioning — Creates the Azure Subscription

Task 2: Entra ID groups — We create 3 Entra ID groups(reader, contributor, owner) that will be used for access control purposes for a landing zone

Task 3: Privileged Identity Management role configuration — This task is making sure that we have PIM configured for our landing zone where we create the role definitions based on Entra ID groups in previous task

Task 4: Resource Providers — We have 35 resource providers that we enable by default for all our landing zones. This will be updated according to the needs of our users.

Task 5: Subscription Configuration — This is the last task where we configure our landing zone.

Sample Bicep for 05-sub-config

Steps in this Bicep deployment:

  • Management Group — move the subscription to the correct management group hierarchy based on the parameter “managementGroup” in landingZoneName.json
  • Tagging — create subscription level tags
  • Defender — enabling Defender for Cloud
  • Budget — creating a budget that will be used to alert the users if they exceed the limit they provided in the ordering schema, this is not a hard cap on the budget, the goal is to make our users more aware of their costs by alerting them on thresholds at 70%, 95% and 100%.
  • Lastly, we configure default landing zone access. This consists of basic read permissions and just-in-time access to the landing zone using PIM
lz-network pipeline task

The second pipeline that will be triggered by our Power Apps is the Network pipeline(lz-network). This is where we configure the connectivity part of our landing zone. This pipeline contains only 1 task that has all our networking setup in one main.bicep template. This template calls out to other Bicep templates (or modules):

Sample Bicep for 01-net-networking
  • Resource Group — for Network Watchers is created first.
  • Network Watchers — are created for a selection of regions. These are used by the Network Security Group Flow logs.
  • Resource Group — for other Network Resources. Network Watcher Resources could also reside in this Resource Group. Due to added code complexity when handling multiple regions, we decided to keep Network Watchers separate. Long term we will merge all network Resource to the same Resource Group
  • Application Security Groups — created to simplify Network Security Rules
  • Network Security Group — Each Virtual Network will in our case have a default NSG that is assigned to all subnets. Flow logs are enabled by using policy and forwards the logs to our common Log Analytics workspace for insight and troubleshooting. A selection of default rules are applied (deny all inbound/outbound) and basic platform rules for DNS etc are added to all NSG’s are part of their base deployment.
  • Virtual Network — A virtual Network is created based on the input included in the landingZoneName.json file
  • Virtual WAN Connection — We are using Azure Virtual WAN as our network backbone and each Virtual Network will be connected to this vwan (if bool is set to true in landingZoneName.json file)

How we organize our code

Now, lets take a look at how we organize and setup our deployment process.

Pipeline repos

Each pipeline has its own Azure DevOps Repository. Within each repository we create a folder for each task that will be triggered. Each repository also includes a folder called .pipeline that contains the yaml file for the pipeline.

root.yaml sample

Looking at the first folder, .pipeline, we have 2 files that are being used.

  1. pipeline-create.ps1 — is a PowerShell script that will create the pipeline for us.
  2. root.yaml — is the pipeline itself. We start off with some input parameters, next we clone the repositories that contains our scripts, settings, and templates used by the deployment tasks. Lastly, we have the tasks that will be triggered within that pipeline.

As shown with this example, we correlate the naming of folders with their respective tasks. By following this standardized and consistent approach it makes it:

  • Easier to document how the pipelines work
  • Creates predictability in the configuration across all existing and new pipelines
  • Results in easier and faster creation of new pipelines
  • Makes debugging existing pipelines easier

If we take a look at one of the deployment tasks (05-sub-config) in root.yaml, we can easily map this to our folder structure. As shown in the task on the right side, the scriptPath and template being used is pointed to the files under the 05-sub-config folder. This is just to show the relation between tasks and files in our setup.

eaz-task.ps1

The eaz-task.ps1 is the file that is referenced in scriptPath for each of our tasks. This is a PowerShell script that is designed to offload some complexity from the Bicep Template.

We start by defining the mandatory and optional parameters needed for the task and Bicep templates. Mandatory parameters must be provided for the script to run and come as input through the yaml deployment task.

Next, we are wrapping all our code inside a try-catch-finally for error handling purposes. Error handling can be made significantly more sophisticated than what we are doing in this example but a very simple try/catch has done the job well (enough) for us so far.

eaz-task.ps1 try{}

The first thing we do inside the try-catch is to import our script module file “eunomia-common.psm1” where we have a set of PowerShell functions that can be used for the deployment.

One of the functions we use is “Get-eunomiaAsciiArt”, this is just a silly function we use to display ASCII art and give visual feedback in our pipeline. We highlight this function because (well it’s fun) and it is a good example of reusable PowerShell functions in our deployment framework.

Next, we are preparing to build the variables that will be forwarded to the Bicep deployment — by importing a combination of configuration files.

Moving on, our script generates a deployment parameter object. This object contains all the necessary information our deployment-function needs, as well as the template parameters used inside the Bicep template when the deployment runs.

New-eunomiaTemplateDeployment

The script then executes the deployment by calling the function “New-eunomiaTemplateDeployment” from our already imported script module file “eunomia-common.psm1”. The function is expecting the deploymentParameters as input for the execution.

Lastly, we have the catch-finally block. The catch block will handle exceptions that occurs during the execution and displays it as output. The finally block is used to execute code regardless of whether an error occurred or not, this is just for outputting final messages and cleanup purposes.

The Epilogue

Wrapping up this part of our Azure landing zone Vending series, we have shared a glimpse into our approaches and processes on provisioning Azure landing zones. Hopefully, this peek behind the curtain has given you some inspiration or ideas that you can use to create your own vending machine. This example, while focused on a single tenant, is just the tip of the iceberg. We are already managing our landing zone provisioning in multiple tenants. More precisely 8 tenants and counting with a lower future estimate of 14–15 and a higher estimate in the 30s!

Special thanks to Matthew Greenham & Roger Carson for guidance and participating in this post.

Further reading
Keep an eye out for Part 4 ;)

Part 1:
https://medium.com/sparebank1-digital/azure-landing-zone-vending-part-1-4a9333dc4569

Part 2:
https://medium.com/sparebank1-digital/azure-landing-zone-vending-part-2-ba60d29984ec

--

--