Docker: creando un contenedor de TetriNET con Windows Server Containers

Docker: creando un contenedor de TetriNET con Windows Server Containers

Todos los elementos a los que hace referencia este artículo se pueden encontrar en un repositorio de Github que he creado donde publicaré todos los temas relacionados con Docker, siéntete totalmente libre de examinarlo en https://github.com/cmilanf/docker

Hace meses que hablé sobre cómo crear un contenedor Docker de Tetrinetx, una venerable pieza de software del año 2001 que si bien no ha envejecido mal, su ejecución a día de hoy en un servidor plantea problemas de dependencias, mantenimiento y seguridad. El artículo es una guía de cómo a partir del software original de Tetrinetx creamos una imagen Docker que podemos utilizar para instanciar tanto contenedores como queramos. La imagen se construyó utilizando Alpine Linux como base, ocupando en su totalidad unos ligeros 35 MB aproximadamente.

Hoy voy a plantear un escenario muy parecido, pero con contenedores para Windows. Mark Russinovich allá por agosto de 2015 hizo un repaso tecnológico general a Docker, dejándonos ver cómo llegaría a Windows Server.

A día de hoy, con Windows Server 2016 y Windows 10 1703 Creators Update esto es ya una realidad.

Docker en Windows

En Linux, las tecnologías de contenerización se basan en estructuras específicas del kernel como los cgroups para aportar aislamiento entre contenedores ejecutándose bajo un mismo kernel.

Microsoft por su parte ha desarrollado estructuras en el sistema operativo que permitan también este tipo de aislamiento de forma nativa, resultando en dos posibles manaeras de instanciar imágenes de Docker:

  • Windows Server Containers. Donde -al igual que en Linux- los contenedores comparten kernel. Microsoft provee en este modo de aislamiento a nivel de namespace, control de recursos y proceso.
  • Hyper-V Containers. Como el aislamiento de los Windows Server Containers no es total, este modelo de implementación utiliza Hyper-V para garantizarlo. En este caso cada contenedor tiene su propia copia del kernel y... de forma efectiva estamos ejecutando una máquina virtual. Los contenedores Hyper-V sacrifican rendimiento y eficiencia en el consumo de recursos en pro de una mayor seguridad del equipo anfitrión.

¿Confuso aún ante lo que supone cada nivel de aislamiento? Microsoft explica en esta documentación las diferencias con un pequeño laboratorio que podemos llevar a cabo nosotros mismos.

Un aspecto realmente beneficioso es que elegir un modelo u otro es que se trata de una decisión operativa de infraestructura. Esto quiere decir que la misma imagen de Docker puede desplegarse como contenedor Windows Server o como contenedor Hyper-V sin llevar a cabo ninguna modificación.

Jetrix, un servidor de TetriNET desarrollado en Java

Volviendo al TetriNET, dado el artículo anterior me pareció buena idea que la primera imagen Docker para Windows que servidor construye estuviera basada en un concepto similar.

Como Tetrinetx es un desarrollo muy ligado al mundo POSIX (llegó a tener algunas builds experimentales para Windows), si quería hospedar un servidor en Windows era mejor utilizar alguna alternativa. El servidor nativo original de TetriNET se ejecuta sobre Windows precisamente, pero su operación está totalmente ligada a la interfaz de gráfica de usuario, por lo que no es apto para entornos headless desatendidos.

Así que a servidor se le vino a la cabeza Jetrix, una implementación de servidor de TetriNET escrita en Java que, si bien no es tan eficiente como Tetrinetx, es relativamente escalable, extensible y muy sencilla de utilizar. Al ser un desarrollo Java, tiene facilidad para ejecutarse en casi cualquier plataforma, si bien como he apuntado lo haremos sobre Windows.

He querido realizar el proceso de construcción de la imagen lo más análogo posible al de Tetrinetx, así que seguiré una operativa similar:

  1. Utilizar como basae una imagen de Windows Server 2016 NanoServer.
  2. Descargar e instalar una máquina virtual de Java, necesaria para la ejecución. Por facilidad de distribución del runtime he decidido utilizar OpenJDK.
  3. Instalar en NanoServer la feature de IIS. Lo utilizaremos para hospedar la web informativa sobre el juego y el servidor.
  4. Descargar e instalar Jetrix.
  5. Declarar los puertos TCP que el contenedor expondrá.
  6. Declarar el ENTRYPOINT.

Antes de entrar al detalle de cómo pinta un Dockerfile para Windows, hay un par de temas que me gustaría mencionar explícitamente.

NanoServer, no tan "nano"

Aunque Microsoft no ha escatimado en esfuerzos para aligerar Windows Server -teniendo como prueba de ello la edición NanoServer-, me temo que aún no estamos en los niveles de eficiencia que obtenemos con Linux. Estas palabras las motivan el hecho de que:

  • La base de NanoServer ocupa aproximadamente 1 GB de espacio de almacenamiento.
  • El footprint base de consumo de memoria RAM está en torno a los 350 MB.

Teniendo en cuenta que antes de NanoServer, estos consumos superaban el doble de los actuales; la optimización que Microsoft ha llevado a cabo ha sido muy notable, aunque nos encontremos lejos de los ridículos 5 MB de Alpine Linux.

