C#, WebApi et Docker

Mono est l’implémentation open-source de .NET. La plate-forme Mono fournit un ensemble complet de classes qui fournissent une base solide pour construire des applications. Ces classes sont compatibles avec les classes .NET Framework de Microsoft. Le hub Docker fournit une image Mono. Dans cet article, nous allons créer une image qui démarre un micro-service WebApi en tâche de fond.

Préambule et premier test

Après avoir téléchargé l’image Mono depuis le hub , nous pouvons nous y connecter en faisant :

sudo docker run -it mono bash

Cette instruction démarre une invite de commande dans le conteneur qui nous permet de vérifier l’installation de Mono, de constater la présence de nuget, du compilateur, et l’absence de git (un peu pénible pour la récupération en local du code source).

root@ed7e75e0514c:/# mono
Usage is: mono [options] program [program-options]

Development:
    --aot[=<options>]      Compiles the assembly to native code
    --debug[=<options>]    Enable debugging support, use --help-debug for details
    --debugger-agent=options Enable the debugger agent
    --profile[=profiler]   Runs in profiling mode with the specified profiler module
    --trace[=EXPR]         Enable tracing, use --help-trace for details
    --jitmap               Output a jit method map to /tmp/perf-PID.map
    --help-devel           Shows more options available to developers

Runtime:
    --config FILE          Loads FILE as the Mono config
    --verbose, -v          Increases the verbosity level
    --help, -h             Show usage information
    --version, -V          Show version information
    --runtime=VERSION      Use the VERSION runtime, instead of autodetecting
    --optimize=OPT         Turns on or off a specific optimization
                           Use --list-opt to get a list of optimizations
    --security[=mode]      Turns on the unsupported security manager (off by default)
                           mode is one of cas, core-clr, verifiable or validil
    --attach=OPTIONS       Pass OPTIONS to the attach agent in the runtime.
                           Currently the only supported option is 'disable'.
    --llvm, --nollvm       Controls whenever the runtime uses LLVM to compile code.
    --gc=[sgen,boehm]      Select SGen or Boehm GC (runs mono or mono-sgen)

Mono est bien là. Sur la page Mono du hub Docker, nous avons un exemple de fichier Dockerfile visant à exécuter un programme au démarrage du conteneur (notez le tag “onbuild” de l’image de base) :

FROM mono:3.10-onbuild
CMD [ "mono", "./TestingConsoleApp.exe" ]

En l’état, l’exécution de ce fichier Dockerfile est bien évidemment en erreur. Par parce que l’exécutable spécifié est introuvable, mais parce que le script s’attend à trouver une solution dans le répertoire courant.

rvv@rvv-ubuntu:~/Source/WebService/WebService/tmp$ sudo docker build -t rvv/tmp .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM mono:3.10-onbuild
3.10-onbuild: Pulling from library/mono

b65f32901846: Already exists 
1c3d46c13659: Already exists 
72d8730859bd: Already exists 
d0c1915bbb62: Pull complete 
4281c1bac11b: Pull complete 
9de140ab588b: Pull complete 
c852b8452076: Pull complete 
Digest: sha256:58a979cd33caf2c978039196f84a48c0b668cbe48c26e812577b5a646c580327
Status: Downloaded newer image for mono:3.10-onbuild
# Executing 4 build triggers...
Step 1 : COPY . /usr/src/app/source
Step 1 : RUN nuget restore -NonInteractive
 ---> Running in 5f2c6f912241
This folder contains no solution files, nor packages.config files.
The command '/bin/sh -c nuget restore -NonInteractive' returned a non-zero code: 1

Modifions le fichier Dockerfile et revenons sur l’image Mono de base (sans le tag) :

FROM mono
CMD [ "mono", "./TestingConsoleApp.exe" ]

La construction de l’image se passe bien, mais l’exécution renvoie une erreur :

rvv@rvv-ubuntu:~/Source/WebService/WebService/tmp$ sudo docker build -t rvv/tmp .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM mono
 ---> d6baf4255887
