menú

Uso de Pipelines para Administrar Entornos empleando Código como Infraestructura

Las herramientas como Terraform, CloudFormation y Heat son una excelente manera de definir la infraestructura del servidor para implementar software. La configuración para aprovisionar, modificar y reconstruir un entorno se captura de manera transparente, repetible y comprobable. Si se usan correctamente, estas herramientas nos dan confianza para afinar, cambiar y refactorizar nuestra infraestructura de manera fácil y cómoda.

Pero como la mayoría de nosotros descubrimos después de usar estas herramientas por un tiempo, hay dificultades. Cualquier herramienta de automatización que facilita la implementación de una corrección en una infraestructura en expansión también facilita la aparición de un cock-up (algo hecho mal o ineficientemente) en una infraestructura en expansión.  

¿Alguna vez has dañado los archivos /etc/hosts en cada servidor en su estado de no producción, haciendo imposible la inserción (mediante ssh) en cualquiera de ellos, o ejecutar la herramienta nuevamente para corregir el error? Yo sí.

Lo que necesitamos es una forma de realizar y probar cambios de forma segura, antes de aplicarlos a los entornos que te interesan. Esto es  Entrega de Software 101: siempre prueba una nueva compilación en un entorno de prueba antes de implementarla en vivo. Pero la mejor manera de estructurar el código de su infraestructura para hacer esto no es necesariamente obvia.

Voy a describir algunas formas diferentes en que las personas hacen esto:
  • Coloca todos los entornos en una sola pila
  • Define cada entorno en una pila separada
  • Crea una sola definición de pila y la promueve a través del pipeline
En pocas palabras, la primera forma es mala; la segunda forma funciona bien para configuraciones simples (dos o tres entornos, sin muchas personas trabajando en ellos), y la tercera tiene más partes móviles, pero funciona bien para grupos más grandes y complejos.

Antes de profundizar en esto, aquí hay un par de definiciones: uso el término pila (o instancia de pila) para referirme a un conjunto de infraestructura que es definida y administrada como una unidad. Esto se corresponde directamente con una pila de AWS CloudFormation, y también con el conjunto de infraestructura correspondiente a un archivo de estado de Terraform. También hablo de una definición de pila como un archivo, o un conjunto de archivos, usados por una herramienta para crear una pila. Esto podría ser una carpeta con un montón de archivos *.tf para Terraform, o una carpeta de archivos de plantilla de CloudFormation.

Mostraré algunos ejemplos de código utilizando Terraform y AWS, pero los conceptos se aplican a casi cualquier herramienta declarativa de Código como Infraestructura (Infrastructure as Code) y cualquier plataforma de infraestructura dinámica y automatizada.
También, en aras de la simplicidad, asumiré dos entornos — pre-producción (staging) y producción. Muchos equipos terminan con más entornos (desarrollo, control de calidad, aceptación de usuarios (UAT), pruebas de rendimiento, etc.) — pero, nuevamente, los conceptos son los mismos.

Una pila con todos los ambientes

Este es el enfoque más directo, el que la mayoría de la gente comienza a usar y el más problemático. Todos los entornos, desde desarrollo hasta producción, se definen en una sola definición de pila, y todos se crean y administran como una única instancia de pila.


[Múltiples entornos gestionados como una sola pila]

El ejemplo de código a continuación, muestra una única configuración de Terraform para los entornos de pre-producción y producción:

# STAGING ENVIRONMENT

resource “aws_vpc” “staging_vpc” {

  cidr_block = “10.0.0.0/16”

}

resource “aws_subnet” “staging_subnet” {

  vpc_id = “${aws_vpc.staging_vpc.id}”

  cidr_block = “10.0.1.0/24”

}

resource “aws_security_group” “staging_access” {

  name = “staging_access”

  vpc_id = “${aws_vpc.staging_vpc.id}”

}

resource “aws_instance” “staging_server” {

  instance_type = “t2.micro”

  ami = “ami-ac772edf”

  vpc_security_group_ids = [“${aws_security_group.staging_access.id}”]

  subnet_id = “${aws_subnet.staging_subnet.id}”

}

# PRODUCTION ENVIRONMENT

resource “aws_vpc” “production_vpc” {

  cidr_block = “10.0.0.0/16”

}

resource “aws_subnet” “production_subnet” {

  vpc_id = “${aws_vpc.production_vpc.id}”

  cidr_block = “10.0.1.0/24”

}

