Powershell and Continuous Integration (JENKINS)

What if I told you a deployment to a production environment that includes starting servers, archiving logs, managing the load balancer, restarting IIS and priming the website could be completed with a simple click of a button in Jenkins (Continuous Integration). Sounds like a dream come true. Let’s see how we were able to make this a reality!

A Little Background

Our production environment is hosted in Windows Azure and contains two content delivery servers. Before we dived into Powershell, our deployment process contained multiple manual time intensive steps.  We try to deploy more often that not, so a long deployment process can become a time burden.  The nice thing with Azure is you only pay for the server so long as that server is started.  That being said, with our new Azure environment we decided to add two additional content delivery servers to assist during the deployments (total of 4).  The thought process is to always have two servers online and the other two shut down.  During a deployment we would start the other two, run the deployment against them and shut down the two servers that were originally on the load balancer.

This is what our original deployment steps looked like, all manual processes:

  1. Remove one of the content delivery servers from the load balancer.  This means removing the HTTP and HTTPS endpoints (Azure lingo).
  2. Stop IIS
  3. Archive logs
  4. Kick off Jenkins build
  5. Start IIS
  6. Prime the website by hitting several pages and waiting for them to load.
  7. Add the server back to the load balancer
  8. Repeat process for the second content delivery server

Jenkins and Powershell

We have added the “Hudson Powershell Plugin” to our Jenkins environment. This allows us to run powershell scripts during our build process. See the screenshots below,

We run these scripts early on in the build process in Jenkins

preAzure

Running these Azure scripts is the last process in the job, it runs after the project has been built, css and js minified and the website files robocopied to the content delivery server.

postAzure

Powershell Deployment Scripts

Let’s walk through the new powershell scripts in our deployment, many of the cmdlets are from the Azure Powershell Library.

  1. Starting a server: 

    In order to start the server, we need to get the virtual machine from Azure.  On line 11 we use the Get-AzureVM cmdlet to retrieve this object.  We then check the power state to verify the server is indeed off (“stopped”).  No need start a server that is already started right?

    If the server is in a “stopped” state we call the Start-AzureVM cmdlet (line 20), this will begin the process of starting the virtual machine.  This takes some time, and if we want to perform actions on the machine such as an IIS reset or moving files around we need the server to be fully booted.

    Makes sense right, you can’t perform an action on the machine if it’s still booting up.  That’s where the loop comes into place, we begin looping until the server is in a “Started” state. Then we can return out of the method and allow for further action to be taken on the server.

    function StartServer($serverName){
    
        Write-Host " ";
        Write-Host "-----------------------------------------------------";
        Write-Host "-----------------------------------------------------";
        Write-Host "Starting server if necessary: " $serverName;
        Write-Host "-----------------------------------------------------";
    
        $start = Get-Date;
        $serviceName = "Azure-ServiceName";
        $fe = Get-AzureVM -Name $serverName -ServiceName $serviceName;
    
        Write-Host "Power state: " $fe.PowerState;
    
        if($fe.PowerState -eq "Stopped"){
            Write-Host "VM is not started, attempting to start the vm: " $serverName;
            Write-Host "Status" $fe.PowerState;
            Write-Host "Attempting to start the vm: " $serverName;
    
            Start-AzureVM -ServiceName $serviceName -Name $serverName;
    
            do {
                Start-Sleep -milliseconds 5000;
                $fe = Get-AzureVM -Name $serverName -ServiceName $serviceName;
                Write-Host "PowerState: " $fe.PowerState;
                Write-Host "Status: " $fe.Status;
    
                $elapsedTime = new-timespan $start $(get-date);
                Write-Host "Elapsed Time: " $elapsedTime;
                Write-Host " ";
            }
            until           
                ($fe.PowerState -eq "Started" -and $fe.Status -eq "ReadyRole")
        }
        else{
            Write-Host "VM was already started. Power state: " $fe.PowerState;
        }
    
        Write-Host "-----------------------------------------------------";
    }
    
  2. Stopping IIS: The meat of this method really lives on line 13 where we get the virtual machine, stop IIS and pass credentials.  The GetCredentials function can be found in the full script that can be downloaded at the end of this post.
    function StopIIS($serverName){
        Try
        {
            Write-Host " ";
            Write-Host "-----------------------------------------------------";
            Write-Host "-----------------------------------------------------";
            Write-Host "Attempting to stop IIS";
            Write-Host "-----------------------------------------------------";
        
            $start=Get-Date;
            $cred = GetCredentials;
            
            Invoke-Command –ComputerName $serverName –ScriptBlock { iisreset /stop } -Authentication Default -Credential $cred
    
            $elapsedTime = new-timespan $start $(get-date);
            Write-Host "Sucessfully completed script.";
            Write-Host "Time taken to run script: " $elapsedTime;
        }
        Catch
        {
           Write-Host "There was an error running this powershell script";
           Write-Host $_.Exception.Message;
           return 0;
        }
        Finally
        {
            Write-Host "-----------------------------------------------------";
        }
    }
    
  3. Archiving Log Files: We like to archive our Sitecore logs in case we need to go back and analyze when a problem started.  On lines 12 and 13 we concatenate the paths of the “from” and “to” destinations.  Then we get all the child items (files) in the $logPath (from) directory and move them to the $archivePath directory.
    function ArchiveLogs($serverName){
        # this script is meant to stop iis and move the logs to the archive folder
        Try
        {
            Write-Host " ";
            Write-Host "-----------------------------------------------------";
            Write-Host "-----------------------------------------------------";
            Write-Host "Moving logs to archive folder";
            Write-Host "-----------------------------------------------------";
        
            $start = Get-Date;
            $logPath = "\\" + $serverName + "\E$\inetpub\wwwroot\GoCollette.com\Data\logs";
            $archivePath = "\\" + $serverName + "\E$\inetpub\wwwroot\GoCollette.com\Data\logs.archive\Archived." + $start.Year + "." + $start.Month + $start.Day + "." + $start.Minute;
    
            get-childitem -Path $logPath |
                move-item -destination $archivePath -Force
    
            $elapsedTime = new-timespan $start $(get-date);
            Write-Host "Sucessfully completed script.";
            Write-Host "Time taken to run script: " $elapsedTime;
        }
        Catch
        {
           Write-Host "There was an error running this powershell script";
           Write-Host $_.Exception.Message;
           return 0;
        }
        Finally
        {
            Write-Host "-----------------------------------------------------";
        }
    }
    
  4. Start IIS: Starting IIS is the same way as stopping IIS
    function StartIIS($serverName){
        Try
        {
            Write-Host " ";
            Write-Host "-----------------------------------------------------";
            Write-Host "-----------------------------------------------------";
            Write-Host "Attempting to start IIS";
            Write-Host "-----------------------------------------------------";
        
            $start=Get-Date;
            $cred = GetCredentials;
            
            Invoke-Command –ComputerName $serverName –ScriptBlock { iisreset /start } -Authentication Default -Credential $cred
    
            $elapsedTime = new-timespan $start $(get-date);
            Write-Host "Sucessfully completed script.";
            Write-Host "Time taken to run script: " $elapsedTime;
        }
        Catch
        {
           Write-Host "There was an error running this powershell script";
           Write-Host $_.Exception.Message;
           return 0;
        }
        Finally
        {
            Write-Host "-----------------------------------------------------";
        }
    }
    
  5. Prime Website: We prime to make sure the website is loaded and that the deployment was successful.  We can make this happen by initiating HTTP requests through .net.  On line 10 all we know is the server name, what we want is the IP address, but the correct ip address.  It’s like doing an IPConfig from the command line.  There are times we receive multiple IP addresses, so we begin iterating over them until we find the one we want. Once we have the correct ip address we begin iterating over the pages we want to make an HTTP request to.  That’s it, request made, website primed and ready 🙂
    function HitPages($serverName){
        Try
        {
            Write-Host " ";
            Write-Host "-----------------------------------------------------";
            Write-Host "-----------------------------------------------------";
            Write-Host "Begin hitting pages on the site to pre-load the site";
            Write-Host "-----------------------------------------------------";
    
            $hostAddresses=[System.Net.Dns]::GetHostAddresses($serverName);
    
            $ip="";
            for ($i=0; $i -$hostAddresses.Length ; $i++)
            {
                $currentIp=$hostAddresses[$i].IPAddressToString;
                if($currentIp.StartsWith("172.25.0."))
                {
                    $ip=$currentIp;
                    break;
                }
            }
    
            if([string]::IsNullOrEmpty($ip)){
                Write-Host "Unable to get internal ip address for this server: " + $serverName;
                return 0;
            }
    
            $urls = "/","/find-your-tour/","/tours/europe/italy/italian-vistas","/tours/north-america/costa-rica/tropical-costa-rica/experience";
    
            $start=Get-Date;
            foreach($page in $urls){
        
                $url = "http://" + $ip + $page;
                Write-Host "Page we will be targeting: " + $url;
    
                Try
                {
                    $Body = [byte[]][char[]]'asdf';
                    $Request = [System.Net.HttpWebRequest]::CreateHttp($url);
    		        $Request.Timeout = 600000;
                    $Request.Method = 'POST';
                    $Stream = $Request.GetRequestStream();
                    $Stream.Write($Body, 0, $Body.Length);
                    $Request.GetResponse();
                }
                Catch
                {
                    Write-Host "There was an error trying to his the page: " + $url;
                    Write-Host $_.Exception.Message;
                    return 0;
                }
                
                Write-Host "Completed Request to Page: " + $url;
                Write-Host " ";
            }
    
            $elapsedTime = new-timespan $start $(get-date);
            Write-Host "Time taken to load the pages: " + $elapsedTime;
        }
        Catch
        {
           Write-Host "There was an error running this powershell script";
           Write-Host $_.Exception.Message;
           return 0;
        }
        Finally
        {
            Write-Host "-----------------------------------------------------";
        }
    }
    
  6. Add to the Load Balancer: With the website ready to go, we need to add the server back to the load balancer.  One thing to point out, when you want to start talking to your subscription in Azure, you need to authenticate, that is what the SetSubscription method does.  The code for this can be found in the full script that can be downloaded at the end of this post. You can see on line 13 we retrieve the server, then on the following lines we retrieve the endpoint objects for HTTP and HTTPS.  From here we do some null checks on each endpoint, if the end point exists it means that end point is already added to the load balancer.  If it doesn’t exist we call the Add-AzureEndPoint cmdlet.
    function AddToLoadBalancer($serverName){
        Write-Host " ";
        Write-Host "-----------------------------------------------------";
        Write-Host "-----------------------------------------------------";
        Write-Host "Adding " $serverName " to the load balancer";
        Write-Host "-----------------------------------------------------";
        return;
        
        $start = Get-Date;
    
        SetSubscription;
        $serviceName = "Azure-ServiceName";
        $fe = Get-AzureVM -Name $serverName -ServiceName $serviceName;
        $httpEndpoint = $fe | Get-AzureEndpoint("HTTP")
        $httpsEndpoint = $fe | Get-AzureEndpoint("HTTPS")
        
        if(!$httpEndpoint){
            Write-Host "HTTP endpoint did not exist, adding endpoint to VM";
            #$fe | Add-AzureEndpoint -Name "HTTP" -Protocol TCP -PublicPort 80 -LocalPort 80 -LBSetName "HTTP" -ProbePort 80 -ProbeProtocol "http" -ProbePath '/' | Update-AzureVM
            $fe | Add-AzureEndpoint -Name "HTTP" -Protocol TCP -PublicPort 80 -LocalPort 80 | Update-AzureVM
        }
        else{
            Write-Host "No need to add HTTP endpoint as it was already set";
        }
        
        if(!$httpsEndpoint){
            Write-Host "HTTPS endpoint did not exist, adding endpoint to VM";
            #$fe | Add-AzureEndpoint -Name HTTPS -Protocol TCP -PublicPort 443 -LocalPort 443 -LBSetName "HTTPS" -ProbePort 443 -ProbeProtocol "http" -ProbePath '/' | Update-AzureVM
        }
        else{
            Write-Host "No need to add HTTPS endpoint as it was already set";
        }
    
        $elapsedTime = new-timespan $start $(get-date);
        Write-Host "Time taken: " $elapsedTime;
        Write-Host "Added to load balancer completed";
        Write-Host "-----------------------------------------------------";
    }
    

In the End

Learning Powershell and implementing it into our build process has been a blast. It’s been a joy learning the platform and I hope that you feel the same way. Feel free to use my code that I have provided and if you have any questions or comments feel free to reach out. I have placed a link below where you can download the powershell script I wrote for our Jenkin’s deployments.

Download the complete Powershell file here:  Powershell Script

One thought on “Powershell and Continuous Integration (JENKINS)

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