miércoles, 25 de abril de 2018

Kubernetes [Best Practices] Crear imágenes de Docker "pequeñas"


Sandeep Dinesh nos ha anunciado que esta realizando una serie de videos y blogs en siete partes de Google  sobre cómo aprovechar al máximo su entorno de Kubernetes.



El primero aborda la teoría y los aspectos prácticos de mantener las imágenes de sus contenedores lo más pequeñas posible,  coincidimos con el. Docker hace que construir contenedores sea muy fácil.

Simplemente ponga un archivo Docker estándar en su carpeta, ejecute el comando 'build' de la línea de comandos y ¡Buala! ¡Tu imagen de docker está construida!.

La desventaja de esta simplicidad es que es fácil construir enormes contenedores llenos de cosas que no necesitas, incluidos posibles agujeros de seguridad.

Vamos a seguir  las  "Buenas prácticas de Kubernetes", explicaremos  cómo crear imágenes de contenedores listas para producción utilizando Alpine Linux y el patrón de generador Docker, y luego ejecutamos algunos puntos de referencia que pueden determinar cómo funcionan estos contenedores dentro de su clúster de Kubernetes.

El proceso para crear imágenes de contenedores es diferente dependiendo de si está utilizando un lenguaje interpretado o un lenguaje compilado. ¡Vamos a empezar!

Containerizing interpreted languages

Los lenguajes interpretados, como Ruby, Python, Node.js, PHP y otros envían el código fuente a través de un intérprete que ejecuta el código. Esto le da la ventaja de omitir el paso de compilación, pero tiene el inconveniente de que debe enviar el intérprete junto con el código. Afortunadamente, la mayoría de estos idiomas ofrecen contenedores Docker preconstruidos que incluyen un entorno ligero que le permite utilizar contenedores mucho más pequeños. Tomemos una aplicación Node.js y dockerizemosla.



Primero, utilicemos la imagen Docker "node: onbuild" como base. La versión "onbuild" de un contenedor Docker pre-paquetes todo lo que necesita para ejecutar, por lo que no necesita realizar una gran cantidad de configuración para que las cosas funcionen. Esto significa que Dockerfile es muy simple (¡solo dos líneas!). Pero sufriras con  el precio en términos de tamaño del disco:

FROM node: onbuild
EXPOSE 8080


Al utilizar una imagen base más pequeña como Alpine, podemos reducir significativamente el tamaño de su contenedor. Alpine Linux es una distribución de Linux pequeña y liviana que es muy popular entre los usuarios de Docker porque es compatible con muchas aplicaciones y al mismo tiempo mantiene los contenedores pequeños.

Afortunadamente, hay una imagen oficial de Alpine para Node.js (así como otros lenguajes populares) que tiene todo lo que necesita. A diferencia de la imagen predeterminada de Docker "nodo", "node: alpine" elimina muchos archivos y programas, dejando solo lo suficiente para ejecutar su aplicación. El Dockerfile basado en Alpine Linux es un poco más complicado de crear ya que tiene que ejecutar algunos comandos que la imagen de construcción.


FROM node:alpine
WORKDIR /app
COPY package.json /app/package.json
RUN npm install --production
COPY server.js /app/server.js
EXPOSE 8080
CMD npm start


Pero, vale la pena, porque la imagen resultante es mucho más pequeña con solo 65 MB

Containerizing compiled languages

Los lenguajes compilados como Go, C, C ++, Rust, Haskell y otros crean binarios que pueden ejecutarse sin muchas dependencias externas. Esto significa que puede construir el binario por adelantado y enviarlo a producción sin tener que enviar las herramientas para crear el binario, como el compilador.

Con el soporte de Docker para construcciones de varios pasos, puede crear fácilmente solo el binario y una cantidad mínima de recursos. Vamos a ver cómo.

Tomemos una aplicación Go y conterizemosla usando este patrón. Primero, utilicemos la imagen Docker "golang: onbuild" como base. Como antes, el Dockerfile es solo dos líneas, pero una vez más, pagas el precio en términos de tamaño del disco: ¡más de 700 MB!

  
FROM goland:onbuild
EXPOSE 8080



