Software Development Outsourcing
Development

Transforming a monolithic application to a micro-services oriented architecture – Part 2 – Move to Docker

Now that I have presented how we used to do deploys, the next step is to move our current application to be hosted into Docker.

First thing first, we have updated QA and STAGE servers to Windows Server 2016 since it is the lower version of Windows Server with support for Docker.

After updating, we have made new deployments of the app for all tenants and on all configurations to check that nothing has broken in the meantime.

The next step we did is that we have installed Docker EE on both machines and on our local machines.

After that, we have started to launch local containers with the code of the application copied inside to check that it is working fine. Here a lot of problems pop out (rewrite problems, problem with the allowed memory for the container, missing url rewrite on the container, missing write access on the App_Data folder for the IIS user, missing dlls in system32 and so on) but finally we did manage to run a container with our code inside it.

The final base Dockerfile we used is this:

# escape=`

# This runtime script is only tested with Windows Server 2016 and Docker EE 17.06
# Builds ASP.NET Ready container with .NET up to 4.6.2 installed on it.
# For applications which are not tested 
# running on higher .NET versions than .NET 4.6.2.

# The `FROM` instruction specifies the base image.
# You are extending the `microsoft/windowsservercore:ltsc2016` image.

# Start from image 4.6.2-windowsservercore-ltsc2016 on dockerhub

FROM microsoft/windowsservercore:ltsc2016

# Install .NET Framework 4

RUN set COMPLUS_NGenProtectedProcess_FeatureEnabled=0 & `
	\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen update & `
	\Windows\Microsoft.NET\Framework\v4.0.30319\ngen update

# USE powershell and set command parameters for it

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

# Enable IIS and add ServiceMonitor Features on the Container

RUN Add-WindowsFeature Web-Server; `
	Add-WindowsFeature NET-Framework-45-ASPNET; `
	Add-WindowsFeature Web-Asp-Net45; `
	Remove-Item -Recurse C:\inetpub\wwwroot\*; `
	Invoke-WebRequest -UseBasicParsing -Uri "https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.2/ServiceMonitor.exe" -OutFile "C:\ServiceMonitor.exe"
	
# Download Roslyn nupkg and ngen the compiler binaries

RUN Invoke-WebRequest https://api.nuget.org/packages/microsoft.net.compilers.2.3.1.nupkg -OutFile c:\microsoft.net.compilers.2.3.1.zip ; `	
    Expand-Archive -Path c:\microsoft.net.compilers.2.3.1.zip -DestinationPath c:\RoslynCompilers ; `
    Remove-Item c:\microsoft.net.compilers.2.3.1.zip -Force ; `
    &C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\csc.exe /ExeConfig:c:\RoslynCompilers\tools\csc.exe | `
    &C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\vbc.exe /ExeConfig:c:\RoslynCompilers\tools\vbc.exe  | `
    &C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\VBCSCompiler.exe /ExeConfig:c:\RoslynCompilers\tools\VBCSCompiler.exe | `
    &C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\csc.exe /ExeConfig:c:\RoslynCompilers\tools\csc.exe | `
    &C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\vbc.exe /ExeConfig:c:\RoslynCompilers\tools\vbc.exe | `
    &C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen.exe install c:\RoslynCompilers\tools\VBCSCompiler.exe  /ExeConfig:c:\RoslynCompilers\tools\VBCSCompiler.exe ;

# Set Environmnet Variable for the Roslyn Compyler Location
	
ENV ROSLYN_COMPILER_LOCATION c:\\RoslynCompilers\\tools

# Set Service Monitor asn the Main ENTRYPOINT of the Container

ENTRYPOINT ["C:\\ServiceMonitor.exe", "w3svc"]

# Fix DNS on the Container until it will be fixed (in 1709 Creators Update I guess) - https://github.com/moby/moby/issues/27499

RUN Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' `
  -Name ServerPriorityTimeLimit -Value 0 -Type DWord
  
# Install Url Rewrite.

WORKDIR /install
ADD https://download.microsoft.com/download/C/9/E/C9E8180D-4E51-40A6-A9BF-776990D8BCA9/rewrite_amd64.msi rewrite_amd64.msi
RUN Write-Host 'Installing URL Rewrite' ; `
    Start-Process msiexec.exe -ArgumentList '/i', 'rewrite_amd64.msi', '/quiet', '/norestart' -NoNewWindow -Wait
	
# This is not the best option but for the moment do it so the App Pool has e.g. write access rights for logs and other stuff

RUN Add-LocalGroupMember -Group 'Administrators' -Member 'IIS AppPool\DefaultAppPool';

# Remove the default website and create a new one for the REST API.

RUN Remove-Website -Name 'Default Web Site'; `
	New-Item -Path 'C:\inetpub\wwwroot\app' -Type Directory; `
	New-Website -Name 'app' -PhysicalPath 'C:\inetpub\wwwroot\app' -Port 80 -Force

EXPOSE 80

# Print current websites created website

RUN Import-Module Webadministration; `
	Get-ChildItem -Path IIS:\Sites

