Las herramientas de Docker de VS2017

Una de las novedades de VS2017 es el soporte integrado para Docker: podemos desplegar fácilmente nuestras soluciones en contenedores locales y depurar nuestro código Docker que se ejecuta en un contenedor. Pero… ¿cómo funciona exactamente?

Como habilitar el soporte para Docker

Para añadir soporte Docker a un proyecto, basta con hacer click con el botón derecho en el solution explorer y seleccionar la opción “Add –> Docker support”. Esto hace dos cosas:

  1. Crea el Dockerfile necesario para crear una imágen de Docker de nuestro proyecto
  2. Crea los ficheros de Compose necesarios para poner en marcha nuestro proyecto
  3. Crea el proyecto “docker-compose.dcproj” (que aparece en el solution explorer):

El proyecto docker-compose en el solution explorer

Si seleccionamos el docker-compose como proyecto de inicial, entonces veremos como el botón de “Debug Target” (el triángulo verde vamos :P) cambia a Docker. Y ahora pulsando F5, VS2017 va a lanzar nuestro proyecto usando docker compose.

Si tenemos varios proyectos en una solución podemos agregar todos los que queramos a Docker y VS2017 creará un Dockerfile para cada uno de ellos y los agregará al fichero de compose. Al usar F5 se lanzarán todos los proyectos de Docker, y podremos depurar cualquiera de ellos.

El proyecto dcproj

El proyecto dcproj es el encargado de incluir las tareas necesarias para ejecutar Docker dentro del pipeline de msbuild. A nivel de código es muy simple:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
  <PropertyGroup Label="Globals">
    <ProjectGuid>b0f0c876-c3ee-47fa-91f8-228b6948c08c</ProjectGuid>
    <DockerLaunchBrowser>True</DockerLaunchBrowser>
    <DockerServiceUrl>http://localhost:{ServicePort}/api/values</DockerServiceUrl>
    <DockerServiceName>testdocker</DockerServiceName>
  </PropertyGroup>
  <ItemGroup>
    <None Include="docker-compose.ci.build.yml" />
    <None Include="docker-compose.override.yml">
      <DependentUpon>docker-compose.yml</DependentUpon>
    </None>
    <None Include="docker-compose.vs.debug.yml">
      <DependentUpon>docker-compose.yml</DependentUpon>
    </None>
    <None Include="docker-compose.vs.release.yml">
      <DependentUpon>docker-compose.yml</DependentUpon>
    </None>
    <None Include="docker-compose.yml" />
  </ItemGroup>
</Project>

Si usas VS2017 Update 3 Preview 4 entonces el contenido es ligeramente distinto:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
  <PropertyGroup Label="Globals">
    <ProjectVersion>2.0</ProjectVersion>
    <DockerTargetOS>Linux</DockerTargetOS>
    <ProjectGuid>0b3c0c08-b9fe-4dee-b774-0494efab0535</ProjectGuid>
    <DockerLaunchBrowser>True</DockerLaunchBrowser>
    <DockerServiceUrl>http://localhost:{ServicePort}/api/values</DockerServiceUrl>
    <DockerServiceName>testdockerpreview</DockerServiceName>
  </PropertyGroup>
  <ItemGroup>
    <None Include="docker-compose.ci.build.yml" />
    <None Include="docker-compose.override.yml">
      <DependentUpon>docker-compose.yml</DependentUpon>
    </None>
    <None Include="docker-compose.yml" />
  </ItemGroup>
</Project>

Nota: A partir de ahora nos referiremos a VS2017 Update 2 (actualmente en RTM) como VS15.2 y a VS2017 Update 3 (actualmente en Preview 4) como VS15.3.

La principal diferencia es la aparición de DockerTargetOS en VS15.3. Y esto es porque una de las novedades que incorpora VS15.3 es el soporte para contenedores windows nativos. También vemos como en el dcproj de VS15.2 hay unos ficheros docker-compose.vs.debug.yml y docker-compose.vs.release.yml que no aparecen en el dcproj de VS15.3. Dichos ficheros aparecen el solution explorer de VS15.2. No es que hayan desaparecido en VS15.3, pero la idea es que dichos ficheros son generados por VS, así que no tiene sentido ni que aparezcan en el solution explorer ni que estén en el control de código fuente. Así a partir de VS15.3, dichos ficheros los encontrarás en el directorio obj/Docker de la solución.

