Recomendaciones para trabajar con contenedores

En este artículo se describe un conjunto de recomendaciones que facilitan la operación de los contenedores. Estas recomendaciones abordan una amplia gama de temas, desde la seguridad hasta la supervisión y el registro. Su objetivo es facilitar la ejecución de las aplicaciones en Google Kubernetes Engine y en los contenedores en general. Varias de las recomendaciones que se analizan en esta sección se inspiraron en la Metodología de doce factores, que es un excelente recurso para compilar aplicaciones nativas de la nube.

No todas estas recomendaciones son igual de importantes. Por ejemplo, podrás ejecutar una carga de trabajo de producción de manera correcta, aunque no sigas algunas de ellas, pero otras son indispensables. En particular, la importancia de las recomendaciones sobre seguridad es subjetiva. Su puesta en práctica dependerá de tu entorno y tus limitaciones.

Para sacarle el máximo provecho a este artículo, necesitas tener algo de conocimiento sobre Docker y Kubernetes. Algunas recomendaciones que se tratan aquí también se aplican a los contenedores de Windows, pero la mayoría supone que trabajas con contenedores de Linux. A fin de recibir asesoramiento sobre cómo compilar contenedores, consulta Recomendaciones para compilar contenedores.

Cómo usar los mecanismos de registro nativos de los contenedores.

Importancia: ALTA

Los registros contienen información valiosa sobre los eventos que suceden en la aplicación como parte integral de la administración de esta. Docker y Kubernetes se esfuerzan por facilitar la administración de registros.

Es probable que en un servidor clásico necesites escribir los registros en un archivo específico y gestionar la rotación del registro para evitar que se llenen los discos. Si tienes un sistema de registro avanzado, puedes reenviar los registros a un servidor remoto para centralizarlos.

Los contenedores ofrecen una manera sencilla y estandarizada de gestionar los registros debido a que puedes escribirlos en stdout y stderr. Docker captura estas líneas de registro y te permite acceder a ellas mediante el comando docker logs. Como desarrollador de aplicaciones, no necesitas implementar mecanismos de registro avanzados. En lugar de esto, usa los mecanismos de registro nativos.

El operador de la plataforma debe proporcionar un sistema para centralizar los registros y hacer que se puedan buscar. En GKE, fluentd y Stackdriver Logging proporcionan este servicio. En otros métodos comunes, se incluye el uso de una pila EFK (Elasticsearch, Fluentd, Kibana).

Diagrama de un sistema de administración de registros clásico en Kubernetes.
Figura 1. Diagrama de un sistema de administración de registros típico en Kubernetes

Registros JSON

La mayoría de los sistemas de administración de registros son bases de datos de serie temporal que almacenan documentos indexados en el tiempo. Por lo general, esos documentos se pueden proporcionar en formato JSON. En Stackdriver Logging y EFK, una línea de registro única se almacena como documento, junto con algunos metadatos (información sobre pod, contenedor, nodo, etcétera).

Puedes aprovechar ese comportamiento mediante el registro directo en formato JSON con diferentes campos. Luego, puedes buscar los registros de manera más eficiente con base en esos campos.

Por ejemplo, considera transformar el registro a continuación en formato JSON:

[2018-01-01 01:01:01] foo - WARNING - foo.bar - There is something wrong.

Este es el registro transformado:

{
  "date": "2018-01-01 01:01:01",
  "component": "foo",
  "subcomponent": "foo.bar",
  "level": "WARNING",
  "message": "There is something wrong."
}

Esta transformación te permite buscar fácilmente en tus registros todos los registros de nivel WARNING o todos los registros del subcomponente foo.bar.

Si decides escribir registros con formato JSON, ten en cuenta que debes escribir cada evento en una línea para que se analice correctamente. Se verá como se muestra a continuación:

{"date":"2018-01-01 01:01:01","component":"foo","subcomponent":"foo.bar","level": "WARNING","message": "There is something wrong."}