Agregando IIS a NanoServer

NanoServer no viene por defecto con IIS, sino que es necesario agregar el rol en el momento de construir la imagen. Como en dicho momento NanoServer ya está arrancado y funcionando, no es una opción llevar a cabo una instalación offline; sin embargo esto no es problema porque está perfectamente soportado agregar el rol online.

OneGet y NanoServerPackage

Para no depender de tener a mano el DVD de instalación de Windows Server 2016, pensé que sería muy buena alternativa utilizar proveedor NanoServerPackage de OneGet de forma que podríamos instalar la extensión de la misma manera que hacemos un apt-get en GNU/Linux. Craso error.

Para que este método funcione es necesario tener instalado el último Cumulative Update de NanoServer. ¿Es esto un problema? Sí, porque la instalación de Cumulative Update nos exige reiniciar. Hasta donde servidor ha investigado, el proceso de construcción de una imagen de Docker no funciona si hay reinicio de por medio. Tiene bastante sentido ya que en Linux sería extremadamente raro tener la necesidad de reiniciar si no cambias algo en el kernel.

DISM

Una alternativa para hacer la instalación es utilizar DISM, tal y como se explica aquí.

La diferencia es que tendemos que tener preparados los archivos de las extensiones desde nuestro DVD de instalación de Windows Server 2016, concretamente los siguientes:

  • Microsoft-NanoServer-IIS-Package.cab
  • Microsoft-NanoServer-IIS-Package_en-US.cab

Como no son de libre distribución, no he podido adjuntarlos al repositorio de Github. ¡Tenlos preparados si quieres construirte tu propia imagen!

Dockerfile para Windows

Dicho esto, así queda el Dockerfile de la imagen:

FROM microsoft/nanoserver:latest

LABEL title "Jetrix Tetrinet Server Docker Image"  
LABEL maintainer "Carlos Milán Figueredo"  
LABEL email "cmilanf@hispamsx.org"  
LABEL version "1.0"  
LABEL contrib1 "Jetrix - http://jetrix.sourceforge.net/"  
LABEL url "https://calnus.com"  
LABEL twitter "@cmilanf"  
LABEL usage "docker run -d -p 31457:31457 -p 8080:8080 -p 80:80 -h myhostname.domain.com --name jetrix cmilanf/jetrix"  
LABEL thanksto "Beatriz Sebastián Peña"

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

ENV JAVA_VERSION 1.8.0.141-1  
ENV JAVA_ZIP_VERSION 1.8.0-openjdk-1.8.0.141-1.b16  
ENV JAVA_SHA256 2911ccece06500cc5bd37cb76028d4bd2b6261cb7f77e39404895e18d430d383  
ENV JAVA_HOME C:\\java-${JAVA_ZIP_VERSION}.ojdkbuild.windows.x86_64

COPY Packages/Microsoft-NanoServer-IIS-Package.cab C:/Windows/Temp  
COPY Packages/Microsoft-NanoServer-IIS-Package_en-US.cab C:/Windows/Temp  
COPY unattend.xml C:/Windows/Temp  
COPY tetriweb.zip C:/Windows/Temp

WORKDIR C:/Windows/Temp  
RUN Invoke-WebRequest $('https://github.com/ojdkbuild/ojdkbuild/releases/download/{0}/java-{1}.ojdkbuild.windows.x86_64.zip' -f $env:JAVA_VERSION, $env:JAVA_ZIP_VERSION) -OutFile 'openjdk.zip' -UseBasicParsing ; \  
    if ((Get-FileHash openjdk.zip -Algorithm sha256).Hash -ne $env:JAVA_SHA256) {exit 1} ; \
    Expand-Archive openjdk.zip -DestinationPath C:/ ; \
    $env:PATH = '{0}\bin;{1}' -f $env:JAVA_HOME, $env:PATH ; \
    Set-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' -Name Path -Value $env:PATH ; \
    Remove-Item -Path openjdk.zip

RUN cmd.exe /c "dism.exe /online /apply-unattend:.\unattend.xml" ; \  
    Start-Service w3svc ; \
    Expand-Archive -Path tetriweb.zip -DestinationPath C:/inetpub/wwwroot -Force

RUN Invoke-WebRequest 'https://downloads.sourceforge.net/project/jetrix/Jetrix%20TetriNET%20Server/0.2.3/jetrix-0.2.3.zip' -UserAgent 'NativeHost' -OutFile jetrix-0.2.3.zip ; \  
    Expand-Archive -Path jetrix-0.2.3.zip -DestinationPath C:/ -Force

EXPOSE 31457 8080 80

WORKDIR C:/jetrix-0.2.3  
ENTRYPOINT [ "c:\\jetrix-0.2.3\\jetrix.bat" ]  