resource “aws_security_group” “production_access” {

  name = “production_access”

  vpc_id = “${aws_vpc.production_vpc.id}”

}

resource “aws_instance” “production_server” {

  instance_type = “t2.micro”

  ami = “ami-ac772edf”

  vpc_security_group_ids = [“${aws_security_group.production_access.id}”]

  subnet_id = “${aws_subnet.production_subnet.id}”

}

Este es un enfoque simple que hace que todo sea visible en un solo lugar, pero no aísla los entornos entre sí. Hacer un cambio en el entorno de ensayo corre el riesgo de romper el de producción. Y los recursos pueden filtrarse o confundirse entre los entornos, lo que hace que sea aún más fácil afectar accidentalmente a un entorno no deseado.

Charity Majors compartió los problemas que enfrentó con este enfoque  utilizando Terraform. El radio de explosión (¡un gran término!) de un cambio es todo lo incluido en la pila. Y ten en cuenta que esto sigue siendo cierto incluso sin los archivos de estado de Terraform. Definir múltiples entornos en una sola pila de CloudFormation es pedir por problemas.  

Una definición de pila separada para cada entorno

Charity (y otros) sugieren dividir sus entornos en definiciones de pila separadas. Cada entorno tendría su propio directorio con su propia configuración Terraform:


./our-project/staging/main.tf

./our-project/production/main.tf


Estas dos configuraciones diferentes deben ser idénticas, o casi iguales. Al ejecutar tu herramienta de infraestructura contra cada una de ellas por separado, se aíslan los entornos entre sí (al menos cuando se trata de usar la herramienta, aunque obviamente pueden estar aislados o no en términos de redes, permisos de cuenta en la nube, etc.) Y como cada entorno tiene su propio conjunto de archivos de definición, el estado deseado de cada entorno es muy claro.


[Cada instancia de pila definida en su propia pila]

Cuando necesites realizar un cambio, edita los archivos para la pila del entorno de pre-producción, aplícalos y pruébalos. Repite hasta que funcione como  esperas. A continuación, realiza los mismos cambios en los archivos para el entorno de producción y aplícalos a la instancia de la pila de producción.

Un inconveniente de este enfoque es que es fácil que las diferencias se introduzcan en los entornos. Alguien podría tener prisa y hacer un cambio en el entorno de producción sin probarlo primero en el estado de pre-producción. Incluso si funciona bien, es posible que se olvide de respaldar sus cambios en los archivos del entorno de pre-producción. Estas diferencias tienen una forma de acumularse hasta el punto en que las personas ya no confían en que pre-producción realmente se parece a producción. Debería ser posible eliminar el entorno de pre-producción y copiar los archivos de producción actuales para actualizar pre-producción, pero esto puede causar problemas con más grandes equipos y bases de código.

Así que este patrón requiere vigilancia para mantener el código base consistente. Esto se puede gestionar mediante módulos, compartiendo código entre entornos. Pero esto agrega complejidad de versionado. Si alguien realiza un cambio en un módulo para realizar pruebas en el módulo de ensayo, debe tener cuidado de evitar que el cambio se aplique a la producción antes de que esté listo. Los módulos pueden extraerse de la base de código del proyecto e importarse mediante el control de versiones. Pero esto añade más partes móviles.

Una definición de pila manejada con pipeline

Una alternativa es utilizar un pipeline de entrega continua para promover un archivo de definición de pila en todos los entornos. Cada entorno tiene su propia instancia de pila, por lo que el radio de explosión para un cambio está contenido en el entorno.

Pero una sola definición de pila se reutiliza para crear y actualizar cada entorno. La definición se puede parametrizar para capturar las diferencias entre instancias, como el tamaño del clúster. El archivo de definición está versionado, por lo que tenemos visibilidad de qué código se utilizó para cualquier entorno, en cualquier momento.



[Un solo archivo de definición utilizado para crear múltiples instancias de pila en el pipeline]

Las partes móviles para implementar esto son: un repositorio de origen, como un repositorio Git; un depósito de artefactos; y un servidor de CI (integración continua) o CD (entrega continua) como GoCD, Jenkins, etc.

Un ejemplo simple de flujo de trabajo es:
  1. Alguien aplica un cambio en el repositorio de origen.
  2. El servidor de CD detecta el cambio y coloca una copia de los archivos de definición en el repositorio de artefactos, con un número de versión.
  3. El servidor de CD aplica la versión de definición al primer entorno, luego ejecuta pruebas automatizadas para verificarlo.
  4. Alguien gatilla el servidor de CD para aplicar la versión de definición a la producción

Beneficios


[Flujo básico de una definición de pila a través de un pipeline]

Los equipos con los que trabajo utilizan pipelines para infraestructura por las mismas razones que los equipos de desarrollo usan pipelines para su código de aplicación. Garantiza que cada cambio se haya aplicado a cada entorno y que las pruebas automatizadas hayan pasado. Sabemos que todos los entornos son definidos y creados consistentemente.

Con el enfoque anterior de "definición de una pila por entorno", crear un nuevo entorno requiere crear una nueva carpeta con su propia copia de los archivos. Luego, estos archivos deben mantenerse y actualizarse con los cambios realizados en otros entornos.

Pero el enfoque con pipeline es más flexible. Las nuevas instancias de entorno pueden activarse bajo pedido, lo que tiene varios beneficios:
  • Los desarrolladores pueden crear sus propias instancias de sandbox (para “jugar”), para que puedan implementar y probar aplicaciones basadas en la nube, o trabajar en los cambios en las definiciones de entorno, sin entrar en conflicto con otros miembros del equipo.
  • Los cambios y las implementaciones se pueden manejar con un enfoque blue-green: crea una nueva instancia de la pila, la pruebas, luego intercambias el tráfico y destruyes la instancia anterior.
  • Quienes prueban, revisores y otros pueden crear entornos según sea necesario, derribándolos cuando no se utilizan,


Repositorio de artefactos, promoción y control de versiones

Un pipeline trata una definición de pila como un artefacto versionado, promoviéndolo de una etapa a la siguiente. Según Entrega Continua, un artefacto nunca es cambiado. Es por esto que sugiero un repositorio de artefactos separado además del repositorio de control de versiones. El control de versión se utiliza para gestionar los cambios en la definición, el repositorio de artefactos se utiliza para preservar artefactos inmutables y versionados. Sin embargo, he visto a personas usar su repositorio de código para ambos, lo que funciona bien siempre y cuando se asegure de que el principio de no realizar cambios en una versión de definición una vez que haya sido "publicado" y utilizado para cualquier entorno.

Para las definiciones de pila, me gusta usar un depósito de S3 (o un equivalente) como repositorio. Tengo una etapa de pipeline que "publica" las definiciones creando una carpeta en el depósito con un número de versión en el nombre y copiando los archivos en ella. Este código es ejecutado por un agente de servidor de CI/CD:

aws s3 sync ./our-project/ s3://our-project-repository/1.0.123/


Promover una versión de una definición de pila se puede hacer de diferentes maneras. Con el depósito S3, a veces tendré una carpeta con el nombre del entorno y copiaré los archivos de la versión correspondiente en esa carpeta. De nuevo, este código se ejecuta desde un agente de CI/CD:

aws s3 sync — delete \
  s3://our-project-repository/1.0.123/ \
  s3://our-project-repository/staging/


La etapa de pipeline para cualquier entorno simplemente ejecuta Terraform (o CloudFormation), capturando las definiciones de la carpeta correspondiente.


Ejecutando la herramienta

Los comandos están prácticamente ejecutados por el servidor de CI o CD en un agente. Esto hace algunas cosas buenas. Primero, asegura que el comando esté completamente automatizado. No tienes que confiar en "solo un" paso o ajuste manual, que se entiende como "solo uno más, por ahora". No terminas con diferentes personas haciendo las cosas a su manera. No terminas con pasos indocumentados. Y no tienes que preocuparte, en una situación de emergencia, de que alguien se olvide de un paso crucial y entorpezca el entorno. Si hay un paso que no se puede automatizar fácilmente, busca un camino.

Otra cosa buena es que no debes preocuparte que diferentes personas apliquen cambios al mismo entorno. Nadie aplica cambios a un entorno común ejecutando la herramienta desde su propia máquina. No hay que preocuparse por el bloqueo y desbloqueo. No hay que preocuparse por los cambios editados localmente. Los únicos cambios realizados en un entorno que te interesa son los que se han introducido en el pipeline.

Esto también ayuda con la consistencia. Nadie puede realizar un cambio que requiera un complemento especial o una utilidad que hayan instalado en su propia computadora portátil. No hay ninguna modificación que alguien haya hecho al aplicar un cambio en pre-producción, que podría olvidarse en producción. La herramienta siempre se ejecuta desde un agente de CD, que se crea y configura de forma coherente, siempre utilizando el mismo script, sin importar desde qué entorno se ejecute.

