Docker: consejos no dañinos

En los comentarios de mi artículo Docker: consejos perjudiciales hubo muchas solicitudes para explicar por qué es tan malo el Dockerfile descrito en él.

Resumen del episodio anterior: dos desarrolladores bajo una estricta fecha límite están creando un Dockerfile. En el proceso, entra al cuarto Ops Igor Ivanovich. El Dockerfile final es tan malo que el IA está al borde de un infarto.

Docker: consejos no dañinos

Ahora vamos a averiguar qué está mal con este Dockerfile.

Así que ha pasado una semana.

El Dev Petya se encuentra en la cafetería con Ops Igor Ivanovich para tomar un café.

P: Igor Ivanovich, ¿está muy ocupado? Me gustaría aclarar dónde nos equivocamos.

II: Eso es bueno, no se encuentra a menudo con desarrolladores que se interesen en la operación.
Primero acordemos algunas cosas:

  1. La ideología de Docker: un contenedor — un proceso.
  2. Cuanto más pequeño sea el contenedor, mejor.
  3. Cuanto más se saque de la caché, mejor.

P: ¿Y por qué debería haber un proceso en un solo contenedor?

II: Docker, al iniciar un contenedor, supervisa el estado del proceso con pid 1. Si el proceso muere, Docker intenta reiniciar el contenedor. Supongamos que tienes varias aplicaciones en el contenedor o que la aplicación principal no se ejecuta con pid 1. Si el proceso muere, Docker no se enterará.

Si no hay más preguntas, muestra tu Dockerfile.

Y Petya mostró:

FROM ubuntu:latest

# Copiamos el código fuente
COPY .\/ \/app
WORKDIR \/app

# Actualizamos la lista de paquetes
RUN apt-get update 

# Actualizamos los paquetes
RUN apt-get upgrade

# Instalamos los paquetes necesarios
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Instalamos bundler
RUN gem install bundler

# Instalamos nodejs que se utiliza para construir estática
RUN curl -sL https:\/\/deb.nodesource.com\/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Instalamos dependencias
RUN bundle install --without development test --path vendor\/bundle

