Hay una excelente publicación de Steve Laster en 2016 sobre la optimización de los tamaños de imágenes de ASP.NET Docker. Desde entonces, Docker ha agregado archivos de compilación de varias etapas para que pueda hacer más en un Dockerfile... lo que se siente como un paso aunque no lo sea. Los contenedores tienen que ver con la implementación fácil y confiable, y también tienen que ver con la densidad. Desea usar la menor cantidad de memoria posible, claro, pero también es bueno hacerlos lo más pequeños posible para que no pierda tiempo moviéndolos por la red. El tamaño del archivo de imagen también puede afectar el tiempo de inicio del contenedor. Además, está ordenado.
He estado construyendo un pequeño clúster de Kubenetes de Raspberry Pi (ARM) de 6 nodos en mi escritorio, como lo hace usted, esta semana, y noté que el tamaño de mis imágenes era un poco más grande de lo que me gustaría. Este es un problema mayor porque es un sistema de relativamente poca potencia, pero nuevamente, ¿por qué cargar con x megabytes innecesarios si no es necesario?
Alex Ellis tiene un excelente blog sobre la creación de aplicaciones .NET Core para Raspberry Pi junto con un video de YouTube. En su video y blog, crea una aplicación de consola "Console.WriteLine()", que es excelente para OpenFaas (plataforma sin servidor de código abierto), pero también quería tener aplicaciones ASP.NET Core en mi clúster Raspberry Pi k8s. Incluyó esto como un "desafío" en su blog, ¡así que aceptó el desafío! ¡Gracias por toda tu ayuda y apoyo, Alex!
ASP.NET Core en Docker (en ARM)
Primero hago una aplicación ASP.NET Core básica. Podría hacer una API web, pero esta vez haré una MVC con Razor Pages. Para ser claros, son lo mismo solo que con diferentes puntos de partida. Siempre puedo agregar páginas o agregar JSON a cualquiera, más tarde.
Comienzo con "dotnet new mvc" (o dotnet new razor, etc.). Voy a ejecutar esto en Docker, administrado por Kuberenetes, y aunque siempre puedo cambiar WebHost en Program.cs para cambiar cómo se inicia el servidor web Kestrel de esta manera:
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")
Para los casos de uso de Docker, es más fácil cambiar la URL de escucha con una variable de entorno. Claro, podría ser 80, pero me gusta 5000. Estableceré la variable de entorno ASPNETCORE_URLS en http://+:5000 cuando cree el Dockerfile.
Dockerfile multietapa optimizado para ASP.NET
Hay varias formas "correctas" de hacer esto, por lo que querrá pensar en sus escenarios. Verás a continuación que estoy usando ARM (porque Raspberry Pi), así que si ves errores ejecutando su contenedor como "qemu:syscall no compatible:345", entonces está intentando ejecutar una imagen ARM en x86/x64. Voy a construir un contenedor ARM desde Windows, pero no puedo ejecutarlo aquí. Tengo que enviarlo a un registro de contenedores y luego decirle a mi clúster de Raspberry Pi que lo baje y ENTONCES se ejecutará allí.
Esto es lo que tengo hasta ahora. NOTA:hay algunas cosas comentadas, así que sé consciente. Este es/fue un ejercicio de aprendizaje para mí. ¡No copies/pegues a menos que sepas lo que pasa! Y si hay un error, aquí hay un GitHub Gist de mi Dockerfile para que lo cambie y mejore.
Es importante comprender que .NET Core tiene un SDK con herramientas de compilación y kits de desarrollo y compiladores y demás, y luego tiene un tiempo de ejecución. El tiempo de ejecución no tiene las cosas de "crear una aplicación", solo tiene las cosas de "ejecutar una aplicación". Actualmente no hay un SDK para ARM, por lo que es una limitación con la que estamos trabajando (algo elegantemente) con el archivo de compilación de varias etapas. Pero, incluso si HUBIERA un SDK para ARM, aún querríamos usar un Dockerfile como este porque es más eficiente con el espacio y crea una imagen más pequeña.
Analicemos esto. Hay dos etapas. El primer FROM es la imagen del SDK que crea el código. Estamos haciendo la compilación dentro de Docker, que es una forma hermosa y confiable de hacer compilaciones.
CONSEJO PROFESIONAL: Docker es inteligente a la hora de crear imágenes intermedias y hacer el mínimo trabajo , pero es útil si nosotros (los autores) también hacemos lo correcto para ayudarlo.
Por ejemplo, ¿ve dónde COPIAMOS el .csproj y luego hacemos una "restauración dotnet"? A menudo verá que la gente hace una "COPIA...". y luego hacer una restauración. Eso no permite que Docker detecte qué ha cambiado y terminará pagando por la restauración en CADA CONSTRUCCIÓN.
Al hacer estos dos pasos:copiar el proyecto, restaurar, copiar el código, esto significa que Docker almacenará en caché el paso intermedio de "restauración de dotnet" y todo será MUCHO más rápido.
Después de compilar, realizará una publicación. Si conoce el destino como yo (linux-arm), puede hacer una publicación RID (identificador de tiempo de ejecución) que sea independiente con -r linux-arm (o debian, o lo que sea) y obtendrá un completo auto- versión contenida de su aplicación.
De lo contrario, puede simplemente publicar el código de su aplicación y usar una imagen de tiempo de ejecución de .NET Core para ejecutarlo. Dado que estoy usando una compilación completa e independiente para esta imagen, sería exagerado incluir TAMBIÉN el tiempo de ejecución de .NET. Si observa el concentrador de Docker para Microsoft/dotnet, verá imágenes llamadas "deps" para "dependencias". Esas son imágenes que se ubican sobre Debian que incluyen las cosas que .NET necesita para ejecutarse, pero no .NET en sí mismo.
La pila de imágenes se ve generalmente así (por ejemplo)
- DESDE debian:stretch
- DESDE microsoft/dotnet:2.0-runtime-deps
- DESDE microsoft/dotnet:2.0-runtime
Entonces tiene su imagen base, sus dependencias y su tiempo de ejecución de .NET. La imagen SDK incluiría aún más cosas, ya que necesita compilar código. Nuevamente, es por eso que usamos eso para la imagen "como constructor" y luego copiamos los resultados de la compilación y ponerlos en otra imagen de tiempo de ejecución. Obtienes lo mejor de todos los mundos.
FROM microsoft/dotnet:2.0-sdk as builder
RUN mkdir -p /root/src/app/aspnetcoreapp
WORKDIR /root/src/app/aspnetcoreapp
#copy just the project file over
# this prevents additional extraneous restores
# and allows us to re-use the intermediate layer
# This only happens again if we change the csproj.
# This means WAY faster builds!
COPY aspnetcoreapp.csproj .
#Because we have a custom nuget.config, copy it in
COPY nuget.config .
RUN dotnet restore ./aspnetcoreapp.csproj
COPY . .
RUN dotnet publish -c release -o published -r linux-arm
#Smaller - Best for apps with self-contained .NETs, as it doesn't include the runtime
# It has the *dependencies* to run .NET Apps. The .NET runtime image sits on this
FROM microsoft/dotnet:2.0.0-runtime-deps-stretch-arm32v7
#Bigger - Best for apps .NETs that aren't self-contained.
#FROM microsoft/dotnet:2.0.0-runtime-stretch-arm32v7
# These are the non-ARM images.
#FROM microsoft/dotnet:2.0.0-runtime-deps
#FROM microsoft/dotnet:2.0.0-runtime
WORKDIR /root/
COPY --from=builder /root/src/app/aspnetcoreapp/published .
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000/tcp
# This runs your app with the dotnet exe included with the runtime or SDK
#CMD ["dotnet", "./aspnetcoreapp.dll"]
# This runs your self-contained .NET Core app. You built with -r to get this
CMD ["./aspnetcoreapp"]
Tenga en cuenta también que tengo un nuget.config personalizado, por lo que si lo tiene también deberá asegurarse de que esté disponible en el momento de la compilación para que la restauración de dotnet recoja todos los paquetes.
Incluí comentando un montón de FROM en la segunda etapa. Estoy usando solo el ARM, pero quería que vieras los demás.
Una vez que tenemos el código que construimos copiado en nuestra imagen de tiempo de ejecución, configuramos nuestra variable de entorno para que todas nuestras escuchas en el puerto 5000 internamente (¿recuerdas eso de arriba?) Luego ejecutamos nuestra aplicación. Tenga en cuenta que puede ejecutarlo con "dotnet foo.dll" si tiene el tiempo de ejecución, pero si es como yo y usa una compilación independiente, simplemente ejecutará "foo".
En resumen:
- Compila con FROM microsoft/dotnet:2.0-sdk como constructor
- Copiar los resultados a un tiempo de ejecución
- Use el tiempo de ejecución DE adecuado para usted
- ¿Arquitectura de CPU correcta?
- Usando .NET Runtime (típico) o usando una compilación independiente (menos)
- ¿Está escuchando en el puerto correcto (si es una aplicación web)?
- ¿Estás ejecutando tu aplicación correctamente y con éxito?
- ¿Tienes un .dockerignore? Súper importante para compilaciones .NET, ya que no desea copiar sobre /obj, /bin, etc., pero sí desea /published.
obj/
bin/
!published //li>
Optimizando un poco más
Hay algunas herramientas preliminares de "recorte de árboles" que pueden ver su aplicación y eliminar el código y los archivos binarios a los que no está llamando. También incluí Microsoft.Packaging.Tools.Trimming para probarlo y obtener aún más código sin usar de mi imagen final simplemente agregando un paquete a mi proyecto.
Step 8/14 : RUN dotnet publish -c release -o published -r linux-arm /p:LinkDuringPublish=true
---> Running in 39404479945f
Microsoft (R) Build Engine version 15.4.8.50001 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Trimmed 152 out of 347 files for a savings of 20.54 MB
Final app size is 33.56 MB
aspnetcoreapp -> /root/src/app/aspnetcoreapp/bin/release/netcoreapp2.0/linux-arm/aspnetcoreapp.dll
Trimmed 152 out of 347 files for a savings of 20.54 MB
Final app size is 33.56 MB
Si ejecuta el historial de Docker en su imagen final, puede ver exactamente de dónde proviene el tamaño. Si/cuando Microsoft cambia de una imagen base de Debian a una de Alpine, esta debería reducirse aún más.
C:\Users\scott\Desktop\k8s for pi\aspnetcoreapp>docker history c60
IMAGE CREATED CREATED BY SIZE COMMENT
c6094ca46c3b 3 minutes ago /bin/sh -c #(nop) CMD ["dotnet" "./aspnet... 0B
b7dfcf137587 3 minutes ago /bin/sh -c #(nop) EXPOSE 5000/tcp 0B
a5ba51b91d9d 3 minutes ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=htt... 0B
8742269735bc 3 minutes ago /bin/sh -c #(nop) COPY dir:cc64bd3b9bacaeb... 56.5MB
28c008e38973 3 minutes ago /bin/sh -c #(nop) WORKDIR /root/ 0B
4bafd6e2811a 4 hours ago /bin/sh -c apt-get update && apt-get i... 45.4MB
<missing> 3 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:8b7cf813a113aa2... 85.7MB
Aquí está la evolución de mi Dockerfile a medida que realicé cambios y el resultado final se hizo cada vez más pequeño. Parece 45 megas recortados con un poco de trabajo o un 20 % menos.
C:\Users\scott\Desktop\k8s for pi\aspnetcoreapp>docker images | find /i "aspnetcoreapp"
shanselman/aspnetcoreapp 0.5 c6094ca46c3b About a minute ago 188MB
shanselman/aspnetcoreapp 0.4 083bfbdc4e01 12 minutes ago 196MB
shanselman/aspnetcoreapp 0.3 fa053b4ee2b4 About an hour ago 199MB
shanselman/aspnetcoreapp 0.2 ba73f14e29aa 4 hours ago 207MB
shanselman/aspnetcoreapp 0.1 cac2f0e3826c 3 hours ago 233MB
Más adelante haré una publicación de blog en la que coloco esta aplicación web ASP.NET Core estándar en Kubernetes usando esta descripción YAML y la escalaré en Raspberry Pi. ¡Estoy aprendiendo mucho! ¡Gracias a Alex Ellis, Glenn Condron y Jessie Frazelle por su tiempo!
Patrocinador: Cree potentes aplicaciones web para administrar cada paso del ciclo de vida de un documento con DocuVieware HTML5 Viewer y Document Management Kit. ¡Consulte nuestras demostraciones para adquirir, escanear, editar, anotar más de 100 formatos y personalizar su interfaz de usuario!