# Files required to run ffmpeg.exe
# https://social.msdn.microsoft.com/Forums/en-US/0c13bd1a-388f-48cf-a190-7259d39a080f/ffmpeg-doesnt-work-from-inside-a-container-but-works-on-the-host?forum=windowscontainers

COPY System32/avicap32.dll C:/Windows/System32/
COPY System32/en-US/avicap32.dll.mui C:/Windows/System32/en-US/

COPY System32/msvfw32.dll C:/Windows/System32/
COPY System32/en-US/msvfw32.dll.mui C:/Windows/System32/en-US/

COPY SysWOW64/avicap32.dll C:/Windows/SysWOW64/
COPY SysWOW64/en-US/avicap32.dll.mui C:/Windows/SysWOW64/en-US/

COPY SysWOW64/msvfw32.dll C:/Windows/SysWOW64/
COPY SysWOW64/en-US/msvfw32.dll.mui C:/Windows/SysWOW64/en-US/

We build it and pushed it to docker hub and let’s name it further the BASE_IMAGE.

And all we have left to do now is to mount a volume on our server to c:\inetpub\wwwroot\app folder on the container or just copy the publish files on the running container.

For QA we have choose to mount, while for production we will build images with all files in them and upload them to a private registry from where we will run them on production.

QA deploy and unit tests

This time we build the project directly, using a .pubxml file which tells msbuild to publish the code on a local folder:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <PublishProvider>FileSystem</PublishProvider>
    <LastUsedBuildConfiguration>CI</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <publishUrl>..\..\..\publish\API\bin\Release\PublishOutput</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
  </PropertyGroup>
</Project>
  • Write the tenant name in a text file (that way we decide what projects to build)
echo Tenant1 > src/.config/tenant.txt
  • Restore nuget packages:
"src/.nuget/NuGet.exe" restore src/OurProjectSolution.sln
  • Run the msbuild command to build and publish the project:
msbuild Project.csproj 
        /p:DeployOnBuild=true 
        /p:PublishProfile=FolderProfile.pubxml 
        /p:Configuration=QA 
        /p:Tenant=Tenant1
  • Pull the docker image we have created previously and use it to start a new container on the QA instance:
docker pull BASE_IMAGE
  • Run powershell script which starts the container on the QA machine:
Write-Host Run API Image on Docker Container

$containerName="container-name"

Write-Host [Step 1] Delete the old container
docker rm -f $containerName

Write-Host [Step 2] Delete old content
Remove-Item -Recurse -Force C:\MountedFolders\MY-TENANT\*

Write-Host [Step 3] Copy the new content
Copy-Item -Path .\publish\API\bin\Release\PublishOutput\* `
          -Recurse `
          -Force `
          -Destination C:\MountedFolders\MY-TENANT\ -Container

Write-Host [Step 4] Run new container...
docker run -d -p 8080:80 `
           --restart unless-stopped `
           -v C:\MountedFolders\MY-TENANT\:C:\inetpub\wwwroot\app `
           --name $containerName BASE_IMAGE

Write-Host [Step 5] Inspect new container...
docker inspect -f "{{.NetworkSettings.Networks.nat.IPAddress }}" $containerName

Write-Host [Step 6] Make sure it is running
docker start $containerName
  • Create URL rewrite rule(s) on the QA machine to route the requests to the newly created container.
  • Finally, to run the unit tests we are using the following command:
msbuild build2.build 
        /p:VisualStudioVersion=15.0 
        /toolsversion:15.0 
        /p:Tenant=Tenant1 
        /target:Tests 
        /p:Configuration=QA 
        /p:TestsCategory=TestsCategory

STAGE deploy and unit tests

For stage, we have have done the exact same steps as for the QA, just that this time we have deployed the container on the STAGE machine instead.

After the deployment, we have made sure to create correct URL Rewrite rules in IIS to route requests to stage domains to the containers.

PRODUCTION deploy and unit tests

For production deployment, we are doing the exactly same steps as on the STAGE deployment, just that we use different environment and we make sure that URL Rewrites are correctly created to route requests to the containers.

After that, the deployment is exactly as we presented on the first chapter of this article series, where we have introduced our current infrastructure.

This means in production we will have an instance with the docker containers installed on it which we will use to build production Image AMIs. This images are then used to replace production instances.

After we have completely switched to an infrastructure where our monolith is hosted on docker, we were ready to transition the next step, deploying our docker containers using Amazon ECS as the container orchestration tool.

In the next article, we will describe how we are using Amazon Cloud Formation to build reusable stacks of resources on AWS.