# Limpiamos los caches
RUN rm -rf \/usr\/local\/bundle\/cache\/*.gem 
RUN apt-get clean 
RUN rm -rf \/var\/lib\/apt\/lists\/* \/tmp\/* \/var\/tmp\/* 
RUN rake assets:precompile
# Ejecutamos el script al iniciar el contenedor que lanzará todo lo demás.
CMD ["\/app\/init.sh"]

II: Oh, vamos a resolverlo por partes. Comencemos con la primera línea:

FROM ubuntu:latest

Tomas el tag latest. Usar el tag latest conduce a consecuencias impredecibles. Imagina que el mantenedor de la imagen compila una nueva versión de la imagen con una lista de software diferente, esta imagen recibe el tag latest. Y tu contenedor en el mejor de los casos deja de compilar, y en el peor, te encuentras con errores que antes no existían.

Estás tomando una imagen con un sistema operativo completo y una gran cantidad de software innecesario, lo que aumenta el tamaño del contenedor. Cuanto más software haya, más agujeros y vulnerabilidades hay.

Además, cuanto más grande sea la imagen, más espacio ocupará en el host y en el registro (¿Dónde guardas las imágenes?).

P: Sí, claro, tenemos un registro, tú mismo lo configuraste.

IA: A ver, ¿de qué estaba hablando?... Ah, sí, sobre los tamaños... La carga sobre la red también aumenta. Para una sola imagen, no es notable, pero cuando hay una construcción continua, pruebas y despliegue, se siente. Y si no tienes el modo Dios en AWS, también recibirás una factura astronómica.

Por eso hay que elegir la imagen más adecuada, con la versión exacta y el mínimo de software. Por ejemplo, toma: FROM ruby:2.5.5-stretch

P: Oh, entiendo. ¿Cómo y dónde puedo ver las imágenes disponibles? ¿Cómo sé cuál necesito?

IA: Normalmente, las imágenes se obtienen de docker hub, no lo confundas con Pornhub :). Por lo general, hay varias construcciones para una imagen:
Alpine: las imágenes se construyen sobre una imagen minimalista de Linux, con solo 5 MB. Su inconveniente: se construye con una implementación propia de libc, y los paquetes estándar no funcionan. Buscar e instalar el paquete necesario llevará un buen tiempo.
Scratch: imagen base, no se utiliza para construir otras imágenes. Se destina exclusivamente a ejecutar binarios, datos preparados. Ideal para ejecutar aplicaciones binarias que incluyen todo lo necesario, como aplicaciones en Go.
Sobre la base de algún sistema operativo, como Ubuntu o Debian. Aquí, creo que no necesito explicar más.

IA: Ahora necesitamos instalar todos los paquetes adicionales y limpiar las cachés. Y se puede eliminar de inmediato apt-get upgrade. De lo contrario, en cada construcción, a pesar de la etiqueta fija de la imagen base, se obtendrán diferentes imágenes. La actualización de paquetes en la imagen es una tarea del mantenedor, y viene acompañada de cambios en la etiqueta.

P: Sí, intenté hacerlo, así me quedó:

WORKDIR /app
COPY ./ /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

IA: No está mal, pero aquí también hay cosas en las que trabajar. Mira, este comando:

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

… no elimina los datos de la imagen final, solo crea una capa adicional sin esos datos. Así es correcto:

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Pero eso no es todo. ¿Tienes Ruby allí? Entonces no es necesario copiar todo el proyecto al principio. Basta con copiar Gemfile y Gemfile.lock.

Con este enfoque, bundle install no se ejecutará en cada cambio de los archivos fuente, solo si cambió Gemfile o Gemfile.lock.

Los mismos métodos funcionan para otros lenguajes con gestores de dependencias, como npm, pip, composer y otros que se basan en un archivo de lista de dependencias.

Y, por último, ¿recuerdas que al principio hablé sobre la ideología de Docker de "un contenedor — un proceso"? Esto significa que no se necesita un supervisor. Tampoco es necesario instalar systemd, por las mismas razones. En esencia, Docker en sí mismo es el supervisor. Y cuando intentas ejecutar múltiples procesos en él, es como ejecutar varias aplicaciones en un solo proceso de supervisor.
Al construir, crearás una imagen única y luego iniciarás la cantidad necesaria de contenedores, para que cada uno ejecute un proceso.

Pero de eso hablaremos más tarde.

P: Parece que lo entiendo. Mira lo que resulta:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

¿Y cómo redefiniremos el lanzamiento de demonios al iniciar el contenedor?

IA: Sí, eso es correcto. Por cierto, se puede usar tanto CMD como ENTRYPOINT. Y averiguar cuál es la diferencia es tu tarea para casa. Hay un buen artículo sobre esto en Habr. Windows.

Entonces, sigamos. Estás descargando el archivo para instalar node, pero no hay ninguna garantía de que contenga lo que necesitas. Necesitamos agregar validación. Por ejemplo, así:

EJECUTAR curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - && bash setup_9.x && rm -rf setup_9.x && apt-get -y install libpq-dev imagemagick gsfonts nodejs && gem install bundler && bundle install --without development test --path vendor/bundle && rm -rf /usr/local/bundle/cache/*.gem && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Con la suma de verificación podrás comprobar que descargaste el archivo correcto.

P: Pero si el archivo cambia, la compilación no pasará.

IA: Sí, y curiosamente, eso también es una ventaja. Sabrás que el archivo ha cambiado y podrás ver qué se modificó. Puede ser que hayan añadido, digamos, un script que elimina todo lo que alcanza o crea un backdoor.

P: Gracias. Entonces, el Dockerfile final se verá así:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - && bash setup_9.x && rm -rf setup_9.x && apt-get -y install libpq-dev imagemagick gsfonts nodejs && gem install bundler && bundle install --without development test --path vendor/bundle && rm -rf /usr/local/bundle/cache/*.gem && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

P: Igor Ivanovich, gracias por tu ayuda. Ya tengo que irme, me quedan 10 commits por hacer hoy.

Igor Ivanovich, deteniendo con la mirada a su apresurado colega, toma un sorbo de café fuerte. Tras reflexionar unos segundos sobre el SLA del 99.9% y el código sin errores, plantea una pregunta.

IA: ¿Dónde guardan los logs?

P: Claro, en production.log. By the way, ¿cómo accedemos a ellos sin ssh?

IA: Si los dejan en archivos, ya hay una solución para ustedes. El comando docker exec permite ejecutar cualquier comando en el contenedor. Por ejemplo, pueden hacer cat para los logs. Y al usar la opción -it y lanzar bash (si está instalado en el contenedor), tendrán acceso interactivo al contenedor.

Pero no deben almacenar los logs en archivos. Al menos, esto lleva a un crecimiento descontrolado del contenedor, ya que los logs no se rotan. Todos los logs deben enviarse a stdout. Allí podrán revisarlos usando el comando docker logs.

P: Igor Ivanovich, ¿y si trasladamos los logs a un directorio montado, en la nodo física, como los datos de los usuarios?

IA: Es bueno que no se olviden de mover los datos cargados en el disco de la nodo. También se puede hacer con los logs, solo no olviden configurar la rotación.
Listo, puedes irte.

P: Igor Ivanovich, ¿me recomendarías algo para leer?

II: Para comenzar, lee las recomendaciones de los desarrolladores de Docker, dudo que alguien sepa Docker mejor que ellos.

Y si quieres hacer una práctica, asiste a un intensivo. La teoría sin práctica está muerta.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster