Que cambia con Docker para los desarroladores?

La semana pasada tuve una charla con un nuevo amigo (@rdiazconcha) sobre Docker y uno de los temas de la mesa virtual fue: "¿Cuáles son las implicaciones de la adopción de Docker para los desarrolladores? ¿Necesitamos seguir haciendo las mismas cosas que hemos estado haciendo en los últimos años (o meses) o debemos empezar a considerar otras cosas también?" Bueno, en este artículo voy a dar mi perspectiva y lo que cambió para mi y el buen grupo de desarrolladores con el que me rodeo.

Pero primero, no creo que esta sea una lista completa de los temas que necesitas considerar (estoy asumiendo que ya tienes algunos conocimientos básicos sobre Docker) de hecho creo que algunos de ellos necesitarián más de un artículo para discutir, así que tu pudes agregar más en los comentarios.

No es nada mas de tener los mismos entornos

Siempre leemos que la frase común "funciona en mi máquina" está muerta con Docker, pero déjame decirte que eso no siempre es cierto, pero ¿por qué? Porque necesitamos ser conscientes de que hay muchos otros componentes que son necesarios para nuestro trabajo, digamos por ejemplo: redis, elasticsearch, mysql, memcached, etc.

Tenemos diferentes entornos también, donde todas las pruebas deben ser verdes (todo el mundo prueba su código, ¿verdad?), Los más comunes son: local, dev, qa, staging y producción (por nombrar algunos). Y la idea de tener Docker es que podemos "construir una vez y correr en cualquier lugar" por lo que tenemos que tener en cuenta que la aplicación no se ejecutará sólo en mi máquina, sino también en cualquier otro miembro del equipo (sí, tratemos a los servidores como un miembro del equipo, lol). El flujo habitual es el siguiente: Creo mi rama para las nuevas funcionalidades, hago los cambios, pruebo los cambios, hago push del código, hago un merge request, mi lider de equipo lo aprueba, se hace merge a master, entonces la herramienta de CI se dispara para construir La imagen y se publica por primera vez con la herramienta de CD para el entorno dev, después de esto sólo basta con publicar la misma imagen para el resto de entornos ... sí, cuando todas las pruebas pasaron.

Suena bien fácil ¿no? No siempre, a veces hay inconsistencias que causan tanto dolor. Por ejemplo, uno de los problemas más comunes que teniamos es que los endpoints para los recursos no fueron siempre de la misma manera, para redis algunos usaron la IP del servidor (que a veces cambiaba) otros usaban los sub dominios pero nos dimos cuenta de que necesitabamos tener un estandar alli asi que cambiamos los dominios de nuevo y siempre se nos olvidaba cambiar algo porque a pesar de que las pruebas pasaran en verde, cuando apagamos un servidor viejo empezabamos a notar que algunas cosas todavia apuntaban allí. Asi que incluso nostros teniendo un flujo similar al de arriba, siempre teniamos problemas entre entornos.

