diff --git a/src/bicep/examples/iaas-activedirectory-domaincontrollers/README.md b/src/bicep/examples/iaas-activedirectory-domaincontrollers/README.md new file mode 100644 index 000000000..37ace2c6c --- /dev/null +++ b/src/bicep/examples/iaas-activedirectory-domaincontrollers/README.md @@ -0,0 +1,99 @@ +# Azure IaaS DNS Forwarders example + +This example deploys Active Directory Domain Controller Virtual Machines in the MLZ identity tier. There is the option to add the domain controllers to an existing domain, or create a brand new forest. + +## What this example does + +### Builds Virtual Machines and configures them as Active Directory Domain Controllers + +1. Deploys 2 Virtual Machines in an Availability Set + - a Data Disk gets configured without caching to host the AD databases, as per [best practices](https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/identity/adds-extend-domain#vm-recommendations) + +2. Runs a DSC configuration to: + - Install AD DS roles and features. + - Promote the server to Domain Controller of a new or existing domain. + - Configure the DNS Server forwarders. + +## Pre-requisites + +1. A Mission LZ deployment (a deployment of mlz.bicep) +2. The outputs from a deployment of mlz.bicep (./src/bicep/examples/deploymentVariables.json). +3. A proper DNS resolution flow. See [DNS Resolution](README.md#dns-resolution) + +### Generate MLZ Variable File (deploymentVariables.json) + +For instructions on generating 'deploymentVariables.json' using both Azure PowerShell and Azure CLI, please see the [README at the root of the examples folder](../README.md). + +Place the resulting 'deploymentVariables.json' file within the ./src/bicep/examples folder. + +## Deployment + +### Template Parameters + +Template Parameters Name | Description +--- | --- +vmNamePrefix | 3 to 12 characters VM name prefix. -01 and -02 will get appended to that prefix. +nicPrivateIPAddresses | array of two static IP addresses available in the Identity VNET subnet. +extensionsFilesContainerUri | uri to the storage account used to host the DSC configuration and custom script file (if not relying on the public repo) +extensionsFilesContainerSas | storage account account SAS token used to host the DSC configuration and custom script file (if not relying on the public repo) +dnsForwarders | default DNS server forwarders (for instance: DISA's). Defaults to Azure DNS. +createOrAdd | Whether to create a new forest or add both domain controllers to an existing domain. +dnsDomainName | Active Directory DNS Domain name. +netbiosDomainName | Active Directory NETBIOS Domain name. +managementSubnets | Managements subnets allowed in the Windows Firewall. +domainAdminUsername | Domain Administrator username. +domainAdminPassword | Domain Administrator password. +safemodeAdminUsername | Safe mode username. +safemodeAdminPassword | Safe mode password. +domainJoinUsername | Domain join username (gets created to join other servers to the domain with minimal privileges). +domainJoinUserPassword | Domain join password. + +### Deploying IaaS DNS Forwarders + +Connect to the appropriate Azure Environment and set appropriate context, see getting started with Azure PowerShell for help if needed. The commands below assume you are deploying in Azure Government and show the entire process from deploying MLZ and then adding Domain Controllers post-deployment. + +```PowerShell +cd .\src\bicep +Connect-AzAccount -Environment AzureUSGovernment +New-AzSubscriptionDeployment -Name contoso -TemplateFile .\mlz.bicep -resourcePrefix 'contoso' -Location 'USGovVirginia' +cd .\examples +(Get-AzSubscriptionDeployment -Name contoso).outputs | ConvertTo-Json | Out-File -FilePath .\deploymentVariables.json +cd .\iaas-activedirectory-domaincontrollers +$vmDomainAdminPassword = Read-Host -Prompt "Please provide a password for the domain administrator account, with a length of at least 12 characters" -AsSecureString +$vmDomainJoinPassword = Read-Host -Prompt "Please provide a password for the domain join account, with a length of at least 12 characters" -AsSecureString +New-AzResourceGroupDeployment -DeploymentName adDomainControllers ` + -TemplateFile .\forwarderVm.bicep ` + -ResourceGroupName 'contoso-rg-identity-mlz' ` + -vmNamePrefix 'contoso-adds' ` + -nicPrivateIPAddresses "10.9.1.4", "10.9.1.5" ` + -dnsForwarders "168.63.129.16" ` + -dnsDomainName "ad.contoso.com" ` + -netbiosDomainName "CONTOSO" ` + -managementSubnets "10.9.0.0/24" ` + -domainAdminUsername "superuser1" ` + -domainAdminPassword $vmDomainAdminPassword` + -safemodeAdminUsername "safemodeuser1" ` + -safemodeAdminPassword $vmDomainAdminPassword ` + -domainJoinUsername "domainjoinuser1" ` + -domainJoinUserPassword $vmDomainJoinPassword +``` + +### DNS Resolution + +The MLZ deployment needs to be configured for private DNS resolution, one of two ways depending on your scenario. + +### 1. DNS Forwarders in the Hub + +Use the [IaaS DNS forwarders example](../iaas-dns-forwarders) to create DNS forwarders in the MLZ HUB, that will forward client DNS requests to Azure, Active Directory and the Internet as needed. + +### 2. AD Domain Controllers act as DNS servers + +![AD DNS Resolution diagram](diagram.png) + +In this scenario all of the Virtual Networks use the Domain Controllers as DNS servers. Prior to deploying this example, the Identity Virtual Network needs to be configured with the following DNS servers: + +- Identity VNET DNS Server 1: Domain Controller VM 1 IP Address +- Identity VNET DNS Server 2: Domain Controller VM 2 IP Address +- Identity VNET DNS Server 3: 168.63.129.16 + +The Domain Controller VM's need proper DNS resolution during deployment, to access the DSC configuration. The above DNS configuration will allow the 1st DC to resolve, using the 3rd DNS server. The 2nd DC will be able to resolve the Active Directory domain using the 1st DNS server. diff --git a/src/bicep/examples/iaas-activedirectory-domaincontrollers/activeDirectoryDomainControllers.bicep b/src/bicep/examples/iaas-activedirectory-domaincontrollers/activeDirectoryDomainControllers.bicep new file mode 100644 index 000000000..35f3161fd --- /dev/null +++ b/src/bicep/examples/iaas-activedirectory-domaincontrollers/activeDirectoryDomainControllers.bicep @@ -0,0 +1,311 @@ +@description('MLZ Deployment output variables in json format. It defaults to the deploymentVariables.json.') +param mlzDeploymentVariables object = json(loadTextContent('../deploymentVariables.json')) +param identityVirtualNetworkSubnetId string = mlzDeploymentVariables.spokes.Value[0].subnetResourceId +param logAnalyticsWorkspaceResourceId string = mlzDeploymentVariables.logAnalyticsWorkspaceResourceId.Value + +@description('The region to deploy resources into. It defaults to the deployment location.') +param location string = resourceGroup().location + +@description('A string dictionary of tags to add to deployed resources. See https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json#arm-templates for valid settings.') +param tags object = {} + +@description('Prefix the VM names will start with.') +param vmNamePrefix string + +@description('Number of VM to build.') +param vmCount int = 2 + +@description('The size of the Virtual Machine. It defaults to "Standard_DS1_v2".') +param vmSize string = 'Standard_DS1_v2' + +@description('The publisher of the Virtual Machine. It defaults to "MicrosoftWindowsServer".') +param vmPublisher string = 'MicrosoftWindowsServer' + +@description('The offer of the Virtual Machine. It defaults to "WindowsServer".') +param vmOffer string = 'WindowsServer' + +@description('The SKU of the Virtual Machine. It defaults to "2022-datacenter".') +param vmSku string = '2022-datacenter' + +@description('The version of the Virtual Machine. It defaults to "latest".') +param vmVersion string = 'latest' + +@description('The disk creation option of the Virtual Machine. It defaults to "FromImage".') +param vmCreateOption string = 'FromImage' + +@description('The storage account type of the Virtual Machine. It defaults to "StandardSSD_LRS".') +param vmStorageAccountType string = 'StandardSSD_LRS' + +@description('The size of the VM Data Disk. It defaults to 16GB.') +param vmDataDiskSizeGB int = 16 + +@allowed([ + 'Static' + 'Dynamic' +]) +@description('[Static/Dynamic] The private IP Address allocation method for the Virtual Machine. It defaults to "Static".') +param nicPrivateIPAddressAllocationMethod string = 'Static' + +@description('Array of static private IP addresses for the VM Network Interface Cards') +param nicPrivateIPAddresses array = [] + +@description('Uri to the container that contains the DSC configuration and the Custom Script') +param extensionsFilesContainerUri string = 'https://raw.githubusercontent.com/Azure/missionlz/main/src/bicep/examples/iaas-dns-forwarders/extensions' + +@description('SAS Token to access the container that contains the DSC configuration and the Custom Script. Defaults to none for a public container') +@secure() +param extensionsFilesContainerSas string = '' + +@description('New AD forest DSC Configurations Name') +param newForestDscConfigName string = 'firstDomainController' + +@description('Add to existing domain DSC Configurations Name') +param addDcDscConfigName string = 'secondDomainController' + +@description('Active Directory DNS Domain Name') +param dnsDomainName string + +@description('Active Directory Netbios Domain Name') +param netbiosDomainName string + +@description('DNS Forwarder IP Addresses that gets configured in the Windows DNS server. Defaults to Azure DNS servers.') +param dnsForwarders array = ['168.63.129.16'] + +@description('Whether to create a new Active Directory Forest, or add the domain controllers to an existing domain.') +@allowed([ + 'NewForest' + 'AddToExistingDomain' +]) +param createOrAdd string = 'NewForest' + +@description('Management Subnets IP Prefixes that get allowed in the Windows Firewall') +param managementSubnets array + +@description('Domain Administrator Username') +param domainAdminUsername string + +@description('Domain Administrator password') +@secure() +param domainAdminPassword string + +@description('Sademode Administrator Username') +param safemodeAdminUsername string + +@description('Sademode Administrator Password') +@secure() +param safemodeAdminPassword string + +@description('Domain Join User Username') +param domainJoinUsername string + +@description('Domain Join User Password') +@secure() +param domainJoinUserPassword string + + +var vmAvSetName = '${vmNamePrefix}-avset-01' +var NetworkInterfaceIpConfigurationName = 'ipConfiguration1' +var sasToken = ((extensionsFilesContainerSas != '') ? '?${extensionsFilesContainerSas}' : '') + +resource vmAvSet 'Microsoft.Compute/availabilitySets@2022-03-01' = { + name: vmAvSetName + location: location + tags: tags + sku: { + name: 'Aligned' + } + properties: { + platformUpdateDomainCount: 2 + platformFaultDomainCount: 2 + } +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2021-02-01' = [for i in range(0, vmCount): { + name: '${vmNamePrefix}-0${(i + 1)}-nic-01' + location: location + tags: tags + + properties: { + ipConfigurations: [ + { + name: NetworkInterfaceIpConfigurationName + properties: { + subnet: { + id: identityVirtualNetworkSubnetId + } + privateIPAllocationMethod: nicPrivateIPAddressAllocationMethod + privateIPAddress: ((nicPrivateIPAddressAllocationMethod == 'Static') ? nicPrivateIPAddresses[i] : null) + } + } + ] + } +}] + +module domainControllerVM '../../modules/windows-virtual-machine.bicep' = [for vmi in range(0, vmCount): { + name: 'domainControllerVirtualMachines${(vmi + 1)}' + params: { + name: '${vmNamePrefix}-0${(vmi + 1)}' + location: location + tags: tags + + size: vmSize + adminUsername: domainAdminUsername + adminPassword: domainAdminPassword + publisher: vmPublisher + offer: vmOffer + sku: vmSku + version: vmVersion + createOption: vmCreateOption + storageAccountType: vmStorageAccountType + networkInterfaceName: '${vmNamePrefix}-0${(vmi + 1)}-nic-01' + logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId + availabilitySet: { + id: vmAvSet.id + } + dataDisks: [ + { + createOption: 'Empty' + caching: 'None' + diskSizeGB: vmDataDiskSizeGB + lun: 1 + name: '${vmNamePrefix}-0${(vmi + 1)}-dataDisk-1' + managedDisk: { + storageAccountType: vmStorageAccountType + } + } + ] + } + dependsOn: [ + networkInterface + ] +}] + +resource domainControllerVirtualMachines 'Microsoft.Compute/virtualMachines@2022-03-01' existing = [for i in range(0, vmCount): { + name: '${vmNamePrefix}-0${(i + 1)}' +}] + +resource NewADForestDSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = if(createOrAdd == 'NewForest') { + name: 'NewADForestDSC' + parent: domainControllerVirtualMachines[0] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.24' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: '${extensionsFilesContainerUri}/${newForestDscConfigName}.ps1.zip${sasToken}' + script: '${newForestDscConfigName}.ps1' + function: newForestDscConfigName + } + configurationArguments: { + dnsDomainName: dnsDomainName + netbiosDomainName: netbiosDomainName + dnsForwarders: dnsForwarders + managementSubnets: managementSubnets + } + } + protectedSettings: { + configurationArguments: { + domainAdminCredentials: { + UserName: domainAdminUsername + Password: domainAdminPassword + } + safemodeAdminCredentials: { + UserName: safemodeAdminUsername + Password: safemodeAdminPassword + } + domainJoinUserCredentials: { + UserName: domainJoinUsername + Password: domainJoinUserPassword + } + } + } + } + dependsOn: [ + domainControllerVM + ] +} + +resource AddFirstDCDSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = if(createOrAdd == 'AddToExistingDomain') { + name: 'AddFirstDCDSC' + parent: domainControllerVirtualMachines[0] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.24' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: '${extensionsFilesContainerUri}/${addDcDscConfigName}.ps1.zip${sasToken}' + script: '${addDcDscConfigName}.ps1' + function: addDcDscConfigName + } + configurationArguments: { + dnsDomainName: dnsDomainName + dnsForwarders: dnsForwarders + managementSubnets: managementSubnets + } + } + protectedSettings: { + configurationArguments: { + domainAdminCredentials: { + UserName: '${netbiosDomainName}\\${domainAdminUsername}' + Password: domainAdminPassword + } + safemodeAdminCredentials: { + UserName: safemodeAdminUsername + Password: safemodeAdminPassword + } + } + } + } + dependsOn: [ + domainControllerVM + ] +} + +resource AddSecondDCDSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { + name: 'AddSecondDCDSC' + parent: domainControllerVirtualMachines[1] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.24' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: '${extensionsFilesContainerUri}/${addDcDscConfigName}.ps1.zip${sasToken}' + script: '${addDcDscConfigName}.ps1' + function: addDcDscConfigName + } + configurationArguments: { + dnsDomainName: dnsDomainName + dnsForwarders: dnsForwarders + managementSubnets: managementSubnets + } + } + protectedSettings: { + configurationArguments: { + domainAdminCredentials: { + UserName: '${domainAdminUsername}@${dnsDomainName}' + Password: domainAdminPassword + } + safemodeAdminCredentials: { + UserName: safemodeAdminUsername + Password: safemodeAdminPassword + } + } + } + } + dependsOn: [ + NewADForestDSC + AddFirstDCDSC + ] +} diff --git a/src/bicep/examples/iaas-activedirectory-domaincontrollers/diagram.png b/src/bicep/examples/iaas-activedirectory-domaincontrollers/diagram.png new file mode 100644 index 000000000..dafcb764a Binary files /dev/null and b/src/bicep/examples/iaas-activedirectory-domaincontrollers/diagram.png differ diff --git a/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/firstDomainController.ps1.zip b/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/firstDomainController.ps1.zip new file mode 100644 index 000000000..077fec424 Binary files /dev/null and b/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/firstDomainController.ps1.zip differ diff --git a/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/secondDomainController.ps1.zip b/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/secondDomainController.ps1.zip new file mode 100644 index 000000000..e3dfb11e2 Binary files /dev/null and b/src/bicep/examples/iaas-activedirectory-domaincontrollers/extensions/secondDomainController.ps1.zip differ diff --git a/src/bicep/modules/windows-virtual-machine.bicep b/src/bicep/modules/windows-virtual-machine.bicep index 14bec0036..a9a2fbfed 100644 --- a/src/bicep/modules/windows-virtual-machine.bicep +++ b/src/bicep/modules/windows-virtual-machine.bicep @@ -22,6 +22,7 @@ param createOption string param storageAccountType string param logAnalyticsWorkspaceId string param availabilitySet object = {} +param dataDisks array = [] resource networkInterface 'Microsoft.Network/networkInterfaces@2021-02-01' existing = { name: networkInterfaceName @@ -55,6 +56,7 @@ resource windowsVirtualMachine 'Microsoft.Compute/virtualMachines@2021-04-01' = storageAccountType: storageAccountType } } + dataDisks: dataDisks } networkProfile: { networkInterfaces: [