Home » Pipeline seguro para Terraform
Como crear un Pipeline seguro con Terraform

Pipeline seguro para Terraform

Aprende a construir un pipeline de Terraform robusto y seguro usando GitHub Actions. Este tutorial te guía paso a paso, desde la configuración de SOPS y KMS para la gestión de secretos, hasta la automatización con scripts Bash. ¡Despliega tu infraestructura con confianza y eficiencia!

Hoy decidí escribir mi primer artículo sobre un caso de uso real enfocado a una problemática o necesidad frecuente: Crear un pipeline CI/CD seguro para Terraform.

Y es que muchas veces se requiere automatizar la gestión, aprovisionamiento y mantenimiento de Infraestructura como Código usando Terraform. Pero, en el camino nos topamos con la necesidad de usar secretos (tokens, API keys, credenciales, etc.) como parte del trabajo. Lo más fácil podría ser el almacenar esos datos sensibles directamente en el código y luego versionarlo en Git, pero esto es altamente desaconsejable por los peligros que conlleva.

De esto y más te hablaré a continuación.

Introducción

Desafío enfrentado

Antecedentes

En la compañía donde prestaba servicios, se gestionaba la Infraestructura en AWS de una forma no uniforme y poco óptima:

  • Muchas veces manual desde la consola AWS
  • A veces con Terraform, otras con Ansible, o AWS CLI
  • A veces con jobs de Jenkins (manuales y automáticos)
  • Usando módulos de Terraform dispersos y desorganizados
  • Con secretos guardados «en duro» en el código y versionado en Git
  • Distintos repositorios Git con código Terraform
  • Código disperso entre diferentes ramas «temporales» de Git
  • Entre otros problemas

Mucho de esto venía de un trabajo legado de más de 5 años, así que era en parte entendible.

Problemática

Principales problemas que enfrentaba:

  1. No hay punto único o centralizado de gestión de la infraestructura
  2. Divergencias en el estado de infraestructura y su código (configuration drifts)
  3. Demasiado código legado, duplicado, hecho muy a medida sin reutilización
  4. Cientos de secretos expuestos en Git e incrustadas en el código
  5. Temor a «romper» algo por no saber qué hace qué, cuándo, ni dónde
  6. Mucho tiempo operativo manual invertido: escasa automatización

Las consecuencias de todo esto eran varias, pero principalmente era que todo esto creaba una dependencia casi exclusiva en el creador de todo ese trabajo legado. Es decir, casi solamente él (antiguo ingeniero a cargo) era el único que podía operar y mantener toda esta maraña.

Eso no permitía que los demás ingenieros pudiesen involucrarse en la operación de forma colaborativa. Mucho menos había espacio para las mejoras en eficiencia operativa, seguridad, centralización, observabilidad, entre otros. La verdad, ya el ingeniero a cargo no se daba abasto: estaba muy ocupado manteniendo su «Frankenstein».

Solución propuesta

No voy a decir que cuando llegué yo traje la varita mágica y propuse la solución de la noche a la mañana. Por más que quisiera y supiera el cómo, hacer cambios drásticos genera un impacto grande que toma tiempo en asimilarse por completo. Más aún si había mucha infraestructura ya en Producción que era imposible tocarla para rehacer las cosas de manera mejor y diferente.

Lo que describo a continuación es un compilado muy resumido de las diferentes cosas que tocó hacer durante algunos meses:

  1. Crear repositorios nuevos de Git (imposible reusar los anteriores llenos de secretos en un historial de más de 5 años de git)
  2. Crear módulos reutilizables de Terraform
  3. Gestionar los módulos en unos pocos repositorios Git
  4. Versionar todos los módulos con tags específicos
  5. Crear un repositorio de despliegues de Terraform
  6. Crear un pipeline CI/CD seguro para Terraform en el repositorio de despliegues
  7. Dejar de incrustar secretos en código, o sino hacerlo con los valores cifrados
  8. Forzar el uso de Pull Requests para todo cambio en módulos y/o despliegues de Terraform

De hecho, con el paso del tiempo, hice muchas más cosas de las mencionadas arriba, pero por ahora decido enforcarme solo en las más importantes: todas las directamente relacionadas con conseguir un Pipeline seguro para Terraform.

Arquitectura de la solución

Parte de la planificación del diseño de un Pipeline seguro para Terraform, es necesario tener claridad de todo lo que involucra. Y nada mejor que un diagrama atractivo a la vista, fácil de entender, como el siguiente:

