Thursday, July 19, 2018

Automated TFS builds with Docker

In this post, I'll show you how to get MSBuild, xUnit, and JustMock working in a Docker Windows container and have it compile an Asp.Net MVC application with some Unit Tests.
From Environment perspective, we have TFS 2017 Update1 on Windows 2016 servers.

The key driving factor for this solution is to isolate builds. With the increase in the variety of builds generated by build server, the list of dependencies installed locally also increases. The list was getting bigger starting from various versions of Visual Studio, multiple versions of the unit testing framework to name a few.

So let's get going.

Step number #1 is to setup environment

  1. Create a Win2016 DataCenter VM for TFS builds. Make sure the Version is 1607 (LTSB) as Docker UCP has some compatibility issues with latest versions. Let's call it Build Server.
  2. Create a Service account which will be used to run VSTS agent
  3. Install and configure VSTS agent. You can refer to this article.
  4. Create a new local group called 'docker-users'
  5. Add the user which is used to configure VSTS agent to this group e.g. TFSSERVICE@org.com
  6. Install Docker on Windows 2016. Refer this article for instructions.
  7. Test your Docker EE installation by running the hello-world container
docker container run hello-world:nanoserver

        You may run into issues if you have anti-virus software installed on Build Server.
        For me adding the exceptions to SEP worked.
        Add exceptions to SEP for following folders:
  • C:\ProgramData\docker\*
  • C:\Program Files\docker\*

