Publishing Microsoft Azure Stack TP3 on the Internet via NAT

TP3Stack.png

As you may know? Azure Stack TP3 is here. This blog will outline how to publish your azure stack instance on the internet using NAT rules to redirect your external IP Address to the internal, external IPs. Our group published another article on how to do this for TP2 and this is the updated version for TP3. Starting Point This article assumes you have a host ready for installation with the TP3 VHDx loaded onto your host and you are familiar with the Azure Stack installation Process. The code in this article is extracted from a larger process but should be enough to get you through the process end to end. Azure Stack Installation First things first, I like to install a few other tools to help me edit code and access the portal, this is not required.

[powershell] iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex choco install notepadplusplus -y choco install googlechrome -y --ignore-checksums choco install visualstudiocode -y choco install beyondcompare -y choco install baretail -y choco install powergui -y --ignore-checksums [/powershell]

Next, you want to open up this file C:\clouddeployment\setup\DeploySingleNode.ps1

Editing these values allow you to create different internal naming and external space.  As you can see the ExternalDomainFQDN is made up of the region and external suffix.

This is a lot easier now the domain parameters are used from the same place, no need to hunt down domain names in files.

[powershell] $AdminPassword = 'SuperSecret!'|ConvertTo-SecureString -AsPlainText -force $AadAdminPass= 'SuperSecret!'|ConvertTo-SecureString -AsPlainText -Force $aadCred = New-Object PSCredential('stackadmin@poc.xxxxx.com',$AadAdminPass)