Arquitectura de la solución a implementar

Fig. 1: Arquitectura propuesta para el Pipeline seguro con Terraform

Componentes y propósito

La arquitectura mostrada arriba incluye algunos componentes y entidades con funciones específicas, tal como se describe abajo:

  • Ingeniero: Es la persona que tiene conocimientos para operar este flujo. Como mínimo debe conocer Terraform y el manejo básico de SOPS.
  • Secretos: Pueden ser tokens, API keys, llaves privadas, entre otros. Suelen usarse como parte de un despliegue con Terraform (Ejm: API Key para conectar con MongoDB Cloud para desplegar una instancia de Mongo).
  • SOPS: Herramienta usada para el cifrado de archivos. Si no sabes qué es, te recomiendo leer Cifrado simple con SOPS y KMS. Se usa para cifrar secretos que están en texto plano antes de guardarlos en el repositorio Git.
  • Amazon KMS: Servicio de Gestión de Llaves Criptográficas que es usado por SOPS para el cifrado y descifrado de secretos.
  • Repositorio Git: Es donde se almacena el código Terraform y los archivos (secretos) usados como parte de los despliegues de infraestructura. El contenido debe estar libre de cualquier secreto en texto plano.
  • Eventos Git: Cuando se publica código en el repositorio Git se hace a través de un commit seguido de una operación de push. Este evento se usa para disparar un pipeline o workflow que automatiza tareas.
  • GitHub Actions: Plataforma de Integración Continua gestionada por GitHub que tiene la lógica y detalle de los jobs a ejecutar como parte del pipeline o workflow. Este invoca a Terraform u otras posibles herramientas como parte del proceso.
  • Terraform: Herramienta que desde el código .tf aprovisiona la infraestructura en la nube de AWS (puede ser en cualquier otra nube). A su vez, Terraform invoca a SOPS para descifrar los secretos cifrados y luego poder usarlos en texto plano como parte de su despliegue.

El porqué de cada componente

En el listado anterior mencioné algunos componentes como herramientas tecnológicas a tener en cuenta en la arquitectura propuesta para un Pipeline seguro para Terraform. Si el lector ya conoce de otras opciones equivalentes, quizá se pregunte el porqué de mi elección por estas:

1. ¿Por qué SOPS y no GitHub Secrets o AWS Secrets Manager?

Estas tres herramientas, incluyendo otras equivalentes como Azure Key Vault o Google Secret Manager son fantásticas. Todas podrían cumplir el mismo objetivo, pero algunas de ellas podrían no cumplir con ciertas necesidades y el entorno. Por ejemplo:

  • GitHub Secrets no permite versionar los secretos. En cambio, secretos cifrados en texto con SOPS sí serían versionados en Git.
  • AWS Secrets Manager (o sus equivalentes en otras nubes) sí versionan secretos, pero no lo hacen dentro de Git. Es decir, si buscas que Git sea tu principal fuente de versionamiento, tanto de código y de secretos (ligados al código), entonces no lo lograrás con esta opción. Tendrás que ir a la consola de la herramienta y consultar las versiones, de forma separada a las versiones del histórico del repositorio Git.
  • AWS Secrets Manager (o sus equivalentes en otras nubes) tienen un costo por secreto, el cual podría ser considerable si hablamos de cientos o miles de ellos. En cambio, secretos cifrados en texto con SOPS, no implicarían gasto extra, salvo el costo mensual de mantener una llave de Amazon KMS (u otra equivalente).

2. ¿Por qué Amazon KMS para uso con SOPS?

Algo distinto a Amazon KMS para la integración con SOPS en el cifrado de texto es perfectamente posible. Puede usarse servicios equivalentes como Azure Key Vault, Google KMS, Hashicorp Vault, o incluso llaves PGP. Usa cualquiera de las opciones que se acomode mejor a tu entorno.

3. ¿Por qué GitHub Actions y no Jenkins?

GitHub Actions es una de las herramientas de CI/CD más modernas y usadas en la actualidad. Pero, si en cambio usas GitLab CI, Jenkins u otra opción equivalente, es totalmente válido. Opté por GH Actions porque podría ser lo que es más usado en estos días y además funciona muy bien.

4. ¿Por qué Terraform, si ya no es Open Source?

No es materia de este artículo entrar en los detalles sobre el cambio de licencia de Terraform, pero opté por esta herramienta porque es la más conocida y ampliamente usada para la gestión de Infraestructura como Código. Está muy bien documentada y con amplio soporte comunitario.

Alternativamente, podrías usar OpenTofu (fork netamente Open Source de Terraform) o Pulumi.

Implementación

Llegó el momento de poner manos a la obra de todo lo antes descrito y empezar la implementación de tu Pipeline seguro para Terraform.

1. Nuevos repositorios Git

Si vas a empezar a hacer bien las cosas, empieza por tener los repositorios de Git limpios de secretos y otros datos sensibles. Por ello, sugiero empezar con repos nuevos y se requerirán por lo menos dos:

  • Repositorio de módulos reutilizables de Terraform
  • Repositorio de despliegues de Terraform

2. Crear módulos reutilizables de Terraform

Si decides crear tus propios módulos de Terraform, es importante que los organices de forma apropiada en uno o más repositorios dedicados.

Si no tienes claro cómo crear tus propios módulos, te recomiendo leer Escribiendo módulos de Terraform, artículo que escribí hace un tiempo atrás.

En cambio, si decides hacer uso de repositorios externos o públicos, te recomiendo empezar a revisar si ya existe uno para tu necesidad en el Terrafom Registry.

3. Gestionar los módulos en unos pocos repositorios Git

Si optas por crear tus propios módulos de Terraform, principalmente tienes dos opciones de cómo organizarlos:

  • Un único repositorio para todos tus módulos: Aquí se debe crear subdirectorios, uno por módulo, para que sean la fuente única de consulta a la hora de desplegar infraestructura. Revisa Fig. 2 para una mejor idea.
  • Un repositorio por cada módulo: En este caso el contenido del módulo (archivos .tf) se coloca directamente en la raíz del repositorio.

El primero sería recomendable si tienes pocos módulos, aún sencillos y que no se modifican muy a menudo. Si ese es tu caso, aquí debajo te muestro cómo sería la estructura interna del repositorio y los módulos dentro.

Estructura de un repositorio centralizado de módulos de Terraform

Fig. 2: Repositorio de módulos reutilizables de Terraform

4. Versionar todos los módulos con tags específicos

Independientemente si usas un único repositorio centralizado, o uno por cada módulo, siempre aplica tags de Git siguiendo el versionamiento semántico.

De este modo, cuando decidas desplegar infraestructura a partir de tus módulos, puedes elegir qué versión específica usar. En caso de problemas, puedes retornar a una versión previa.

5. Crear un repositorio de despliegues de Terraform

En este caso, se crea un único repositorio donde se centraliza todas las configuraciones de Terraform para los despliegues de infraestructura.

Acá se debe colocar solamente los módulos root. Es decir, módulos que llamen a módulos hijo desde el repositorio indicado en el punto 1. No se recomienda crear código reutilizable dentro de este repositorio.

Estos módulos root deben, preferentemente, solo invocar instancias de los módulos reutilizables a los cuales se le pasan parámetros (inputs o variables) para decidir cómo aprovisionar cierto tipo de infraestructura.

Por ejemplo, usando el módulo aws-network (ver Fig. 2), desplegar una VPC en 3 AZs, con 2 subnets por cada AZ, usando el bloque CIDR 10.78.0.0/16 para la VPC, con 1 NAT Gateway por AZ, y además que… (más personalizaciones soportadas por el módulo).

Para una mejor idea, revisa Fig. 3 y 4, a continuación.

Estructura de un repositorio centralizado de despliegues de Terraform

Fig. 3: Repositorio de despliegues de Terraform

Ejemplo de despliegue de un módulo reutilizable de Terraform

Fig. 4: Ejemplo de despliegue de módulo reutilizable de Terraform

6. Crear un pipeline CI/CD seguro para Terraform en el repositorio de despliegues

Como mencioné líneas arriba, opté por usar GitHub Actions para crear el pipeline CI/CD. Pero, explicar toda la lógica detrás del pipeline, paso a paso, convertiría a este post en uno muy largo.

Así que el workflow de GitHub Actions para Terraform lo puedes revisar en mi repositorio GitHub, directamente en este enlace:

https://github.com/arengifoc/github-actions/terraform-workflow