Step number #2 is to build docker image

  1. Expand maximum container disk size
    • Stop docker service by executing:

      sc.exe stop docker

    • From elevated POSH console execute:

      New-Item -Name daemon.json -Path C:\ProgramData\docker\config\ -ItemType File
    • Edit daemon.json and add

      {
       "storage-opts": [
          "size=120GB"
        ],
        "group" : "docker-users"
      }

      Refer this and this.

    • Start docker service

      sc.exe start docker

  2. Create a directory for necessary tools and files
    mkdir f:\BuildTools
  3. Create a dockerfile
    New-Item -Name Dockerfile -Path F:\BuildTools\ -ItemType File -Force
  4. Edit Dockerfile to have contents as:
    # escape=`
    # Use the latest Windows Server Core image with .NET Framework 4.7.2.
    FROM microsoft/dotnet-framework:4.7.2-sdk
    LABEL maintainer="me@org.com"

    # Download the WebDeploy.
    RUN mkdir c:\install
    ADD WebDeploy_amd64_en-US.msi c:\install\WebDeploy_amd64_en-US.msi
    WORKDIR c:\install
    RUN powershell start-Process msiexec.exe -ArgumentList "/i","c:\install\WebDeploy_amd64_en-US.msi","/qn" -Wait

    # Download the Build Tools bootstrapper.
    ADD https://aka.ms/vs/15/release/vs_buildtools.exe C:\install\vs_buildtools.exe

    # Install Build Tools excluding workloads and components with known issues.
    # https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio
    # https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools
    SHELL ["cmd", "/S", "/C"]
    RUN C:\install\vs_buildtools.exe --quiet --wait --norestart --nocache `
    --installPath C:\BuildTools `
    --add Microsoft.Net.Component.4.7.1.SDK `
    || IF "%ERRORLEVEL%"=="3010" EXIT 0

    # Install DotNet 3.5
    RUN mkdir c:\install\sxs
    ADD microsoft-windows-netfx3-ondemand-package.cab c:\install\sxs\microsoft-windows-netfx3-ondemand-package.cab
    WORKDIR c:\install
    RUN powershell Install-WindowsFeature -Name NET-Framework-Features -Source C:\install\sxs -Verbose

    # Environment Variables for Telerik JustMock ENV COR_ENABLE_PROFILING 1
    ENV JUSTMOCK_INSTANCE 0
    ENV COR_PROFILER="{B7ABE522-A68F-44F2-925B-81E7488E9EC0}"

    # Start developer command prompt with any other commands specified.
    # ENTRYPOINT C:\BuildTools\Common7\Tools\VsDevCmd.bat &&

    # Default to PowerShell if no other command specified.
    CMD ["powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
  5. Copy the WebDeploy installer in F:\BuildTools along with the DockerFile. You can get WebDeploy_amd64_en-US.msi from here
  6. Copy the .Net Framework 3.5 installer in F:\BuildTools along with the DockerFile. This is needed because our projects have a dependency on Telerik JustMock. And integrating JM via MSBuild requires legacy .Net framework. You can grab the microsoft-windows-netfx3-ondemand-package.cab by mounting Windows Server Core iso.
  7. Build the image
    # docker build -t vsbuildtools2017:web-v1 -m 2GB .
  8. Test by running the container
    docker run -it vsbuildtools2017:web-v1

Step number #3 is to upload your Docker image to Azure registry

This step is optional but I added it here just in case it is applicable in your scenarios too. We used Azure Container Registry for hosting our images.
  1. Install the latest version of AZ CLI from here
  2. Login with
    az login --use-device-code az acr login --name {RegistryName}
  3. Find the tag
    az acr list --resource-group {ResourceGroupName} --query "[].{acrLoginServer:loginServer}" --output table
    Output: {RegistryName}.azurecr.io
  4. Add tag
    docker tag vsbuildtools2017:web-v1 {RegistryName}.azurecr.io/vsbuildtools2017:web-v1
  5. Push the image to ACR
    docker push {RegistryName}.azurecr.io/vsbuildtools2017:web-v1
  6. And to pull the image from ACR, I used a "Run Powershell" task in my build definition which essentially does this:
    docker login $(RegistryName) --username $(DockerRegistryUserId) --password $(DockerRegistryPassword)
    docker pull vsbuildtools2017:web-v1

Step number #4 is to configure your Visual Studio MVC 5 project

  1. xUnit configuration
    • Refer to this article
    • Make sure UnitTest project in your solution does not refer "Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll" as it's in GAC and not found by docker container. Moreover, we are using xUnit Testing framework which is not dependent on this assembly as it can be added to a simple class library project as well via Nuget. It doesn't need to be a UnitTest project which by default brings down this assembly reference.
    • Add NuGet reference in Unit Test Project for "xunit.runner.msbuild" version 2.3.1
      If your project is built using .Net Framework 4.5.1 and lower then you need to:
      Install 4.5.2 Developer Pack from within VS (or here) by selecting properties of UnitTest project. Change the target framework for UnitTest project to 4.5.2
    • In your TFS source code repo add a new targets file XUnitRunner.targets. I placed it under $/{TFSProject}/Dev/build/ORG/MSBuild
      Contents of this target file :

      <Project DefaultTargets="RunXunitTests" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

      <PropertyGroup>

      <XunitRunnerAssembly>$(BUILD_SOURCESDIRECTORY)\packages\xunit.runner.msbuild.2.3.1\build\net452\xunit.runner.msbuild.net452.dll </XunitRunnerAssembly>

      <ProjectSourcesDirectory>$(BUILD_SOURCESDIRECTORY)</ProjectSourcesDirectory>

      <ProjectBuildConfiguration>$(BuildConfiguration)</ProjectBuildConfiguration>

      <TestRunOutputInHtml>$(PackageStagingDirectory)\TestRun.html </TestRunOutputInHtml>

      <TestRunOutputInXml>$(PackageStagingDirectory)\TestRun.xml</TestRunOutputInXml>

      <JustMockRunnerAssembly>$(ProjectSourcesDirectory)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.JustMock.MSBuild.dll</JustMockRunnerAssembly>

      <JustMockProfilerAssembly>$(ProjectSourcesDirectory)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.CodeWeaver.Profiler.dll </JustMockProfilerAssembly>

      </PropertyGroup>

      <UsingTask AssemblyFile="$(XunitRunnerAssembly)" TaskName="Xunit.Runner.MSBuild.xunit" />

      <ItemGroup>
      <TestAssemblies Include="$(ProjectSourcesDirectory)\**\bin\$(ProjectBuildConfiguration)\**\*test*.dll" Exclude="$(ProjectSourcesDirectory)\**\bin\$(ProjectBuildConfiguration)\**\*xunit*.dll" />

      </ItemGroup>

      <UsingTask AssemblyFile="$(JustMockRunnerAssembly)" TaskName="Telerik.JustMock.MSBuild.JustMockStart" />

      <UsingTask AssemblyFile="$(JustMockRunnerAssembly)" TaskName="Telerik.JustMock.MSBuild.JustMockStop" />

      <Target Name="BeforeTestConfiguration"> <JustMockStart ProfilerPath="$(JustMockProfilerAssembly)" />

      </Target>

      <Target Name="AfterTestConfiguration">

      <JustMockStop />

      </Target>

      <Target Name="RunXunitTests">

      <Message Text="(BUILD SOURCESDIRECTORY is) - $(ProjectSourcesDirectory)" Importance="high"/>

      <Message Text="(Assemblies from) - %(TestAssemblies.Identity)" Importance="High" />

      <JustMockStart />

      <xunit Assemblies="@(TestAssemblies)" DiagnosticMessages="true" Html="$(TestRunOutputInHtml)" Xml="$(TestRunOutputInXml)" IgnoreFailures="false" />

      <JustMockStop />

      </Target>

      </Project>

  2. Telerik JustMock (JM) integration
    • Add necessary binaries under $/{TFSProject}/Dev/SharedBinary/Telerik/JustMockProEdition/2018.2.511.5 directory. You can find these files from your Dev machine where JM is installed locally.
      • ImageBlacklist.cfg
      • Telerik.CodeWeaver.Profiler.dll
      • Telerik.JustMock.dll
      • Telerik.JustMock.MSBuild.dll
      • TelerikCodeWeaverProfiler.tlb
    • Edit the ImageBlacklist.cfg file and remove MSBuild entry from it.
    • JM needs some ENV variables which will be set by TFS Build definition. Some are made available in Docker image.
      In my build definition, I have a "Run Powershell" task which does set the ENV variables and looks like this:
      Write-Host "##vso[task.setvariable variable=COR_PROFILER]{B7ABE522-A68F-44F2-925B-81E7488E9EC0}"

      Write-Host "##vso[task.setvariable variable=COR_ENABLE_PROFILING]1"

      Write-Host "##vso[task.setvariable variable=JUSTMOCK_INSTANCE]0"

      Write-Host "##vso[task.setvariable variable=COR_PROFILER_PATH]$(BUILD.SOURCESDIRECTORY)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.CodeWeaver.Profiler.dll"

    • Refer more from here https://www.telerik.com/forums/use-justmock-in-msbuild
  3. Create a Publishing profile
    • Right-click Web project in Solution Explorer and select Publish
    • Click "Create new profile"
    • Select "IIS, FTP etc." and click OK
    • Select "Web Deploy Package" as publish method. Enter "AppNameWebDeployPkg" as Package location
    • Leave Site name blank
    • Click Next
    • Select Release from Configuration
    • Leave File Publish Options unchecked
    • Select "Use this connection string at runtime" checkbox for all Databases connection strings with these placeholder values. E.g.
      • AuthContext: __AuthContextStr__
      • MktgDatabaseEntities: __MktgDatabaseEntitiesStr__
      • Logging: __LoggingStr__
    • Click Save
    • Rename "CustomProfile" (newly created as above) as "Release"
    • Edit the profile pubxml file to include following statement as first after PropertGroup tags. Refer this
      <IncludeSetACLProviderOnDestination>False</IncludeSetACLProviderOnDestination>

Step number #5 is to run the container to build and test your solution

  1. Run the following command in a "Run Powershell" task to build the VS solution:
    docker run -i --rm -m 2GB --cpus="2" --name myweb -w C:/users/Administrator/Documents -v "$(Agent.BuildDirectory):c:/users/Administrator/Documents/myweb:rw" {RegistryName}.azurecr.io/vsbuildtools2017:web-v1 msbuild "myweb\s\CSS.sln" /nr:false /fl /flp:"logfile=myweb\s\css.sln-build.log" /p:platform="any cpu" /p:configuration="release" /p:RunCodeAnalysis=true /p:DeployOnBuild=true /p:PublishProfile=Release /p:PackageLocation="c:/users/Administrator/Documents/myweb/a/PublishedPackage"
  2. Run the following command in a "Run Powershell" from file task to run the unit test (xUnit) cases. These are file contents:
    param
    (
    $BuildDir #$(Agent.BuildDirectory)
    )

    $workDir = "C:/users/Administrator/Documents"
    docker run -i --rm -m 2GB --cpus="2" --name csswebtest -w "${workDir}"
    -v "${BuildDir}:${workDir}/cssweb:rw"
    -e COR_PROFILER_PATH="${workDir}/cssweb/s/SharedBinary/Telerik/JustMockProEdition/2018.2.511.5/Telerik.CodeWeaver.Profiler.dll" a23459876.azurecr.io/arch/ref/vsbuildtools2017:web-v1
    msbuild "cssweb\s\build\im\raft\msbuild\IM.XunitRunner.targets" /nr:false /fl /flp:"logfile=cssweb\s\css.xunitrunner.log" /p:platform="any cpu" /p:configuration="release" /p:XunitRunnerAssembly="${workDir}/cssweb/s/packages/xunit.runner.msbuild.2.3.1/build/net452/xunit.runner.msbuild.net452.dll" /p:ProjectSourcesDirectory="${workDir}/cssweb/s" /p:ProjectBuildConfiguration="release" /p:TestRunOutputInHtml="${workDir}/cssweb/a/CSS/TestRun.html" /p:TestRunOutputInXml="${workDir}/cssweb/a/CSS/TestRun.xml"

I hope this will prove a useful template and save some efforts in your CI/CD journey with Windows Containers.