Publishing Microsoft Azure Stack TP3 on the Internet via NAT

3 Mar

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.

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

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.

$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' `

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

InstallAzureStackPOC.ps1 -rerun

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.

{
"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",
}

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

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

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

[
    {
        "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"
    }
]

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

[
    {
        "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"
    }
]

Bringing it altogether with this script

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
		
	}
}

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

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

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

#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 

Returning something like this.

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

 

Matthew Quickenden

Working with private cloud solutions for several years. Heavy focus on virtualization and automation. Recently working to help business move into and consume true cloud solutions.

LinkedIn