The League was asked by twitter to show how to build and deploy a web app to multiple on prem IIS Servers and the schema of the database for the web app to an on prem SQL Server. Totally a fair request. All of my demos recently have been using PaaS, Azure SQL and Containers. I have totally neglected on prem servers and IaaS! So to remedy that situation, here is a detailed walkthrough.
If you don’t want to read all this nonsense, skip to the bottom of the blog to my tldr section
Scenario
Application
For this blog, I created a ci/cd pipeline that built and deployed an asp.net web app which connected to a SQL Server. The schema of the DB was captured in a SSDT Database project and checked into source control, right along side the actual source.
In the web project, the Web.config’s connection string was pointing to localdb. This way, the developer could easily dev on his own box. Also in the Web.config, there was an appSettings with key = Environment and value = (LOCAL). This appSettings key and value was used by the web app ui to display where the app was running from.
Infrastructure
In this example, my infrastructure had two environments. A Canary environment and a Production environment. Both Canary and Production were identical. They consisted of three machines. Two IIS web servers sitting behind a load balancer and a SQL server.
Setup
Setup was done in two parts. First, I set up my infrastructure to support deploying using Deployment Groups. Then, I created my build and release pipelines in VSTS.
Setting Up My Infrastructure For Deployment
My infrastructure was already in place. IIS was installed and configured on the web server machines and SQL Server was also installed and configured on the db machine. All I needed to do was setup my infrastructure for deployment. I used Deployment Groups in VSTS to deploy to all the servers in my environment (2 web servers and a sql server). Using Deployment Groups allowed me to easily deploy my web app to multiple servers in parallel, as well as my db schema to my db server. To learn more about deployment groups, read this.
One of the pre-requisites for this type of deployment was .NET 3.5 on all of my servers. This was because the deloyment ultimately used msdeploy.exe and sqlpackage.exe to deploy my web app and database schema and those libraries required .NET 3.5. When I built out my environment, .NET 3.5 was not installed on my web servers so to install it, I opened an admin level power shell prompt and entered:
dism.exe /online /enable-feature /all /featurename:NetFX3 /Source:D:\sources\sxs
Creating Deployment Group In VSTS
The first thing I needed to do was create a Deployment Group in VSTS. To do that, I hovered over the Build and Release tab and then selected Deployment Groups.
Next, I clicked on + New
Gave the deployment group a name (I named this deployment group MercuryHealth-Canary) and clicked Create
This took me to the Details page where I got the powershell script needed to install and configure the VSTS agent on all the machines in my environment.
Install VSTS Agent on Environment Machines
Next, I installed and configured the VSTS agent on all the machines in my environment. I clicked the Use a personal access token in the script for authentication and then clicked Copy script to clipboard.
This copied the script I needed to my clipboard, including a PAT for authentication. Then, I went to each of the machines in my Canary environment (my two web servers and my db server), brought up an admin level PowerShell command prompt and pasted the script and ran it. After installing the VSTS agent, the script asked me two questions. Enter deployment group tags for agent (I entered Database for my database server and Webserver for my web servers) and enter user account to use for the service (I chose the default NT AUTHORITY\SYSTEM).
This installed the VSTS agents and registered them to the MercuryHealth-Canary deployment group. Clicking on Targets
I saw all my machines in the deployment group. Notice how the web servers were tagged as WebServer and the db was tagged as Database.
Next I did the same thing again for my MercuryHealth-Prod environment/deployment group.
Setting Up the Build
Since I was using an asp.net app, I just used the asp.net template to create my build pipeline. I hovered over Build and Release and then selected Builds
Clicked + New
And selected the ASP.NET (PREVIEW) template.
This created a build pipeline that was appropriate for an asp.net application.
There were some minor tweaks that I did to the configuration specific to my project. I picked a hosted VS2017 as my Agent queue
and also tweaked my test task to only run my tests with the TestCategory = Unit Test. I also turned on code coverage.
one final tweak, I needed to copy the “built” dacpac file to the staging directory so I added a Copy Files task
After tweaking the build and making sure it worked, I created my release pipeline
Setting Up The Release
I hovered over Build and Release and selected Release
Then clicked + New definition
And selected the IIS Website and SQL Database deployment template
I then renamed the environment to Canary and clicked on + Add artifact
Selected the build I just created and clicked Add
Next I needed to configure my release tasks so I clicked the tasks
Which brought up the release pipeline configuration page.
For my app, the website was named MercuryHealth on port 80, the application pool name was MercuryHealth the web app was named MercuryWeb and my database name was MercuryWeb. The website path was C:\MercuryHealth. The web app path was C:\MercuryHealth\MercuryWeb.
Next in my Environment, I unlinked all. You can keep everything linked. Just enter in the correct configuration. I unlinked them because I thought I was going to do a more involved deploy with even more machines so I unlinked them at the environment level and just configured my info at the task level.
Next I selected the IIS Deployment phase
For deployment group i selected MercuryHealth-Canary. For Required tags I entered in WebServer. So now all tasks in the IIS Deployment phase will deploy in parallel to the machines in the deployment group MercuryHealth-Canary with the tag Webserver.
Next I clicked on the IIS Web App Manage task
Because I set things up as a WebApp, I changed the Configuration type to IIS Web Application, Parent website name to MercuryHealth, Virtual path to /MercuryWeb and Physical path to C:\MercuryHealth\MercuryWeb, check Create or update app pool, set the app pool name to MercuryHealth.
Next, I clicked on IIS Web App Deploy to configure that task
I entered MercuryHealth for Website Name, MercuryWeb for my Virtual Application
Next, I needed to enable variable substitution for my web deploy. Remember that web.config file?
I needed to be able to substitute the connection string with the name DefaultConnection and the appsettings with the key Environment with the correct values for the environment I’m deploying to. At this moment, my deployment bits are all zipped up and they are holding values used for local dev. Not my Canary environment. The IIS Web App Deploy task makes swapping in the correct value super easy.
First, I clicked File Transforms & Variable Substitution Options, then I checked XML variable substitution
Now, if there are variables defined in my release definition that matches a connectionString name or appSettings key in my config files, this task will swap in the values in my variables section!!! How cool is that?
Ok, so to add in my Canary connection string and my Canary environment, I clicked Variables
And added variables for DefaultConnection with the connection string to my canary sql server, then locked it. Added Environment with value (Canary) and I scoped both of those variables to the Canary environment
I still needed to finish configuring my tasks for my db deployment so i next clicked back to Canary Task and selected the SQL Deployment phase.
Selected MercuryHealth-Canary for my Deployment group, typed in Database for Required tags
Clicked on SQL DB Deploy task
Entered MercuryWeb for my database name. Selected SQL Server Authentication for my DB and entered in my DB user name and password (i added them to my variables for canary). Also, since sometimes I want to delete columns or do stuff that destroys data, i added /p:BlockOnPossibleDataLoss=False to the Additional Arguments
And my variables now looked like this
That’s it for the canary environment. This release definition deploys my web app in parallel to all my web app servers in the canary environment, then my db schema deploys to the db server in the canary environment.
To create a the release to Production, I cloned the Canary environment
Then I tweaked the Deployment Group to be MercuryHealth-Prod for both the IIS Deployment phase and the SQL Deployment phase
And then added the variables and values for my prod environment
And that builds out my full CI/CD pipeline into multiple IIS servers and a SQL server database.
TLDR
Ok, that was really long. So if all that nonsense was way too much to read, here is the condensed version.
- use deployment groups to deploy to each on prem/IaaS environment
- tag the servers in your deployment group so you can easily deploy in parallel to similar servers
- Because the dacpac deploy task and the website deploy tasks require .net 3.5, make sure .net 3.5 is installed on machines you are deploying to
- to change values in your config files, just check the XML variable substitution checkbox under File Transforms & Variable Substitution options in the IIS Web App Deploy task and then add the variable values for the release variables and scope it to the right environment
- If you are using dacpacs to deploy your DB schema, it is using SqlPackage.exe. So to enable destroying data, add /p:BlockOnPossibleDataLoss=False as an additional argument to the SQL Server Database Deploy task.
For more information about Deployment Groups in VSTS, check out http://cda.ms/43
Great blog post! I only wish I would’ve found it sooner as I just spent the past few weeks figuring all of this out on my own (still using on-prem TFS 2017 though)!
I have a couple questions though:
1. What do you recommend for replacing application settings that are not in the or sections of the app/web.config? The easiest thing I could find was to use the Tokenizer task in the Release Management Utility tasks (https://marketplace.visualstudio.com/items?itemName=ms-devlabs.utilitytasks). However, doing that involves me having to unzip the web deploy package, use the task to replace tokens via a JSON file, and then zip them back up. It’s not hard, but seems a bit cumbersome. Do you know an an easier/better way?
2. We host all of our SQL servers in availability groups. When doing a SQL deployment via the .dacpac file, it tries to modify database properties that aren’t allowed when the database is in an availability group, such as changing the database’s Recovery mode. Do you know of any way around this?
I understand that you may not have these answers, so I’ll likely post them on Stack Overflow as well, but though it’s worth a shot. Thanks again for the very thorough and detailed post. It will be a great help to lots of organizations that still have hardware purchased and aren’t ready/able to fully commit to moving to the cloud.
Seems like the website doesn’t like the greater-than/less-than signs, and I don’t appear to be able to edit my previous comment. My question 1 above was supposed to read:
1. What do you recommend for replacing application settings that are not in the appSettings or connectionStrings sections of the app/web.config?
Also, I found a stack overflow post related to my question #2: https://stackoverflow.com/questions/32705393/deploying-dacpacs-to-an-availability-group-in-a-locked-down-production
I’d also love to know what you use for swapping out config that’s not in appSettings. We still have applications that call WCF services and need to change endpoint urls or other configuration values depending on environment.
At the moment we use web.xxxx.config transforms but those files don’t get published in the default artifacts and so I end up creating artifacts from the projects, but then they include all the uncompiled source files.
There must be a better way!
If you are using the IIS Web App Deploy Task (or Azure App Service Task if you are going into Azure) you can do file transforms and variable substitutions https://cda.ms/rm. There are also tasks in marketplace that will let you swap values from config files. Also you can tweak your copy task in build so you only copy your config files and not all the uncompiled source!
Awesome post! I wish MS would have a post like this every time they develop new tech.
I still don’t have a good way to replace variables in custom web.config sections. I ended up creating a parameters.xml file for each of my projects that the Azure DevOps release pipeline replaces with pipeline variables at release time. It was the only way I could replace stuff in custom config sections.