Using Windows Container to isolate builds, useful for CI/CD

When we use a build server to build our projects, we may get in trouble if we build several project on the same build server over time. For example if we have a project that was created and built with a specific compiler version and tools. Then we put that project a side and created a new project that will now target a new version of a compiler and new version of the tools we used before, maybe also install new components that are needed for the new project. Everything is perfect and works great for the new project. But suddenly we need to fix a bug or have a change request for the old project we put aside. Now we open that old project, make some changes we build the project. BANG! Failed! It can fail because of new tools that aren’t compatible with the old version, or some new components installed did some strange thing that break the build.

To make sure we build our projects in isolation of others we can for example use Windows Container. So I’m going to show you how we can use Windows Container to isolate builds.

In this example I have created a simple Console Application named to Dockerbuild:

class Program
{
 static void Main(string[] args)
 {
   Console.WriteLine("Hello this was build withing a Docer Container.");
 }
}

 

I will use several tools for this project. It will target .Net framework 4.6.1, use Nuget version3.5.0, FAKE version 4.50.0 and Microsoft Build Tools 2015. I also know that I will run this project in production on Windows Server Core. I use FAKE to create my build script for the project. FAKE will use F# Sharp so I also need to make sure F# is installed.

To make thing more fun I will use a Windows Container to run my program on a Windows Server Core. To make sure my code will build with specific versions of tools and also run on a Windows Servce Core I need to create a docker image that I will use as the base for building my project. Here is my Docerfile that creates an image with all the tools I need to build my project (I have this file checked-in with my source code, only to make sure I can easily upgrade my tools if I decided to do that, or recreate my image if somehow the created image will be lost):

 

FROM microsoft/windowsservercore

SHELL ["powershell"]

# Install Microsoft Build Tools 2015
RUN Invoke-WebRequest "https://download.microsoft.com/download/E/E/D/EEDF18A8-4AED-4CE0-BEBE-70A83094FC5A/BuildTools_Full.exe" -OutFile "$env:TEMP\BuildTools_Full.exe" -UseBasicParsing
RUN &  "$env:TEMP\BuildTools_Full.exe" /Silent /Full

RUN Install-WindowsFeature NET-Framework-45-ASPNET ; \
    Install-WindowsFeature Web-Asp-Net45

# Install .Net 4.6.1
RUN Invoke-WebRequest "https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe" -OutFile "$env:TEMP\NDP461-DevPack-KB3105179-ENU.exe" -UseBasicParsing
RUN &  "$env:TEMP\NDP461-DevPack-KB3105179-ENU.exe" /q

# Intstall F# Sharp
RUN Invoke-WebRequest "http://download.microsoft.com/download/9/1/2/9122D406-F1E3-4880-A66D-D6C65E8B1545/FSharp_Bundle.exe" -OutFile "$env:TEMP\FSharp_Bundle.exe" -UseBasicParsing
RUN &  "$env:TEMP\FSharp_Bundle.exe" /install /quiet

# Add NuGet v 3.5.0
RUN MKDIR "C:\windows\nuget"
RUN Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/v3.5.0/NuGet.exe" -OutFile "C:\windows\nuget\nuget.exe" -UseBasicParsing
WORKDIR "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0"

# Install Web Targets
RUN &  "C:\windows\nuget\nuget.exe" Install MSBuild.Microsoft.VisualStudio.Web.targets -Version 14.0.0.3
RUN mv 'C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\MSBuild.Microsoft.VisualStudio.Web.targets.14.0.0.3\tools\VSToolsPath\*' 'C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\'

# Add Msbuild to path
RUN setx PATH '%PATH%;C:\\Program Files (x86)\\MSBuild\\14.0\\Bin\\'

# Install FAKE
RUN MKDIR "c:\windows\fake"
WORKDIR "c:\windows\fake"

RUN &  "C:\windows\nuget\nuget.exe" Install FAKE -Version 4.50.0

