Mejores prácticas para crear contenedores de aplicaciones Go con Docker
Jekayin-Oluwa Olabemiwo
23 de marzo de 2022
0 minutos de lecturaLas aplicaciones Go y los contenedores están hechos el uno para el otro. El pequeño tamaño de los binarios de las aplicaciones Go es ideal para las estrategias de implementación de microservicios que Docker y Kubernetes ofrecen con gran eficacia. Sin embargo, esta sinergia también presenta desafíos. Por eso, es importante comprender las mejores prácticas y los conceptos clave de los contenedores para evitar problemas de seguridad que pueden introducirse fácilmente en las imágenes de contenedor.
En este artículo, programarás una aplicación Go de ejemplo y aprenderás a crear su contenedor y ejecutarla de forma segura.
Requisitos previos
Para seguir este tutorial, necesitas tener Docker y Go instalados en tu equipo y conocer sus conceptos básicos. Encontrarás instrucciones introductorias y descargas de Docker en la página de orientación y configuración de la documentación de Docker. Puedes encontrar buenos tutoriales de Go en la documentación oficial de Go o ir directamente a la página de instalación de Go para Linux.
Crear una aplicación Go de ejemplo
Para empezar, creemos nuestra API de Go. Primero, ve al directorio de tu equipo donde quieres guardar la aplicación Go. Luego, crea un directorio llamado “godocker”. En este directorio godocker/, ejecuta el siguiente comando para definir tu módulo Go:
Luego, crea un archivo llamado “main.go” dentro del directorio “godocker”. Este archivo contendrá el código de tu API. Ahora puedes agregar la lógica para proporcionar y gestionar la hora actual mediante la API.
Ingresa el siguiente código en godocker/main.go:
Este código proporciona la hora actual mediante la API usando la variable TimeFormat del paquete http. Luego, definimos la biblioteca NewServeMux como mux para configurar el servidor HTTP. Por último, registramos el endpoint /time para devolver la hora actual en la API y codificar la respuesta en el endpoint /time como JSON.
Hagamos una prueba rápida. Ejecuta la aplicación con el siguiente comando en la terminal:
Deberías obtener un resultado similar a este:
Ahora, probemos la API en otra ventana de terminal con cURL. Ingresa el siguiente comando en la terminal:
El resultado debería verse así:
Preparar el Dockerfile
Un Dockerfile contiene una serie de instrucciones para empaquetar e implementar una aplicación como contenedor. En esta sección, crearemos un Dockerfile y revisaremos algunas de las instrucciones que podemos usar para empaquetar nuestra aplicación de ejemplo como contenedor.
Especificar la versión de la sintaxis de Docker
Primero, agregamos la directiva de sintaxis. En el directorio godocker/, crea un archivo llamado "Dockerfile" e ingresa el siguiente código en la primera línea:
La directiva de sintaxis especifica la ubicación de la sintaxis de Dockerfile que usaremos para crear nuestro Dockerfile. Esta línea de código define la ubicación de la sintaxis de Dockerfile como docker/dockerfile:1, que es la versión más reciente. Docker comprobará la versión de la sintaxis antes de usar el backend Buildkit para crear el Dockerfile.
Asegúrate de que esta línea tenga un comentario al inicio del archivo. Después de la directiva de sintaxis, la convención es dejar una línea en blanco.
Heredar una imagen base con poca memoria
Luego, especificamos la imagen base de Docker que queremos heredar con la instrucción FROM.
Agrega el siguiente código a godocker/Dockerfil`:
Esta instrucción garantiza que no tengamos que crear nuestra propia imagen base de Docker. En su lugar, heredamos la imagen oficial de Docker para aplicaciones Go en la variante Alpine Linux. La versión de Go de la imagen base es 1.17. La imagen alpine es muy pequeña en comparación con una variante como la imagen ubuntu.
Ahora completamos el resto del Dockerfile, incluidos los comentarios en el código, para explicar brevemente el propósito de cada línea.
Actualiza godocker/Dockerfile para que contenga el siguiente código:
Crear la imagen
Con nuestro Dockerfile listo, creemos nuestra imagen de Docker a partir de él con el comando docker build. Docker usa el daemon de Docker para crear imágenes. Usamos la opción --tag —que podemos abreviar como -t— con el comando docker build para asignar un nombre personalizado a nuestra imagen de Docker.
Ingresa el siguiente comando en la terminal:
Agregamos . al comando docker build para que la imagen se cree en el directorio actual, que sirve como contexto de compilación. Evita usar la ruta / como contexto de compilación, ya que esto puede transferir todo el código fuente al daemon de Docker.
El resultado de la compilación debería incluir la línea FINISHED y verse similar a esto:
Este resultado significa que la imagen de Docker se creó por completo con la etiqueta godocker.
Para ver la lista de imágenes locales, ingresa el siguiente comando en la terminal:
El resultado debería verse similar a este:
En nuestro resultado, el tamaño de la imagen godocker recién creada es de 319 MB, lo cual es mucho para una aplicación de API sencilla. Por eso, debemos optimizar la compilación para crear una imagen más ligera. En la siguiente sección, implementaremos el concepto de compilaciones multietapa para lograrlo.
Usar compilaciones multietapa
El enfoque de compilaciones multietapa ayuda a crear imágenes mucho más pequeñas que las que se obtienen con el enfoque de una sola etapa que mostramos en la sección anterior. Una compilación multietapa usa una imagen para crear fragmentos que se empaquetan en una imagen más pequeña, compuesta únicamente por las partes esenciales para que esos fragmentos se ejecuten. Al reducir nuestras imágenes al mínimo necesario para ejecutar la aplicación, podemos reducir el riesgo de vulnerabilidades de seguridad. Para lograrlo, debemos usar varias instrucciones FROM en nuestro Dockerfile.
Usar la imagen oficial scratch
Podemos iniciar la compilación con una imagen vacía heredando la imagen oficial de Docker scratch. En esta sección, mostraremos cómo usar la imagen scratch en compilaciones multietapa.
Primero, ve al directorio raíz de tu aplicación. Luego, crea un archivo llamado “Dockerfile.multistage” e ingresa el siguiente código:
Este código especifica que se heredará la imagen base oficial golang:1.17-alpine, con el nombre de etapa build. Luego, usamos otra instrucción FROM para implementar el concepto multietapa y copiar el binario compilado de la primera etapa a la imagen vacía de la segunda.
Luego, debemos crear una nueva imagen con el nuevo archivo Dockerfile.multistage. También debemos asignarle la etiqueta “multistage”. Esto permite distinguirla de la imagen que creamos antes.
Ingresa el siguiente comando en la terminal:
Cuando la compilación termine correctamente, revisa la lista de imágenes ingresando el siguiente comando en la terminal:
El resultado debería verse similar a este:
Este resultado muestra la gran diferencia de tamaño entre las imágenes godocker:multistage y godocker:latest. La mejora es evidente: pasamos de 319 MB en la imagen de una sola etapa a 6,1 MB en la imagen multietapa. Como dependemos de los contenedores para iniciarse rápidamente, optimizaciones como esta son fundamentales al crear contenedores para aplicaciones Go.
Implementar el contenedor
Además de optimizar el rendimiento y la eficiencia, también debemos pensar en la mejor manera de implementar nuestros contenedores para que se ejecuten de forma segura. En esta etapa del tutorial, pondremos en práctica algunas de las mejores prácticas.
Ejecutar como usuario no root
El principio de privilegio mínimo exige que limitemos el acceso a los recursos del sistema. Nuestros contenedores Docker de Go son contenedores de aplicaciones y no necesitan ejecutarse con privilegios de root. Por eso, debemos crear un nuevo usuario y grupo con acceso limitado en nuestro Dockerfile para mejorar la seguridad.
Para crear un usuario que no sea root, agrega las siguientes líneas inmediatamente después de la primera instrucción FROM en godocker/Dockerfile.multistage:
Esta instrucción define los argumentos USERNAME y PASSWORD con la palabra clave ARG y luego crea un usuario con la instrucción RUN adduser.
Luego, implementemos las instrucciones para copiar los datos del usuario de la primera etapa y aplicarlos a la segunda.
Agrega el código en negrita del siguiente ejemplo a la segunda etapa de godocker/Dockerfile.multistage, que ahora debería terminar con el siguiente código:
En Kubernetes, puedes especificar runAsuser: UID en el campo securityContext. Consulta la documentación de Kubernetes para aprender a establecer el contexto de seguridad de un Pod.
Ejecutar con un sistema de archivos raíz de solo lectura
Otra forma de mejorar la seguridad de nuestra aplicación es ejecutar el contenedor con un sistema de archivos de solo lectura. Para aplicar el acceso de solo lectura al sistema de archivos de nuestro contenedor, usaremos la opción de solo lectura con el comando docker run.
Ingresa el siguiente comando en la terminal:
Eliminar o denegar capacidades de Linux
Las capacidades de Linux son el conjunto de privilegios que podemos habilitar o deshabilitar al usar Linux. Como nuestro contenedor se basa en una variante de Linux, también podemos usar estas capacidades para mejorar la seguridad de nuestra aplicación. Eliminar algunas capacidades reduce el riesgo para nuestro contenedor.
En este tutorial, eliminaremos todas las capacidades excepto setuid.
Ingresa el siguiente comando en la terminal:
Limitar el uso de CPU y memoria
El comando docker run nos permite establecer límites de uso de recursos del equipo host para nuestro contenedor de Docker. Docker usa la opción --cpus.
Por ejemplo, para evitar que el contenedor use más del 50 % de una sola CPU, ingresa el siguiente comando en la CLI de Docker:
Si necesitas usar 2 CPU, puedes limitar el uso con el siguiente comando en la CLI de Docker:
Si quieres limitar el uso de memoria de un contenedor Docker a 1024 MB, puedes usar el comando docker run así:
Este comando también establece un límite de memoria de 256 MB que se aplica cuando Docker detecta que el host tiene poca memoria disponible.
Conclusión
En este artículo, recorrimos el proceso de configurar una aplicación Go y crear su contenedor con Docker. También implementamos el concepto de compilaciones multietapa para optimizar el rendimiento, creamos una imagen desde cero con una imagen oficial de Docker vacía y analizamos algunas mejores prácticas que pueden ser útiles al crear e implementar contenedores. Estas pautas son un punto de partida para lograr una eficiencia sólida, seguridad y administración de memoria al crear contenedores para aplicaciones web Go con Docker.
Para obtener más información sobre las mejores prácticas de seguridad, visita el centro de recursos Snyk Learn.