¿Qué ocurre cuando compilamos y ejecutamos el dcproj?

Cuando hacemos un Build o ejecutamos el dcproj desde VS, se van a crear los contenedores Docker, pero… ¿qué ocurre entre bambalinas?

Como te puedes imaginar, VS va a ejecutar las tareas definidas en el SDK “Microsoft.Docker.Sdk”. Ese SDK es un SDK instalado por VS que contiene los targets de msbuild necesarios para terminar invocando a docker-compose para crear y levantar los contenedores indicados en el fichero docker-compose.yml.

Dicho así, podríamos pensar que ejecutar el proyecto con F5 desde VS o bien hacerlo desde la CLI usando docker-compose sería equivalente, pero realmente no es del todo así.

La razón de esa diferencia (entre usar la CLI y VS) es, como te puedes imaginar, el fichero docker-compose.vs.debug.yml (o su equivalente release). ¿Qué hace ese fichero? Para responder esa a pregunta debemos empezar por analizar el Dockerfile que nos genera VS:

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "TestDockerPreview.dll"]

Lo importante ahí es la línea

COPY ${source:-obj/Docker/publish} .

Esa línea es la que indica qué ficheros deben desplegarse en el contenedor: Si existe la variable source definida se copiará lo que haya en la variable source. En otro caso se copiará lo que haya en el directorio obj/Docker/publish.

Si queremos usar la CLI para crear contenedores cuyos Dockerfile han sido creados por VS, la clave es que el resultado de la publicación esté en obj/Docker/publish (es decir que hayamos hecho “dotnet publish –o obj/Docker/publish”). Por lo tanto los binarios de mi aplicación se copiaran en el contenedor (en el directorio /app del contenedor). Por lo tanto habitualmente usando la CLI haríamos dos cosas:

  1. dotnet publish –o obj/Docker/publish
  2. docker-compose –f docker-compose.yml –f docker-compose.override.yml up

El primer comando publica el proyecto en /obj/Docker/publish y el segundo comando usa el fichero docker-compose.yml que contiene la definición de las imágenes y el fichero docker-compose.override.yml (que contiene configuración) para crear y ejecutar los contenedores.

Básicamente VS2017 hace lo mismo, pero añade el fichero docker-compose.vs.debug.yml a la sentencia docker-compose. Y este fichero es importante porque es parecido al siguiente (en 15.2):

version: '2'

services:
  testdocker:
    image: testdocker:dev
    build:
      args:
        source: ${DOCKER_BUILD_SOURCE}
    environment:
      - DOTNET_USE_POLLING_FILE_WATCHER=1
    volumes:
      - ./TestDocker:/app
      - ~/.nuget/packages:/root/.nuget/packages:ro
      - ~/clrdbg:/clrdbg:ro
    entrypoint: tail -f /dev/null
    labels:
      - "com.microsoft.visualstudio.targetoperatingsystem=linux"

VS añade el fichero docker-compose.vs.debug.yml en último lugar cuando llama a docker-compose, por lo que eso significa que dicho fichero tiene preferencia (en caso de conflicto se van a tomar los valores de este fichero).

En un escenario multicontainer simplemente tendrás varias entradas en services, por supuesto. Lo interesante de este fichero es que hace lo siguiente:

  1. Asigna el nombre de la imagen Docker para que tenga el tag “dev”, con independencia del nombre y/o tag que tenga en el docker-compose.yml. Eso hace que todas las imágenes que lances con VS y las Docker tools (en debug) tendran el tag “dev”.
  2. Crea tres volúmenes (directorios compartidos entre host y container). Por un lado mapea al directorio /app del contenedor el directorio del proyecto. Es decir, el container contendrá en /app, no el resultado de la publicación, si no los ficheros de código fuente. Es lógico, ya que VS no publica el proyecto cuando lo ejecuta.
  3. El segundo volúmen mapea la cache de NuGet del host al contenedor, para que la resolución de paquetes en el contenedor funcione.
  4. El tercer volúmen mapea un directorio llamado clrdbg. Este directorio contiene el depurador clrdbg que permite a VS depurar el contenedor. VS descarga clrdbg en el directorio %USERPROFILE%\clrdbg (si no existe) y lo mapea al contenedor para que así este lo tenga instalado.

VS15.3 sigue la misma filosofía, pero recuerda que el fichero se docker-compose.vs.debug.g.yml  se encuentra en el directorio obj. El fichero es ligeramente distinto:

version: '3'

services:
  testdockerpreview:
    image: testdockerpreview:dev
    build:
      args:
        source: obj/Docker/empty/
    environment:
      - DOTNET_USE_POLLING_FILE_WATCHER=1
      - NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages
    volumes:
      - c:\users\etoma\documents\visual studio 2017\Projects\TestDockerPreview\TestDockerPreview:/app
      - C:\Users\etoma\vsdbg:/remote_debugger:ro
      - C:\Users\etoma\.nuget\packages\:/root/.nuget/packages:ro
      - C:\Users\etoma\.nuget\packages\:/root/.nuget/fallbackpackages:ro

    entrypoint: tail -f /dev/null
    labels:
      com.microsoft.visualstudio.debuggee.program: "dotnet"
      com.microsoft.visualstudio.debuggee.arguments: " --additionalProbingPath /root/.nuget/fallbackpackages  bin/Debug/netcoreapp1.1/TestDockerPreview.dll"
      com.microsoft.visualstudio.debuggee.workingdirectory: "/app"
      com.microsoft.visualstudio.debuggee.killprogram: "/bin/bash -c \"if PID=$$(pidof -x dotnet); then kill $$PID; fi\""

Al igual que VS15.2, VS15.3 también fija el nombre y tag de la imagen Docker. La principal diferencia es  que en lujgar de clrdbg se usa vsdbg que es la versión actualizada (y que al igual que 15.2, 15.3 se descarga automáticamente). Por el resto el fichero es más o menos parecido (la sección labels es distinta porque 15.3 añade más metadatos a la imgen, pero eso es irrelevante para lo que a nosotros nos afecta).

En resumen, en Debug los contenedores creados por VS:

  • Tendrán siempre el tag dev, con independencia del tag indicado en docker-compose.yml.
  • Tendrán un nombre fijo (el del proyecto). Dicho nombre es por defecto el mismo que el de docker-compose.yml, pero si lo cambias en docker-compose.yml (p. ej. para crear imágenes que sean de una organización) entonces este cambio no se reflejará cuando ejecutes los containers desde VS
  • Contendrán en /app el código fuente (y los directorios /bin y /obj) en lugar del resultado de la publicación
  • Contendrán en root/.nuget la cache local del host de nuget (a través de un volúmen)
  • Contendrán clrdbg (o vsdbg)  (a través de un volúmen)

¿Y en release? Pues en release lo único que hace VS es añadir el volúmen para que el contenedor tenga clrdbg y/o vsdbg y así se pueda depurar. Nada más. Eso sí, cuando construyas el proyecto en Release se va a publicar en el directorio /obj/Docker/publish para que así pueda copiarse desde ese directorio al directorio /app del container. Y no, ese directorio (obj/Docker/publish) no se puede modificar. De hecho está metido “a fuego” en el fichero Microsoft.DotNet.Docker.Targets (que es referenciado por el Docker.Sdk):

$(DockerIntermediateOutputPath)\publish

Limitaciones

Actualmente VS2017 solo soporta tener un .dcproj cargado y además hay una cosa curiosa (que personalmente no me gusta) y es que crear el dcproj poluciona el csproj asociado. En efecto cuando añadimos un csproj al dcproj se añade la siguiente línea en el csproj:

<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>

Es bastante molesto, ya que el csproj debería ser independiente del dcproj pero bueno… 🙁

Bueno… Así es como funciona la interacción entre las docker tools, docker y VS2017. Espero que ahora te haya quedado un poco más claro, como VS crea y gestiona nuestros contenedores Docker.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *