Continuous Delivery with Containers – Say Goodbye to IIS Express and LocalDB, with Visual Studio 2017, Docker and Windows Containers
A view I've heard expressed a few times recently, and which I completely agree with, is that we need to be discovering problems with our applications as far to the left as possible since it's much cheaper to fix problems there than further down the line towards—or even in—production. So with this in mind is it just me who feels slightly uneasy that in the Visual Studio world the development and debugging of applications destined for Windows servers tends be on Windows desktop machines using lightweight counterparts of server applications such as IIS Express to host ASP.NET websites and LocalDB to host SQL Server databases? With this setup it seems like we could be storing up trouble for later in the pipeline...
Whether my unease is justified or not, I need feel troubled no more since the world of containers offer us a solution! Since Docker for Windows now supports Windows Containers and Visual Studio 2017 has support for Docker built-in we can now develop server applications on Windows 10 and run and debug them on the exact same operating systems they will run on in production.
In this post I take my version of Contoso University that I've been using for several years now and amend it so that in the developer inner loop phase (ie everything that happens before code is checked in to the build server) the website runs in a Windows Server 2016 container running IIS (rather than IIS Express) and the SQL Server Database Project runs on SQL Server 2016 (rather than LocalDB).
Development Environment
The world of containers is evolving rapidly and the tooling might have changed by the time you read this. At the time of writing my environment is as follows:
- Windows 10 Professional version 1703 (OS Build 15063.250)
- Visual Studio Enterprise 2017 version 15.1 (26403.7) with the ASP.NET and web development workload
- Docker for Windows 17.03.1-ce running Windows containers (I recommend the stable channel as at the time of writing the edge version had a bug that caused a problem for Docker support in Visual Studio)
Depending on the speed of your internet connection you might want to docker pull the following images if you are planning on following along:
It's perhaps worth saying here that I'm using these images for convenience because they are available on Docker Hub. In a production scenario you probably wouldn't want to rely on an image as fully formed as microsoft/aspnet and you would probably start with microsoft/windowsservercore or microsoft/nanoserver and have full control of what is installed. You definitely wouldn't start with microsoft/mssql-server-windows-developer of course.
The Contoso University sample application is essentially the same as Microsoft's version except I've changed the database from Entity Framework Code First to a SQL Server Database Project. I've also changed the application to work with SQL Server authentication (rather than Windows authentication) thus removing the need for a domain controller to supply a domain account. You can get the starting point code from here and the final code here.
Adding Initial Docker Support
The first step towards Dockerizing Contoso University is to add initial Docker support for the ASP.NET web application (out-of-the-box support for SQL Server Database Projects isn't available). This is a simple as right-clicking the ContosoUniversity.Web project and choosing Add > Docker Support. This has three main visible effects:
- A new docker-compose ‘project' is added at Solution level and is made the Startup Project. This project contains several .yml files.
- A Dockerfile file and a (nested) .dockerignore file are added to ContosoUniversity.Web.
- The toolbar button that normally launches a browser has now switched to launching Docker:
The Dockerfile added to ContosoUniversity.Web is based on the microsoft/aspnet image so at this point you should now be able to run the application using the Docker toolbar button and have the website run in a Windows Container based on that image. The database side of things isn't working at this stage of course—Web.config is pointing to LocalDB and the container running the website can't see LocalDB.
To understand what has been created, open a PowerShell session and run docker images followed by docker ps. You should see that an image called contosouniversity.web has been created with a dev tag, and that this image has been used to create a container called something like dockercompose362878786_contosouniversity.web_1.
Adding Docker Support for the SQL Server Database Project
Adding Docker support for the SQL Server Database Project requires the following steps:
- Manually add a Dockerfile file and .dockerignore file to the root of ContosoUniversity.Database. Given that these files don't have file extensions and that database projects are quite prescriptive about what they think you should be adding it's easier to add them outside of Visual Studio and then add them in as existing items. (Note that if you are using Windows Explorer you'll need to create .dockerignore as .dockerignore.—Windows will drop the trailing period).
- Optionally, close Visual Studio and reopen the solution folder in a text editor such as Visual Studio Code. Open ContosoUniversity.Database.sqlproj and search for the Dockerfile and .dockerignore entries. Change them to look as follows to achieve the nested file effect in Visual Studio:
1234<None Include="Dockerfile" /><None Include=".dockerignore"><DependentUpon>Dockerfile</DependentUpon></None> - .dockerignore just needs to contain an asterisk—meaning everything should be ignored.
- Dockerfile should contain the following code:
123456789# escape=`FROM microsoft/mssql-server-windows-developerSHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"]EXPOSE 1433VOLUME c:\databaseENV sa_password Very$trOngPa$$word!ENV ACCEPT_EULA=Y - Switching to the docker-compose ‘project', docker-compose.yml should be amended to the following:
123456789101112version: '2.1'services:contosouniversity.database:image: contosouniversity.databasebuild: .\ContosoUniversity.Databasecontosouniversity.web:image: contosouniversity.webbuild: .\ContosoUniversity.Webdepends_on:- contosouniversity.database - A change is also needed to docker-compose.vs.debug.yml which should be amended to the following:
123456789101112131415161718version: '2.1'services:contosouniversity.web:image: contosouniversity.web:devbuild:args:source: ${DOCKER_BUILD_SOURCE}volumes:- .\ContosoUniversity.Web:C:\inetpub\wwwroot- ~\msvsmon:C:\msvsmon:rolabels:- "com.microsoft.visualstudio.targetoperatingsystem=windows"contosouniversity.database:image: contosouniversity.database:devlabels:- "com.microsoft.visualstudio.targetoperatingsystem=windows"
At this point you should be able to run the application using the Docker toolbar button and again see the website running—in a Windows container. However this time a second image (contosouniversity.database, tagged with dev) and corresponding container (named something like dockercompose362878786_contosouniversity.database_1) will have been created, with the container now running SQL Server. This is a newly minted instance of SQL Server and doesn't have a database for our website to connect to, which is the next issue to address.
Connecting the Contoso University Website to its Database
These next steps assume you are following on from the previous section, ie that the website is open in a browser and that Visual Studio is still debugging.
- Leave the browser open but stop debugging in Visual Studio.
- In ContosoUniversity.Web edit Web.config so that the connection string Data Source points to contosouniversity.database:
123<connectionStrings><add name="SchoolContext" connectionString="Data Source=contosouniversity.database;Initial Catalog=ContosoUniversity;User Id=ContosoUniversity;Password=MySuperStrongPassw0rd!" providerName="System.Data.SqlClient" /></connectionStrings> - In a PowerShell session, find the IP address of the container running SQL Server using docker inspect and passing in enough of the container's ID to make it unique:
1docker inspect --format="{{.NetworkSettings.Networks.nat.IPAddress}}" <container ID> - In ContosoUniversity.Database edit ContosoUniversity.publish.xml so that the Target database connection points to the IP address of the SQL Server container and change the the authentication to SQL Server Authentication. The User Name should be sa (yes—I know) the password should be the same as the one specified in the Dockerfile used to build the database image. Save the profile and then click Publish.
- Back in the web browser running the Contoso University website, click on one of the menu bar links (eg Departments) that causes a database query. If everything has worked you should now have a fully functioning application.
Understanding the Developer Inner Loop Workflow
At this point we have achieved our aim of running and debugging both the website and database components of Contoso University in containers running operating systems that are the same as would be used in production. Once the images and containers have been created they will—as far as my testing is concerned—continue to be used as long as nothing changes. This is the case even if Visual Studio, Docker or even the workstation are restarted. The great thing is that any changes made to the containers—for example updating the database schema—will be preserved. Of course, if something changes in one of the Dockerfile files the images and containers will be rebuilt and in the case of the database the publish file will need to be updated with a new IP address and the database will need to be published again from scratch. Also, if the solution is cleaned (ie Build > Clean Solution) the containers are removed and rebuilt, again necessitating publishing the database from scratch. Overall though, the developer inner loop workflow feels quite slick.
Next Steps
As things stand the compose and Dockerfile files are not ready to be used in a continuous delivery pipeline. The website Dockerfile for example has Contoso University being deployed as the Default Web Site rather than a ContosoUniversity website and the database Dockerfile doesn't cater for any persistent storage. There is also the problem of checking in the database project's publish profile with an IP address specific to one developer's workstation—a real pain for other developers. I'll address these issues as part of getting Contoso University working in a Docker-based continuous delivery pipeline in the next post in this series.
Cheers -- Graham