. c:\clouddeployment\setup\InstallAzureStackPOC.ps1 -AzureEnvironment "AzureCloud" ` -AdminPassword $AdminPassword ` -PublicVLanId 97 ` -NATIPv4Subnet '172.20.51.0/24' ` -NATIPv4Address '172.20.51.51' ` -NATIPv4DefaultGateway '172.20.51.1' ` -InfraAzureDirectoryTenantAdminCredential $aadCred ` -InfraAzureDirectoryTenantName 'poc.xxxxx.com' ` -EnvironmentDNS '172.20.11.21' ` [/powershell]

Remember to only have one nic enabled. We also have slightly less than the minimum space required for the OS disk and simply edit the XML file here C:\CloudDeployment\Configuration\Roles\Infrastructure\BareMetal\OneNodeRole.xml and change the value of this node Role.PrivateInfo.ValidationRequirements.MinimumSizeOfSystemDiskGB. The rest is over to TP3 installation, so far our experience of TP3 is much more stable to install, just the occasional rerun using

[powershell]InstallAzureStackPOC.ps1 -rerun[/powershell]

Once the installation completes obviously check you can access the portal.  I use chrome as it asks a lot less questions to confirm the portal is running.  We use a JSON file defined by a larger automation script to deploy these NAT rules.   Here I will simply share a portion of the resulting JSON file that is saved to C:\CloudDeployment\Setup\StackRecord.json.

[xml] { "Region": "SV5", "ExternalDomain": "AS01.poc.xxxxx.com", "nr_Table": "192.168.102.2:80,443:172.20.51.133:3x.7x.xx5.133", "nr_Queue": "192.168.102.3:80,443:172.20.51.134:3x.7x.xx5.134", "nr_blob": "192.168.102.4:80,443:172.20.51.135:3x.7x.xx5.135", "nr_adfs": "192.168.102.5:80,443:172.20.51.136:3x.7x.xx5.136", "nr_graph": "192.168.102.6:80,443:172.20.51.137:3x.7x.xx5.137", "nr_api": "192.168.102.7:443:172.20.51.138:3x.7x.xx5.138", "nr_portal": "192.168.102.8:13011,30015,13001,13010,13021,13020,443,13003,13026,12648,12650,12499,12495,12647,12646,12649:172.20.51.139:3x.7x.xx5.139", "nr_publicapi": "192.168.102.9:443:172.20.51.140:3x.7x.xx5.140", "nr_publicportal": "192.168.102.10:13011,30015,13001,13010,13021,13020,443,13003,12495,12649:172.20.51.141:3x.7x.xx5.141", "nr_crl": "192.168.102.11:80:172.20.51.142:3x.7x.xx5.142", "nr_extensions": "192.168.102.12:443,12490,12491,12498:172.20.51.143:3x.7x.xx5.143", }

[/xml]

This is used by this script also saved to the setup folder

[powershell] param ( $StackBuildJSONPath='C:\CloudDeployment\Setup\StackRecord.json' )

$server = 'mas-bgpnat01' $StackBuild = Get-Content $StackBuildJSONPath | ConvertFrom-Json

[scriptblock]$ScriptBlockAddExternal = { param($ExIp) $NatSetup=Get-NetNat Write-Verbose 'Adding External Address $ExIp' Add-NetNatExternalAddress -NatName $NatSetup.Name -IPAddress $ExIp -PortStart 80 -PortEnd 63356 }

[scriptblock]$ScriptblockAddPorts = { param( $ExIp, $natport, $InternalIp ) Write-Verbose "Adding NAT Mapping $($ExIp):$($natport)->$($InternalIp):$($natport)" Add-NetNatStaticMapping -NatName $NatSetup.Name -Protocol TCP -ExternalIPAddress $ExIp -InternalIPAddress $InternalIp -ExternalPort $natport -InternalPort $NatPort }

$NatRules = @() $NatRuleNames = ($StackBuild | get-member | ? {$_.name -like "nr_*"}).name foreach ($NATName in $NatRuleNames ) { $NatRule = '' | select name, Internal, External, Ports $NatRule.name = $NATName.Replace('nr_','') $rules = $StackBuild.($NATName).split(':') $natrule.Internal = $rules[0] $natrule.External = $rules[2] $natrule.Ports = $rules[1] $NatRules += $NatRule }

$session = New-PSSession -ComputerName $server

foreach ($NatRule in $NatRules) { Invoke-Command -Session $session -ScriptBlock $ScriptBlockAddExternal -ArgumentList $NatRule.External $NatPorts = $NatRule.Ports.Split(',').trim() foreach ($NatPort in $NatPorts) { Invoke-Command -Session $session -ScriptBlock $ScriptblockAddPorts -ArgumentList $NatRule.External,$NatPort,$NatRule.Internal } }

remove-pssession $session [/powershell]

Next, you need to publish your DNS Records. You can do this by hand if you know your NAT Mappings and as a reference, you can open up the DNS server on the MAS-DC01.

However, here are some scripts I have created to help automate this process. I do run this from another machine but have edited it to run in the context of the AzureStack Host. First, we need a couple of reference files.

DNSMappings C:\clouddeployment\setup\DNSMapping.json

[xml] [ { "Name": "nr_Table", "A": "*", "Subdomain": "table", "Zone": "RegionZone.DomainZone" }, { "Name": "nr_Queue", "A": "*", "Subdomain": "queue", "Zone": "RegionZone.DomainZone" }, { "Name": "nr_blob", "A": "*", "Subdomain": "blob", "Zone": "RegionZone.DomainZone" }, { "Name": "nr_adfs", "A": "adfs", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_graph", "A": "graph", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_api", "A": "api", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_portal", "A": "portal", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_publicapi", "A": "publicapi", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_publicportal", "A": "publicportal", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_crl", "A": "crl", "Subdomain": "RegionZone", "Zone": "DomainZone" }, { "Name": "nr_extensions", "A": "*", "Subdomain": "vault", "Zone": "RegionZone.DomainZone" }, { "Name": "nr_extensions", "A": "*", "Subdomain": "vaultcore", "Zone": "RegionZone.DomainZone" } ]

[/xml]

ExternalMapping C:\clouddeployment\setup\ExternalMapping.json This is a smaller section the contain on the NAT mappings reference in this example.

[xml] [ { "External": "3x.7x.2xx.133", "Internal": "172.20.51.133" }, { "External": "3x.7x.2xx.134", "Internal": "172.20.51.134" }, { "External": "3x.7x.2xx.135", "Internal": "172.20.51.135" }, { "External": "3x.7x.2xx.136", "Internal": "172.20.51.136" }, { "External": "3x.7x.2xx.137", "Internal": "172.20.51.137" }, { "External": "3x.7x.2xx.138", "Internal": "172.20.51.138" }, { "External": "3x.7x.2xx.139", "Internal": "172.20.51.139" }, { "External": "3x.7x.2xx.140", "Internal": "172.20.51.140" }, { "External": "3x.7x.2xx.141", "Internal": "172.20.51.141" }, { "External": "3x.7x.2xx.142", "Internal": "172.20.51.142" }, { "External": "3x.7x.2xx.143", "Internal": "172.20.51.143" } ] [/xml]

Bringing it altogether with this script

[powershell] Param ( $StackJSONPath = 'c:\clouddeployment\setup\StackRecord.json' )

$stackRecord = Get-Content $StackJSONPath | ConvertFrom-Json $DNSMappings = get-content c:\clouddeployment\setup\DNSMapping.json | ConvertFrom-Json $ExternalMapping = get-content c:\clouddeployment\setup\ExternalMapping.json | ConvertFrom-Json

$DNSRecords = @() foreach ($DNSMapping in $DNSMappings) { $DNSRecord = '' | select Name, A, IP, Subdomain, Domain $DNS = $stackRecord.($DNSMapping.Name).split(':') $DNSRecord.IP = ($ExternalMapping | ? {$_.Internal -eq $DNS[2]}).external $DNSRecord.Name = $DNSMapping $DNSRecord.A = $DNSMapping.A $DNSRecord.Subdomain = $DNSMapping.Subdomain.Replace("RegionZone",$stackRecord.Region.ToLower()).Replace("DomainZone",$stackRecord.ExternalDomain.ToLower()) $DNSRecord.Domain = $DNSMapping.zone.Replace("RegionZone",$stackRecord.Region.ToLower()).Replace("DomainZone",$stackRecord.ExternalDomain.ToLower()) $DNSRecords += $DNSRecord } #here you can use this array to do what you need, 2 examples follow

#CSV host file for import $DNSRecords | select a,IP, Subdomain, domain | ConvertTo-CSV -NoTypeInformation | Set-Content c:\clouddeployment\setup\DNSRecords.csv

$SubDomains = $DNSRecords | group subdomain foreach ($SubDomain in ($SubDomains | Where {$_.name -ne ''}) ) { Write-Output ("Records for " +$SubDomain.name) foreach ($record in $SubDomain.Group) { # Initialize $resourceAName = $record.A $PublicIP = $record.ip $resourceSubDomainName = $record.Subdomain $zoneName = $record.Domain $resourceName = $resourceAName + "." + $resourceSubDomainName + "." + $zoneName

Write-Output ("Record for $resourceName ") #Create individual DNS records here

} } [/powershell]

The array will give you the records you need to create.

All things being equal and a little bit of luck...

To access this external Azure Stack instance via Powershell you will need a few details and IDs. Most of this is easy enough, however, to get your $EnvironmentID from the deployment Host, open c:\ecetore\ and find your deployment XML. Approx 573kb. Inside this file search for 'DeploymentGuid' This is your Environment ID.  Or you can run this code on the host, you may need to change the $deploymentfile parameter

[powershell] param ( $DeploymentFile = 'C:\EceStore\403314e1-d945-9558-fad2-42ba21985248\80e0921f-56b5-17d3-29f5-cd41bf862787' )

[Xml]$DeploymentStore=Get-Content $DeploymentFile | Out-String $InfraRole=$DeploymentStore.CustomerConfiguration.Role.Roles.Role|? Id -eq Infrastructure $BareMetalInfo=$InfraRole.Roles.Role|? Id -eq BareMetal|Select -ExpandProperty PublicInfo $PublicInfoRoles=$DeploymentStore.CustomerConfiguration.Role.Roles.Role.Roles.Role|Select Id,PublicInfo|Where-Object PublicInfo -ne $null $DeploymentDeets=@{ DeploymentGuid=$BareMetalInfo.DeploymentGuid; IdentityApplications=($PublicInfoRoles.PublicInfo|? IdentityApplications -ne $null|Select -ExpandProperty IdentityApplications|Select -ExpandProperty IdentityApplication|Select Name,ResourceId); VIPs=($PublicInfoRoles.PublicInfo|? Vips -ne $null|Select -ExpandProperty Vips|Select -ExpandProperty Vip); } $DeploymentDeets.DeploymentGuid [/powershell]

Plug all the details into this connection script to access your stack instance. Well Commented code credit to Chris Speers.

[powershell] #Random Per Insall $EnvironmentID='xxxxxxxx-xxxx-4e03-aac2-6c2e2f0a517a' #The DNS Domain used for the Install $StackDomain='sv5.as01.poc.xxxxx.com' #The AAD Domain Name (e.g. bobsdomain.onmicrosoft.com) $AADDomainName='poc.xxxxx.com' #The AAD Tenant ID $AADTenantID = 'poc.xxxxx.com' #The Username to be used $AADUserName='stackadmin@poc.xxxxx.com' #The Password to be used $AADPassword='SuperSecret!'|ConvertTo-SecureString -Force -AsPlainText #The Credential to be used. Alternatively could use Get-Credential $AADCredential=New-Object PSCredential($AADUserName,$AADPassword) #The AAD Application Resource URI $ApiAADResourceID="https://api.$StackDomain/$EnvironmentID" #The ARM Endpoint $StackARMUri="Https://api.$StackDomain/" #The Gallery Endpoint $StackGalleryUri="Https://portal.$($StackDomain):30016/" #The OAuth Redirect Uri $AadAuthUri="https://login.windows.net/$AADTenantID/" #The MS Graph API Endpoint $GraphApiEndpoint="graph.$($StackDomain)"

$ResourceManager = "https://api.$($StackDomain)/$($EnvironmentID)" $Portal = "https://portal.$($StackDomain)/$($EnvironmentID)" $PublicPortal = "https://publicportal.$($StackDomain)/$($EnvironmentID)" $Policy = "https://policy.$($StackDomain)/$($EnvironmentID)" $Monitoring = "https://monitoring.$($StackDomain)/$($EnvironmentID)"

#Add the Azure Stack Environment Get-azurermenvironment -Name 'Azure Stack AS01'|Remove-AzureRmEnvironment Add-AzureRmEnvironment -Name "Azure Stack AS01" ` -ActiveDirectoryEndpoint $AadAuthUri ` -ActiveDirectoryServiceEndpointResourceId $ApiAADResourceID ` -ResourceManagerEndpoint $StackARMUri ` -GalleryEndpoint $StackGalleryUri ` -GraphEndpoint $GraphApiEndpoint

#Add the environment to the context using the credential $env = Get-azurermenvironment -Name 'Azure Stack AS01' Add-AzureRmAccount -Environment $env -Credential $AADCredential -Verbose Login-AzureRmAccount -EnvironmentName 'Azure Stack AS01'

get-azurermcontext Write-output "ResourceManager" Write-output $ResourceManager Write-output "`nPortal" Write-output $Portal Write-output "`nPublicPortal" Write-output $PublicPortal Write-output "`nPolicy" Write-output $policy Write-output "`nMonitoring " Write-output $Monitoring [/powershell]

Returning something like this.

Thanks for reading.  Hopefullly this helped you in some way.