Flujo de trabajo del Desarrollador

Las personas que trabajan en la infraestructura probablemente necesiten realizar y probar rápidamente cambios en las definiciones de pila, antes de aplicarlas en el pipeline. Este es el único lugar donde puedes querer ejecutar la herramienta localmente. Pero cuando lo haces, lo está haciendo con su propia instancia de la pila de entorno. Obtén la última copia de las definiciones del repositorio de origen, ejecuta la herramienta para crear el entorno y luego comienza a realizar cambios. (Por supuesto, escribe algunas pruebas automatizadas, que también deberían estar en el repositorio de origen y ejecutarse automáticamente en los entornos relevantes en el pipeline).

Cuando estés satisfecho, asegúrate de haber sacado las últimas actualizaciones del master (rama principal del repositorio de código) y de que se hayan superado las pruebas. Luego realiza los cambios y destruye tu entorno.

No hay que preocuparse por quebrar un entorno común como el de prueba. Si alguien más está trabajando en cambios que podrían entrar en conflicto con los tuyos, debería verlos cuando los obtenga del master. Si los cambios se combinan bien, pero causan problemas, deben quedar atrapados en el primer entorno de prueba en el pipeline. La construcción se pone roja, todos se detienen y miran qué se necesita hacer para solucionarlo.

¿Cuándo es esto apropiado?

He estado utilizando pipelines para gestionar la infraestructura durante años. Parecía una forma obvia de hacerlo, ya que los equipos de ThoughtWorks usan pipelines para publicar software como una tarea rutinaria. Pero recientemente me he encontrado con personas experimentadas que mantienen un archivo de definición separado para cada entorno.

En algunos casos, creo que las personas simplemente no han estado expuestas a la idea de utilizar un pipeline para la infraestructura. De ahí este artículo. Pero también soy consciente de que estos dos enfoques diferentes (y quizás otros que no conozco) son apropiados para diferentes contextos.

El enfoque de pipeline tiene más partes móviles que simplemente tener una definición de pila separada para cada entorno. Para equipos más pequeños, con un proceso de publicación simple y sin muchos entornos, un pipeline puede ser una exageración. Pero cuando hay más personas trabajando en un sistema, y ​​especialmente cuando hay varios equipos y roles, un pipeline crea un proceso consistente y confiable para implementar cambios en la infraestructura. También puede tener sentido incluso para los equipos más pequeños que ya están utilizando un pipeline para su software para agregar sus archivos de infraestructura en él, en lugar de tener un proceso separado para entornos y aplicaciones.

Hay algunas advertencias. Se necesita tiempo para sentirse cómodo haciendo cambios a la infraestructura mediante el uso de un pipeline. Es posible que no obtenga tanto valor sin al menos un cierto grado de pruebas automatizadas, que es otra disciplina a aprender. Usar herramientas de administración de infraestructura, automáticamente, desde un agente de servidor de CD requiere un enfoque seguro para administrar secretos, para evitar que su sistema de automatización se convierta en un blanco de ataque fácil.

Más allá del pipeline

El enfoque de pipeline que he descrito responde a la pregunta de cómo administrar varias instancias de un entorno en particular, como un "camino a la producción". Existen otros desafíos con la administración de una base de código de infraestructura compleja. ¿Cómo compartes código, infraestructura y/o servicios entre equipos? ¿Cuál es la mejor manera de estructurar y publicar módulos de código de infraestructura compartida?

También he pasado algún tiempo pensando en los patrones que he visto para dividir un solo entorno en varias pilas, los cuales describí en una charla. Si este artículo trata sobre la aplicación de los principios y prácticas de la Entrega Continua a la infraestructura, aquella charla fue sobre la aplicación de principios de Microservicios a la infraestructura.

Espero que este artículo le brinde a la gente algo para pensar, y me gustaría escuchar qué otros enfoques han probado y han sido útiles.
 

Reconocimietos 

Además de la inspiración de Charity Majors y Yevgeniy Brikman, recibí comentarios de mis colegas de ThoughtWorks, Nassos Antoniou, Andrew L., Pat Downey, Rafael Gomes, Kevin Yeung, y Joe Ray, y también de Antonio Terreno.

Traducción al español realizada por: Daniel Santibañez