El siguiente paso es utilizar una imagen base más pequeña posible, en este caso, la imagen "golang: alpine". Hasta ahora, este es el mismo proceso que seguimos para un lenguaje interpretado.

De nuevo, crear Dockerfile con una imagen base Alpine es un poco más complicado ya que tiene que ejecutar algunos comandos que debe realizar par construir la imagen.


FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp


La imagen resultante es mucho más pequeña, ¡con un peso de solo 256 MB!

Sin embargo, podemos hacer que la imagen sea aún más pequeña: no necesita ninguno de los compiladores u otras herramientas de compilación y depuración con las que viene Go, que pueden ser eliminarlas del contenedor final.

Usemos una compilación de varios pasos para tomar el binario creado por el contenedor golang: alpine y empaquetarlo por sí mismo.


FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp

FROM alpine
RUN apk update && \
   apk add ca-certificates && \
   update-ca-certificates && \
   rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build-env /app/goapp /app
EXPOSE 8080
ENTRYPOINT ./goapp

¡Mira eso! ¡Este contenedor tiene solo 12 MB de tamaño!

Al compilar este contenedor, es posible que observe que Dockerfile hace cosas extrañas, como la instalación manual de certificados HTTPS en el contenedor. Esto se debe a que la base Alpine Linux se envía con casi nada preinstalado. ¡Así que, aunque necesita instalar manualmente todas y cada una de las dependencias, el resultado final es contenedores súper pequeños!


Nota: Si desea ahorrar aún más espacio, puede compilar estadísticamente su aplicación y usar el contenedor "cero". Usar "scratch" como contenedor base significa que estás literalmente comenzando desde cero sin ninguna capa base. Sin embargo, recomiendo usar Alpine como su imagen base en lugar de "scratch" porque los pocos MB adicionales en la imagen Alpine hacen que sea mucho más fácil usar herramientas estándar e instalar dependencias.



Pulling en  Kubernetes

Si bien es posible que no le importe el tiempo que lleva construir y empujar el contenedor, realmente debería preocuparse por el tiempo que lleva hacer el PULL el contenedor. Cuando se trata de Kubernetes, esta es probablemente la métrica más importante para su clúster de producción. 

Por ejemplo, supongamos que tiene un clúster de tres nodos y uno de los nodos esta roto. Si está utilizando un sistema administrado como Kubernetes Engine, el sistema automáticamente activa un nuevo nodo para tomar su lugar. Sin embargo, este nuevo nodo será completamente nuevo y deberá extraer todos sus contenedores antes de que pueda comenzar a funcionar. ¡Cuanto más tiempo lleve hacer pull de los contenedores, más tiempo su clúster no funcionará tan bien como debería! 

Esto puede ocurrir cuando aumenta el tamaño de su clúster (por ejemplo, usando el ajuste automático del motor de Kubernetes) o actualiza sus nodos a una nueva versión de Kubernetes (estad atentos para un episodio futuro sobre esto). Podemos ver que el rendimiento de extracción de múltiples contenedores de múltiples implementaciones realmente puede sumar aquí, y el uso de contenedores pequeños puede reducir los minutos de los tiempos de implementación.



Seguridad y vulnerabilidades

Además del rendimiento, existen importantes ventajas de seguridad al usar contenedores más pequeños. Los contenedores pequeños generalmente tienen una superficie de ataque más pequeña en comparación con los contenedores que usan imágenes de base grandes. 

Construí los contenedores Go "onbuild" y "multi-etapa" hace unos meses, por lo que probablemente contengan algunas vulnerabilidades que se han descubierto desde entonces. Usando el Escaneo de Vulnerabilidades integrado de Container Registry, es fácil escanear sus contenedores en busca de vulnerabilidades conocidas. Veamos lo que encontramos



¡Guau, esa es una gran diferencia entre los dos! Solo tres vulnerabilidades "medianas" en el contenedor más pequeño, en comparación con 16 vulnerabilidades críticas y más de 300 en el contenedor más grande. 
Vamos a profundizar y ver qué problemas tiene el contenedor más grande.
¡Puedes ver que la mayoría de los problemas no tienen nada que ver con nuestra aplicación, sino con programas que ni siquiera estamos usando! Debido a que la imagen de varias etapas usa una imagen base mucho más pequeña, hay menos cosas que pueden verse comprometidas.