Step 2 : CMD mono ./TestingConsoleApp.exe
 ---> Running in 3552d7823dd8
 ---> 18e66da268d2
Removing intermediate container 3552d7823dd8
Successfully built 18e66da268d2
rvv@rvv-ubuntu:~/Source/WebService/WebService/tmp$ sudo docker run rvv/tmp
Cannot open assembly './TestingConsoleApp.exe': No such file or directory.

L’instruction “CMD” indique donc la commande à exécuter au démarrage de l’image.

Le fichier Dockerfile

Sur le page du hub, nous avons un ensemble d’images (leurs tags sont très importants), dont le tag “onbuild”. Ce tag indique que l’image intègre le build de l’exécutable que l’on souhaite ajouter à l’image. Chacune de ces images est réalisée à l’exécution par l’exploitation d’un fichier Dockerfile, attendu dans le répertoire courant. Le fichier Dockerfile permet du construire une image, voir plus particulièrement, de modifier une image de base, en empilant une série d’instructions. En sortie de construction, l’image est prête à l’emploi. Le fichier Dockerfile est exploité par l’instruction “build” de Docker en prenant pour référence le répertoire courant. Par exemple :

sudo docker build -t rvv/webservice .

Le fichier Dockerfile de l’image de Mono intégrant le build de la solution est le suivant :

FROM mono:4.6.2.7

MAINTAINER Jo Shields <jo.shields@xamarin.com>

RUN mkdir -p /usr/src/app/source /usr/src/app/build
WORKDIR /usr/src/app/source

ONBUILD COPY . /usr/src/app/source
ONBUILD RUN nuget restore -NonInteractive
ONBUILD RUN xbuild /property:Configuration=Release /property:OutDir=/usr/src/app/build/
ONBUILD WORKDIR /usr/src/app/build

Par convention, les instructions Docker sont écrites en majuscules. Viennent ensuite les arguments d’exécution. L’instruction “FROM” indique l’image de référence à partir de laquelle construire l’image actuelle. L’instruction “MAINTAINER” précise l’auteur du script. “RUN” indique une ou une série d’instructions à exécuter dans le conteneur, au moment de sa création. “WORKDIR” place le répertoire de travail du conteneur. L’instruction “ONBUILD” permet de définir un trigger, lorsque l’image cible (la vôtre) est construite. Toutes ces instructions sont empilées et modifient séquentiellement l’image de base (ici mono:4.6.2.7) selon les besoins des utilisateurs, en recréant, pour chacune de ces instructions, une nouvelle image de travail (il est donc impossible de faire transiter une variable d’une ligne à l’autre, et il est nécessaire d’utiliser des variables d’environnement pour cela). Enfin, le caractère “#” indique un commentaire (référence des instructions ici). En regardant le fichier, nous voyons donc que sont invoqués à la construction de l’image :

  • La création de 2 répertoires : /usr/src/app/source et /usr/src/app/build
  • Le positionnement du répertoire de travail sur /usr/src/app/source
  • La copie des fichiers du répertoire courant (celui de la machine hôte) vers le répertoire de travail
  • La restitution de paquets nuget d’une éventuelle solution se trouvant dans le répertoire de travail du conteneur
  • Et finalement la compilation de cette solution (les instructions nuget et xbuild travaillent par défaut dans le répertoire courant, aucun besoin de spécifier le chemin de la solution si cette dernière se trouve à hauteur du fichier Dockerfile)

Il nous faut donc avoir le fichier Dockerfile dans le répertoire de la solution.

Le projet WebApi

Pour faire simple, nous allons développer une API auto-portée grâce à Owin.

using System;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Hosting;
using System.Web.Http;
using System.Collections.Generic;

namespace WebService
{
    class Program
    {
        static void Main(string[] args)
        {
            string baseUrl = "http://*:1234";

            using (WebApp.Start<Startup>(baseUrl))
            {
                Console.WriteLine("Press Enter to quit.");
                Console.ReadKey();
            }
        }
    }
}

Nous allons demander au service d’écouter sur l’ensemble des interfaces réseau de la machine. La classe Startup permettant de configurer les routes et le format de la réponse est définie comme suit :

using System;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Hosting;
using System.Web.Http;
using System.Collections.Generic;

[assembly: OwinStartup(typeof(WebService.Startup))]
namespace WebService
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Configure Web API for self-host. 
            HttpConfiguration config = new HttpConfiguration();

            config.EnableCors();

            config.Routes.MapHttpRoute(
                name: "DefaultApi", 
                routeTemplate: "api/{controller}/{id}", 
                defaults: new { id = RouteParameter.Optional } 
            );

            config.Formatters.Remove(config.Formatters.XmlFormatter);
            config.Formatters.Add(config.Formatters.JsonFormatter);

            app.UseWebApi(config); 
        }
    }
}

Et enfin le contrôleur :

using System;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Hosting;
using System.Web.Http;
using System.Collections.Generic;
using System.Net;
using System.Web.Http.Cors;

namespace WebService
{
    [EnableCors(origins: "*", headers: "*", methods: "*")]
    public class MessageController : ApiController
    {
        public Message Get()
        { 
            return new Message(){ Now = DateTime.Now, WhoAmI = Dns.GetHostEntry(Dns.GetHostName()).HostName, Content = @"Nice to see you !" }; 
        }

        public void Post([FromBody]Message value)
        { 
        }
    }
}

L’API répond simplement en précisant la date de la demande, son adresse, accompagnés d’un message très succinct. Notre fichier Dockerfile est on ne peut plus simple :

FROM mono:4.0-onbuild
CMD [ "mono", "/usr/src/app/build/WebService.exe" ]
EXPOSE 1234

Passons à la suite, la finalisation de l’image…

Finalisation de l’image

La compilation de la solution et son importation dans l’image cible est prise en charge par l’image source (mono:4.0-onbuild). Il ne reste donc qu’à copier ce fichier dans le répertoire de la solution, et à invoquer dans le fichier Dockerfile l’exécution de l’applicatif au démarrage de celle-ci. Nous décidons aussi de préciser à l’image quel port nous souhaitons exposer à l’extérieur du conteneur, ici le port 1234. Cela ne rend pas pour autant le port accessible depuis l’extérieur, il s’agit d’une donnée informative à l’attention des utilisateurs du script. Lançons maintenant la construction de l’image :

rvv@rvv-ubuntu:~/Source/WebService/WebService$ sudo docker build -t rvv/webservice .
[sudo] Mot de passe de rvv : 
Sending build context to Docker daemon 15.14 MB
Step 1 : FROM mono:4.0-onbuild
# Executing 4 build triggers...
Step 1 : COPY . /usr/src/app/source
Step 1 : RUN nuget restore -NonInteractive
 ---> Running in 4628781c1a30
All packages listed in packages.config are already installed.
Step 1 : RUN xbuild /property:Configuration=Release /property:OutDir=/usr/src/app/build/
 ---> Running in f883ab88508e
XBuild Engine Version 12.0
Mono, Version 4.0.5.0
Copyright (C) 2005-2013 Various Mono authors

