Lösungen
Märkte
Referenzen
Services
Unternehmen
Dynamics NAV 2017 in a Windows Container with Docker

Dynamics NAV 2017 in a Windows Container with Docker

18. November 2016

Dynamics NAV 2017 in a Windows Container with Docker

For quite some time now Containers and especially Docker are gaining traction. If you haven’t heard about them, there are a lot of good resources on the internet to help you get started but the idea is to take virtualization one step further. We moved from physical machines to virtual machines to allow more density and better usage of the hardware as well as easier failover and scaling and also better standardization through images. Containers are sometimes called “application virtualization”, which I think is a pretty well fitting term. You package everything you need for an application, e.g. .NET, the NAV Server and some custom dlls and put them in layers on top of a basic image. All containers share the host OS and only have a very small (relatively speaking) kernel. That means that e.g. 10 containers on a host still only have 1 OS and not 10 guest OS like it would be the case for VMs. If you go to Docker (while containers were around in Linux for a very long time only with the emergence of Docker Inc started the rise of containers) and scroll down a bit you find a good graphical representation. Through that concept it is also easy to package every dependency, put it with your application in a box and isolate it from other applications without the need to put it in a dedicated VM. No more “it ran in the dev environment because we have Visual Studio there but not in production” or “it worked in the test environment because we had a .NET fix implemented that isn’t in production yet”. Or from a software vendor standpoint it is a lot easier to make sure that no other application introduces some dependency which breaks your application. In the end, a container makes sure that the rest of the system is not affecting your application and vice versa while only having the minimally necessary overhead. All this is just the beginning as you also have clusters, orchestration, network tools and a lot more on top, but this post is mainly about NAV (and SQL Server) in containers, not containers themselves. To get more information especially about Windows Containers, this is a good starting point

Windows Containers and NAV 2017

I was thinking about trying containers for quite some time but the final push came when I attended the Continuous Lifecycle conference which happened to be held together with ContainerConf. There I went to a very good talk about Docker, Containers and Windows by Rainer Stropek (@rstropek on twitter and blogging here) where I got a good overview. As I had some time to kill afterwards I just started the Windows Server 2016 machine on Azure that I had already prepared a couple of weeks ago. It took a couple of minutes to install Docker and start it but then came the first positive impression: I found a ready-to-use Windows Server 2016 image by Microsoft and could easily run it. All I had to do was

docker pull microsoft/mssql-server-windows-express
docker run -d -p 1433:1433 -v d:\downloads:c:\downloads --env sa_password=VerySecret --env accept_eula=y microsoft/mssql-server-windows-express

and I had my first Container up and running! The first step is to download the image, the second is to run it with parameters for port forwarding, sharing the host file system and setting the sa password. To connect to it I installed SQL Server Management Studio in the host machine which actually took a lot longer than downloading and starting the Container. After that I restored a Cronus DB from NAV 2017 RTM which worked just as expected. As there are no prepared Container images for NAV I had to create one. As base image I used a Windows Server 2016 Core with .NET framework 3.5. Again, two simple commands and it was up and running:

docker pull microsoft/dotnet-framework:3.5
docker run -it -p 7045-7048:7045-7048 -v d:\downloads:c:\downloads microsoft/dotnet-framework:3.5 microsoft/dotnet-framework:3.5 cmd

As you can probably guess this is exposing port 7045 to 7048 and running a cmd inside the image (-it tells it to open an interactive terminal). Now came the tricky part: installing NAV 2017 in a Windows Server 2016 Core container. For those really interested I will list all the details in the end but the short version is:

But in the end I have NAV 2017 running inside a Windows Server 2016 Core container connected to a SQL Server 2016 Express also running inside a Windows Server 2016 Core container. It cost me a couple of hours while I should have been sleeping, but it works 🙂

Why?

If you are like me, then maybe you can even understand that I to some degree did it just to find out if it works. But that is not the only reason. First of all there are the same benefits as stated in the beginning for hosters and customers: You could isolate NAV Server instances from other instances while still packing your host quite densely. E.g. you could have some instances for pre-production use, some for integration testing and some for development and each of those could use different CUs while still running on the same machine. Small customers could have only one NAV Server machine but would still be able to test new CUs and releases of vertical solutions. Custom dlls can be deployed without an interference. Disaster recovery scenarios can be implemented with very little downtime because containers start very fast. You could even move into some sort of “micro-service architecture” (not really because of the monolithic design of NAV, but at least an improvement) by separating SOAP, OData, Client Services and Job Queues into dedicated containers.

But I think it is even more interesting for partners developing their own solutions: Each developer can have his or her own environment very easily configured and updated using a container repository (I’ll get into some detail how that works in a future post). You can tag and version control the full environment necessary to run a given release of your solution including NAV CU, custom dlls and C/AL objects. If a customer asks you to fix something in an older release you just start the matching containers and work on it. No need to keep x different configurations and setups running or VMs lying around as the container technology very smartly only stores the deltas. And last but not least you can integrate this into your automated testing by not only creating your test databases from scratch but very easily creating the full environment.

From my point of view, a really very promising trend and technology where Microsoft definitely is working in a good direction by not implementing some proprietary stuff but openly cooperating with the rest of the world. Hopefully NAV will also start to really support it.

The technical detail

Before the explanation I have to say that I also only just started to work with containers. Probably a lot of the things I did can be achieved easier and better but this is the way that worked for me:

  1. Prepare the host
    1. create an Azure VM with image Windows Server 2016 Datacenter – with Containers
    2. Open a PowerShell ISE as Admin and run:
      Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
      Install-Module -Name DockerMsftProvider -Force
      Install-Package -Name docker -ProviderName DockerMsftProvider -Force
    3. Restart the VM
    4. Open a cmd and enter “docker version”. You should see something like this:
      Client:
       Version:      1.12.2-cs2-ws-beta
       API version:  1.25
       Go version:   go1.7.1
       Git commit:   050b611
       Built:        Tue Oct 11 02:35:40 2016
       OS/Arch:      windows/amd64
      Server:
       Version:      1.12.2-cs2-ws-beta
       API version:  1.25
       Go version:   go1.7.1
       Git commit:   050b611
       Built:        Tue Oct 11 02:35:40 2016
       OS/Arch:      windows/amd64
    5. In order to work faster I moved docker to d:. But be careful, this is only a temporary storage, so if you restart the machine make sure to copy it to c: because otherwise it will be lost. Open a PowerShell ISE as Admin and run:
      Stop-Service docker
      dockerd --unregister-service
      dockerd -g d:\docker --register-service
      Start-Service docker
    6. Download a NAV 2017 RTM DVD and extract it to d:\downloads\NAV2017-DVD
    7. Install the NAV Client in the host machine (use NAV User Password as authentication method)
    8. Install a SQL Server Management Studio
  2. Install SQL and prepare the database
    1. Open a cmd and run (thanks to Pascal for the hint!):
      docker pull microsoft/mssql-server-windows-express
      docker run -d -p 1433:1433 -v d:\downloads:c:\downloads --env sa_password=VerySecret --env accept_eula=y microsoft/mssql-server-windows-express
    2. Open SSMS in the host (use database authentication with user sa and the password you just entered in docker run) and restore a Cronus DB from the DVD
    3. Find out which IP the SQL container got by running the following in the host cmd:
      docker ps --> here you see the name of the container, in my case determined_einstein
      docker inspect -f="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" determined_einstein --> this returns the IP, in my case 172.24.152.70
  3. Install NAV Server and configure the instance
    1. Open a cmd and run:
      docker pull microsoft/dotnet-framework:3.5
      docker run -it -p 7045-7048:7045-7048 -v d:\downloads:c:\downloads microsoft/dotnet-framework:3.5 microsoft/dotnet-framework:3.5 cmd
    2. We can’t install through setup because the Windows Identity Foundation feature is not available. I am not sure if this is a Windows Server Core or a Windows Server 2016 limitation but I couldn’t get it to install. Therefore, just start the Service msi
    3. Open a PowerShell by just entering powershell and run the following to connect to the correct database server (database name is already correct):
      Open a PowerShell by just entering powershell and run the following to connect to the correct database server (database name is already correct):
    4. Because we want to use Database auth we need to create an encryption key, import it and configure the instance
      $password = ConvertTo-SecureString "VerySecret" -AsPlainText -Force
      $cred = new-object -typename System.Management.Automation.PSCredential -ArgumentList sa, $password
      New-NAVEncryptionKey -KeyPath "c:\downloads\key" -Password $password
      Import-NAVEncryptionKey DynamicsNAV100 -KeyPath "c:\downloads\key" -Password $password -ApplicationDatabaseServer 172.24.152.70 -ApplicationDatabaseCredentials $cred -ApplicationDatabaseName "Demo Database NAV (10-0)" -Force
    5. We want to use NavUsers, therefore we need a cert. As makecert or the PowerShell script both need a GUI, we need to do this in the host and export a pfx (you can use the Scripts from the NAV DVD, folder WindowsPowerShellScripts\NAV). Then again in the powershell in the container run the following if the pfx key is in d:\downloads\key.pfx on the host
      Import-PfxCertificate c:\downloads\key.pfx cert:\localmachine\my -Password $password --> this returns the thumbprint
      Set-NAVServerConfiguration DynamicsNAV100 -KeyName ServicesCertificateThumbprint -KeyValue 587E21B9FD05CF33BB4756B2D0683ED7E6E4C562
      Set-NAVServerConfiguration DynamicsNAV100 -KeyName ServicesCertificateValidationEnabled -KeyValue false
      Set-NAVServerConfiguration DynamicsNAV100 -KeyName ClientServicesCredentialType -KeyValue NavUserPassword
    6. Now we need to allow all users to access the private key as I didn’t find a way to limit it to the NAV Server account in the container
      $testCertificate = Get-ChildItem cert:\localmachine\my\587E21B9FD05CF33BB4756B2D0683ED7E6E4C562
      $privateKeyPath = (Join-Path (Join-Path $env:ProgramData "Microsoft\Crypto\RSA\MachineKeys") $testCertificate.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName)
      $acl = Get-Acl $privateKeyPath -ErrorAction Stop
      $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("Users", [System.Security.AccessControl.FileSystemRights]::Read, [System.Security.AccessControl.AccessControlType]::Allow)
      $acl.AddAccessRule($accessRule)
      Set-Acl -aclobject $acl $privateKeyPath -ErrorAction Stop
    7. Some DLLs are missing because we couldn’t install the prereqs, therefore: copy Microsoft.IdentityModel.dll, Microsoft.ReportViewer.Common.dll, Microsoft.ReportViewer.DataVisualization.dll, Microsoft.ReportViewer.ProcessingObjectModel.dll, Microsoft.ReportViewer.WinForms.dll, Microsoft.SqlServer.Types.dll to the Service folder and copy msvcr120.dll to c:\windows\System32
    8. master.dbo.$ndo$srvproperty could not be generated, importing the license instead, again in powershell
      Import-NAVServerLicense -LicenseFile 'C:\downloads\NAV2017-DVD\SQLDemoDatabase\CommonAppData\Microsoft\Microsoft Dynamics NAV\100\Database\Cronus.flf' -Database NavDatabase -SerVerInstance DynamicsNAV100
    9. Restart the NAV Server instance to make sure everything works
      Set-NAVServerInstance DynamicsNAV100 -Restart
    10. Create a new NAV user for the Client connection
      New-NAVServerUser -UserName Verwalter -Password $password -ServerInstance DynamicsNAV100
  4. Configure the Client in the host machine
    1. In your user folder under AppData\Roaming\Microsoft\Microsoft Dynamics NAV\100 you find a file called ClientUserSettings.config. Change key=”ServicesCertificateValidationEnabled” to false and key=”DnsIdentity” to whatever you used in the certificate. If you are not sure just open the client, it tells you what it expects
    2. Run ipconfig in the NAV Server container to get the IP
    3. Open the Windows Client in the host and connect to :7046/DynamicsNAV71
  5. If you run into problems you can use the following powershell cmdlet to check the event log inside the container
    Get-EventLog Application -newest 10 | ForEach-Object { Write-Host $_.TimeGenerated $_.Message }
  6. Persist the changes by stopping and commiting the container
    1. Open a cmd and run
      docker stop <name>
      docker commit <name> nav2017
    2. If you want to use that image, run
      docker run -it -p 7045-7048:7045-7048 nav2017 cmd

If you have any questions, feel free to contact me through the comments below. I’ll try to help (if I can)