Office 365: Deploying your SSO Identity Infrastructure in Microsoft Azure – Part 2

This is part 2 of 3 in a series where we go through how to create a highly available SSO infrastructure for Office 365 in Microsoft Azure. In this part we will deploy all infrastructure components such as virtual machines and load balancers, all with help from PowerShell!
Part 1 of the series can be found here.
Part 3 of the series can be found here.

As mentioned, in order to automate the process of creating the SSO-infrastructure in Azure, I’ve created a PowerShell script/Hydration Kit that does that for you. Based on input from a CSV file, the Machines will not only be provisioned, they will also be configured based on their specific role as follows:

All Machines will

  • Join the domain specified in the $DomainName parameter. If you for example would like to exclude the WAP-servers from getting AD-joined, that’s something for a later version of the script.
  • Get the Telnet-Client Installed (Important 🙂 ).
  • Get an Azure static ip assignment/subnet and InstanceSize based on the input from the CSV file.
  • Be placed in the correct cloud service, availability set (if not set to ‘None’) and subnet as specified in the CSV file.
  • Get the Operating System specified in the $ImageFamily parameter installed.

Domain Controller

  • One 60gb extra DataDisk with HostCaching will be added to the machine (will be initialized and formatted)
  • The role AD-Domain-Services will be installed. Note that the Domain Controller will not be promoted automatically by the script.

ADFS Server

  • An Azure Internal Load Balancer will be added with the IP Address specified in the CSV file.
  • Endpoints for HTTPS will be added to the Load Balancer pointing at the servers.
  • The role ADFS-Federation will be installed.

2014-11-26 14-47-53

Web Application Proxy

  • An external load balanced HTTPS endpoint will be added.
  • The role Web-Application-Proxy will be installed.

2014-11-26 14-39-03
2014-11-26 14-44-26
2014-11-26 14-38-45

GETTING STARTED

Assumptions
In order for the script to work properly, you need to have the base infrastructure (AffinityGroups, Virtual Networks/Subnets/DNS) in place prior to running the script. Since the script is automatically joining the VM’s to the domain specified, you need to have connectivity to your local domain as well.
The Default WinRM Endpoint is used to configure the machines, which means you might get issues if your firewall is blocking high ports, such as in the screenshot below.
2014-11-26 14-05-59

The Azure PowerShell module do of course need to be installed on the machine you are running the script.

RUNNING THE SCRIPT
1. First, download the script and the csv sample file from here.
2. Edit the csv file according to your needs, make sure that you put the related servers (WAP, ADFS) in the same CloudService, Subnet and Availabilityset etc. For the ADFS servers, you also need to add the Internal Load Balancer IP in the InternalLB column.
2014-11-26 15-08-14
3. Run the script. Remember to change the parameters to match your Azure tenant configuration.If you haven’t already added your Account to the session, you will be prompted to do so. You will find screenshots of the steps below. If you lose your Azure session (internet problems etc.) while running, the script can safely be restarted, and will pickup where it stopped after some verification steps.

.\CreateO365AzureEnvironment.ps1 -Verbose -InputFile .\AzureMachines.csv

a. Prompted for domain join credentials
2014-11-26 12-59-23
b. Prompted to set local admin credentials for the new servers
2014-11-26 12-59-57
c. Prompted for Azure tenant credentials
2014-11-26 15-28-03
4. Grab some coffee and watch PowerShell do the magic. The entire deployment has taken between 35-45 minutes when I’ve been running it. If you don’t like having the WinRM Endpoint on each exposed to the internet, you need to remove them manually after deployment.
2014-11-26 15-00-16

CreateO365AzureEnvironment.ps1

<#
.SYNOPSIS
    The scripts creates and configures virtual machines in Microsoft Azure based on an input CSV file.
.EXAMPLE
   .\CreateO365AzureEnvironment.ps1 -InputFile '.\AzureMachines.csv' -Verbose
.NOTES
    File Name: CreateO365AzureEnvironment.ps1
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net
    The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.
    Requires PowerShell Version 3.0!
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param (
  [Parameter(Mandatory=$false)][string]$AffinityGroup = '365lab-affinitygroup',
  [Parameter(Mandatory=$false)][string]$DomainName = '365lab.internal',
  [Parameter(Mandatory=$false)][string]$DomainShort = '365lab',
  [Parameter(Mandatory=$false)][string]$ImageFamily = 'Windows Server 2012 R2 Datacenter',
  [Parameter(Mandatory=$false)][string]$VnetName = '365lab-azure',
  [Parameter(Mandatory=$false)][string]$AdminUsername = '365_admin',
  [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()]$DomainJoinCreds = (Get-Credential -Message 'Please enter credentials to join the server to the domain'),
  [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()]$VMCredentials = (Get-Credential -Message 'Please enter the local admin password for the servers' -UserName $AdminUsername),
  [Parameter(Mandatory=$false)][ValidateScript({Import-Csv -Path $_})]$InputFile =  '.\AzureMachines.csv'
)
#Check that script is running in an elevated command prompt
if (-not([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("S-1-5-32-544")) {
  Write-Warning "You need to have Administrator rights to run this script!`nPlease re-run this script as an Administrator in an elevated powershell prompt!"
  break
}
#region functions
function Connect-Azure {
  begin {
      try {
          Import-Module Azure
      } catch {
          throw "Azure module not installed"
      }
  } process {
    if (-not(Get-AzureSubscription -ErrorAction Ignore).IsCurrent -eq $true) {
      try {
        Add-AzureAccount -WarningAction stop -ErrorAction Stop
      } catch {
        throw "No connection to Azure"
      }
    }
  } end {
    $AzureSubscription = Get-AzureSubscription -Default -ErrorAction Ignore
    Set-AzureSubscription -SubscriptionId $AzureSubscription.SubscriptionId -CurrentStorageAccountName (Get-AzureStorageAccount -WarningAction Ignore).StorageAccountName
    Write-Verbose "Connected to Azure"
  }
}
function Configure-JDAzureVM {
  [CmdletBinding(SupportsShouldProcess=$true)]
  Param(
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Role,
    [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()]$VMCredentials,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$VM,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$CloudService,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$VMUri,
    [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()][string]$InternalLBIP,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Subnet
  )
  begin {
    Write-Verbose "Starting to configure $VM..."
  }
  process {
    #Install mandatory features
    $AzureVM = Get-AzureVM -ServiceName $CloudService -Name $VM
    #Establish a PS Session to the AzureVM
    $SessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck
    $VMSessionConfig = @{
      ConnectionUri = $VMUri
      SessionOption = $SessionOptions
      Credential = $VMCredentials
    }
    $VMSession = New-PSSession @VMSessionConfig -ErrorAction Stop
    #Install mandatory features
    Write-Verbose "Installing mandatory features on $VM"
    Invoke-Command -Session $VMSession -ScriptBlock {
      #Install the MOST IMPORTANT FEATURE of them all!
      Add-WindowsFeature -Name Telnet-Client -IncludeManagementTools -WarningAction Ignore
    }
    #Install features per role
    Write-Verbose "$VM is $Role."
    switch ($Role) {
    'Domain Controller' {
      #Add additional data disk to the domain controller, with host caching disabled
      if (-not(Get-AzureDataDisk -VM $AzureVM)) {
        Write-Verbose "Adding additional data disk..."
        Add-AzureDataDisk -CreateNew -DiskSizeInGB 60 -DiskLabel "ADDB" -HostCaching None -LUN 0 -VM $AzureVM | Update-AzureVM -ErrorAction Stop
      }
      Write-Verbose "Installing role 'AD-Domain-Services'"
      Invoke-Command -Session $VMSession -ScriptBlock {
        #Initialize and format additional disks
        Get-Disk | Where-Object {$_.PartitionStyle -eq 'raw'} | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize |
            Format-Volume -FileSystem NTFS -NewFileSystemLabel "ADDB" -Confirm:$false
        #Install Role specific features
        Add-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools -WarningAction Ignore
      }
    } 'ADFS Server' {
      #Create Internal Load Balancer for the ADFS farm if not already created
      if (-not(Get-AzureInternalLoadBalancer -ServiceName $CloudService) -and $InternalLBIP) {
        Write-Verbose "Creating/adding $vm to Internal Load Balancer $($AzureVM.AvailabilitySetName) ($InternalLBIP)"
        Add-AzureInternalLoadBalancer -ServiceName $CloudService -InternalLoadBalancerName $azurevm.AvailabilitySetName -SubnetName $Subnet -StaticVNetIPAddress $InternalLBIP
      }
      #Add the server to the Internal Load Balanced set and add 443 as endpoint
      if (-not((Get-AzureEndpoint -VM $AzureVM).InternalLoadBalancerName -eq $AzureVM.AvailabilitySetName)) {
        Write-Verbose "Adding Internal Load Balanced endpoint on port 443 to $VM"
        Add-AzureEndpoint -Name HTTPS -Protocol tcp -LocalPort 443 -PublicPort 443 -LBSetName $AzureVM.AvailabilitySetName -InternalLoadBalancerName $AzureVM.AvailabilitySetName -VM $AzureVM -DefaultProbe | Update-AzureVM -ErrorAction Stop
      }
      Write-Verbose "Installing role 'ADFS-Federation'"
      Invoke-Command -Session $VMSession -ScriptBlock {
        #Install Role specific features
        Add-WindowsFeature -Name ADFS-Federation -IncludeManagementTools -WarningAction Ignore
      }
    } 'Web Application Proxy' {
      #Add the server to the Cloud Service Load Balanced set and add 443 as endpoint
      if (-not((Get-AzureEndpoint -VM $AzureVM).LBSetName -eq $AzureVM.AvailabilitySetName)) {
        Write-Verbose "Adding Load Balanced endpoint on port 443 to $CloudService.cloudapp.net"
        Add-AzureEndpoint -Name HTTPS -Protocol tcp -LocalPort 443 -PublicPort 443 -LBSetName $AzureVM.AvailabilitySetName -VM $AzureVM -DefaultProbe -LoadBalancerDistribution sourceIP | Update-AzureVM -ErrorAction Stop
      }
      Write-Verbose "Installing role 'Web-Application-Proxy'"
      Invoke-Command -Session $VMSession -ScriptBlock {
        #Install Role specific features
        Add-WindowsFeature -Name Web-Application-Proxy -IncludeManagementTools -WarningAction Ignore
      }
    }
   }
  }
  end {
    Write-Verbose "Finished configuring $VM" 

  }
}
function New-JDAzureVM {
  [CmdletBinding(SupportsShouldProcess=$true)]
  Param(
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$VMName = '',
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$CloudService,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$IPAddress,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Size,
    $VMCredentials,
    $DomainJoinCreds,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$SubnetName,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$OperatingSystem,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$VnetName,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$AvailabilitySet,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$AffinityGroup
  )

  if (-not(Get-AzureVM -ServiceName $CloudService -Name $VMName -WarningAction Ignore))  {
    #region VM Configuration
    #Get the latest VM image based on the operating system choice
    $ImageName = (Get-AzureVMImage | Where-Object {$_.ImageFamily -eq $OperatingSystem} | Sort-Object -Property PublishedDate -Descending | Select-Object -First 1).ImageName
    #Construct VM Configuration
    $VMConfigHash = @{
      Name =  $VMName
      InstanceSize = $Size
      ImageName = $ImageName
    }
    if ($AvailabilitySet -ne 'None') {
      $VMConfigHash["AvailabilitySetName"] = $AvailabilitySet
    }
    #VM Provisioning details
    $VMProvisionHash = @{
      AdminUsername = $VMCredentials.UserName
      Password = $VMCredentials.GetNetworkCredential().Password
      JoinDomain = $DomainName
      DomainUserName = $DomainJoinCreds.UserName.split("\")[1]
      DomainPassword = $DomainJoinCreds.GetNetworkCredential().Password
      Domain = $DomainShort
    }
    #Create VM configuration before deploying the new VM
    $VMConfig = New-AzureVMConfig @VMConfigHash | Add-AzureProvisioningConfig @VMProvisionHash -WindowsDomain -NoRDPEndpoint |
                    Set-AzureStaticVNetIP -IPAddress $IPAddress | Set-AzureSubnet -SubnetNames $SubnetName |
                        Set-AzureVMBGInfoExtension -ReferenceName 'BGInfo'
    #endregion VM Configuration
    #region Create VM
    Write-Verbose "Creating $VMName in the Cloud Service $CloudService"
    $AzureVMHash = @{
      ServiceName = $CloudService
      VMs = $VMConfig
      VnetName = $VnetName
      AffinityGroup  = $AffinityGroup
    }
    New-AzureVM @AzureVMHash -WaitForBoot -ErrorAction Stop -WarningAction Ignore
    #endregion Create VM
  } else {
    Write-Warning "$VMName do already exist..."
  }
}
 #endregion functions
 #Connect with Azure PowerShell
  Connect-Azure
  $AzureMachines = Import-Csv -Path $InputFile
  Write-Verbose "Starting deployment of machines at: $(Get-date -format u)"
foreach ($Machine in $AzureMachines) {
  Write-Verbose "Starting to process $($Machine.Name)"
  #Create VM Configuration hashtable based on input from CSV file
  $VirtualMachine = @{
    VMName = $Machine.Name
    CloudService = $Machine.CloudService
    IPAddress = $Machine.IP
    VMCredentials = $VMCredentials
    DomainJoinCreds = $DomainJoinCreds
    SubnetName = $Machine.SubnetName
    AvailabilitySet = $Machine.AvailabilitySet
    AffinityGroup = $AffinityGroup
    Size = $Machine.Size
    VnetName = $VnetName
    OperatingSystem = $ImageFamily
  }
  try {
    #Create virtual machine
    New-JDAzureVM @VirtualMachine -ErrorAction Stop
    #Wait for machine to get to the state "ReadyRole" before continuing.
    while ((Get-AzureVM -ServiceName $Machine.CloudService -Name $Machine.Name).status -ne "ReadyRole") {
      Write-Verbose "Waiting for $($Machine.Name) to start..."
      Start-Sleep -Seconds 15
    }
    $VMUri = Get-AzureWinRMUri -ServiceName $machine.CloudService -Name $Machine.Name
    #Create VM Role configuration
    $VMRoleConfiguration = @{
      Role = $Machine.Role
      VMUri = $VMUri
      VMCredentials = $VMCredentials
      VM = $Machine.Name
      CloudService = $Machine.CloudService
      Subnet = $machine.SubnetName
    }
    #Add internal load balancer IP to the role configuration
    if ($Machine.InternalLB) {
      $VMRoleConfiguration["InternalLBIP"] = $Machine.InternalLB
    }
    #Role specific configuration of the virtual machine
    Configure-JDAzureVM @VMRoleConfiguration -ErrorAction Stop
  } catch {
    Write-Warning $_
  }
}
Write-Verbose "Finished deployment of machines at: $(Get-date -format u)"

RESULTS
After some waiting for the provisioning, we now have the infrastructure in place in order to complete the configuration of our SSO infrastructure for Office 365 in Microsoft Azure. In the next part of this series we will put all the pieces together and finish AADSync, ADFS and WAP configuration against Office 365 / Azure Active Directory.
2014-11-26 15-54-05

Enjoy, let me know if you have any feedback!

/Johan

Advertisements

One thought on “Office 365: Deploying your SSO Identity Infrastructure in Microsoft Azure – Part 2

  1. Pingback: Office 365: Deploying your SSO Identity Infrastructure in Microsoft Azure (Using Azure AD Connect) – Part 3 | Tailspintoys – 365lab.net

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s