Guía para la gestión de Terraform en AWS: Organización, nomenclaturas y despliegue

Introducción

La revolución de la infraestructura y servicios cloud ha traído de la mano el auge de la infraestructura como código, la cual nos presenta grandes ventajas como la agilidad en la creación de nuevos recursos/infraestructura, igualdad y consistencia entre entornos, limitación de intervenciones manuales y errores humanos, recuperación frente a desastres, etc. Sin embargo, también presenta nuevos problemas como la necesidad de aprender un nuevo lenguaje de programación, organización y mantenimiento del código, dependencias entre diferentes bloques de IaC, impactos cruzados entre aplicativos causados por un despliegue erróneo, etc.

En este post vamos a plantear un modelo de gestión de la IaC que va desde organización del código, despliegues y CI/CD hasta la orientación a autoservicios y obsolescencia del código, que nos permite crear infraestructura de forma ágil y automatizada, impactos localizados en los despliegues y un mantenimiento eficaz.

En los ejemplos que veremos a lo largo del post usaremos Terraform como herramienta de IaC y AWS como servicio cloud, pero lo que vamos a contar es aplicable a cualquier tipo de IaC, orquestador de CI/CD y cloud service provider.

Organización

Cada estructura de IaC que definamos debe ser almacenada en un repositorio de git independiente.

Vamos a realizar una segregación del código de IaC (y de repositorios) en función de su contenido y del uso que se le vaya a dar a los recursos que generare. En base a esto vamos a diferenciar 3 tipologías de repositorios en base a su contenido:

  • Módulos unitarios: Serán los elementos más básicos de nuestra IaC. Definirán un solo recurso aplicando las buenas prácticas para ese recurso como nomenclaturas, seguridad, etc. Por ejemplo, S3 Bucket, IAM Policy, Lambda Function, etc. Estos repositorios no se invocarán directamente, se invocarán desde una estructura de IaC de orden superior, por lo que no tendrán un state. Estos módulos deben ofrecer las variables de entrada necesarias para poder configurar cualquier aspecto del recurso en cuestión. A su vez, debemos facilitar la invocación al módulo dando un valor por defecto a todas las configuraciones en las que sea posible, de modo que no sea necesario especificar todas las opciones cada vez que se invoca al módulo.
  • Blueprints: Esta tipología de repositorio contendrá una IaC que invocará a varios módulos unitarios que se relacionarán entre sí para conformar una arquitectura funcional, o un cierto bloque de recursos que se deben generar en bloque y de manera repetitiva. La idea es que estos blueprints sean genéricos y puedan ser reutilizados en muchas partes de nuestro ecosistema. Por ejemplo, un blueprint de microservicio que nos genere una Lambda Function, el rol de la Lambda, una tabla de DynamoDB, el repositorio de código de la Lambda y el pipeline para su despliegue. En función de cómo organicemos los states de nuestra IaC, estos repositorios pueden ser invocados directamente y tener su state o ser invocados a través de otra IaC de orden superior.
  • Live: Esta tipología de repositorio es opcional, se usaría si no invocamos directamente a los blueprints y se usaría para invocar a todos los blueprints necesarios dentro de una agrupación lógica de recursos.

El primer paso a la hora de empezar con la IaC es definir si vamos a ir con un modelo de usar repositorios de live donde agrupemos todos los blueprints en base a cierta agrupación lógica y tener un state para esa agrupación o si decidimos ir con un modelo en el que se apliquen directamente los blueprints.

Las ventajas del modelo de usar repositorios de live son que puedes relacionar directamente varios blueprints y establecer dependencias entre ellos si es necesario, y que te permite crear recursos adicionales a parte de los blueprints dentro de la misma estructura de IaC. Como desventajas tenemos que hay más repositorios de terraform que manejar y se dificultan los procesos de autoservicios.

La ventaja del modelo de invocar directamente a los blueprints es que no hay que manejar repositorios de terraform adicionales y facilita los procesos de autoservicios. Como desventaja tenemos que para establecer dependencias entre blueprints debemos recurrir a elementos externos como SSM Parameters.

Dentro de los repositorios que tengan state en función del ámbito de los recursos que se creen podemos dividir estos repositorios en 2 tipologías:

  • Común: Este repositorio contendrá recursos cross o comunes a múltiples repositorios, por ejemplo, VPCs, elementos de networking, etc.
  • Específicos: Este repositorio contendrá recursos acotados a un proyecto o una agrupación lógica en concreto, siendo habitual que referencie elementos creados en un repositorio común.

Los repositorios de tipo común deberán almacenar en algún sitio ciertos outputs para que el resto de los repositorios de IaC puedan leer esta información sin tener que hardcodearla en el código (se verá más en detalle en la guía).