Este workflow asume que se usa un repositorio de despliegues de Terraform, como se menciona en el paso 6. Cada vez que se crea un Pull Request con la rama main como destino y que se agreguen o modifiquen archivos Terraform .tf se disparan unos jobs que tiene por objetivo detectar cambios y luego ejecutar terraform plan (mientras el PR esté abierto) o terraform apply (al fusionar y cerrar el PR).

Importante:

  • Este workflow asume que ya se tienen configurados los secretos de GitHub con los nombres AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY, los mismos que son credenciales válidas de AWS.

7. Dejar de incrustar secretos en código, o sino hacerlo con los valores cifrados

Todo este esfuerzo no valdría la pena si se insiste en la misma mala práctica de escribir secretos y otros datos sensibles en texto plano en el código. En su lugar, debería usarse alguna alternativa, tal como es el caso de SOPS con AWS KMS.

Veamos este ejemplo de cómo alguien normalmente crearía un recurso con Terraform:

# main.tf
resource "aws_db_instance" "default" {
  allocated_storage    = 10
  db_name              = "mydb"
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.micro"
  username             = "foo"
  password             = "foobarbaz"
  parameter_group_name = "default.mysql8.0"
  skip_final_snapshot  = true
}

Claramente, el problema es que username y password están en texto plano. Si esto lo versionamos en Git, sería un gran error. ¿Cómo lo hacemos de forma correcta, entonces?

Primero, creo un archivo de credenciales en JSON, llamado credenciales.json:

{
    "username": "foo",
    "password": "foobarbaz"
}

Luego, procedo a cifrarlo usando SOPS y mi llave AWS KMS preconfigurada. El resultado lo almaceno en credenciales.enc.json:

sops -e --output credenciales.enc.json credenciales.json

Si no conoces SOPS, te recomiendo leer Cifrado simple con SOPS y KMS.

Ahora, hay que modificar nuestro código Terraform para que haga uso de SOPS, como sigue:

# main.tf
terraform {
  required_providers {
    sops = {
      source  = "carlpett/sops"
      version = "1.1.1"
    }
  }
}

provider "sops" {}

data "sops_file" "secrets" {
  source_file = "credenciales.enc.json"
}

resource "aws_db_instance" "default" {
  allocated_storage    = 10
  db_name              = "mydb"
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.micro"
  username             = data.sops_file.secrets.data["username"]
  password             = data.sops_file.secrets.data["password"]
  parameter_group_name = "default.mysql8.0"
  skip_final_snapshot  = true
}

Ahora, sí es seguro versionar en Git los archivos main.tf y credenciales.enc.json. Esto es lo que realmente logra el objetivo de crear un Pipeline seguro para Terraform, precisamente lo que buscamos con este post.

Importante:

  • Dado que el archivo de credenciales cifrado con SOPS requiere el uso una llave Amazon KMS, se supone también que las credenciales AWS configuradas en el paso 6 como secretos de GitHub permiten el uso de dicha llave KMS como parte del pipeline CI/CD.

8. Forzar el uso de Pull Requests para todo cambio en módulos y/o despliegues de Terraform

Por último, a fin de proteger el contenido de los repositorios de módulos y despliegues de Terraform, es necesario configurar reglas para que no se hagan cambios en la rama principal (Ejm: main) con operaciones de push directo, sino a través de la creación de Pull Requests.

En GitHub, se va a las opciones de configuración del repositorio, luego a la sección de Rules y se agrega una donde se debe marcar la casilla «Require a pull request before merging», similar a esto:

Regla que fuerza el uso de Pull Requests para hacer cambios en una rama de Git

De este modo, el workflow CI/CD se activará solo cuando se creen, actualicen y/o cierren los Pull Requests.

Conclusión

El post fue extenso, pero creo que pudo valer la pena. He intentado no escatimar en detalles, lo necesario para que se entienda bien la problemática y solución frente a la necesidad de crear un Pipeline seguro para Terraform.

Esto representa un caso de uso real y es así como se trabaja hoy en día en muchos lados. Personalmente, yo uso variantes de esta misma estrategia, así que no es algo «inventado», sino un enfoque realista.

Ahora es tu turno: Cuéntame qué te pareció el artículo, ¿te animas a ponerlo en práctica? ¿crees que hay algo más por mejorar en el proceso? Házmelo saber con comentarios.

Y si el artículo te fue de valor o crees que podría interesarle a alguien más, no dudes en compartirlo.

Hasta la próxima.

Post navigation

Deja un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *