Click here to Skip to main content
Click here to Skip to main content

An Application Deployment System for ASP.NET

, 9 May 2005
Rate this:
Please Sign up or sign in to vote.
This is an application deployment system for ASP.NET, including code-based or non-code publishing of DEV to QA, QA to Staging and Staging to Production. Asynchronous technique is used in this project to handle long-running processes such as file copy, status update and application warm-up.

Summary

This is an application deployment system for ASP.NET, including code-based or non-code publishing of DEV to QA, QA to Staging and Staging to Production. Asynchronous technique is used in this project to handle long-running processes such as file copy, status update, application warm-up and simultaneous publishing to multiple servers.

Introduction

When building an application deployment system, we expect it to be able to manage different stages of deployment of DEV to QA, QA to Staging and Staging to Production, in an efficient, robust and convenient way.

Additionally, it is expected to have the following features:

  • Status of the deployment process is reported to the user when there is an update.
  • Application build can be delivered simultaneously to multiple production servers within a cluster.
  • Application warm-up after deployment.
  • File copy should be robust and efficient.
  • Non-code deployment is convenient and requires no build.
  • Certain level of logging is required to audit events.
  • Administration is done via a web interface so that it can be simply accessed by a browser.

Asynchronous Request Handling

The key is to utilize asynchronous techniques in .NET to handle long-running processes such as file copying, status update, application warm-up and simultaneous publishing to multiple servers.

QAPublish, StagingPublish and ProductionPublish are three classes handling the three stages of deployment. They all implement an interface IAsyncRequest. AsyncRequestState is a wrapper class that is used to hold HttpContext, a callback function and extra data passed to IAsyncRequest members. In the case of ProductionPublish, HttpContext.Cache is employed as a temporary storage for deployment status and application warm-up result.

public interface IAsyncRequest
{
    void ProcessRequest();
    AsyncRequestState AsyncRequestState{get;}
}
public class ProductionPublish : IAsyncRequest
{
    private AsyncRequestState _asyncRequestState;
    private static string lockString = string.Empty;

    public ProductionPublish(AsyncRequestState ars)
    {
        _asyncRequestState = ars;
    }

    private void UpdateStatus(string server, PublishStatus status)
    {
        lock(lockString)
        {
            Hashtable ht = 
              _asyncRequestState._ctx.Cache[SR.PubStatusSessionName] as Hashtable;
            if (ht.ContainsKey(server))
            {
                ht[server] = status;
            }
            else
            {
                ht.Add(server, status);
            }
            _asyncRequestState._ctx.Cache[SR.PubStatusSessionName] = ht;
        }
    }

    private void CheckStatus()
    {
        lock(lockString)
        {
            Hashtable ht = 
              _asyncRequestState._ctx.Cache[SR.PubStatusSessionName] as Hashtable;
            if (ht == null)
            {
                ht = new Hashtable();
                _asyncRequestState._ctx.Cache[SR.PubStatusSessionName] = ht;
            }
        }
    }

    private PublishStatus GetStatus(string server)
    {
        Hashtable ht = 
          _asyncRequestState._ctx.Cache[SR.PubStatusSessionName] as Hashtable;
        if (ht.ContainsKey(server))
        {
            return (PublishStatus)ht[server];
        }
        return null;
    }

    AsyncRequestState IAsyncRequest.AsyncRequestState
    {
        get
        {
            return _asyncRequestState;
        }
    }

    void IAsyncRequest.ProcessRequest()
    {
        Process proc = null;
        PublishStatus st;
        string output;

        try
        {
            string server = (string)((Pair)_asyncRequestState._extraData).First;

            ProcessStartInfo procInfo = new ProcessStartInfo();

            // run Robocopy batch file
            procInfo.UseShellExecute = true;
            //If this is false, only .exe's can be run.

            procInfo.WorkingDirectory = Settings.ProdPubBatchPath;
            procInfo.FileName = Settings.ProdPubBatchFile;
            // Program or Command to Execute.

            procInfo.Arguments = string.Format("{0} {1}", 
                                 server, Settings.StagingRootFolder);
            procInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;

            CheckStatus();
            UpdateStatus(server, new PublishStatus(server, SR.Copying, null));

            System.Diagnostics.Trace.WriteLine(SR.CopyStartedLogMsg(server, 
                                                           Utility.Now()));
            proc = Process.Start(procInfo);
            proc.WaitForExit();
            System.Diagnostics.Trace.WriteLine(SR.CopyEndedLogMsg(server, 
                                                         Utility.Now()));

            UpdateStatus(server, new PublishStatus(server, 
                                 SR.Copied, SR.WarmingUp));

            // warm up
            System.Diagnostics.Trace.WriteLine(SR.FirstLoadStartedLogMsg(server, 
                                                                Utility.Now()));
            output = Utility.MakeWebRequest(server);
            System.Diagnostics.Trace.WriteLine(SR.FirstLoadEndedLogMsg(server, 
                                                              Utility.Now()));
            st = GetStatus(server);
            if (output != null)
            {
                st.WarmUpResult = output;
                st.WarmUpStatus = SR.WarmedUp;
            }
            else
            {
                st.WarmUpResult = string.Empty;
                st.WarmUpStatus = SR.Timeout;
            }
            UpdateStatus(server, st);

            st = GetStatus(server);
            st.LoadStatus = SR.Loading;
            UpdateStatus(server, st);

            // 2nd attempt warm up
            System.Diagnostics.Trace.WriteLine(
               SR.SecondLoadStartedLogMsg(server, Utility.Now()));
            output = Utility.MakeWebRequest(server);
            System.Diagnostics.Trace.WriteLine(
               SR.SecondLoadEndedLogMsg(server, Utility.Now()));
            st = GetStatus(server);
            if (output != null)
            {
                st.LoadResult = output;
                st.LoadStatus = SR.Loaded;
            }
            else
            {
                st.LoadResult = string.Empty;
                st.LoadStatus = SR.Timeout;
            }
            UpdateStatus(server, st);

            _asyncRequestState.CompleteRequest();

        }
        catch (Exception ex)
        {
            // exception management
        }
    }
}

The following is the code to start the asynchronous publishing process in the web-based administration interface:

AsyncRequestState reqState = 
    new AsyncRequestState(Context, null, new Pair(srv, null));
ProductionPublish ar = new ProductionPublish(reqState);
Publisher pub = new Publisher();    
pub.BeginProcessRequest(ar);

File Copy Engine – Robocopy

It is not necessary to develop a new file copy program since one is already available in the Windows Resource Kit --Robocopy. This powerful command-line tool can accomplish a variety of scripted copying tasks, including large data migrations and server consolidations. It can be configured to filter files, manipulate file attributes, and do proper logging in the copying process. Without reinventing the wheels, this tool can be readily adapted as the file copy engine. It can be downloaded from various Internet locations by Googling ‘robocopy’. A manual is available here.

We will be impersonating an account which will have sufficient rights to run Robocopy as an external process and access the internal networks where all involved servers reside, to the web-enabled deployment system.

Robocopy is used via batch files which are scripted to deal with various stages of file copying and different pre and post deployment scenarios.

A sample batch file is included in the project.

Site Refresh / Non-code Publishing

In addition to normal daily or nightly builds that are deployed to QA or Staging environments on a certain schedule, quick fixes are sometimes required to be deployed without rerunning a build for non-code files such as aspx, ascs, js, css and so on. I will refer this as a site refresh.

In this system, I included a site refresh managing interface where multiple non-code files from DEV can be added to the publish-queue and deployed to QA or Staging with proper Visual SourceSafe labeling on these files. (You can remove VSS access from the system if your DEV build doesn’t do VSS labeling.)

VSS access is achieved via methods in a SourceSafe helper class which is taken from Microsoft BuildIt tool with some minor modifications. Please refer to BuildIt.

Configurations

The system uses web.config to store application settings. It is important that certain configurations have to be made in order to get the application running.

The following appSettings section in web.config should be fairly self-explanatory:

<appSettings>
       <!--<span class="code-comment"> Site refresh section --></span>
       <add key="RootPath" value="C:\Projects\NPublisher\Web\"/>
       <!--<span class="code-comment"> Web root of application in Dev for site refresh --></span>

       <add key="AllowExtensions" 
          value="|.aspx|.ascx|.xml|.ico|.config|.js|.txt|.html|.css|"/>
    <!--<span class="code-comment"> File extensions allowed in site refresh --></span>

       <!--<span class="code-comment"> VSS & Dev specification section --></span>
       <add key="VSSUsername" value="Username"/>
       <add key="VSSPassword" value="Password"/>
       <add key="IniFilePath" value="\\VssServer\DBFolder"/>
       <!--<span class="code-comment"> VSS DB folder --></span>

       <add key="SrcVSSRootFolder" value="$/NPublisher/Web/"/>
       <!--<span class="code-comment"> Web root on VSS --></span>

       <add key="SrcFileRootFolder" value="C:\Projects\NPublisher\Web\"/>
       <!--<span class="code-comment"> Web root of application in Dev --></span>

       <!--<span class="code-comment"> QA section --></span>
       <add key="QAServer" value="QAServer"/>
       <add key="QARootFolder" value="\\QAServer\WebRoot\"/>
       <!--<span class="code-comment"> Web root of application in QA --></span>

        <add key="QAPubBatchFile" value="Publisher.bat" />
        <add key="QAPubBatchPath" value="C:\Batch\QA\" />

       <!--<span class="code-comment"> Staging section --></span>
       <add key="StagingServer" value="StagingServer"/>
       <add key="StagingRootFolder" value="\\StagingServer\WebRoot\"/>
       <!--<span class="code-comment"> Web root of application in Staging --></span>

       <add key="StagingPubBatchFile" value="Publisher.bat" />
       <add key="StagingPubBatchPath" value="C:\Batch\Staging\" />

       <!--<span class="code-comment"> Production section --></span>
       <add key="ProdServers" 
         value="ProdServer1|ProdServer2|ProdServer3|ProdServer4|ProdServer5" />
       <add key="ProdPubBatchFile" value="Publisher.bat" />
       <add key="ProdPubBatchPath" value="C:\Batch\Production\" />  
     
       <!--<span class="code-comment"> Warmup section --></span>
       <add key="WarmUpUrl" value="http://{0}/Warmup.aspx" />
       <add key="WarmUpTimeout" value="100000" />
       <!--<span class="code-comment"> Http request timeout setting(in milliseconds) --></span>
</appSettings>

Also, make sure the account ASP.NET is running under has sufficient permissions to write tracing logs and access files locally or on remote network shares.

You can impersonate a domain account in the application by adding the following line in the web.config:

<identity impersonate="true" userName="username" password="password" />

Latest Release

Please get the latest release at NPublisher GotDotNet Workplace.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Ting Huang

United States United States
Currently playing Wii...Just love it!

Comments and Discussions

 
GeneralAppSettings PinmemberEric Newton25-Jul-05 8:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 9 May 2005
Article Copyright 2005 by Ting Huang
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid