Introduction
You can find a lot of articles on how to make continuous delivery of a single ASP.NET web application. Many of those articles model a perfect world where a simple, a slightly-modified VS.NET template web application is deployed using Web Deploy. Everything works smoothly in a perfect world.
Deploying real applications is hard. Questions arise when there are configuration settings in the Registry, custom folders structure, custom permissions, Windows services hosting WCF back-end or you have to deploy to a web cluster.
In this article we look at the process of setting up continuous delivery for a solution consisting of ASP.NET web application and Windows service to a staging and production environments using PowerShell remoting and AppVeyor CI.
Solution overview
Our demo solution consists of four projects:
- DemoApp.Web - ASP.NET application, front-end
- DemoApp.Web.Tests - web application unit tests using VisualStudio testing framework
- DemoApp.Service - Windows service hosting WCF services, backend
- DemoApp.Service.Tests - Windows service unit tests using NUnit framework
Demo application repository is hosted on BitBucket:
How are we going to deploy?
The first question - how are we going to deploy Windows service? We don’t have “Publish” menu on Windows service and we don’t have config transforms either! Web Deploy doesn’t work here.
To automate project deployment we’ll use PowerShell deployment framework - AppRolla.
AppRolla uses PowerShell remoting to execute deployment tasks on the target machine. Deployment task downloads application package, unpacks it, updates configuration settings and creates or updates application website and pool. Application package is just a zip with application folder contents uploaded to some external storage and available via HTTP. There is no magic at all - the module is written in PowerShell and could be easily explored or modified.
To give you a sneak peek of PowerShell deployment let’s create a simple web application and deploy it to a server.
Perhaps, the most challenging part in the whole process is setting up PowerShell remoting with SSL certificate. We strongly recommend using HTTPS to communicate with remote servers as all traffic is encrypted.
When you create a new virtual machine on Windows Azure PowerShell remoting will be automatically enabled and configured. PowerShell remoting HTTPS endpoint port 5986 is allowed on firewall and we also add HTTP endpoint to our demo server:
For any other server you can follow this very detailed guide on how to configure PowerShell remoting.
To quickly install IIS 7.5 on demo machine we use this PowerShell command:
Add-WindowsFeature -Name Web-Default-Doc,Web-Dir-Browsing,Web-Http-Errors,Web-Static-Content,Web-Http-Logging,Web-Stat-Compression,Web-Filtering,Web-Net-Ext,Web-Net-Ext45,Web-Asp-Net,Web-Asp-Net45,Web-ISAPI-Ext,Web-Mgmt-Console
Let’s create a trivial web application with a single “Hello, world!” page and deploy it to the demo server.
Create a new SimpleWebApp-1.0.zip archive with the first version of default.aspx inside.
Now, we have to upload application package to some external storage to make it available from target server via HTTP. It could be a web server with FTP enabled, Amazon S3 or Azure blob storage. For our demo we are going to use DropBox. It provides public download links to any items in your DropBox folder.
Copy SimpleWebApp-1.0.zip to your DropBox folder then right-click the file and select “Share DropBox link”. Open the link in the browser and copy URL of “Download” button:
Open PowerShell console “As administrator” and change execution policy to allow remote PowerShell scripts:
Set-ExecutionPolicy RemoteSigned
Install AppRolla.ps1 module (it will be installed to your user profile):
(new-object Net.WebClient).DownloadString("https://raw.github.com/AppVeyor/AppRolla/master/install.ps1") | iex
Import AppRolla module into your current session:
Import-Module AppRolla
AppRolla has two groups of cmdlets: configuration and deployment. You use configuration cmdlets to define your application and environments.
Go ahead and add a new “SimpleWebApp” application with only single “website” role:
New-Application SimpleWebApp
Add-WebsiteRole SimpleWebApp Web -PackageURL "<your-dropbox-download-link>"
Define “demo” environment with one server (enter demo server admin credentials when prompted - more about this later):
New-Environment demo
Add-EnvironmentServer demo "appveyor-demo.cloudapp.net" -Credential (Get-Credential)
Deploy “SimpleWebApp” application to “Demo” environment as version 1.0:
New-Deployment SimpleWebApp 1.0 -to demo
(Click on the image to enlarge it)
Voila! You just deployed web application to a demo server using PowerShell:
Now, let’s change page contents and deploy another version of our demo app. Change “Hello, world!” to “Hello, world 2.0!” Create a new SimpleWebApp-1.1.zip archive with changed default.aspx and upload to DropBox again.
Update “website” role to change its package URL to a new value:
Set-WebsiteRole SimpleWebApp Web -PackageUrl <public-URL-of-SimpleWebApp-1.1>
and make a new deployment as version 1.1:
New-Deployment SimpleWebApp 1.1 -to demo
As you can see from the log a separate folder is created for every new deployment in location c:\applications\<application-name>\<role-name>\<version>. By default, 5 previous deployments are stored on the target server, so the application could be easily rollback to previous version:
Restore-Deployment SimpleWebApp -on demo
To delete all application deployments from demo environment:
Remove-Deployment SimpleWebApp -from demo
We use simple, concise commands to the job!
Continuous builds with AppVeyor CI
AppVeyor CI is a cloud-based continuous integration and deployment platform for Windows developers. It’s hosted, so you don’t need to install and configure it and it’s super easy to setup CI for your project. Oh, and forgot to mention that AppVeyor CI is free for open-source projects!
To setup your project in AppVeyor CI its sources must be hosted in online source control repository like GitHub, BitBucket or Kiln. Both Git and Mercurial are supported.
In our tutorial we use Mercurial repository hosted on BitBucket. BitBucket is pretty cool for commercial projects as it offers unlimited private repositories for free.
Enable NuGet restore
If your solution depends on NuGet package manager do not forget to enable NuGet restore to automatically download packages on build server. Use this guide to enable NuGet package restore and make sure NuGet.exe inside .nuget folder is added to the repository.
Add new project
OK, let’s start by creating a new project:
Once the project is created a new web hook is automatically added to the project repository to kick-off new build on every push.
Build configuration and config transforms
When deploying with Web Deploy config transforms is a key part in application configuration process. For each environment you are going to deploy to you define a new VS.NET solution configuration and then use config transforms to generate web.config with database connection strings and other application settings specific for each environment. This approach seems natural, but has a number of disadvantages:
- Sensitive information like database connection strings is stored in a source control.
- Config transform is applied during the build and every time the project is deployed to a different environment it has to be re-built.
When deploying with AppVeyor config transforms are helpful, but not essential. By default, VS generates two configurations Debug and Release and this is perfectly OK for many cases. Debug configuration is used locally during development and Release configuration is used by CI process to produce a package that could be deployed to any environment. Config transforms should be “really” used to transform configuration file structure, such as disabling “debug” flag, enabling custom errors, disable tracing or replacing Autofac modules, i.e. configuration common for all environments.
Go ahead and change project build configuration to Release on its settings page:
AppVeyor offers three build scenarios:
- Visual Studio solution - runs MSBuild against VS.NET solution or project file (search for the first .sln or .*proj if not specified) and packages build results of all projects as artifacts.
- MSBuild - runs MSBuild on your terms and allow to define custom build artifacts on “Packaging” page.
- Script - runs specified PowerShell script or batch file. Gives maximum freedom in controlling build process and its results.
Assemblies versioning
Every new project build receives a new version which format is specified on “General” tab.
AppVeyor offers Windows style versioning by default (major.minor.{build}.revision), but you can implement any, for example SemVer (major.minor.patch.{build}).
When “Update assembly version attributes” is enabled AppVeyor will patch all AssemblyInfo.* files in solution directory to set current version.
Running tests
AppVeyor could discover and run tests in assemblies using these testing frameworks:
- MSTest
- NUnit
- xUnit
When “Test” stage is enabled AppVeyor will analyze all assemblies in “out” folder to check if they have references to any supported testing framework. If they do all tests within the assembly will be run using appropriate test runner. Test results from all assemblies are aggregated and shown on UI:
(Click on the image to enlarge it)
Build. Test. Package!
OK, now let’s kick-off a new build by pushing some changes to a repository or clicking “New build” button.
For our demo project the build process produced two artifacts: web application and Windows service. Download them to check their contents - they are just regular zips with application files!
As Windows service package is just basically contents of its “bin” folder to produce a package for web application there are more steps involved:
- Web application is built as part of solution.
- A new publishing profile with “File system” publishing method is created and WAP project is published using MSBuild and this profile to make sure web.config transforms and other publishing settings are applied.
- Published web application is packaged to a zip.
Artifacts are stored in Geo-redundant cloud storage and available by unique private download links. To have your own artifacts naming structure and to enable public access you can configure a custom storage.
Deploying successful builds to staging
Now, let’s setup automated deployment to staging as part of the build process.
Deployment should be done in a script that can be PowerShell or batch file. Create “deployment” folder in the root of solution repository to hold our deployment scripts.
Open PowerShell console, navigate to “deployment” folder and run the following command to download template scripts into to current directory:
(new-object Net.WebClient).DownloadString("https://raw.github.com/AppVeyor/Deployment/master/install.ps1") | iex
There are three scripts will be added: configure.ps1, project.ps1 and deploy.ps1.
Basically, to setup deployment we have to edit only one file - project.ps1. This file defines environments we are going to deploy to. Uncomment the line next to new staging environment and add our demo server:
New-Environment Staging
Add-EnvironmentServer Staging “appveyor-demo.cloudapp.net”
Go back to AppVeyor CI and open “Deployment” tab of project settings.
Select “Run deployment script” and specify script path:
deployment\deploy.ps1
Set the following deployment variables:
Environment: Staging
ServerUsername: <target-server-username>
ServerPassword: <target-server-password>
ApiAccessKey: <your-appveyor-api-access-key>
ApiSecretKey: <your-appveyor-api-secret-key>
Both ServerUsername and Password are used to create Credential object for authenticating calls via PowerShell remoting. API keys are required for: a) reading project artifacts to get their package URLs and b) authenticating target server to download artifact packages. AppVeyor API keys could be found on “API Keys” tab of your user profile.
That’s it! Commit “deployment” folder and push it to repository to start a new build with deployment.
How to update connection string in web.config?
Deployment variables are passed to the script in $variables parameter which represents a hashtable. To make additional configuration values available to the application or role use “Configuration” parameter for Set-Application, Set-WebsiteRole or Set-ServiceRole cmdlets. Open project.ps1 and add the following statement:
Set-WebsiteRole $projectName DemoApp.Web -Configuration @{
“ConnectionStrings.DefaultConnection” = $variables.DefaultConnection
}
Then define “DefaultConnection” variable on Deployment settings screen to pass connection string for web application.
Deployment script applies role configuration to web.config for web apps and app.config for Windows apps using the following rules:
- Setting with name “ConnectionStrings.<name>” updates connection string with <name> name in “connectionStrings” section.
- Setting with name “AppSettings.<name>” updates “appSettings” value with <name> name.
Conclusion
In this article we are not trying to downplay Web Deploy tool which might work well for your web application. But if your project is beyond of a blueprint web application that must be deployed to a clustered environment or you need more flexibility and control over the deployment process it’s definitely worth considering alternative solution like PowerShell remoting. This could be especially appealing for developers with PowerShell skills or those ones using PSake.
About the Author
Feodor Fitsner is a .NET developer with entrepreneur spirit, being around Windows Web platform for more than 10 years now. Feodor's newest project is Appveyor CI - hosted Continuous Integration solution for .NET developers. Prior to AppVeyor Feodor developed DotNetPanel control panel for Windows hosting and then was working at Microsoft in Azure org.