Nunca supimos exactamente lo que estaba pasando (todo funcionaba en nuestras máquinas, ¿recuerdas?), pero no aprovechábamos algo muy útil (estamos en eso): el depurador. A veces tenemos que ir al límite de decir: "No tenemos el mismo entorno que en la producción por lo que tendremos que depurar allí" y terminamos teniendo feos mensajes de depuración que a veces están expuestos a nuestros clientes. No, no, no, no podemos seguir trabajando de esa manera, tenemos una mejor manera ahora, porque es posible incluso depurar nuestro código dentro de un contenedor, por ejemplo, si eres un desarrollador de .NET, con Visual Studio 2017 ya solo necesitas "Add Docker Support" a tus proyectos y VS construirá todo lo que sea necesario para correr y depurar localmente tu aplicación, utilizará alguna estructura base y de acuerdo con la versión de tu código creará un Dockerfile adecuado, pero bueno, como Scott Hanselman dijo en el lanzamiento de VS: "Puedes usar esto incluso para aprender Docker" (También puedes hacerlo con Java: [aquí].

Probar mi aplicación puede ser automatizado fácilmente

No podemos simplemente probar (y/o depurar) localmente, si tenemos control total del entorno, siempre encontraremos el hack para que nuestra aplicación funcione y a medida como nuestra aplicación crece, más y más pruebas serán necesarias y esto significa que más y más tiempo va a ser necesario, esto podría empezar a bajarnos la velocidad.

Ayer, me enteré de que el equipo de SQL Server utilizaa Docker para automatizar las pruebas (link) y también Yelp corre millones de pruebas cada día (link), eso esta genial! Tenemos dos ejemplos de grandes empresas sobre cómo podemos aprovechar los contenedores para probar nuestra aplicación hasta el límite de incluso correr ese tipo de pruebas a nivel local. El enfoque que has estado utilizando para probar tu aplicación necesita cambiar porque asi como el equipo de SQL Server, ahora tu tienes la capacidad de iniciar la base de datos en un estado deseado como primer paso, corres las pruebas con varios contenedores al mismo tiempo y esperas un poco de tiempo para obtener los resultados (necesitará más recursos, pero aprovechariamos la nube, ¿verdad?).

Imagina que puedes correr todo el conjunto de pruebas después de haber publicado tu aplicacion en el entorno dev (u otro) con este enfoque, ganarás confianza cuanto más practiques y también reducirás no sólo el tiempo de entrega sino tambien riesgos al publicar algo nuevo a producción.

El almacenamiento es efímero

Es necesario saber que todos los cambios que se producen en un contenedor cuando se ejecuta, pueden perderse para siempre porque cuando se inicia de nuevo se creará un nuevo contenedor (a menos que iniciaras el mismo contenedor). En el momento en que se detiene no hay garantía de que sus archivos se quedarán allí, pueden ser logs, archivos que faltan al publicar y se subieron manualmente, nuevos archivos cargados al usar la aplicación, componentes que faltaron y se instalaron, etc .. Definitivamente te recomiendo leer este artículo [aquí] para saber como funciona esto.

¿Por qué esto es importante? Como dije antes, cuando tienes el control del entorno siempre encontrarás la manera de hacer que tu aplicación funcione y como humanos (especialmente con deadlines) tendemos a olvidar lo que hicimos para arreglarlo y por eso nuestra aplicación deja de funcionar en otros entornos. Además, esto significa que no puedes depender de tener el estado dentro de un contenedor, el contenedor siempre debe ser stateless.

Permíteme darte un ejemplo, digamos que un usuario carga su imagen de perfil con tu aplicación y almacena esa imagen en el contenedor, el usuario está feliz porque la imagen de perfil se muestra, pero luego sucede algo con el contenedor y se detiene. Boom, la imagen de perfil ya no existe, a gran escala es molesto ir y volver a iniciar un contenedor detenido. En este caso específico, debes mover la imagen a un almacenamiento permanente como S3, Azure Blob o lo que prefieras.

Variables de entorno

Ahora, esta es una de las cosas más importantes donde verás ganancias significativas si aplicas esto correctamente. Esto fue para mí el momento "aja" para "correr en cualquier lugar", porque al implementar las variables de entorno sólo tiene que "inyectar" los valores que necesitas para el entorno en que el contenedor correrá. Vamos a utilizar el endpoint de base de datos como un ejemplo, utilizando una variable de entorno como DB_HOST, podemos empezar a hacer los cambios necesarios para tu aplicación y que tome el endpoint desde el entorno, entonces tenemos que añadir esa variable a mi equipo ... ¿verdad? ... ahmm puedes hacer eso, pero no vas a cambiar mucho las cosas, sólo estarias haciendo todo más complejo porque lo que hiciste en su computadora tendrás que hacerlo en el resto de los entornos.

Entonces, ¿cómo se puede lograr esto? Es necesario hacer uso de las variables de entorno al ejecutar el contenedor, de esta manera vas a inyectar los valores deseados. Esta recomendación se basa en la guía de "12 Factor App" y específicamente para la parte de configuración, puedes encontrar más información [aquí] y también escribí un post sobre el uso de docker env vars con .NET Core [aquí].

Logs, logs, logs

Los datos podrían perderse, ¿recuerdas? Entonces, ¿cómo sabemos lo que sucedió después de que un contenedor se detuvó? Logs. Esto es crucial en producción por lo que necesitas estar preparado asegurándote de tener esto en todos los entornos (incluso en tu máquina, sí, para probar que está funcionando). Necesitas saber que por defecto todos los logs van a la salida estándar (/dev/stdout) y error estándar (/dev/stderr), dale un vistazo por ejemplo a un contenedor nginx, la ubicacion /var/log/nginx/access.log es un enlace simbólico a /dev/stdout. De esta manera, puedes utilizar el comando "docker logs" para obtener la información sin iniciar sesión en el contenedor o puedes utilizar otros "logging drivers" y tener un servidor de logs centralizado donde todos los contenedores mandarian los registros, como la guia de "12 Factor App" dice "tratar los logs como flujos de eventos" [aquí]. Pero debes elegir sólo uno, si decides ir con el enfoque de logging drivers, ya no es posible obtener registros con el comando "docker logs" y debo mencionar que sólo puedes tener un loggin driver a la vez, al menos por el momento (si, no bueno).

¿Cómo esto te afecta como desarrollador? Necesitas saber que si envías logs a otra ruta distinta a stdout o stderr (esto se traduce a todas las escrituras de consola que puede hacer tu aplicación), los registros pueden perderse (a menos que los envíes a un volumen compartido, en este caso los registros se mantendrá en el host, pero todavía hay una oportunidad de perderlos). Así que te esto en cuenta.

Puertos

Debes encontrar una manera que esto no sea un impedimento para tu aplicación, especialmente si deseas escalar los contenedores en el mismo host y hacer uso de la mayoria de los recursos. La aplicación dentro de tu contenedor va a exponer un puerto pero el host no va a exponer el mismo puerto necesariamente (hay un rango predeterminado pero como desarrollador no necesitas saber esto, pero si tienes curiosidad ve [aquí].

Y esto también es una característica de una "12 Factor App", puedes encontrar más información [aquí]. ¿Pero Por qué es importante? Al tener esta opción, tu no necesitas preocuparte donde el contenedor estará viviendo, en tu computadora local podría ser el puerto 8080, pero en desarrollo podría ser el puerto 32888. Puedes pensar que esto hará las cosas más complejas porque necesitas saber en qué puerto se ejecuta el contenedor, esto se complica cuando se ejecuta más de un contenedor en un clúster y que la aplicacion sea tolerante a fallos.

También tienes algunas opciones para trabajar con esto y aquí están: 1. Puede utilizar docker componer y vincular los contenedores, más información [[aquí]] (https://docs.docker.com/compose/networking/) 2. Utilice un equilibrador de carga, esto expondrá obviamente un puerto que siempre será el mismo y que se utilizará para llamar a un contenedor, la diferencia es que esto proxy las llamadas al puerto del contenedor en el host que están registrados , Hay varias maneras de hacer esto y [aquí hay algunos] (https://docs.docker.com/datacenter/dtr/2.2/guides/admin/configure/use-a-load-balancer/). Y todos los orquestadores tienen esta característica de alguna manera, pero como desarrollador no es necesario saber cómo hacer la aplicación. 3. Ignore esta sección y continúe trabajando con un número de puerto fijo, la desventaja aquí es que sólo podrá tener un contenedor por host

Herramientas para tener mi entorno local

Ahora, usted no necesita aprender un montón de herramientas para comenzar, pero hay algunas cosas que usted debe considerar al comenzar este viaje:

  1. Docker instalado, es un poco obvio pero de todos modos, más información [aquí]
  2. En el caso de Windows, puede que tengas que compartir la unidad donde tienes el código para poder depurar o utilizar volúmenes en esa unidad
  3. En algunos casos, asegúrate de que estás proporcionando suficientes recursos al daemon de docker, es decir, para ejecutar contenedores de SQL Server necesitas tener al menos 3.5 Gb de memoria reservada
  4. Un script para iniciar la aplicación y todas sus dependencias (por ejemplo un redis local), esto puede ser tema para otro post, sólo ten en cuenta que tendrás que utilizar compose para facilitar el trabajo. De esta manera cuando un nuevo desarrollador(a) entra, sólo tendrá que descargar el repositorio, leer el README.md, tal vez seguir algunas instrucciones, ejecutar docker compose y comenzar a hackear. Esto suena fácil, pero puede ser complicado llegar a este punto, pero vale la pena.

Crear el Dockerfile con el equipo de operaciones

Necesitas tener esto codificado, le hará la vida de todos más fácil, confía en mí. Puedes lanzar un contenedor con una imagen base (incluso de scratch), instalar lo que necesitas, copiar tu aplicación y luego probar, después de que estés feliz con el resultado haces commit del contenedor, creas una nueva etiqueta de la imagen, haces push de la imagen al repo y eso es todo. En otras palabras, podrías tratar el contenedor como una máquina virtual, no lo hagas, por favor.

En su lugar, haz uso del Dockerfile, puedes comenzar con lo que sabes que va a necesitar tu aplicación y luego pedir al equipo de operaciones que haga una revisión, quizas tienen algo que añadir o eliminar y sobre todo sabrán lo que el contenedor trae instalado. Lo mismo sucederia si otra persona está construyendo la imagen, tu quisieras saber que está adentro, tal vez algo que está ahí es lo que está haciendo que tu aplicación se bloquee o corra muy lento.

Algunas cosas a considerar aquí: evita el uso de un Dockerfile para construir localmente, otro para el desarrollo y otro para producción, crearas un desastre, en lugar de eso utiliza los "multi-stage builds", más información [aquí]. Otra cosa es el tamaño de la imagen, recuerda que la idea es tener sólo lo que necesitas para ejecutar tu aplicación, nada más, así que idealmente intentarás tener un tamaño de imagen lo más bajo posible, por ejemplo, puedes lograrlo usando imágenes base como Alpine o CoreOS (crear tu propia imagen con Linux Kit?) En lugar de utilizar la de Ubuntu. ¿Porque es esto importante? Bueno, cuando necesitas escalar y un nuevo servidor es lanzado, la imagen necesita ser descargada y si la imagen son GBs de tamaño, se necesitará algún tiempo antes de poder lanzar tu primer contenedor, tus usuarios quizas ni lo noten pero podria afectar a unos cuantos.

Resumen

Espero que tengas una buena idea ahora de los cambios que puedas necesitar implementar en tus aplicaciones. Pero no trates de hacer todos los cambios a la vez, comienza poco a poco, siempre tendrás algunos beneficios. Desde el principio podrás tener un nivel de consistencia entre publicaciones, puede que no puedas obtener todas las ventajas de Docker, pero al menos da el primer paso: conteneriza tu aplicación :)