Saltar al contenido

Blog técnico | Destruye tu entorno de desarrollo

Muchos entornos de desarrollo requieren una buena cantidad de configuración manual. Lleva un tiempo y no es el tipo de cosas que querrías hacer todos los días. Este tipo de configuración me ha causado mucho dolor a lo largo de los años. Los equipos con los que he trabajado han puesto varios grados de esfuerzo para asegurar que su documentación sea precisa. Algunos incluso han automatizado parte o incluso todo el montaje. Pero las cosas siempre van a la deriva. Los guiones dejan de funcionar. La documentación se vuelve inexacta. Y para cuando un nuevo desarrollador se incorpora, necesita una buena actualización.

Creo que la raíz del problema es la frecuencia con la que se destruye y se recrea el entorno de desarrollo. Propongo que una configuración ideal incluya un guión que no sólo establezca un nuevo entorno, sino que también destruya y recree un entorno existente. Además, para los entornos existentes, no debería tomar más de un minuto para ejecutarse y debería ser ejecutado al menos diariamente.

Blog técnico | Destruye tu entorno de desarrollo
Blog técnico | Destruye tu entorno de desarrollo

¿Por qué?

Cuando no se destruye el entorno de desarrollo con frecuencia, tienden a ocurrir los siguientes problemas:

  • Se siente aceptable requerir pasos manuales para configurar o actualizar un entorno. Esto hace que la incorporación de nuevos desarrolladores sea difícil y posiblemente confusa. También hace difícil para los desarrolladores existentes conseguir un nuevo ordenador.
  • La confianza en que los guiones de configuración funcionan y las instrucciones son precisas se vuelve baja ya que probablemente no se han usado en un tiempo.
  • El estado creado manualmente tiende a entrar en sus almacenes de datos. El estado de cada máquina deriva. Te encuentras necesitando una cierta máquina de desarrollo para trabajar en una cierta tarea porque tiene el estado que necesitas. La incorporación del desarrollador se vuelve aún más dolorosa debido a esto.
  • John hace un cambio que rompe el ambiente de Susan y o bien se olvidó de decírselo o ella olvidó lo que John dijo. Susan pasa la mayor parte del día rastreando lo que está mal.

Estos costos afectan diariamente a todos los desarrolladores de un equipo. El coste de la capitalización es enorme.

Costo inicial

Destruir su entorno de desarrollo con frecuencia es básicamente una función de forzamiento que lo retrasará inicialmente de las siguientes maneras:

  • Estás obligado a automatizar todo.
  • Estás obligado a hacer que el guión que reconstruye tu entorno sea bastante rápido. Si es lento, alguien se molestará por él rápidamente y encontrará la manera de hacerlo más rápido.
  • Usted se ve obligado a crear un estado importante como parte de su entorno de bootstrapping porque de lo contrario se irá más tarde esa tarde.

Beneficios

Estos costos son mucho menores que los costos de los puntos de dolor que abordan. Si su entorno de desarrollo está totalmente automatizado de tal manera que puede (y lo hace) destruirlo diariamente, esto es lo que puede esperar:

  • Cada vez que otro desarrollador hace un cambio que rompe tu entorno, ejecutas el script de configuración antes de molestarte en comprobar por qué está roto. A menos que haya un error real, esto resuelve tu problema cada vez en menos de un minuto. De hecho, te acostumbras a ejecutar el script de forma proactiva cada vez que empiezas a trabajar, así que nunca te encuentras con problemas en primer lugar.
  • Los nuevos desarrolladores pueden obtener la codificación y la experimentación en su propio entorno local de inmediato. Son felices y se sienten en control de su propio aprendizaje y de su incorporación. El equipo confía en que la configuración de este nuevo desarrollador funcionará porque todos ellos ejecutaron la configuración muy recientemente.
  • Cambias de computadora libremente y sin dolor cuando es ventajoso. Te sientes libre de conseguir un nuevo portátil sin preocuparte por su configuración.

Cómo hacerlo

Hay muchas buenas maneras de establecer su entorno. Destruir tu entorno de desarrollo con frecuencia te obligará naturalmente a trabajar en tus propios problemas únicos y a encontrar una solución adecuada. Sin embargo, encuentro útiles los ejemplos concretos. Así es como yo he creado el mío.

Hazlo auto-explicativo