Como podréis ver, hay diferencias sutiles entre un Dockerfile para Linux y otro para Windows, a destacar:

  • La shell por defecto es cmd.exe, excepto que especifiquemos otra mediante SHELL, como el presente caso donde utilizamos PowerShell.
  • La barra diagonal, / y la barra diagonal inversa \ que se utiliza en las rutas del sistema de archivos causa bastante confusión aquí. Como ya sabéis, / es para rutas de sistemas de archivos UNIX mientras \ es la que se utiliza en los sistemas de archivos de Windows. Docker fue concebido para Linux, por lo que la barra diagonal invertida \ es un carácter de escape. ¿Cómo trabajamos en Windows pues?
    • En el Dockerfile podemos especificar las rutas de Windows con la barra diagonal. Docker se encargará de hacer la conversión. Así podemos poner cosas como C:/Windows/Temp.
    • Dentro de los literales (cadenas constantes) tales como parámetros de un cmdlet de PowerShell la conversión de Docker no aplica, por lo que en este caso debemos duplicar la barra diagonal inversa para evitar la secuencia de escape. Así es como por ejemplo ENTRYPOINT [ "c:\jetrix-0.2.3\jetrix.bat" ] se convierte en ENTRYPOINT [ "c:\\jetrix-0.2.3\\jetrix.bat" ].

En cuanto al archivo en sí:

  • Línea 1. FROM. La imagen base va a ser NanoServer.
  • Líneas 3-11. LABEL. Metadatos del contenedor.
  • Línea 13. SHELL. Indicamos que nuestra shell será PowerShell y además las distintas preferencias, como detener la build ante cualquier error o mostrar toda la información de Verbose por la pantalla.
  • Líneas 15-18. ENV. Variables del entorno para OpenJDK, ¡cuidado que son de entorno y por tanto se referencian utilizando $env:MIVARIABLE.
  • Líneas 20-23. COPY. Archivos que vamos a necesitar para la build: el rol de IIS, el unattend.xml de su instalación, el servidor de Jetrix y el sitio web.
  • Línea 25. WORKDIR. Nuestra carpeta de trabajo va a ser C:\Windows\Temp.
  • Líneas 26-31. RUN. Instalación de OpenJDK.
  • Líneas 33-25. RUN. Instalación del rol de IIS y desempaquetado del website.
  • Líneas 37-38. RUN. Instalación de Jetrix.
  • Línea 40. EXPOSE. Exponer los puertos TCP/31457, TCP/8080 y TCP/80.
  • Línea 42. WORKDIR. Cambiamos la carpeta de trabajo a C:\jetrix-0.2.3.
  • Línea 43. ENTRYPOINT. Jetrix viene con un estupendo BAT que mantiene el servidor en ejecución.

Una vez tenemos todo, construir la imagen Docker es tan sencillo como ocurre con Linux:

docker build -t jetrix .  

¡Buen momento para un café!

Ejecutando la imagen

Para ejecutar la imagen no tenemos más lanzar a Docker la siguiente orden:

docker run -p 31457:31457 -p 8080:8080 -p 80:80 -d -h myhostname.com --name jetrix cmilanf/jetrix  
  • -p 31457:31457 -p 8080:8080 -p 80:80. Exposición de puertos. Por defecto seguirá lo que hayamos marcado en EXPOSE, pero podemos poner nuestra propia configuración si así lo deseamos.
  • -d. Daemonize, ejecutar el contenedor como servicio del sistema.
  • -h myhostname.com. El FQDN del contenedor.
  • --name jetrix. El nombre del contenedor. Puede ser el que queramos.
  • cmilanf/jetrix. Repositorio y nombre de la imagen que estamos desplegando.

Hecho esto ya deberíamos poder conectarnos a nuestro servidor, pero... ¿dónde se encuentra? Obtener esta información es trivial:

> docker inspect jetrix

"Networks": {                                                                             
    "nat": {                                                                              
        "IPAMConfig": null,                                                               
        "Links": null,                                                                    
        "Aliases": null,                                                                  
        "NetworkID": "c875925117e38d1fe38cd480c6fwabef2ef24ce4364f211382f14d0d",  
        "EndpointID": "c9443d95646ff79c84e9346c504gda32451d0e1c5542d6271f084d0", 
        "Gateway": "172.29.224.1",                                                        
        "IPAddress": "172.29.236.249",                                                    
        "IPPrefixLen": 16,                                                                
        "IPv6Gateway": "",                                                                
        "GlobalIPv6Address": "",                                                          
        "GlobalIPv6PrefixLen": 0,                                                         
        "MacAddress": "00:15:5d:16:1c:3e",                                                
        "DriverOpts": null                                                                
    }                                                                                     
}                                                                                         

El valor IPAddress es el que nos sirve para conectar con el servicio. El tema de networking en Docker para Windows daría para un artículo en sí, así que entraremos en otro momento.

Docker Hub

Las imágenes Docker de Windows también pueden publicarse en Docker Hub, y la que hemos aprendido a elaborar en este artículo se puede encontrar en https://hub.docker.com/r/cmilanf/jetrix/.

Contenedor en ejecución

Ya podemos conectarnos con nuestro cliente de TetriNET al contenedor:

Si accedemos por HTTP a http://myhostname:

Y si accedemos por HTTP a http://myhostname:8080 (default user: admin, default password: adminpass):

Happy containerization!

Related Article