Creating Multiple Azure Virtual Machines Part Deux

In this installment (part deux) of creating multiple Azure virtual machine, we are going to actually connect remotely to those machines and start configuring the software on those boxes.  Spinning up a remote machine is fine, but unless you can actually manipulate it, it’s not very useful.

For those of you who missed the first post, please read Creating Multiple Azure Virtual Machines prior to reading this post so you will know how the machines are built in the first place.

You need to make a connection to the remote machine in order to configure it.  In order to do that you need to have the remote certificate for the Azure machine installed on your local system.  Now, I’ve read a LOT of blog posts on how to create the certificate on your local machine and then push that certificate to the certificate store on the Azure machine, such as this one from Microsoft; Create and Upload a Management Certificate for Azure.  I never quite got this to work.  Did you know that every virtual machine you deploy in Azure ALREADY has a certificate that you can use???

Install The Remote Certificate

This leads me to the first function that we need. This method will query the virtual machine and pull down the read-only thumbprint of the certificate that is used with the HTTPS listener for WinRM. This comes free with every Azure virtual machine so instead of creating one and then trying to push it up to the remote machine, I’m simply going to pull the certificate and install it into my local certificate store.

function InstallRemoteCertificate {
    Param([string] $virtualMachineName, [string] $cloudServiceName)

    $WinRMCert = (Get-AzureVM -ServiceName $cloudServiceName -Name $virtualMachineName | select -ExpandProperty vm).DefaultWinRMCertificateThumbprint
	$AzureX509cert = Get-AzureCertificate -ServiceName $cloudServiceName -Thumbprint $WinRMCert -ThumbprintAlgorithm sha1

	$certTempFile = [IO.Path]::GetTempFileName()
	$AzureX509cert.Data | Out-File $certTempFile

	# Target The Cert That Needs To Be Imported
	$CertToImport = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certTempFile

	$store = New-Object System.Security.Cryptography.X509Certificates.X509Store "Root", "LocalMachine"
	$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
	$store.Add($CertToImport)
	$store.Close()
	
	Remove-Item $certTempFile
}

This is the first step to controlling the remote machine, so let’s place this function in a PowerShell script by itself called “RemoteSessionFunctions.ps1“.

Get The Remote Session

Now we are ready to get a remote session from the virtual machine. This is done through the use of the New-PSSession cmdlet. Create another function in your “RemoteSessionFunctions.ps1” file. This will create a secure password and PowerShell credential that will enable you to connect to the remote machine. We will use the Get-AzureWinRMUri command to get the absolute uri that we need to connect and create a persistent connection to the remote computer.

function GetRemoteSession {
    Param([Parameter(Mandatory = $true, HelpMessage="(Required) The name of the virtual machine.`n")] 
          [string] $virtualMachineName, 
          [Parameter(Mandatory = $true, HelpMessage="(Required) The name of the cloud service.`n")] 
          [string] $cloudServiceName,
          [Parameter(Mandatory = $true, HelpMessage="(Required) The name of the administrative user.`n")] 
          [string] $adminName,
          [Parameter(Mandatory = $true, HelpMessage="(Required) The plain text password of the admin user.`n")] 
          [string] $adminPassword)

    Write-Host ('Creating a remote session to VM "' + $virtualMachineName + '" in the cloud service "' + $cloudServiceName + '"')
    Write-Host ('Using Username: "' + $adminName + '", Password: "' + $adminPassword + '"')

    $secureAdminPassword = $adminPassword | ConvertTo-SecureString -AsPlainText -Force
    $adminUserName = "$virtualMachineName\$adminName"

    $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $adminUserName, $secureAdminPassword

    $remoteUri = Get-AzureWinRMUri -ServiceName $cloudServiceName -Name $virtualMachineName
                
    [System.Management.Automation.Runspaces.PSSession] $remoteSession = New-PSSession -ConnectionUri $remoteUri.AbsoluteUri -Credential $credential -EnableNetworkAccess
    if ($remoteSession -eq $null) {
        throw "Unable to create a remote PowerShell session to the computer."
    }
    
    Write-Host ('Remote Session: ' + $remoteSession.Name + ', State: ' + $remoteSession.State)

    return $remoteSession
}

Validating The Remote Session

Since this is a persistent connection to the remote machine, it will only shut down if there is a connection failure or the connection is closed. The following method will validate that the session is ready for use and even attempt to open it again, should it shut down unexpectedly. We will place this in the “RemoteSessionFunctions.ps1” PowerShell script file as well.

function ValidateRemoteSession {
    Param([Parameter(Mandatory=$true, HelpMessage="The PSSession object that represents the remote connection must be provided.`n")]
          [System.Management.Automation.Runspaces.PSSession] $remoteSession)

    if ($remoteSession.State -ne [System.Management.Automation.Runspaces.RunspaceState]::Opened) {
        Write-Host 'The PSSession passed in is not opened.  Attempting to open it again.'
        Connect-PSSession -Session $remoteSession        
    }

    if ($remoteSession.State -ne [System.Management.Automation.Runspaces.RunspaceState]::Opened) {
        throw 'ERROR - The PSSession could not be opened.  Method Call Aborted.'
    }
}

Closing The Remote Session

The last function to add to our “RemoteSessionFunctions.ps1” PowerShell script file allows us to close and cleanup the connection once we are finished with it. Please note that not only does it disconnect from the remote server, but it removes the session from memory as well. Without this little bit of cleanup we could end up with hundreds of disconnected remote sessions just sitting in memory.

function CloseRemoteSession {
    Param([Parameter(Mandatory = $true, HelpMessage="The PSSession object that represents the remote connection must be provided.`n")] 
          [System.Management.Automation.Runspaces.PSSession] $remoteSession)

    Write-Host ('Closing Remote Session ' + $remoteSession.Name)

    if ($remoteSession.State -eq [System.Management.Automation.Runspaces.RunspaceState]::Opened) {
        Disconnect-PSSession -Session $remoteSession -IdleTimeoutSec 60
        Remove-PSSession -Session $remoteSession
    }

    Write-Host ('Remote Session: ' + $remoteSession.Name + ', State: ' + $remoteSession.State)
}

Now we are done with the “RemoteSessionFunctions.ps1” PowerShell script file so you can save and close this file.

What’s Sessions Got To Do, Got To Do With It?

What does the session have to do with it? Everything, this is how we are going to execute commands on the remote system. Let’s have a little fun with this shall we? Open a PowerShell window and navigate to the folder that you saved the PowerShell script file we just created. Let’s try the following commands just to get a lay of the land.

PS C:\PowerShell\Scripts> . .\RemoteSessionFunctions.ps1

PS C:\PowerShell\Scripts> InstallRemoteCertificate -cloudServiceName 'samplecloud' -virtualMachineName 'sample-vm-01'

PS C:\PowerShell\Scripts> $rs = GetRemoteSession -virtualMachineName 'sample-vm-01' -cloudServiceName 'samplecloud' -adminName 'TestAdmin' -adminPassword 'SuperSecretP@ssw0rd'
Creating a remote session to VM "sample-vm-01" in the cloud service "samplecloud"
Using Username: "TestAdmin", Password: "SuperSecretP@ssw0rd"
Remote Session: Session1, State: Opened

PS C:\PowerShell\Scripts> $rs.State
Opened

PS C:\PowerShell\Scripts> Enter-PSSession -Session $rs

[samplecloud.cloudapp.net]: PS C:\Users\TestAdmin\Documents> cd C:\Users

[samplecloud.cloudapp.net]: PS C:\Users> ls

    Directory: C:\Users

Mode                LastWriteTime     Length Name                                                                                                                                                                                                        
----                -------------     ------ ----                                                                                                                                                                                                        
d----         8/24/2014   4:59 AM            .NET v2.0                                                                                                                                                                                                   
d----         8/24/2014   4:59 AM            .NET v2.0 Classic                                                                                                                                                                                           
d----         8/24/2014   4:59 AM            .NET v4.5                                                                                                                                                                                                   
d----         8/24/2014   4:59 AM            .NET v4.5 Classic                                                                                                                                                                                           
d----         8/24/2014   4:59 AM            Classic .NET AppPool                                                                                                                                                                                        
d-r--         7/12/2013  11:45 PM            Public                                                                                                                                                                                                      
d----         8/24/2014   5:59 AM            TestAdmin                                                                                                                                                                                                  

[samplecloud.cloudapp.net]: PS C:\Users> exit

PS C:\PowerShell\Scripts> Get-PSSession

 Id Name            ComputerName    State         ConfigurationName     Availability
 -- ----            ------------    -----         -----------------     ------------
  2 Session1        samplecloud...  Opened        Microsoft.PowerShell     Available

PS C:\PowerShell\Scripts> CloseRemoteSession -remoteSession $rs
Closing Remote Session Session1

 Id Name            ComputerName    State         ConfigurationName     Availability
 -- ----            ------------    -----         -----------------     ------------
  1 Session1        samplecloud...  Disconnected  Microsoft.PowerShell          None
Remote Session: Session1, State: Closed

PS C:\PowerShell\Scripts> Get-PSSession

As you can see, we were able to use the . syntax to load the utility functions into memory. I first install the certificate from the remote Azure machine into my local certificate store. Next will get the remote session to the virtual machine and I use the Enter-PSSession passing the remote session to create a PowerShell window on the remote box.
Please Note: This PowerShell windows does NOT have access to the functions that are local to your computer. It is governed by the PowerShell modules that are loaded on the remote machine.
I then browse to the users folder on the remote machine and list the contents of the folder. To exit, I simply type the word “exit” and remote PowerShell window exits. Please note that the session did not go away! The session is still open and connected to the remote virtual machine. Finally we close the remote session and clean up our variables.

Practical Use

Now that we’ve had our fun; let’s use this for a practical end. We are going to create a new PowerShell script file. Let’s call it “WebServerFunctions.ps1“. It will have a singular function in it

. .\RemoteSessionFunctions.ps1

function EnsureWebServerInstalled {
    Param([Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession] $remoteSession)

    ValidateRemoteSession $remoteSession

    Write-Host "Checking for the existence of IIS Web Server"

    $webServerResults = Invoke-Command -Session $remoteSession -ScriptBlock { Get-WindowsFeature web-server }
    if ($webServerResults.Installed -eq $false) {
        Write-Host "The IIS Web Server Feature was not found on the machine, installing..."
        Invoke-Command -Session $remoteSession -ScriptBlock { Install-WindowsFeature Web-Server -IncludeAllSubFeature -IncludeManagementTools }
        Write-Host "Removing the WebDAV feature..."
        Invoke-Command -Session $remoteSession -ScriptBlock { Uninstall-WindowsFeature Web-DAV-Publishing }
    }

    $webServerVersion = Invoke-Command -Session $remoteSession -ScriptBlock { Get-ItemProperty HKLM:\SOFTWARE\Microsoft\InetStp }
    if ($webServerVersion -ne $null) {
        Write-Host ($webServerVersion.IISProgramGroup + ' (' + $webServerVersion.SetupString + ') ')
        Write-Host ($webServerVersion.VersionString + ' (' + $webServerVersion.MajorVersion + '.' + $webServerVersion.MinorVersion + ')')
        Write-Host ('Install Location: ' + $webServerVersion.InstallPath)
    }
}

Once you have created this file, then we are ready to run it.

PS C:\PowerShell\Scripts> . .\WebServerFunctions.ps1

PS C:\PowerShell\Scripts> $rs = GetRemoteSession -virtualMachineName 'sample-vm-01' -cloudServiceName 'samplecloud' -adminName 'TestAdmin' -adminPassword 'SuperSecretP@ssw0rd'
Creating a remote session to VM "sample-vm-01" in the cloud service "samplecloud"
Using Username: "TestAdmin", Password: "SuperSecretP@ssw0rd"
Remote Session: Session1, State: Opened

PS C:\PowerShell\Scripts> EnsureWebServerInstalled -remoteSession $rs
Checking for the existance of IIS Web Server
The IIS Web Server Feature was not found on the machine, installing...
Removing the WebDAV feature...
Microsoft Internet Information Services (IIS 8.0)
Version 8.0 (8.0)
Install Location: C:\Windows\system32\inetsrv

PS C:\PowerShell\Scripts> CloseRemoteSession -remoteSession $rs
Closing Remote Session Session1

 Id Name            ComputerName    State         ConfigurationName     Availability
 -- ----            ------------    -----         -----------------     ------------
  1 Session1        samplecloud...  Disconnected  Microsoft.PowerShell          None
Remote Session: Session1, State: Closed

Thank you for reading part two of my PowerShell series. Hopefully the use and power of remote sessions is not a little clearer to you. If you have any questions just drop be a line. If you like what I’m doing please let me know that to!
In my next post I’ll talk about… (wait)
Hush, now… Spoilers!

2 thoughts on “Creating Multiple Azure Virtual Machines Part Deux

  1. Pingback: Creating Multiple Azure Virtual Machines Part Tre | OutOfMemoryException

  2. Pingback: Creating Multiple Azure Virtual Machines – Part Vier | OutOfMemoryException

Leave a comment