Hoy en día, una buena interfaz de usuario no requiere que leas la documentación. Se explica por sí misma. En el mundo del desarrollo de software, la documentación sigue siendo necesaria, pero debemos hacer las cosas auto-explicativas donde podamos. Encuentro valioso decirle a un nuevo desarrollador cómo “encender” el proyecto tan rápido como sea posible y dejarle leer más detalles más tarde.

Como desarrollador, espero averiguar cómo empezar mirando el archivo README. Idealmente, en las primeras líneas de ese LÉAME se me señalará un guión que hace que todo se encienda. Debería instalar dependencias, aplicar el esquema de la base de datos, insertar datos de semillas, activar servicios y cualquier otra cosa necesaria para pasar de la pizarra en blanco a la plena operatividad.

Configuración y reconstrucción automatizada

Generalmente prefiero tener un solo guión que funcione para un nuevo ambiente, así como para refrescar un ambiente existente. Los objetivos de diseño de estos dos escenarios son diferentes, así que vamos a examinarlos.

1. Nuevo Ambiente

En este escenario, estamos poniendo en marcha el proyecto por primera vez en un entorno determinado. El único objetivo del diseño es que funcione siempre. No asumas nada. No asumas que tienen Postgres instalado. No asuma que existe algún directorio con los permisos adecuados. No asumas que XCode está instalado. Revisa todo y, siempre que sea posible, ocúpate de ello automáticamente.

2. 2. Medio ambiente existente

En este escenario, un desarrollador ya tiene el proyecto en marcha y quiere actualizarlo. Tenemos dos objetivos de diseño para este escenario:

  1. Destrúyelo todo. Por ejemplo, no sólo actualizar el esquema de la base de datos. Destruye completamente y recrea la base de datos.
  2. Hazlo rápido. En mi experiencia, 1 minuto es un buen tiempo máximo.

Hasta ahora, hacerlo lo suficientemente rápido ha demostrado ser un objetivo razonable. La parte más lenta de la configuración suele ser la descarga de artefactos (paquetes NPM, imágenes Docker, etc.). Las imágenes Docker se almacenan en caché automáticamente. Los paquetes NPM se almacenan bien siempre y cuando lo hagas bien. Por ejemplo, el archivo de instalación de yarn –frozen-lockfile hace poco o nada de cacheo y por lo tanto uso yarn en el desarrollo en su lugar, que es extremadamente bueno en el cacheo.

Configuración

Me gusta que mis aplicaciones lean su configuración del entorno. Para el desarrollo, permitimos una forma alternativa de proporcionar la configuración poniéndola en un archivo .env en la raíz del proyecto y leerla con algo como dotenv. Este archivo es ignorado por git para que los valores sensibles puedan ser colocados allí y también porque la configuración puede variar de un entorno a otro. Sin embargo, si no proporcionamos una configuración de desarrollo estándar, tenemos algunos problemas:

  1. En el mejor de los casos, un nuevo desarrollador debe establecer manualmente su configuración. En el peor de los casos, no pueden hacerlo sin ayuda o clonar la configuración de otro.
  2. El entorno de cada desarrollador tiende a parecer un poco diferente, lo que lleva a problemas de “funciona para mí”.
  3. Agregar o cambiar las claves de configuración requiere que cada dev dev dev actualice su configuración.

Para resolver estos problemas, proporcionamos un archivo de configuración de desarrollo estándar y le permitimos anularlo si es necesario. Lo hacemos creando un archivo llamado .env.example que contiene valores predeterminados cuerdos para el desarrollo. .env.example está comprometido con git para que no pongamos valores sensibles en él. Configuraremos nuestros servicios de fondo localmente para que no se necesiten valores sensibles, pero si terminas necesitando algunos, tendrás que dejarlos en blanco y hacer que cada desarrollador los rellene. El script de configuración debería enlazar el archivo .env con este archivo para que todo funcione. Pero deja abierta la opción de borrar el enlace simbólico y proporcionar valores de configuración personalizados, posiblemente sensibles.

Servicios locales

Tengo una gran preferencia por que todos los servicios de fondo (bases de datos, etc.) funcionen localmente. De hecho, debería ser local no sólo para el ordenador, sino incluso para el proyecto. No quiero compartir una base de datos con algún otro proyecto. Esto permite que mis scripts destruyan y recreen libremente estos servicios según sea necesario. Docker es una gran solución para esto.

Para ilustrar la importancia de esto, consideremos las ramificaciones de compartir una base de datos con otros proyectos. En primer lugar, esto significa que no puedes crearla. Puedes crear el esquema dentro de la base de datos, pero no puedes instalar el servicio en sí. Eso significa que el usuario debe instalarlo por sí mismo. Para crear el esquema, también debemos tener un usuario con los permisos adecuados, lo que el desarrollador también debe hacer manualmente. Si esos permisos necesitan ser cambiados en una fecha posterior, esto debe ser comunicado a todos los que ejecutan el entorno y todos deben actualizar los permisos manualmente. Qué lástima. Tampoco podrás controlar la versión de la base de datos que se está utilizando. Y si quieres cambiar la versión, de nuevo, debes decirle a todos los usuarios que lo hagan manualmente.

Queremos que todos los aspectos del entorno estén completamente bajo nuestro control para que puedas ejecutar el script de configuración y tener un estado garantizado y consistente.

Estado efímero

Cualquier estado debe ser considerado efímero. En general, estamos hablando de su base de datos en este caso. La situación que estoy tratando de evitar es una en la que una característica que se está desarrollando requiere que la base de datos esté en un cierto estado que fue creado manualmente. Para que su entorno funcione plenamente, un desarrollador debe clonar el estado desde la máquina de otro desarrollador o crearlo manualmente. Podemos hacerlo mejor. Si hay algún estado común que necesitemos crear en nuestro almacén de datos o en cualquier otro servicio de backend, este debe ser automáticamente arrancado en nuestro script de configuración.

Sigamos con el ejemplo de la base de datos porque es muy común. Tu guión de configuración debería realizar cuatro pasos para tu base de datos.

  1. Destruya la base de datos actual si existe.
  2. Crear la base de datos desde cero (me gusta usar Docker).
  3. Inicializar el esquema a través del mismo mecanismo que se utilizará en la producción (algo como Knex Migraciones, Pyrseas, o tal vez sólo un simple archivo SQL).
  4. Insertar los datos de las semillas para el desarrollo. Por ejemplo, si está haciendo un sitio web de comercio electrónico, probablemente querrá poner algunos productos allí como mínimo.

Destruir regularmente

Probablemente notaste que el primer paso de la configuración de la base de datos era destruirla. Valoro mucho destruir y recrear todo su entorno diariamente. Debe ser tan fácil y rápido que nadie evite hacerlo.

En los dos últimos equipos en los que he trabajado, se ha convertido en un estándar hacer esto como primer recurso cuando algo no funciona. Hay tantas razones por las que un entorno de desarrollo puede funcionar mal cuando en realidad no hay nada malo. Tal vez sacaste el último código pero olvidaste instalar las dependencias. O tal vez tienes múltiples repositorios y te olvidaste de sacar uno de ellos. Tal vez hubo un cambio de esquema que no has aplicado todavía. Tal vez se agregó un nuevo servicio backend que no has inicializado. Antes de perder el tiempo buscando lo que está mal, siempre ejecutamos el script para recrear todo el entorno. En ambos equipos este guión no toma más de un minuto. Si algo sigue roto, entonces sabes que hay un problema real, no sólo un problema de entorno.

Tenemos otras dos ventajas al destruir los ambientes regularmente. Primero, sabemos que funciona. Como todo lo demás, cuanto más tiempo ha pasado desde la última vez que lo ejecutaste, menor es tu confianza en que funcionará. Como el nuestro se ejecuta varias veces al día, cuando algo va mal con él (lo cual es raro) sabes que el tema se introdujo ese mismo día. Y ya que todo está rastreado en el git, puedes simplemente comprobar los compromisos recientes para saber qué lo rompió.

En segundo lugar, esto nos ayuda a mantener a la gente fuera del hábito de crear manualmente el estado o cualquier otra cosa que tenga que ver con su entorno. Naturalmente actualizará el archivo de semillas para poblar su base de datos en lugar de ejecutar una consulta manual porque sabe que de otra manera lo perderá todo para mañana.

Servicios Externos Simulados

Estoy definiendo los servicios externos como cualquier servicio que consuma y que no esté completamente bajo su control. Este podría ser un servicio de terceros o incluso un servicio proporcionado por otro equipo dentro de su empresa.

Realmente hay tres opciones aquí:

  1. Ejecutar el servicio localmente
  2. Consumir un servicio remoto/compartido
  3. Burlarse del servicio