# Add FAKE to path
RUN setx PATH '%PATH%;C:\\windows\\fake\\fake.4.50.0\tools\'


I created an image out from this Dockerfile and called it just “fake” in this example. In real scenarios I would tag the image with a better name.

docker build –t fake .

I also created a Dockerfild, Dockerfile.build. This file will use the “fake” image and make sure FAKE will be used to build my project. Here is the Dockerfile.build:

 

FROM fake

ARG buildDir
ARG outputDir

SHELL ["powershell"]

ADD . ${buildDir}
WORKDIR ${buildDir}

RUN fake.exe build.fsx outputDir=${outputDir}

If you have not heard about FAKE before, you can read about it here: http://fsharp.github.io/FAKE/

In the Dockerfile.build there is RUN command that will start FAKE to build the project with the build script “build.fsx”. This is a file added to my root folder of my project. Here is how the files looks like:


// include Fake lib
#r "FakeLib.dll"

open Fake

RestorePackages()

// Properties
let buildDir = getBuildParamOrDefault "outputDir" "./out/"

// Targets
Target "Clean" (fun _ ->
  CleanDir buildDir
)

Target "BuildApp" (fun _ ->
   !! "*.csproj"
     |> MSBuildRelease buildDir "Build"
     |> Log "AppBuild-Output: "
)

Target "Default" (fun _ ->
   trace "Hello World from FAKE"
)

// Dependencies
"Clean"
 ==> "BuildApp"
 ==> "Default"

// start build
RunTargetOrDefault "Default"

 

The best thing about using FAKE or other similar tools like CAKE etc, is that we can have our “build script” as part of our source code, and also have it versioned if we use a source control. We can also use it to build our project both locally and with a build server. I prefer this way over having build steps added to a build server. Instead the build server will only have one step and that is to run my FAKE script. By doing so, we can build our project the same way the build server will build it.

The build.fsx script will use MSBuild to build the project and will put the output of the build to a specified output path.

As I mentioned before I want to run my application in a Windows Container, so I have another Dockerfile, Dockerfile.dist. This file will make sure it runs my Console application, here is the Dockerfile.dist:

 

FROM microsoft/windowsservercore

ARG sourceDir

RUN MKDIR app
ADD ${sourceDir}/. app

CMD ["/app/DockerBuild.exe"]

 

Note: The important part here is to make sure the image uses the same base image used by the image that builds the project. This will make sure we run our application on the same image we use to build our application.

The Dockerfile.dist will copy our application from a local path into the image App folder.

To build the project I use a powershell script.

$buildDir = "C:\\build"
$outputDir = "C:\\build\\out"
$localTempOutDir = "out"

docker build --rm -f Dockerfile.build -t myapp-build --build-arg buildDir=$buildDir --build-arg outputDir=$outputDir .
docker create --name myapp-build-tmp myapp-build

# Create an empty out directory
Remove-Item $localTempOutDir -Recurse
New-Item -ItemType directory -Path $localTempOutDir

# Copy the folder and all its files from the outputDir to the working directory
docker cp myapp-build-tmp:$outputDir .

docker rm myapp-build-tmp

docker build --rm -f Dockerfile.dist -t myapp:0.1 --build-arg sourceDir=$localTempOutDir .
docker run myapp:0.1

 

This powershell script is the one that I run every time I want to start a build. First the script will build the Docerfile.build. When building the Docerfile.build it will run FAKE to build my project. Because the output of the build will be part of the image, I need to copy it from a container into my local disc. This is done by creating a Windows Container of the image and then use the “docker cp” command. The next step is to build a new image with the Dockerfile.dist. The Docerfile.dist will copy the files from my local disk into the myapp:0.1 image and then start the Windows Container that will run my program.

Done!

I hope you find thig blog post valuable. If you want to know when I write new posts, please feel free to follow me on twitter: @fredrikn

No Comments

Add a Comment

As it will appear on the website

Not displayed

Your website