Como podemos ver, el resultado es mucho menos legible que la línea normal de un registro. Si decides usar este método, asegúrate de que tus equipos no confíen demasiado en la inspección manual de registros.

Patrón de archivo adicional del agregador de registros

Algunas aplicaciones (como Tomcat) no se pueden configurar con facilidad para escribir registros en stdout y stderr. Debido a que tales aplicaciones se escriben en diferentes archivos de registro en el disco, la forma más adecuada de gestionarlas en Kubernetes es usar el patrón de archivo adicional para el registro. Un archivo adicional es un contenedor pequeño que se ejecuta en el mismo pod que la aplicación. Para ver una descripción más detallada de los archivos adicionales, consulta la Documentación oficial de Kubernetes.

En esta solución, agregarás un agente de registro en el contenedor de un archivo adicional a la aplicación (en el mismo pod) y compartirás un volumen emptyDir entre los dos contenedores, como se muestra en este ejemplo de YAML en GitHub. Luego, configurarás la aplicación para escribir sus registros en el volumen compartido y configurarás el agente de registro a fin de leerlos y reenviarlos cuando sea necesario.

En este patrón, deberás tratar con la rotación del registro debido a que no usas los mecanismos de registro nativos de Docker y Kubernetes. Si el agente de registro no gestiona la rotación del registro, otro contenedor de archivo adicional en el mismo pod puede gestionarla.

Patrón de archivo adicional para la administración de registros.
Figura 2. Patrón de archivo adicional para la administración de registros

Cómo asegurarse de que tus contenedores se encuentren inmutables y sin estado

Importancia: ALTA

Si pruebas los contenedores por primera vez, no los trates como servidores tradicionales. Por ejemplo, es posible que te sientas tentado de actualizar la aplicación dentro de un contenedor en ejecución o de aplicar un parche a un contenedor en ejecución cuando surjan vulnerabilidades.

Los contenedores no están diseñados para funcionar de esta manera. Están diseñados para permanecer inmutables y sin estado.

Sin estado

Sin estado significa que cualquier estado (datos persistentes de cualquier tipo) se almacena fuera de un contenedor. Este almacenamiento externo puede usar varios formularios según lo que necesites:

  • Para almacenar archivos, recomendamos el uso de un depósito de objetos como Cloud Storage.
  • Para almacenar información como las sesiones de usuario, recomendamos el uso de un almacén externo, de latencia baja y de clave-valor, como Redis o Memcached.
  • Si necesitas almacenamiento de nivel de bloqueo (para bases de datos, por ejemplo), puedes usar un disco externo vinculado al contenedor. En el caso de GKE, recomendamos el uso de Discos persistentes.

Mediante el uso de estas opciones, puedes quitar los datos del contenedor, lo que significa que puede cerrarse y destruirse perfectamente en cualquier momento sin temor a la pérdida de datos. Si creas un contenedor nuevo para reemplazar el anterior, solo tienes que conectar el contenedor nuevo al mismo almacén de datos o vincularlo al mismo disco.

Inmutabilidad

Inmutable significa que un contenedor no se modificará durante su vida útil: Sin actualizaciones, sin parches y sin cambios de configuración. Si debes actualizar el código de la aplicación o aplicar un parche, compila una imagen nueva y vuelve a implementarla. La inmutabilidad hace que las implementaciones sean más seguras y que se puedan repetir más. Si necesitas hacer una reversión, simplemente vuelve a implementar la imagen anterior. Este enfoque te permite implementar la misma imagen de contenedor en cada uno de los entornos, lo que los hace más idénticos.

Para usar la misma imagen de contenedor en diferentes entornos, recomendamos que externalices la configuración del contenedor (puerto de escucha, opciones del entorno de ejecución, etcétera). Los contenedores suelen configurase con variables de entorno o archivos de configuración activados en una ruta específica. En Kubernetes, puedes usar Secrets y ConfigMaps para incorporar configuraciones en contenedores como variables de entorno o archivos.

Si necesitas actualizar una configuración, implementa un contenedor nuevo (basado en la misma imagen) con la configuración actualizada.

Ejemplo de cómo actualizar la configuración de una implementación con un ConfigMap activado como archivo de configuración en los pods.
Figura 3. Ejemplo de cómo actualizar la configuración de una implementación con un ConfigMap activado como archivo de configuración en los pods

La combinación de inmutabilidad y sin estado es una de las claves de venta de las infraestructuras basadas en contenedores. Esta combinación te permite automatizar las implementaciones y aumentar su frecuencia y fiabilidad.

Cómo evitar los contenedores privilegiados

Importancia: ALTA

En una máquina virtual o servidor sin sistema operativo, evita ejecutar las aplicaciones como usuario con permisos de administrador por una razón sencilla: Si la aplicación está vulnerable, un atacante tendría acceso total al servidor. Por el mismo motivo, evita usar contenedores privilegiados. Un contenedor privilegiado es un contenedor que tiene acceso a todos los dispositivos de la máquina host y que pasa por alto casi todas las funciones de seguridad de los contenedores.

Si crees que necesitas usar contenedores privilegiados, considera las siguientes alternativas:

  • Asigna funciones específicas al contenedor mediante la Opción securityContext de Kubernetes o el marcador de Docker--cap-add. La Documentación de Docker enumera las funciones habilitadas según la configuración predeterminada y las que debes habilitar de manera explícita.
  • Modifica la configuración del host en un archivo adicional o en un Contenedor init si la aplicación debe modificarla para poder ejecutarse. A diferencia de la aplicación, esos contenedores no necesitan estar expuestos al tráfico interno o externo, lo que los hace más aislados.
  • Si necesitas modificar sysctls en Kubernetes, usa la Anotación específica.

Una Política de seguridad de pods específica en Kubernetes establece que los contenedores privilegiados pueden estar prohibidos. Una política de seguridad de pods es un objeto de Kubernetes que el administrador del clúster configura y administra, y que hace cumplir los requisitos específicos para los pods. En el clúster de Kubernetes, no puedes crear pods que violen esos requisitos.

Cómo facilitar la supervisión de la aplicación

Importancia: ALTA

Del mismo modo que el registro, la supervisión es una parte integral de la administración de la aplicación. En muchos sentidos, la supervisión de aplicaciones en contenedores sigue los mismos principios que se aplican a la supervisión de aplicaciones que no están en contenedores. Sin embargo, debido a que las infraestructuras en contenedores suelen ser altamente dinámicas con contenedores que se crean o se borran con frecuencia, no puedes darte el lujo de configurar de nuevo el sistema de supervisión cada vez que esto suceda.

Puedes distinguir dos clases principales de supervisión: La supervisión de caja negra y la supervisión de caja blanca. La supervisión de caja negra hace referencia al análisis de la aplicación desde el exterior, como si fueras un usuario final. Es útil si el servicio final que deseas ofrecer está disponible y en funcionamiento. No difiere entre las infraestructuras tradicionales y las que están en contenedores debido a que es externa a la infraestructura.

La supervisión de caja blanca hace referencia al análisis de la aplicación con algún tipo de acceso privilegiado y a la recopilación de métricas sobre su comportamiento que un usuario final no puede ver. Difiere de manera significativa para las infraestructuras tradicionales y las que están en contenedores debido a que debe analizar las capas más profundas de la infraestructura.

Una opción popular en la comunidad de Kubernetes para la supervisión de caja blanca es Prometheus, un sistema que descubre automáticamente los pods que debe supervisar. Prometheus extrae los pods en busca de métricas; espera un formato específico para ellas. Stackdriver puede supervisar los clústeres de Kubernetes y las aplicaciones que ejecutan con su propia versión de Prometheus. Obtén información sobre cómo habilitar Stackdriver Kubernetes Monitoring en GKE.

A continuación, se presenta una demostración de Stackdriver Kubernetes Monitoring en acción (haz clic en la imagen para ver una versión más grande):

Panel desde Stackdriver Kubernetes Monitoring.
Figura 4. Panel desde Stackdriver Kubernetes Monitoring

Para beneficiarse de Prometheus o Stackdriver Kubernetes Monitoring, las aplicaciones deben exponer las métricas en el formato Prometheus. Los dos métodos a continuación muestran cómo hacerlo.

Extremo HTTP de métricas

El extremo HTTP de métricas funciona de manera similar a los extremos que se mencionan más adelante en Expón el estado de la aplicación. Presenta las métricas internas de la aplicación, por lo general, en un URI /metrics. Una respuesta se ve como se muestra a continuación:

http_requests_total{method="post",code="200"} 1027
http_requests_total{method="post",code="400"}    3
http_requests_total{method="get",code="200"} 10892
http_requests_total{method="get",code="400"}    97

En este ejemplo, http_requests_total es la métrica, method y code son etiquetas y el número que se encuentra más hacia la derecha es el valor de esta métrica para esas etiquetas. En este punto, la aplicación ha respondido a una petición HTTP GET, desde su inicio, 97 veces con un código de error 400.

La generación de este extremo HTTP se facilita mediante las Bibliotecas cliente de Prometheus que existen para varios lenguajes. OpenCensus también puede exportar métricas con este formato (entre otras funciones). No expongas este extremo a la Internet pública.

En la Documentación de Prometheus oficial, se profundiza en este tema. También puedes leer el Capítulo 6 de Ingeniería de fiabilidad de sitios para obtener información sobre la supervisión de caja blanca (y caja negra).

Patrón de archivo adicional para la supervisión

No todas las aplicaciones pueden instrumentarse con un extremo HTTP /metrics. Recomendamos el uso del patrón de archivo adicional para exportar las métricas en el formato correcto a fin de conservar la supervisión estandarizada.

En la sección Patrón de archivo adicional del agregador de registros se explica cómo usar un contenedor de archivo adicional para administrar los registros de aplicaciones. Puedes usar el mismo patrón para la supervisión: El contenedor de archivo adicional aloja un agente de supervisión que traduce las métricas a medida que la aplicación las expone en un formato y protocolo que el sistema de supervisión global entiende.

Considera un ejemplo concreto: Las aplicaciones de Java y Java Management Extensions (JMX). Varias aplicaciones de Java exponen métricas con JMX. En lugar de escribir de nuevo una aplicación para exponer métricas en el formato Prometheus, puedes aprovechar el jmx_exporter. El jmx_exporter recopila métricas desde una aplicación a través de JMX y las expone mediante un extremo /metrics que Prometheus puede leer. Este enfoque también tiene la ventaja de limitar la exposición del extremo JMX, que puede usarse para modificar la configuración de la aplicación.

Patrón de archivo adicional para la supervisión.
Figura 5. Patrón de archivo adicional para la supervisión

Cómo exponer el estado de la aplicación

Importancia: MEDIA

Para facilitar la administración en la producción de una aplicación, esta debe comunicar su estado al sistema general: ¿Se ejecuta la aplicación? ¿Está en buen estado? ¿Está lista para recibir tráfico? ¿Cómo funciona?

Kubernetes cuenta con dos tipos de verificación de estado: Prueba de capacidad de respuesta y prueba de disponibilidad. Cada uno tiene un uso específico, como se describe en esta sección. Puedes implementar ambos de varias maneras (incluso ejecutar un comando dentro del contenedor o verificar un puerto TCP), pero el método preferente es el uso de los extremos HTTP descritos en estas recomendaciones. Para obtener información sobre este tema, consulta la Documentación de Kubernetes.

Sondeo de capacidad de respuesta

La forma recomendada de implementar la Prueba de capacidad de respuesta es para que la aplicación exponga un extremo HTTP /health. Luego de recibir una solicitud en este extremo, la aplicación debe enviar una respuesta "200 OK" si se considera que está en buen estado. En Kubernetes, buen estado significa que el contenedor no tiene que cerrarse o reiniciarse. Lo que constituye el buen estado varía de una aplicación a otra, pero, por lo general, significa lo siguiente:

  • La aplicación se ejecuta
  • Se cumplen sus dependencias principales (por ejemplo, puede acceder a su base de datos)

Prueba de disponibilidad

La forma recomendada de implementar la prueba de disponibilidad es para que la aplicación exponga un extremo HTTP /ready. Cuando recibe una solicitud en este extremo, la aplicación debe enviar una respuesta "200 OK" si está lista para recibir tráfico. Lista para recibir tráfico significa lo siguiente:

  • La aplicación está en buen estado
  • Se completan los pasos de inicialización posibles
  • Cualquier solicitud válida que se envía a la aplicación no genera un error

Kubernetes usa la prueba de disponibilidad para organizar la implementación de la aplicación. Si actualizas una Implementación, Kubernetes hará una actualización progresiva de los pods que pertenezcan a esa implementación. La política de actualización predeterminada establece actualizar un pod a la vez: Kubernetes espera a que el nuevo pod esté listo (como lo indica la prueba de disponibilidad) antes de actualizar el siguiente.

Cómo evitar la ejecución con permisos de administrador

Importancia: MEDIA

Los contenedores proporcionan aislamiento: Con la configuración predeterminada, un proceso dentro de un contenedor Docker no puede acceder a la información desde la máquina host o desde los demás contenedores implantados. Sin embargo, el aislamiento no es tan completo como lo es con las máquinas virtuales debido a que los contenedores comparten el kernel de la máquina host, como se explica en esta entrada de blog. Un atacante podría encontrar vulnerabilidades aún desconocidas (ya sea en Docker o en el kernel de Linux), que le permitirían escapar de un contenedor. Si el proceso se ejecuta con permisos de administrador dentro del contenedor y el atacante encuentra una vulnerabilidad, este obtendrá acceso con permisos de administrador a la máquina host.

Izquierda: Las máquinas virtuales usan hardware virtualizado.Derecha: Las aplicaciones en contenedores usan el kernel del host.
Figura 6. A la izquierda, las máquinas virtuales usan hardware virtualizado. A la derecha, las aplicaciones en contenedores usan el kernel del host.

Para evitar esta posibilidad, se recomienda no ejecutar procesos con permisos de administrador dentro de los contenedores. Puedes aplicar este comportamiento en Kubernetes mediante una PodSecurityPolicy. Cuando crees un pod en Kubernetes, usa la opción runAsUser para especificar el usuario de Linux que ejecuta el proceso. Este enfoque anula la instrucción USER del Dockerfile.

En realidad, hay desafíos. El proceso principal de varios paquetes de software conocidos se ejecuta con permisos de administrador. Si deseas evitar la ejecución con permisos de administrador, diseña el contenedor de modo que pueda ejecutarse con un usuario desconocido y sin privilegios. Esta recomendación a menudo significa que tienes que modificar los permisos en varias carpetas. En un contenedor, si sigues la recomendación de Una aplicación por contenedor y ejecutas una aplicación con un usuario, preferentemente sin permisos de administrador, puedes otorgar a todos los usuarios permisos de escritura en las carpetas y archivos que deban escribirse y hacer que los demás archivos y carpetas solo se escriban con permiso de administrador.

Una forma sencilla de verificar si el contenedor cumple con esta recomendación es ejecutarlo de manera local con un usuario aleatorio y probar si funciona correctamente. Reemplaza [YOUR_CONTAINER] con el nombre del contenedor.

docker run --user $((RANDOM+1)) [YOUR_CONTAINER]

Si el contenedor necesita un volumen externo, puedes configurar la Opción fsGroup Kubernetes para otorgar propiedad de este volumen a un grupo específico de Linux. Esta configuración resuelve el problema de la propiedad de archivos externos.

Si el proceso lo ejecuta un usuario sin privilegios, no podrá vincularse a puertos por debajo de 1024. Por lo general, esto no es un problema, ya que puedes configurar los servicios de Kubernetes para enrutar el tráfico de un puerto a otro. Por ejemplo, puedes configurar un servidor HTTP para que se vincule al puerto 8080 y redireccione el tráfico desde el puerto 80 con un servicio de Kubernetes.

Cómo elegir cuidadosamente la versión de imagen

Importancia: MEDIA

Cuando uses una imagen de Docker, ya sea como una imagen base en un Dockerfile o como una imagen implementada en Kubernetes, debes elegir la etiqueta de la imagen que usas.

La mayoría de las imágenes públicas y privadas siguen un sistema de etiquetado similar al que se describe en Recomendaciones para compilar contenedores. Si la imagen usa un sistema similar al Control de versiones semántico, debes tener en cuenta algunos datos específicos del etiquetado.

Lo más importante es que la etiqueta "más reciente" se puede mover con frecuencia de una imagen a otra. La consecuencia es que no puedes confiar en esta etiqueta para compilaciones predecibles o que se pueden reproducir. Por ejemplo, sigue este Dockerfile:

FROM debian:latest
RUN apt-get -y update && \ apt-get -y install nginx

Si compilas una imagen de este Dockerfile dos veces y en momentos diferentes, puedes terminar con dos versiones diferentes de Debian y NGINX. En su lugar, considera esta versión revisada:

FROM debian:9.4
RUN apt-get -y update && \ apt-get -y install nginx

Mediante el uso de una etiqueta más precisa, tendrás la seguridad de que la imagen resultante siempre se basará en una versión secundaria específica de Debian. Debido a que una versión específica de Debian también incluye una versión específica de NGINX, tienes mucho más control sobre la imagen que se compila.

Este resultado no solo es verdadero en el tiempo de compilación, sino también en el tiempo de ejecución. Si haces referencia a la etiqueta "más reciente" en un manifiesto de Kubernetes, no tienes garantía de la versión que Kubernetes usará. Es posible que varios nodos del clúster extraigan la misma etiqueta "más reciente" en momentos diferentes. Si la etiqueta se actualizó en un punto entre las extracciones, puedes terminar con diferentes nodos que ejecutan diferentes imágenes (que se etiquetaron como "más reciente" en un punto).

Lo ideal es que siempre uses una etiqueta inmutable en la línea FROM. Esta etiqueta te permite tener compilaciones que se pueden reproducir. Sin embargo, hay algunas ventajas y desventajas de seguridad: Cuanto más fijes la versión que deseas usar, menos automatizados estarán los parches de seguridad en las imágenes. Si la imagen que usas emplea el control de versiones semántico adecuado, la versión del parche (es decir, la "Z" en "X.Y.Z") no debe tener cambios que generen incompatibilidad con versiones anteriores: Puedes usar la etiqueta "X.Y" y obtener correcciones de errores automáticamente.

Imagina una pieza de software denominada "SuperSoft". Supongamos que el proceso de seguridad de SuperSoft consiste en corregir vulnerabilidades a través de una versión nueva de parche. Deseas personalizar SuperSoft y escribiste este Dockerfile:

FROM supersoft:1.2.3
RUN a-command

Después de un tiempo, el proveedor descubre una vulnerabilidad y lanza la versión 1.2.4 de SuperSoft para solucionar el problema. En este caso, depende de ti mantenerte informado sobre los parches de SuperSoft y actualizar el Dockerfile correspondiente. En cambio, si usas FROM supersoft:1.2 en el Dockerfile, la versión nueva se extraerá automáticamente.

Al final, debes examinar cuidadosamente el sistema de etiquetado de cada imagen externa que uses, decidir cuánto confías en las personas que compilan esas imágenes y decidir qué etiqueta usarás.

¿Qué sigue?

Prueba otras funciones de Google Cloud Platform tú mismo. Revisa nuestros instructivos.

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...