La opción 1 es terriblemente problemática en mi experiencia porque es pesada, lenta y poco fiable. No todos los equipos hacen que su servicio sea fácil de ejecutar y actualizar. Terminarás pasando tiempo diariamente manteniendo todo esto actualizado y funcionando. También serás un ciudadano de segunda clase. Cuando el equipo haga cambios de última hora en su proyecto, decírtelo será una idea tardía en el mejor de los casos. Además de eso, ejecutar todos estos servicios significará ejecutar muchos procesos, servicios de backend, instalar muchas dependencias, etc. Hará que la configuración y la reconstrucción sean mucho más lentas. No es una gran experiencia de desarrollo.

La opción 2 resuelve la mayoría de los problemas de la opción 1. Es rápida porque no hay configuración. Se actualiza automáticamente por el equipo que lo posee. Las desventajas son que requiere conectividad a la red y que puede estar caído. Ninguno de estos están en mis objetivos de diseño y por lo tanto considero la opción 2 razonable.

La opción 3 es mi favorita. Tiendo a consumir sólo unos pocos puntos finales de cada API que uso, así que burlarme de ellos es trivial. Son ligeros y fiables. La desventaja obvia es que tu simulacro podría no estar sincronizado con el servicio real. Tengan esto en cuenta y escriban pruebas que capten tales cambios en su entorno de montaje o similar.

Un ejemplo (casi) completo

En Pluralsight, trabajo en un proyecto llamado Guías. Puedes comprobarlo en http://www.pluralsight.com/guides. Para hacer todo esto un poco más concreto, me gustaría compartir algo del código real que usamos para gestionar nuestro entorno de desarrollo.

README & Guión de inicio

Lo primero que encontrarías en nuestro repositorio es un LÉAME que te apunta a un guión llamado todo, que es un guión bash que se parece a este:

<...comprobar las dependencias, actualizar las fuentes, crear la infraestructura, actualizar el esquema de la base de datos, insertar los datos de las semillas, reiniciar las aplicaciones...

El trabajo real se divide en varios otros guiones que llamamos. De esta manera puedes ejecutar guiones individuales cuando tenga sentido. Por ejemplo, si actualizas el esquema de la base de datos, puedes aplicarlo rápidamente ejecutando sólo ./actualizar-esquema de la base de datos.

Dependencias

Check-dependencies busca varias dependencias y las resuelve de la mejor manera posible. En algunos casos puede resolverlas automáticamente, como este bit que vincula su archivo .env con nuestra configuración de desarrollo predeterminada, que llamamos .env.example.

función check_failure {code=$?if[["$code" !="0"]]thenecho "El comando falló. Saliendo. "exit$codefi}if[ ! -f "../api/.env"]thenecho "No se encontró ningún archivo .env en la API. Enlace simbólico a .env.ejemplo" ln -s ../api/.env.ejemplo ../api/.env check_failurefi

En algunos casos, no queríamos automatizar completamente. En estos casos, comprobamos si la dependencia está presente y, si no lo está, le decimos lo que creemos que debe hacer. Aquí comprobamos si XCode está instalado y si no, sugerimos cómo podría instalarlo.

xcodebuild -version &gt; /dev/null 2;&amp;1if[[["$?" !="0"]]thenecho "Missing dependence: XCode "echo "Procedimiento de instalación sugerido: Instalar XCode a través del App Store "echo "Una vez instalado XCode, ve a Preferencias &gt; Configuración &gt; abre el desplegable de Herramientas de la Línea de Comandos y selecciona la versión "exit1fi

En las fuentes de actualización sacamos el último código de git e instalamos dependencias con hilo. El repositorio de Guías es un monorepo, así que tenemos que instalar hilo en varios directorios.

git pull(cd ../compartido &amp;&amp; hilo)(cd ../api &amp;&amp; hilo)(cd ../ui &amp;&amp; hilo)(cd ../contenido herramientas-ui &amp;&amp; hilo)

Ejecutando Nuestro Código

Como Guías está compuesto por varios servicios, nos gusta tenerlos todos funcionando en todo momento y reiniciar automáticamente cada vez que hacemos cambios. Usamos el PM2 para esto. Tenemos un único archivo de configuración de PM2 (llamado archivo de ecosistema) que describe cómo deben ejecutarse todos los servicios, así que todo lo que tenemos que hacer es decirle a PM2 dónde encontrar este archivo de configuración y ejecutar el inicio. Se ve así:

{"apps":[{"name": "api", "watch":true, "ignore_watch":["logs"], "cwd": "api", "interpreter": "node", "node_args":"--nolazy -r ts-node/register --inspect=12345", "script": "src/server. ts"},{"name": "ui", "watch":false, "cwd": "ui", "script": "server". js", "args":"-d"},{"nombre": "toolsui", "watch":false, "cwd": "content-tools-ui", "script": "server". js", "args":"-d"},{"nombre": "oyentes", "watch":true, "cwd": "oyentes", "script": "start-all-listeners.js"},{"nombre": "burlas", "watch":true, "cwd": "burlas", "script": "server.js"}]}

Servicios de simulacro

El último servicio que ves ahí se llama burlas. Para mantener las cosas funcionando rápido, localmente y de forma fiable, queremos tener el control total del entorno. Para cualquier servicio del que dependamos y que sea proporcionado por otro equipo o compañía, creamos una versión de prueba de ese servicio y los ponemos en nuestro servicio de prueba.

¿Qué pasa si nos burlamos de ello? ¿No estamos creando problemas de “trabajos para mí”? Hasta cierto punto, sí. Aquí está nuestro flujo de trabajo. Si algo en un script de terceros cambia o se rompe (o simplemente nos equivocamos en nuestra simulación) entonces descubrimos cuando se ejecutan pruebas automatizadas en nuestro entorno de escenario que utiliza servicios reales, no burlas. Una vez que descubrimos el problema, si es necesario, apuntaremos temporalmente un entorno de desarrollo a un servicio real en lugar de un simulacro para ayudar a diagnosticar. Una vez que lo descubrimos, ajustamos el simulacro si es necesario.

Servicios de Backend (Bases de datos, etc.)

Usamos Docker Compose para sacar a relucir todo esto. Aquí hay una parte de ese archivo:

servicios:db:imagen:Postgres:10-alpinevolumes:-/var/lib/postgresql/dataexpose:-"5432 "ports:-"5432:5432 "environment:POSTGRES_USER:rootPOSTGRES_PASSWORD:passwordPOSTGRES_DB:guidesrabbit:image:rabbitmq:3-managementports:-"15672:15672"-"5672:5672"

Todo lo que necesitas para subir los servicios de docker-compose es el comando docker-compose up -d. Sin embargo, hemos creado un script de envoltura llamado recrear-infraestructura que hace algunas cosas adicionales para nosotros.

¿Recuerdas que dije que destruir es importante? Aquí lo hacemos primero con el DC hacia abajo. También cocinamos la bandera de los huérfanos para que nuestras máquinas no se hinchen.

La espera de los postgrados representa una clase importante de cuestiones cuando se trabaja con Docker. Cuando inicias un contenedor docker con docker run … o docker-compose up -d o similar, el script terminará cuando los contenedores hayan sido creados, pero eso no significa que los servicios que corren dentro de los contenedores estén listos. En nuestro ejemplo aquí, queremos crear un rol llamado guías en nuestra base de datos. Si ejecutásemos el comando create role justo después de dc up, fallaría. Así que introdujimos un script que bloquea hasta que Postgres esté listo. Seguimos un patrón similar para algunos otros servicios también.

Este es el aspecto de nuestro guión de espera para el postgrado:

echo "Waiting for Postgres to be ready"./dc exec -T db pg_isreadywhile[[["$?" !="0"]]do sleep 1 ./dc exec -T db pg_isreadydone

Antes de dormir, primero comprueba si Postgres está listo ejecutando pg_isready dentro del contenedor de Postgres. pg_isready es una utilidad que se envía con Postgres. De esta manera hay un mínimo retraso si Postgres ya está listo. Si no está listo, hacemos un bucle hasta que esté listo, durmiendo cada vez.

Inserción de datos de semillas

Tenemos un directorio de archivos SQL que crean nuestros datos de semillas. Un simple script bash los ejecuta.

para f en `ls dev-seed-data/*.sql`;do cat $f| ./dc exec -T -e PGPASSWORD=contraseña db psql -U guides guidesdone

Algunos marcos y ORMs proporcionan formas más elegantes de hacer esto. A veces se les llama accesorios.

Resumen

He compartido lo que pensé que eran las partes más importantes para ayudar a recrear un entorno de desarrollo similar al mío y para entender los valores que motivaron esta configuración. No podría compartir todo sobre mi entorno y seguramente tu entorno tendrá sus propios desafíos. Por lo tanto, la más importante que tengo para ti es hacer un hábito diario de destruir y recrear tus entornos de desarrollo. Esto los forzará a automatizar todo completamente y a saber que siempre funciona.

Categorías: technicalTags: docker, deep dive, javascript