Por último, para los repositorios que tengan state, debemos hacer una separación del código de la IaC y las variables que se aplican a cada en cada entorno. De esta forma tendremos otras 2 tipologías de repositorios:

  • Código: Este repositorio tendrá exclusivamente código IaC con todos los valores susceptibles de cambiar por entorno parametrizados. Sobre esta tipología de repositorios realizaremos un ALM, con la política de branching correspondiente.
  • Configuración: Este repositorio tendrá exclusivamente las variables de configuración necesarias para cada entorno. Tendremos una rama por cada entorno.

Nomenclaturas

La nomenclatura es una parte fundamental para mantener el orden y la trazabilidad de los repositorios, así como para gestionar de manera lógica y eficiente los procesos de CI/CD.

Cada empresa puede adaptar la nomenclatura a sus necesidades, pero es importante definir una estructura que se mantenga uniforme en todos los repositorios y que haga referencia a las tipologías de repositorios que hemos mencionado.

Un ejemplo de estructura de nomenclatura puede ser:

$proyecto-$identificador-$tipo_repositorio-$tecnologia

Donde:

  • Proyecto es un identificador lógico referente al “grupo de recursos” al que pertenece el repositorio. Puede dividirse en varios campos para hacer referencia al dominio funcional, departamento, etc al que pertenece.
  • Identificador es un valor que pueda referenciar de manera única al repositorio dentro de cada Proyecto. Debería ser un valor que este alineado con el contenido del repositorio.
  • Tipo_repositorio es el valor correspondiente a alguno de los diferentes tipos de repositorio que hemos visto, es decir modulo, blueprint, código y configuración (excluimos común/especifico porque esta tipología no va a afectar a los procesos de despliegue o CI/CD del repositorio).
  • Tecnología es un identificador del lenguaje de programación utilizado en el repositorio.

Estructura con repositorios live

Para una estructura con repositorios live tendríamos repositorios con las siguientes nomenclaturas.

Módulos unitarios:

  • arquitectura-s3_bucket-module-terraform
  • arquitectura-lambda_function-module-terraform
  • arquitectura-dynamodb_table-module-terraform

Blueprints:

  • arquitectura-microservicio_serverless-blueprint-terraform
  • arquitectura-k8s_cluster-blueprint-terraform
  • arquitectura-vpc-blueprint-terraform

Live:

  • proyecto1-identificador1-codigo-terraform
  • proyecto1-identificador1-configuracion-terraform
  • proyecto1-identificador2-codigo-terraform
  • proyecto1-identificador2-configuracion-terraform

Como dijimos los repositorios de módulos son para la creación de recursos unitarios con todas las características de nomenclaturas y compliance requeridos.

Como dijimos los repositorios de blueprints crearán y relacionarán módulos unitarios para generar una arquitectura funcional completa. No se generará state en estos repositorios por lo que no hay que establecer credenciales para los providers.

Dentro de los repositorios live para cada $proyecto-$identficador tendremos 2 repositorios. El de código tendrá la información de los providers y la invocación a los diferentes blueprints, el código debe ser válido para todos los entornos. El de configuración tendrá las variables de la IaC para cada enotorno.

Estructura sin repositorios live

Para una estructura sin repositorios live en los que se aplican directamente los blueprints, tendríamos las siguientes nomenclaturas.

Módulos unitarios:

  • arquitectura-s3_bucket-module-terraform
  • arquitectura-lambda_function-module-terraform
  • arquitectura-dynamodb_table-module-terraform

Blueprints:

  • arquitectura-microservicio_serverless-blueprint-terraform
  • arquitectura-k8s_cluster-blueprint-terraform
  • arquitectura-vpc-blueprint-terraform

Configuración:

  • proyecto1-microservicio_serverless.app1-configuracion-terraform
  • proyecto1-microservicio_serverless.app2-configuracion-terraform
  • proyecto1-microservicio_serverless.app3-configuracion-terraform
  • proyecto1-k8s_cluster.cluster1-configuracion-terraform
  • proyecto2-vpc.ingress-configuracion-terraform

Los repositorios de módulos son para la creación de recursos unitarios con todas las características de nomenclaturas y compliance requeridos.

Los repositorios de blueprints crearán y relacionarán módulos unitarios para generar una arquitectura funcional completa. En este caso sí que debemos configurar los providers aquí, pero debemos dejarlos parametrizados para que el blueprint se pueda aplicar en diferentes cuentas.

Los repositorios de configuración se utilizan para guardar las variables aplicadas (con ramas por entorno) para cada instanciación del módulo.

Despliegue

Una vez definidas las tipologías/estructuras de los repositorios y sus nomenclaturas vamos a ver como organizamos los despliegues y la arquitectura de los states de terraform.

Entornos

Previamente tenemos que entender los diferentes entornos que vamos a provisionar, pondremos como ejemplo una empresa con 3 entornos, desarrollo, preproducción y producción.

Estos entornos son usados por los equipos de desarrollo, uno de ellos para dar servicio a los usuarios finales (producción) y otros 2 para realizar pruebas previas. Desde el punto de vista de arquitectura, estos 3 entornos deben ser considerados como “productivos” ya que son los que utilizan nuestros “usuarios finales” (los desarrolladores) y no se deberían hacer las pruebas/desarrollos de arquitectura en estos entornos porque ya están siendo utilizados y podemos impactarles. Para las pruebas de los equipos de arquitectura debemos tener un entorno adicional para ello, por ejemplo, poc o laboratorio.

