Simplifica las imágenes de contenedor con Ko
10 de octubre de 2022
0 minutos de lecturaEn un artículo anterior, escribí sobre cómo —y por qué— podrías querer usar la herramienta Jib del grupo Google Open Source para crear imágenes de contenedor de tus aplicaciones Java. Jib crea imágenes ligeras, basadas en JVM y compatibles con OCI, que siguen las prácticas recomendadas sin necesidad de un entorno de ejecución de contenedores como Docker, y elimina la necesidad de escribir y mantener Dockerfiles. Pero ¿qué pasa si estás creando aplicaciones en Go? Existe otra herramienta de código abierto para Go que funciona de manera similar: Ko.
Nota: Mientras se redactaba este artículo, el proyecto Ko se migró de un repositorio de GitHub propiedad de Google a su propia organización de nivel superior, «ko-build». Actualizamos el texto de esta publicación para reflejarlo, pero es posible que durante un tiempo sigas encontrando referencias a «Google Ko» en línea y en los nombres de los módulos. Ten la seguridad de que se trata del mismo proyecto. ¡Felicitaciones al proyecto Ko por el éxito que hizo posible esta migración!
En este artículo, veremos cómo usar Ko para crear imágenes de contenedor sin Dockerfiles, generar SBOM e integrarse con Kubernetes.
¿Qué es Ko?
Ko es una herramienta de línea de comandos que consiste en un único binario y está diseñada para usarse en tu proceso de desarrollo en lugar de donde hoy ejecutas el compilador go. Además de compilar tu aplicación, también genera una imagen de contenedor ultraligera con tu aplicación instalada. Al igual que Jib, Ko envía la imagen a un registro o la guarda en la caché local de imágenes de Docker, según cómo la configures o ejecutes.
Ko también tiene algunos trucos adicionales relacionados con la creación de la lista de materiales de software (SBOM) y la integración con Kubernetes, que ayudan a simplificar los procesos iterativos de desarrollo e implementación.
¿Qué problemas busca resolver Ko?
Muchos programadores, independientemente del lenguaje con el que trabajen, son nuevos en la creación de contenedores y suelen tener muchas preguntas sobre cómo crear imágenes, como las siguientes:
¿Qué imagen base debería usar y cumple con las políticas de mi organización?
¿Cuál es la mejor manera de combinar comandos para minimizar el exceso de capas?
¿Cuánto de mi aplicación debería copiar en la imagen para ejecutarla?
¿Qué herramientas necesito aprender para crear la imagen? (¿Docker? ¿Buildah? ¿BuildKit?)
¿Hay anotaciones estándar que deba incluir según los requisitos de mi organización?
Los arquitectos y líderes de aplicaciones también quieren que sus equipos puedan seguir fácilmente la implementación de la gobernanza y los estándares, pero mantener prácticas uniformes puede ser difícil cuando cada equipo tiene patrones de Dockerfile únicos y creados de forma orgánica.
A los equipos de seguridad también les importa mucho qué contiene una imagen y qué herramientas se usan para crearla. Por ejemplo, exponer el motor de Docker —o el docker.sock de la máquina host— a un nodo de compilación de CI puede otorgar niveles elevados de acceso a los entornos de compilación en esos nodos.
“El daemon [de Docker]… tiene muchas más capacidades, además de compilar e interactuar con registros. Sin herramientas de seguridad adicionales, cualquier usuario que pueda activar una compilación de Docker en esta máquina también puede ejecutar un comando de Docker para ejecutar cualquier comando que quiera en ella… No solo puede ejecutar cualquier comando, sino que, si usa este privilegio para realizar una acción maliciosa, será difícil rastrear quién fue responsable.”
— Container Security, de Liz Rice, capítulo 6
Por otro lado, incorporar nuevas herramientas de compilación puede ser complejo y aumentar la curva de aprendizaje para quienes están a cargo de los sistemas de compilación. Ko busca abordar cada una de estas áreas y ofrecer una experiencia superior a los desarrolladores que la usan.
Crear imágenes con Ko y sin Ko
Para explicar cómo Ko aborda los desafíos mencionados, primero veamos un ejemplo de cómo se compara con los pasos existentes para compilar con Go y Docker.
Crear la imagen sin Ko
En este ejemplo, crearemos la clásica aplicación del tutorial de Go: la aplicación web «Hello World».
1. En un directorio vacío, crea el siguiente archivo con el nombre hello.go:
2. Vamos a compilar el binario como parte de nuestro Dockerfile, ya que es un patrón habitual:
Esta es una compilación de varias etapas. La primera etapa, llamada build, se basa en la imagen oficial golang. En esta etapa, copiamos el contenido de nuestra aplicación (que por ahora es solo el archivo hello.go) a la imagen, en /go/src, y ejecutamos la herramienta go build con los parámetros necesarios para generar un binario estático.
En la segunda etapa, partimos de la imagen base static:nonroot del proyecto Google Distroless, establecemos nonroot como usuario predeterminado, copiamos el binario hello de la etapa build al directorio raíz, definimos algunos metadatos sobre el puerto que queremos exponer y establecemos que ./hello se ejecute de forma predeterminada al iniciar el contenedor.
¿Por qué usar dos etapas?
Es habitual compilar la aplicación en el Dockerfile, ya que esto garantiza que se use la versión correcta del compilador tanto en la estación de trabajo de un desarrollador como en las compilaciones automatizadas. Sin embargo, un error común es implementar la misma imagen en la que se realizó la compilación. Esto representa un riesgo de seguridad porque terminas incluyendo en la imagen implementada no solo herramientas innecesarias, como el compilador, sino también el código fuente de tu aplicación. Si un atacante logra explotar una vulnerabilidad u obtener una copia de la imagen, tendrá mucha información y herramientas a su disposición para ampliar su ataque. Al partir de la imagen base distroless/static:nonroot, tenemos un sistema de archivos extremadamente minimalista y un usuario no root predeterminado.
3. Una vez listo nuestro Dockerfile, podemos usar el comando docker build para crear una imagen almacenada en caché local y asignarle una etiqueta:
Nota: Es posible que el resultado se vea un poco diferente, según la versión de Docker que estés ejecutando y si tienes habilitadas las mejoras de BuildKit.
4. Para que otras personas usen esta imagen, tenemos que enviarla a un repositorio de registro. En este ejemplo, estoy ejecutando un repositorio local en el puerto 5000 de mi estación de trabajo mediante la imagen registry:2 de DockerHub. (Por eso etiqueté la imagen con el prefijo localhost:5000/).
Enviar una imagen a un registro con Docker es muy sencillo con el comando docker push:
Nota: Si se tratara de un registro administrado, primero tendría que autenticarme con docker login.
5. Por último, para ejecutar nuestra imagen, podríamos usar docker run o una implementación adecuada de kubectl. Para simplificar, usaremos la primera opción:
y podemos probar la aplicación con un comando sencillo de curl:
Si visualizáramos los pasos que acabamos de realizar, podrían verse más o menos así:

Ahora volvamos al principio y veamos cómo funcionaría esto con Ko.
Crear la imagen con Ko
1. Empezaremos con el mismo archivo hello.go en un directorio vacío:
2. A continuación, guardamos el registro de imágenes en una variable de entorno y ejecutamos ko build:
Ese comando:
compiló nuestra aplicación en un binario estático,
lo incluyó en una imagen bien formada y
la envió a mi registro.
Para hacer todo esto, no se necesitó ningún Dockerfile ni motor de ejecución de contenedores. Además, notarás que el resultado menciona que se creó y publicó una SBOM; volveremos a eso más adelante.
Nota: La etiqueta de imagen en este caso contiene un hash md5 que, de forma predeterminada, corresponde a la ruta de importación de tu aplicación Go. Como en este ejemplo no tenemos un módulo de Go completo, el hash se genera simplemente a partir de hello.go. Consulta la documentación de Ko para obtener más información sobre las etiquetas.
3. Ahora ejecutaremos la imagen con docker run:
... y la probaremos con curl:
El flujo de trabajo de Ko podría verse más o menos así:

Al dejar que Ko se encargue de la creación, elaboración y publicación de imágenes, reducimos la cantidad de pasos, la dependencia del motor de ejecución de contenedores durante la compilación y los requisitos de conocimientos y mantenimiento que implica el Dockerfile.
Valores predeterminados inteligentes y personalizaciones deterministas
Quienes usan Ko se benefician de las prácticas recomendadas definidas colectivamente por la comunidad de código abierto. Pero ¿qué pasa si tu organización tiene estándares personalizados que no coinciden? En ese caso, resultan útiles las opciones de línea de comandos de Ko o el archivo de configuración ko.yaml.
Imagen base personalizada
Por ejemplo, supongamos que tu empresa exige una imagen específica como base para todas las aplicaciones Go. Solo tienes que incluir la línea defaultBaseImage: en un archivo ko.yaml del directorio raíz y establecer esa imagen como su valor.
Indicadores del compilador de Go
Para especificar explícitamente los -ldflags del compilador de Go, como hicimos en nuestro ejemplo original, solo tienes que agregar una entrada builds: al mismo archivo ko.yaml con la configuración adecuada, tal como se indica en la documentación de Ko.
Etiquetas de Docker
Las etiquetas de Docker —también conocidas como anotaciones OCI— permiten agregar metadatos útiles a una imagen, por ejemplo, información sobre el repositorio de origen, la compilación de CI que la creó o cualquier otro dato que tu equipo quiera incluir. Para obtener más información sobre las etiquetas de imagen y cuándo conviene usarlas, consulta mi blog Cómo y cuándo usar las etiquetas de Docker.
Ko permite agregar etiquetas mediante la opción de línea de comandos --image-label:
Nota: Al momento de publicar este artículo, no hay forma de especificar etiquetas de imagen en el archivo de configuración .ko.yaml, pero existe una solicitud abierta para agregar esa funcionalidad.
Otras funciones interesantes de Ko
SBOM de imágenes
Como vimos antes, Ko genera y publica automáticamente una SBOM para tu nueva imagen de contenedor. De forma predeterminada, usa el formato SPDX, pero también puedes usar CycloneDX si pasas la opción --sbom=cyclonedx en la línea de comandos. También puedes desactivar esta función con --sbom=none. Encontrarás estos y otros detalles de configuración en la documentación de Ko.
Un análisis completo de las SBOM y su función en la creación de una cadena de suministro segura queda fuera del alcance de este artículo. Si quieres obtener más información, consulta Cómo crear SBOM para proteger la cadena de suministro de código abierto.
Integración con Kubernetes
Si necesitas probar tu imagen en el contexto de un clúster de Kubernetes, seguramente ya te encontraste con la molestia de tener que repetir un ciclo de pasos como estos:
Crear mi imagen
Implementarla en mi registro (o cargarla de alguna otra forma en el clúster de Kubernetes)
Actualizar el YAML de mi implementación con la nueva etiqueta de imagen
Volver a implementarla con
kubectl
Ko puede simplificar mucho este proceso al automatizar todos estos pasos con un solo comando: ko apply. Si haces un cambio sencillo en el YAML de tu implementación y reemplazas la etiqueta de imagen por una etiqueta ko:// con un formato especial, ko apply realizará automáticamente todos los pasos anteriores y hará la implementación en el clúster que hayas configurado como activo en el contexto de kubectl.
Por ejemplo, imagina que tienes un entorno de pruebas o un clúster de Kubernetes que se ejecuta localmente y necesitas implementar y probar tu trabajo de forma iterativa. Este es un manifiesto de implementación para implementar allí nuestra aplicación «hello»:
Puedes ver que la línea image: contiene ko://, seguida de la ruta del módulo Go de nuestra aplicación en GitHub.
Ahora solo tenemos que ejecutar ko apply y pasarle el archivo YAML con la opción -f, tal como haríamos con kubectl:
Ahora podemos probar nuestra aplicación, hacer un cambio y volver a ejecutar ko apply -f myfile.yaml tantas veces como necesitemos. ¡Ko se encargará de todo el trabajo pesado!
Como es de esperarse, hay otros comandos de Kubernetes que probablemente necesites, como:
deletepara eliminar tus implementaciones, yresolvepara generar YAML que puedas pasar akubectlu otras herramientas.
Consulta https://github.com/ko-build/ko#kubernetes-integration para ver la documentación completa.
Desafíos de usar Ko
Ya hablamos de todas las razones por las que podrías animarte a empezar a usar Ko en tus flujos de trabajo, pero antes de comenzar, hablemos de algunos desafíos que podrías enfrentar al usarlo.
Consideraciones multiplataforma
Si desarrollas en una máquina que no usa Linux, la falta de un entorno de ejecución de contenedores puede complicar un poco las cosas. Una imagen de compilación basada en contenedores te permite incluir las herramientas de compilación, así como el sistema operativo y sus bibliotecas, en una imagen base común y, en la práctica, ignorar que la plataforma de tu estación de trabajo no coincida con la de tu entorno de producción.
Por ejemplo: la parte de Go+Docker del ejemplo anterior funcionará en casi cualquier plataforma porque, sin importar dónde la compiles, la imagen base golang proporciona las bibliotecas basadas en Debian glibc necesarias para que el compilador de Go cree el binario estático. Sin embargo, si intentas ejecutar el ejemplo de Ko con las mismas opciones de ldflags en una máquina con MacOS, por ejemplo, recibirás errores sobre la falta de bibliotecas necesarias para realizar los enlaces estáticos.
Esto se debe a que MacOS/Darwin no incluye las bibliotecas necesarias para la compilación cruzada de un binario estático para Linux (al menos, no en el momento de escribir esto). No se trata solo de Apple: también pueden surgir problemas similares con arquitecturas de CPU distintas en estaciones de trabajo con Linux y Windows WLS.
Deterioro de habilidades
Las habilidades y los conocimientos necesarios para crear y mantener imágenes de contenedores bien diseñadas son muy valiosos. Usar herramientas que abstraen o eliminan la necesidad de contar con ellos puede reducir la capacidad de resolver problemas cuando algo falla. De hecho, cuanto menor sea el número de personas del equipo que entienden las tecnologías de contenedores, mayor será la dependencia de quienes tienen la experiencia necesaria para solucionar problemas y dar mantenimiento en tiempo de ejecución. En casos extremos, esto puede provocar agotamiento, rotación de personal o interrupciones del servicio.
Complacencia con la seguridad
Si el proceso de creación de imágenes de contenedores se automatiza por completo y deja de estar en manos de los desarrolladores, el cumplimiento de procesos como el análisis de vulnerabilidades de imágenes puede verse afectado. Las vulnerabilidades en imágenes, paquetes, bibliotecas, etc. que no se actualizan pueden introducirse y exponer tu aplicación a ataques. Por eso, asegúrate de que el análisis siga realizándose mediante otros métodos automatizados, como:
Scripts de compilación o Makefiles
Hooks de Git
Pasos de compilación de CI
Si aún no analizas tus imágenes, Snyk ofrece análisis gratuito de imágenes de contenedores que detecta vulnerabilidades y brinda recomendaciones de corrección sencillas y prácticas.
Simplificar las imágenes de contenedores
Ko —y otras herramientas alternativas para crear imágenes— puede simplificar mucho la creación de imágenes de contenedores para tus aplicaciones Go y ayudar a optimizar y estandarizar la forma en que tus equipos crean imágenes. Uno de los mayores beneficios es para tus herramientas de CI, ya que ya no necesitas exponer un entorno de ejecución de contenedores en tu entorno de compilación, lo que reduce considerablemente los vectores de ataque que aprovechan el acceso privilegiado en tu SDLC. Independientemente de que elijas una herramienta de compilación como Ko, asegúrate de que tus equipos conozcan bien las tecnologías de contenedores y las implicaciones de seguridad de los procesos y las herramientas que usan para crear sus imágenes.
Empieza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.