Build started 01/14/2017 19:50:01.
__________________________________________________
Project "/usr/src/app/source/WebService.sln" (default target(s)):
	Target ValidateSolutionConfiguration:
		Building solution configuration "Release|x86".
	Target Build:
		Project "/usr/src/app/source/WebService/WebService.csproj" (default target(s)):
			Target PrepareForBuild:
				Configuration: Release Platform: x86
			Target CopyFilesMarkedCopyLocal:
				Copying file from '/usr/src/app/source/packages/Owin.1.0/lib/net40/Owin.dll' to '/usr/src/app/build/Owin.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.Owin.3.0.1/lib/net45/Microsoft.Owin.dll' to '/usr/src/app/build/Microsoft.Owin.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.Owin.Diagnostics.3.0.1/lib/net45/Microsoft.Owin.Diagnostics.dll' to '/usr/src/app/build/Microsoft.Owin.Diagnostics.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.Owin.Hosting.3.0.1/lib/net45/Microsoft.Owin.Hosting.dll' to '/usr/src/app/build/Microsoft.Owin.Hosting.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.Owin.Host.HttpListener.3.0.1/lib/net45/Microsoft.Owin.Host.HttpListener.dll' to '/usr/src/app/build/Microsoft.Owin.Host.HttpListener.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.AspNet.WebApi.Client.5.2.3/lib/net45/System.Net.Http.Formatting.dll' to '/usr/src/app/build/System.Net.Http.Formatting.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.AspNet.WebApi.Core.5.2.3/lib/net45/System.Web.Http.dll' to '/usr/src/app/build/System.Web.Http.dll'
				Copying file from '/usr/src/app/source/packages/Microsoft.AspNet.WebApi.Owin.5.2.3/lib/net45/System.Web.Http.Owin.dll' to '/usr/src/app/build/System.Web.Http.Owin.dll'
				Copying file from '/usr/src/app/source/packages/Newtonsoft.Json.9.0.1/lib/net45/Newtonsoft.Json.dll' to '/usr/src/app/build/Newtonsoft.Json.dll'
			Target GenerateSatelliteAssemblies:
			No input files were specified for target GenerateSatelliteAssemblies, skipping.
			Target GenerateTargetFrameworkMonikerAttribute:
			Skipping target "GenerateTargetFrameworkMonikerAttribute" because its outputs are up-to-date.
			Target CoreCompile:
				Tool /usr/lib/mono/4.5/mcs.exe execution started with arguments: /noconfig /optimize+ /out:obj/x86/Release/WebService.exe Program.cs Startup.cs Message.cs MessageController.cs /target:exe /main:WebService.Program /nostdlib /platform:x86 /reference:../packages/Owin.1.0/lib/net40/Owin.dll /reference:../packages/Microsoft.Owin.3.0.1/lib/net45/Microsoft.Owin.dll /reference:../packages/Microsoft.Owin.Diagnostics.3.0.1/lib/net45/Microsoft.Owin.Diagnostics.dll /reference:../packages/Microsoft.Owin.Hosting.3.0.1/lib/net45/Microsoft.Owin.Hosting.dll /reference:../packages/Microsoft.Owin.Host.HttpListener.3.0.1/lib/net45/Microsoft.Owin.Host.HttpListener.dll /reference:/usr/lib/mono/4.5/System.ServiceProcess.dll /reference:/usr/lib/mono/4.5/System.dll /reference:../packages/Microsoft.AspNet.WebApi.Client.5.2.3/lib/net45/System.Net.Http.Formatting.dll /reference:/usr/lib/mono/4.5/System.Net.Http.dll /reference:../packages/Microsoft.AspNet.WebApi.Core.5.2.3/lib/net45/System.Web.Http.dll /reference:../packages/Microsoft.AspNet.WebApi.Owin.5.2.3/lib/net45/System.Web.Http.Owin.dll /reference:../packages/Newtonsoft.Json.9.0.1/lib/net45/Newtonsoft.Json.dll /reference:/usr/lib/mono/4.5/System.Core.dll /reference:/usr/lib/mono/4.5/mscorlib.dll /warn:4
CSC:  warning CS1701: Assuming assembly reference `Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' matches assembly `Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'. You may need to supply runtime policy
CSC:  warning CS1701: Assuming assembly reference `Microsoft.Owin, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' matches assembly `Microsoft.Owin, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. You may need to supply runtime policy
			Target DeployOutputFiles:
				Copying file from '/usr/src/app/source/WebService/obj/x86/Release/WebService.exe.mdb' to '/usr/src/app/build/WebService.exe.mdb'
				Copying file from '/usr/src/app/source/WebService/obj/x86/Release/WebService.exe' to '/usr/src/app/build/WebService.exe'
		Done building project "/usr/src/app/source/WebService/WebService.csproj".
Done building project "/usr/src/app/source/WebService.sln".

Build succeeded.

Warnings:

/usr/src/app/source/WebService.sln (default targets) ->
(Build target) ->
/usr/src/app/source/WebService/WebService.csproj (default targets) ->
/usr/lib/mono/4.5/Microsoft.CSharp.targets (CoreCompile target) ->

	CSC:  warning CS1701: Assuming assembly reference `Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' matches assembly `Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'. You may need to supply runtime policy
	CSC:  warning CS1701: Assuming assembly reference `Microsoft.Owin, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' matches assembly `Microsoft.Owin, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. You may need to supply runtime policy

	 2 Warning(s)
	 0 Error(s)

Time Elapsed 00:00:00.5416880
Step 1 : WORKDIR /usr/src/app/build
 ---> Running in ef7d4091d3e8
 ---> ee08fbe2893a
Removing intermediate container f883ab88508e
Removing intermediate container ef7d4091d3e8
Removing intermediate container 2b5c37c1778b
Removing intermediate container 4628781c1a30
Step 2 : EXPOSE 1234
 ---> Running in e56436678194
 ---> 9a964b2da564
Removing intermediate container e56436678194
Successfully built 9a964b2da564

Il est maintenant possible de monter l’image et de vérifier que le micro-service répond correctement.

rvv@rvv-ubuntu:~/Source/WebService/WebService/tmp$ sudo docker images
[sudo] Mot de passe de rvv : 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
rvv/webservice      latest              78be1b5ad400        19 hours ago        640.6 MB
sudo docker run -td -p 1234:1234 rvv/webservice

Le flag “-p” de la commande précédente indique que le port 1234 bind sur le port 1234 de l’hôte. Pour binder le port 1234 du conteneur sur le port 1234 de l’hôte en accédant directement au port précisé par l’instruction “EXPOSE”, il serait suffisant d’indiquer le flag “-P” uniquement. Les flags “-td” demandent à Docker de lancer l’image en mode “détaché” (sans ces flags le conteneur sort immédiatement de son exécution…). Pour vérifier que l’image est bien lancée, nous faisons maintenant :

rvv@rvv-ubuntu:~/Source/WebService/WebService$ sudo docker ps
[sudo] Mot de passe de rvv : 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
fca6d4ff7579        rvv/webservice      "mono /usr/src/app/bu"   19 hours ago        Up 19 hours         0.0.0.0:1234->1234/tcp   clever_swanson

Docker nous indique par la même occasion dans la colonne “PORTS” quelles sont les modalités de binding des ports, entre le conteneur et l’hôte. Pour influer sur la vie du conteneur monté, nous devrons spécifier l’identifiant figurant dans la première colonne. Pour afficher spécifiquement les informations de binding, nous pouvons à tout moment faire :

rvv@rvv-ubuntu:~/Source/WebService/WebService$ sudo docker port fca
1234/tcp -> 0.0.0.0:1234

Pour stopper le conteneur :

rvv@rvv-ubuntu:~/Source/WebService/WebService$ sudo docker stop fca

Une ultime version du fichier Dockerfile consiste à apporter un peu de généricité au script, dans lequel figurait encore le nom de l’exécutable à installer. Pour aborder la gestion d’un parc d’images Docker, j’aborderai plus tard quelques possibilités simples de “Infrastructure As A Code” (IAC), avec le framework Chef. Les sources sont téléchargeables ici, sur GitHub.

FROM mono:onbuild
MAINTAINER venantvr

RUN export executable=$(ls --format single-column /usr/src/app/build/*.exe) && \
	echo mono ${executable} > /usr/src/app/build/launcher.sh && \
        chmod +x /usr/src/app/build/launcher.sh

RUN echo Hello, $(ls --format single-column /usr/src/app/build/*.exe) will start on boot ! 

CMD [ "/bin/sh", "/usr/src/app/build/launcher.sh" ]

EXPOSE 1234

Add a Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

30 − = 27