Con esto en mente tenemos que establecer una estrategia para que, en los repos que generan state, tengamos una versión del código (rama develop por ejemplo) sobre la que podamos hacer pruebas. Para el despliegue de ese código se le aplicarán las variables de entorno de poc (ubicadas en el repositorio de configuración, en la rama poc) y se desplegará en dicho entorno.

Una vez finalizadas las pruebas, el código se dará por bueno y se generará una versión final que se desplegará progresivamente en el resto de los entornos, aplicando en cada uno las variables de entorno correspondientes (ubicadas en el repositorio de configuración, en la rama correspondiente).

Política de branching

Una vez aclarado el tema de los entornos y como se debe desplegar en cada uno de ellos vamos a definir una política de branching que nos facilite este ALM. Tenemos 2 tipologías de repositorios que requerirán necesidades ligeramente diferentes.

Por un lado, tenemos los repositorios que no generan state y que serán invocados desde otras estructuras de terraform de orden superior, es decir, los módulos y los blueprints (si utilizamos la estrategia de repositorios live). Para estos repositorios no nos interesa una política de branching que se base en ramas por entorno (ya que el código como tal no se aplica a un solo entorno si no a muchos), debemos usar una política de branching que facilite la creación y liberación de releases como por ejemplo “trunk based development”. De esta forma iremos generando versiones (tags de git) que podrán ser invocadas por diferentes estructuras de terraform.

Por otro lado, tenemos los repositorios que generan state, es decir, los repositorios de live o los repositorios de blueprints (si no utilizamos repositorios de tipo live). Como hemos comentado, en estos repositorios tenemos que poder manejar diferentes versiones del código, una estable para aplicar en los entornos que dan servicio a desarrollo y otra de pruebas para nuevas funcionalidades. Para ello podemos adoptar distintas tipologías de políticas de branching:

  • Política basada en ramas: Se puede utilizar una política estilo Git-Flow, en la que tengamos una rama main/master para la versión estable y una rama develop para las nuevas funcionalidades.
  • Política basada en tags: Se puede utilizar una política estilo “trunk based develpoment”, en el que tengamos identificada la versión estable mediante un tag y la rama trunk/main/master para las nuevas funcionalidades.

Terraform States

Por último, es necesario gestionar la ubicación y nomenclatura de los states para los diferentes entornos.

Para ello lo primero será establecer una configuración de backend de terraform centralizada para todos los repositorios de terraform, es decir usaremos un solo bucket de S3 y tabla de dynamoDB. La ubicación de estos debe ser la cuenta destinada al CI/CD de la compañía.

El siguiente paso es utilizar identificadores únicos para el tfstate de cada repositorio de terraform y sobre eso utilizar los workspaces de terraform para diferenciar cada entorno.

Siguiendo con los ejemplos de nomenclatura del punto anterior tendíamos la siguiente estructura de tfstates:

Con repositorios live: En este caso para la variable de backend “workspace_key_prefix” usaríamos $proyecto y para la variable “key” usaríamos “$identificador.tfstate”.

  • proyecto1/dev/identificador1.tfstate
  • proyecto1/pre/identificador1.tfstate
  • proyecto1/pro/identificador1.tfstate
  • proyecto1/dev/identificador2.tfstate
  • proyecto1/pre/identificador2.tfstate
  • proyecto1/pro/identificador1.tfstate

Sin repositorios live: En este caso para la variable de backend “workspace_key_prefix” usaríamos $proyecto y para la variable “key” usaríamos “$nombre_blueprint-$identificador.tfstate”.

  • proyecto1/dev/microservicio_serverless-app1.tfstate
  • proyecto1/pre/microservicio_serverless-app1.tfstate
  • proyecto1/pro/microservicio_serverless-app1.tfstate
  • proyecto1/dev/microservicio_serverless-app2.tfstate
  • proyecto1/pre/microservicio_serverless-app2.tfstate
  • proyecto1/pro/microservicio_serverless-app2.tfstate
  • proyecto1/dev/k8s_cluster-cluster1.tfstate
  • proyecto1/pre/k8s_cluster-cluster1.tfstate
  • proyecto1/pro/k8s_cluster-cluster1.tfstate
  • proyecto2/dev/vpc-ingress.tfsate
  • proyecto2/pre/vpc-ingress.tfsate
  • proyecto2/pro/vpc-ingress.tfsate

Sigue leyendo sobre cómo establecer pipelines para automatizar los procesos en la segunda parte de este artículo: Guía para la gestión de Terraform en AWS: CI/CD y autoservicio

Referencias

webinar AWS

Tags

Guía de posibilidades profesionales sobre AWS
He leído y acepto la política de privacidad
Acepto recibir emails sobre actividades de recruiting NTT DATA