Following on from my previous post on creating an AVD environment, for those who want to deploy AVD, it’s recommended to build an Azure landing zone first (plus if going for the Microsoft Specialty, a scripted landing zone and AVD deployment is always useful)
Again, using a combination of Bicep, powershell and POSHGUI makes the deployment much easier
UPDATE: Now on Powershell Gallery
Install-Script -Name BuildAZLandingZone
As usual, all scripts are on Github to save having to copy and paste from this post:
GUI
Bicep Scripts
Let’s start with the powershell script which is fairly straight forward. First job is to check that Bicep and the AZ modules are installed:
#Install Bicep
if((Test-Path "$env:USERPROFILE\.bicep") -eq $false) {
# Create the install folder
$installPath = "$env:USERPROFILE\.bicep"
$installDir = New-Item -ItemType Directory -Path $installPath -Force
$installDir.Attributes += 'Hidden'
# Fetch the latest Bicep CLI binary
(New-Object Net.WebClient).DownloadFile("https://github.com/Azure/bicep/releases/latest/download/bicep-win-x64.exe", "$installPath\bicep.exe")
# Add bicep to your PATH
$currentPath = (Get-Item -path "HKCU:\Environment" ).GetValue('Path', '', 'DoNotExpandEnvironmentNames')
if (-not $currentPath.Contains("%USERPROFILE%\.bicep")) { setx PATH ($currentPath + ";%USERPROFILE%\.bicep") }
if (-not $env:path.Contains($installPath)) { $env:path += ";$installPath" }
}
Write-Host "Installing AZ modules if required (current user scope)"
#Install AZ Module if not available
if (Get-Module -ListAvailable -Name Az*) {
Write-Host "AZ Module Already Installed"
}
else {
try {
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force
}
catch [Exception] {
$_.message
exit
}
}
Now, we create a quick and dirty temp folder so we can download the files and then bin at the end of the deployment:
#Create Temp Folder
$random = Get-Random -Maximum 1000
$random = $random.ToString()
$date =get-date -format yyMMddmmss
$date = $date.ToString()
$path2 = $random + "-" + $date
$path = "c:\temp\" + $path2 + "\"
$pathaz = "c:\temp\" + $path2 + "\az-landing-main"
$output3 = "c:\temp\" + $path2 + "\main.zip"
New-Item -ItemType Directory -Path $path
And set the paths into variables so each button can use them:
$jsonfile = [PSCustomObject]@{value=$pathaz+"\parameters.json"}
$path2 = [PSCustomObject]@{value=$path}
$pathaz2 = [PSCustomObject]@{value=$pathaz}
$output2 = [PSCustomObject]@{value=$output3}
Then finally launch the GUI:
Clicking Update Params populates the parameters.json file
Now onto the deployment
First in the tenant context we build the management groups
//Create Parent Management Group
resource parentmanagement 'Microsoft.Management/managementGroups@2020-05-01' = {
name: orggroup
properties: {
displayName: orggroup
details: {
}
}
}
//Create Sub Groups
resource devmanagement 'Microsoft.Management/managementGroups@2020-05-01' = {
name: devgroup
properties: {
displayName: 'string'
details: {
parent: {
id: parentmanagement.id
}
}
}
}
resource testmanagement 'Microsoft.Management/managementGroups@2020-05-01' = {
name: testgroup
properties: {
displayName: 'string'
details: {
parent: {
id: parentmanagement.id
}
}
}
}
resource prodmanagement 'Microsoft.Management/managementGroups@2020-05-01' = {
name: prodgroup
properties: {
displayName: 'string'
details: {
parent: {
id: parentmanagement.id
}
}
}
}
//Create Exceptions Management Group
resource exceptionsmanagement 'Microsoft.Management/managementGroups@2020-05-01' = {
name: excgroup
properties: {
displayName: orggroup
details: {
}
}
}
We then drop to the subscription level to make some policies. I’ve set these to audit only for now, I’d recommend setting to Block after deployment of everything and checking the audit logs, last thing you want is a deployment to fail because it’s being blocked!
//Create Policy to restrict SKUs
targetScope = 'subscription'
param listOfAllowedLocations array = [
'uksouth'
'ukwest'
'northeurope'
'westeurope'
]
param listOfAllowedSKUs array = [
'Standard_B1ls'
'Standard_B1ms'
'Standard_B1s'
'Standard_B2ms'
'Standard_B2s'
'Standard_B4ms'
'Standard_B4s'
'Standard_D2s_v3'
'Standard_D4s_v3'
]
param tagname string
param tagvalue string
var initiativeDefinitionName = 'SKU and Location Policy'
resource initiativeDefinition 'Microsoft.Authorization/policySetDefinitions@2019-09-01' = {
name: initiativeDefinitionName
properties: {
policyType: 'Custom'
displayName: initiativeDefinitionName
description: 'Initiative Definition for Resource Location and VM SKUs'
metadata: {
category: 'SKU and Location Policy'
}
parameters: {
listOfAllowedLocations: {
type: 'Array'
metadata: ({
description: 'The List of Allowed Locations for Resource Groups and Resources.'
strongtype: 'location'
displayName: 'Allowed Locations'
})
}
listOfAllowedSKUs: {
type: 'Array'
metadata: any({
description: 'The List of Allowed SKUs for Virtual Machines.'
strongtype: 'vmSKUs'
displayName: 'Allowed Virtual Machine Size SKUs'
})
}
}
policyDefinitions: [
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/e765b5de-1225-4ba3-bd56-1ac6695af988'
parameters: {
listOfAllowedLocations: {
value: '[parameters(\'listOfAllowedLocations\')]'
}
}
}
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c'
parameters: {
listOfAllowedLocations: {
value: '[parameters(\'listOfAllowedLocations\')]'
}
}
}
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/cccc23c7-8427-4f53-ad12-b6a63eb452b3'
parameters: {
listOfAllowedSKUs: {
value: '[parameters(\'listOfAllowedSKUs\')]'
}
}
}
{
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/0015ea4d-51ff-4ce3-8d8c-f3f8f0179a56'
parameters: {}
}
]
}
}
resource initiativeDefinitionPolicyAssignment 'Microsoft.Authorization/policyAssignments@2019-09-01' = {
name: initiativeDefinitionName
properties: {
scope: subscription().id
enforcementMode: 'Default'
policyDefinitionId: initiativeDefinition.id
parameters: {
listOfAllowedLocations: {
value: listOfAllowedLocations
}
listOfAllowedSKUs: {
value: listOfAllowedSKUs
}
}
}
}
//Create Policy to require tag
resource tagpolicydef 'Microsoft.Authorization/policyDefinitions@2020-09-01' = {
name: 'audit-tags'
properties: {
displayName: 'Audit a tag and its value format on resources'
description: 'Audits existence of a tag and its value format. Does not apply to resource groups.'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
category: 'Tags'
}
parameters: {
tagName: {
type: 'String'
metadata: {
displayName: tagname
description: 'A tag to audit'
}
}
tagFormat: {
type: 'String'
metadata: {
displayName: tagvalue
description: 'An expressions for \'like\' condition' // Use backslash as an escape character for single quotation marks
}
}
}
policyRule: {
if: {
field: '[concat(\'tags[\', parameters(\'tagName\'), \']\')]' // No need to use an additional forward square bracket in the expressions as in ARM templates
notLike: '[parameters(\'tagFormat\')]'
}
then: {
effect: 'Audit'
}
}
}
}
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2020-09-01' = {
name: 'deny-without-tags' //Should be unique whithin your target scope
properties: {
policyDefinitionId: tagpolicydef.id // Reference a policy specified in the same Bicep file
displayName: 'Deny anything without the \'tag_name\' on resources'
description: 'Policy will Deny resources not tagged with a specific tag'
parameters: {
tagName: {
value: tagname
}
tagFormat: {
value: tagvalue
}
}
}
}
//Create Policy to block public IPs
resource blockpublicip 'Microsoft.Authorization/policyDefinitions@2020-09-01' = {
name: 'blockpublicip'
properties: {
displayName: 'Block Public IP'
description: 'Block network interface from having a public IP'
policyType: 'Custom'
mode: 'Indexed'
parameters: {}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Network/networkInterface'
}
{
field: 'Microsoft.Network/networkInterfaces/ipconfigurations[*].publicIpAddress.id'
notlike: '*'
}
]
}
then: {
effect: 'Audit'
}
}
}
}
resource blocktheip 'Microsoft.Authorization/policyAssignments@2020-09-01' = {
name: 'block-public-ip' //Should be unique whithin your target scope
properties: {
policyDefinitionId: blockpublicip.id // Reference a policy specified in the same Bicep file
displayName: 'Audit Public IP'
description: 'Policy block public IP on network interfaces - Audit Only during build'
parameters: { }
}
}
Now we use the infra bicep to create the network and peerings. This calls a number of sub-files so I won’t go through the whole script. Importantly though, resource groups first:
resource hubrg 'Microsoft.Resources/resourceGroups@2020-06-01' = {
name: hubrgname
location: region
}
resource spokerg 'Microsoft.Resources/resourceGroups@2020-06-01' = {
name: spokergname
location: region
}
resource infrarg 'Microsoft.Resources/resourceGroups@2020-06-01' = {
name: serverrg
location: region
}
Build a VNET
param prefix string
param addressSpaces array
param subnets array
resource vnet 'Microsoft.Network/virtualNetworks@2020-06-01' = {
name: prefix
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: addressSpaces
}
subnets: subnets
}
}
output name string = vnet.name
output id string = vnet.id
output subnet2 string = vnet.properties.subnets[1].id
Add your subnets (just the key bits here, grab the source file for full code)
name: 'AzureFirewallSubnet'
properties: {
addressPrefix: hubfwsubnet
}
}
{
name: 'GatewaySubnet'
properties: {
addressPrefix: vpnsubnet
Then the spoke (just the key bits here, grab the source file for full code):
name: spokesnname
properties: {
addressPrefix: spokesnspace
}
}
{
name: 'DeviceSubnet'
properties: {
addressPrefix: devicesubnet
resource vnet 'Microsoft.Network/virtualNetworks@2020-06-01' = {
name: prefix
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: addressSpaces
}
subnets: subnets
}
}
Add a firewall:
param prefix string
param hubId string
resource publicIp 'Microsoft.Network/publicIPAddresses@2020-06-01' = {
name: '${prefix}-fwl-ip'
location: resourceGroup().location
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
sku: {
name: 'Standard'
}
}
resource fwl 'Microsoft.Network/azureFirewalls@2020-06-01' = {
name: '${prefix}-fwl'
location: resourceGroup().location
properties: {
ipConfigurations: [
{
name: '${prefix}-fwl-ipconf'
properties: {
subnet: {
id: '${hubId}/subnets/AzureFirewallSubnet'
}
publicIPAddress: {
id: publicIp.id
}
}
}
]
}
}
output privateIp string = fwl.properties.ipConfigurations[0].properties.privateIPAddress
Peer the hub and spoke
module HubToSpokePeering './modules/peering.bicep' = {
name: 'hub-to-spoke-peering'
scope: hubrg
dependsOn: [
hubVNET
spokeVNET
]
params: {
localVnetName: hubVNET.outputs.name
remoteVnetName: spokename
remoteVnetId: spokeVNET.outputs.id
}
}
module SpokeToHubPeering './modules/peering.bicep' = {
name: 'spoke-to-hub-peering'
scope: spokerg
dependsOn: [
hubVNET
spokeVNET
]
params: {
localVnetName: spokeVNET.outputs.name
remoteVnetName: hubname
remoteVnetId: hubVNET.outputs.id
}
}
MODULES/PEERING:
resource peer 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2020-07-01' = {
name: '${localVnetName}/to-${remoteVnetName}'
properties: {
allowForwardedTraffic: false
allowGatewayTransit: false
allowVirtualNetworkAccess: true
useRemoteGateways: false
remoteVirtualNetwork: {
id: remoteVnetId
}
}
}
Create the VPN Gateway (remember to adjust for your on-prem VPN)
resource vpngwpip 'Microsoft.Network/publicIPAddresses@2020-06-01' = {
name: vpngwpipname
location: location
sku: {
name: 'Standard'
}
properties: {
publicIPAllocationMethod: 'Static'
}
}
resource vpngw 'Microsoft.Network/virtualNetworkGateways@2020-06-01' = {
name: vpngwname
location: location
properties: {
gatewayType: 'Vpn'
ipConfigurations: [
{
name: 'default'
properties: {
privateIPAllocationMethod: 'Dynamic'
subnet: {
id: subnetref
}
publicIPAddress: {
id: vpngwpip.id
}
}
}
]
activeActive: false
enableBgp: true
bgpSettings: {
asn: 65010
}
vpnType: 'RouteBased'
vpnGatewayGeneration: 'Generation1'
sku: {
name: 'VpnGw1AZ'
tier: 'VpnGw1AZ'
}
}
}
output id string = vpngw.id
output ip string = vpngwpip.properties.ipAddress
output bgpaddress string = vpngw.properties.bgpSettings.bgpPeeringAddress
resource localnetworkgw 'Microsoft.Network/localNetworkGateways@2020-06-01' = {
name: localnetworkgwname
location: location
properties: {
localNetworkAddressSpace: {
addressPrefixes: addressprefixes
}
gatewayIpAddress: gwipaddress
bgpSettings: {
asn: 64512
bgpPeeringAddress: bgppeeringpddress
}
}
}
Build a VM to test
param adminUserName string
@secure()
param adminPassword string
param dnsLabelPrefix string
param storageAccountName string
param vmName string
param subnetName string
param networkSecurityGroupName string
param vn string
@allowed([
'2008-R2-SP1'
'2012-Datacenter'
'2012-R2-Datacenter'
'2016-Nano-Server'
'2016-Datacenter-with-Containers'
'2016-Datacenter'
'2019-Datacenter'
])
@description('The Windows version for the VM. This will pick a fully patched image of this given Windows version.')
param windowsOSVersion string = '2019-Datacenter'
@description('Size of the virtual machine.')
param vmSize string = 'Standard_D2s_v3'
@description('location for all resources')
param location string = resourceGroup().location
var nicName = '${vmName}Nic'
var publicIPAddressName = '${vmName}PublicIP'
var subnetRef = '${vn}/subnets/${subnetName}'
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'Storage'
}
resource pip 'Microsoft.Network/publicIPAddresses@2020-06-01' = {
name: publicIPAddressName
location: location
properties: {
publicIPAllocationMethod: 'Dynamic'
dnsSettings: {
domainNameLabel: dnsLabelPrefix
}
}
}
resource sg 'Microsoft.Network/networkSecurityGroups@2020-06-01' = {
name: networkSecurityGroupName
location: location
properties: {
securityRules: [
{
name: 'default-allow-3389'
'properties': {
priority: 1000
access: 'Allow'
direction: 'Inbound'
destinationPortRange: '3389'
protocol: 'Tcp'
sourcePortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
resource nInter 'Microsoft.Network/networkInterfaces@2020-06-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: pip.id
}
subnet: {
id: subnetRef
}
}
}
]
}
}
resource VM 'Microsoft.Compute/virtualMachines@2020-06-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: vmSize
}
osProfile: {
computerName: vmName
adminUsername: adminUserName
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: windowsOSVersion
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
}
dataDisks: [
{
diskSizeGB: 1023
lun: 0
createOption: 'Empty'
}
]
}
networkProfile: {
networkInterfaces: [
{
id: nInter.id
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
storageUri: stg.properties.primaryEndpoints.blob
}
}
}
}
output hostname string = pip.properties.dnsSettings.fqdn
Finally create your log analytics workspace and Azure monitor:
resource monitorrg 'Microsoft.Resources/resourceGroups@2020-06-01' = {
name: monitoringrg
location: logAnalyticslocation
}
//Azure Monitor and Log Analytics
module loganalytics './modules/monitor.bicep' = {
name: 'hubspoke'
scope: resourceGroup(monitorrg.name)
params: {
logAnalyticslocation: logAnalyticslocation
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
}
}
//Create Log Analytics Workspace
resource avdla 'Microsoft.OperationalInsights/workspaces@2020-08-01' = {
name: logAnalyticsWorkspaceName
location: logAnalyticslocation
properties: {
sku: {
name: logAnalyticsWorkspaceSku
}
}
}
And there we go, one landing zone.
Feel free to grab a copy of all of the files, if you change bicep for your environment, remember to change the download path in the powershell script:
#Download files and update parameters.json
$url = "https://github.com/andrew-s-taylor/az-landing/archive/main.zip"
$output = $output2.value
$expath = $path2.value
Next script will be building an Intune environment using powershell, POSHGUI and a pre-built set of policies etc. (yes, a one-click Intune to go with one-click AZ and one-click AVD)
Awsome