Seguramente habrás notado que cada vez que ejecutas los comandos de terraform apply
o terraform plan
, de alguna manera Terraform identifica qué recursos han sido creados, cuáles no, y las diferencias con respecto a las configuraciones. La respuesta es el estado de Terraform, que en resumen es un JSON con toda la información relevante de los recursos manejados y su estado actual. 📝
Código en GitHub
https://github.com/jorgetovar/terraform-aws-remote-state
También habrás notado que para poder trabajar en equipo, normalmente debemos compartir este estado. Al comienzo de un proyecto con IaC, usualmente guardamos el estado de Terraform en el repositorio de GitHub. Sin embargo, esto es un problema debido a que en el estado de Terraform tenemos información, como secretos, que no queremos que esté pública.
Además, resolver conflictos entre los cambios de infraestructura puede ser un dolor de cabeza. En el pasado, cometí este error y en parte se debía a que no usábamos un pipeline para desplegar los recursos; en su lugar, lo hacíamos desde nuestro entorno local. 💥
Problemas con el terraform.tfstate local 🤯
El archivo que se genera de forma local por defecto es terraform.tfstate, y los problemas en proyectos donde existen varios integrantes es que de algún modo tenemos que compartir este archivo. Otro problema son las race conditions; de hecho, dos ingenieros pueden ejecutar un deploy de manera simultánea, generando inconsistencias y la corrupción del archivo de estado.
Finalmente, es importante mantener aislado nuestro archivo de estado en relación a los ambientes que tenemos desplegados. Fue un proyecto difícil en su momento, pero apenas pudimos mover el estado a AWS, generar los bloqueos con DynamoDB y ejecutar desde un pipeline con los permisos estrictamente necesarios, todo volvió a la normalidad y es incluso placentero hacer cambios en la infraestructura.
Rol y privilegios necesarios para crear los recursos del Remote state 🤖
Para crear nuestro remote state necesitamos create un role con los privilegios necesarios para desplegar, una tabla en DynamoDB y finalmente un bucket en el S3.
Vamos a desglosar el código paso a paso:
- Primero, se define un bloque de datos llamado
aws_caller_identity
, que se utiliza para obtener la identidad del que ejecuta los cambios de infra actual. Esto se utiliza más adelante para definir los permisos del rol.
data "aws_caller_identity" "current" {}
- Luego, se define un bloque de variables locales. En este caso, se define una variable local llamada
principal_arns
. Esta variable se asignará avar.principal_arns
si tiene algún valor; de lo contrario, se asignará a un array con el ARN (Amazon Resource Name) obtenido del bloque de datosaws_caller_identity
. Esta variable se utilizará más adelante para especificar quién puede asumir este rol.
locals {
principal_arns = var.principal_arns != null ? var.principal_arns : [data.aws_caller_identity.current.arn]
}
- A continuación, se define un recurso de tipo
aws_iam_role
, que es el rol de IAM que estamos creando. Se le da un nombre basado en la variablelocal.namespace
, y en la política de asunción, se especifica qué entidades (en este caso, los ARNs almacenados enlocal.principal_arns
) pueden asumir este rol.
resource "aws_iam_role" "iam_role" {
name = "${local.namespace}-tf-assume-role"
assume_role_policy = <<-EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"AWS": ${jsonencode(local.principal_arns)}
},
"Effect": "Allow"
}
]
}
EOF
tags = {
ResourceGroup = local.namespace
}
}
- Luego, se define un bloque de datos llamado
aws_iam_policy_document
, que se utiliza para especificar los permisos que se otorgarán a la entidad o servicio que asuma el rol. Aquí se definen tres declaraciones de política para permitir el acceso a recursos específicos, como listado de un bucket de S3, acceso a objetos en el bucket de S3 y acceso a una tabla de DynamoDB.
data "aws_iam_policy_document" "policy_doc" {
statement {
actions = [
"s3:ListBucket"
]
resources = [
aws_s3_bucket.state_bucket.arn
]
}
statement {
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
resources = [
"${aws_s3_bucket.state_bucket.arn}/*",
]
}
statement {
actions = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
resources = [aws_dynamodb_table.state_lock_table.arn]
}
}
- Luego, se crea un recurso de tipo
aws_iam_policy
, que es la política de IAM que contiene los permisos definidos en el bloque de datos anterior.
resource "aws_iam_policy" "iam_policy" {
name = "${local.namespace}-tf-policy"
path = "/"
policy = data.aws_iam_policy_document.policy_doc.json
}
- Finalmente, se adjunta la política creada al rol de IAM, utilizando el recurso
aws_iam_role_policy_attachment
.
resource "aws_iam_role_policy_attachment" "policy_attach" {
role = aws_iam_role.iam_role.name
policy_arn = aws_iam_policy.iam_policy.arn
}
Recursos del Remote state 💾
aws_s3_bucket
: Crea un bucket (depósito) de S3 en AWS. Se le da un nombre basado enlocal.namespace
y se especifica si el bucket debe permitir la eliminación forzada o no, utilizando el valor devar.force_destroy_state
. También se le asigna una etiquetaResourceGroup
basada enlocal.namespace
.aws_s3_bucket_server_side_encryption_configuration
: Configura la encriptación en el lado del servidor para el bucket de S3 que se creó anteriormente. Se define una regla para aplicar la encriptación predeterminada del lado del servidor a todos los objetos en el bucket utilizando una clave de KMS (Key Management Service) especificada enaws_kms_key.kms_key.arn
.aws_s3_bucket_versioning
: Habilita la versión del bucket de S3 que se creó previamente. La versióning_configuration constatus = "Enabled"
indica que el versionado de objetos está habilitado en el bucket.aws_s3_bucket_public_access_block
: Configura el bloqueo de acceso público para el bucket de S3. Esto asegura que ciertas configuraciones no permitan el acceso público a objetos en el bucket, evitando que se configuren políticas o permisos públicos no deseados.aws_dynamodb_table
: Crea una tabla de DynamoDB en AWS. La tabla se nombra con base enlocal.namespace
y tiene una clave de hash llamada "LockID" de tipo "S" (cadena). La modalidad de facturación se establece en "PAY_PER_REQUEST", lo que significa que solo se paga por las operaciones realizadas. También se le asigna una etiquetaResourceGroup
basada enlocal.namespace
.
resource "aws_s3_bucket" "state_bucket" {
bucket = "${local.namespace}-state-bucket"
force_destroy = var.force_destroy_state
tags = {
ResourceGroup = local.namespace
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.state_bucket.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.kms_key.arn
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_versioning" "versioning_example" {
bucket = aws_s3_bucket.state_bucket.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "s3_bucket" {
bucket = aws_s3_bucket.state_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "state_lock_table" {
name = "${local.namespace}-state-lock"
hash_key = "LockID"
billing_mode = "PAY_PER_REQUEST"
attribute {
name = "LockID"
type = "S"
}
tags = {
ResourceGroup = local.namespace
}
}
Output 📚
description
: Es una descripción de lo que contiene este output. Proporciona información sobre qué valores se incluirán en la salida. En este caso, se menciona que el output contendrá detalles de configuración, como el nombre del bucket de S3 creado, la región de AWS del bucket de S3, el ARN del rol de IAM creado para el backend y el nombre de la tabla DynamoDB creada para el bloqueo.-
value
: Es el valor real que se incluirá en el output. Aquí, se define un mapa que contiene los siguientes campos:-
bucket
: Contiene el nombre del bucket de S3 creado. Esto se obtiene a través de la referenciaaws_s3_bucket.state_bucket.bucket
, dondeaws_s3_bucket.state_bucket
es el recurso que crea el bucket y.bucket
se refiere al atributo "bucket" de ese recurso. -
region
: Contiene el nombre de la región de AWS en la que se creó el bucket de S3. Esto se obtiene a través de la referenciadata.aws_region.current.name
, dondedata.aws_region.current
es un bloque de datos que obtiene información sobre la región actual y.name
se refiere al atributo "name" de ese bloque de datos. -
role_arn
: Contiene el ARN (Amazon Resource Name) del rol de IAM creado para el backend. Esto se obtiene a través de la referenciaaws_iam_role.iam_role.arn
, dondeaws_iam_role.iam_role
es el recurso que crea el rol de IAM y.arn
se refiere al atributo "arn" de ese recurso. -
dynamodb_table
: Contiene el nombre de la tabla DynamoDB creada para el bloqueo. Esto se obtiene a través de la referenciaaws_dynamodb_table.state_lock_table.name
, dondeaws_dynamodb_table.state_lock_table
es el recurso que crea la tabla DynamoDB y.name
se refiere al atributo "name" de ese recurso.
-
Aislar el estado por ambiente ♟️
Para aislar el estado de Terraform y evitar conflictos al trabajar con diferentes ambientes (por ejemplo, desarrollo, pruebas y producción), existen dos enfoques principales: el uso de workspaces y el diseño del layout del proyecto.
- Workspaces (Espacios de trabajo): Terraform proporciona el concepto de "workspaces" para manejar múltiples instancias aisladas del estado de configuración. Cada workspace es una copia independiente del estado, lo que permite que diferentes configuraciones coexistan sin interferir entre sí.
Para usar workspaces:
-
Crear un nuevo workspace: Puedes crear un nuevo workspace con el comando
terraform workspace new <nombre_workspace>
. -
Cambiar de workspace: Puedes cambiar entre workspaces con el comando
terraform workspace select <nombre_workspace>
. -
Listar workspaces: Puedes ver una lista de los workspaces disponibles con
terraform workspace list
.
Es importante tener en cuenta que los workspaces comparten el mismo código de configuración, por lo que debes ser cuidadoso al compartir recursos comunes entre ellos para evitar conflictos.
- Layout del proyecto: El diseño del layout del proyecto es una práctica que implica organizar el código de Terraform en diferentes directorios para aislar los ambientes y componentes. Cada directorio contiene su propio archivo de configuración y estado de Terraform.
Por ejemplo:
project
|-- dev
| |-- main.tf
| |-- variables.tf
| |-- ...
|-- staging
| |-- main.tf
| |-- variables.tf
| |-- ...
|-- production
| |-- main.tf
| |-- variables.tf
| |-- ...
|-- modules
| |-- module-aws-community-builder
| | |-- main.tf
| | |-- variables.tf
| | |-- ...
| |-- module-jt-state
| | |-- main.tf
| | |-- variables.tf
| | |-- ...
Cada directorio (dev, staging, production) representa un ambiente diferente y contiene su propio archivo de configuración, variables, y puede tener su propio estado de Terraform.
El diseño del layout del proyecto proporciona un aislamiento más claro entre los ambientes y permite una mayor flexibilidad en la administración del estado y las configuraciones.
Conclusion 📖
Cuando trabajamos con infraestructura como código (IaC), la importancia de aislar, bloquear y gestionar el estado radica en las severas consecuencias que pueden tener los errores en este contexto. A diferencia del desarrollo de aplicaciones, los errores en el código de infraestructura pueden afectar todas las aplicaciones, bases de datos, etc. Por tanto, es crucial incluir mecanismos adicionales al trabajar con IaC.
provider "aws" {
region = "us-west-2"
}
module "remote_state" {
source = "jorgetovar/remote-state/aws"
version = "1.0.2"
}
output "state_config" {
value = module.s3backend.config
}
Happy coding! 🎉
If you enjoyed the articles, visit my blog at jorgetovar.dev.
Top comments (1)
Muy buen articulo, gracias por compartir. Si te parece, te doy una idea para seguir con la misma tematica, mejores practicas para trabajar con Terraform desplegando infraestructura multi region, que opinas? Sigue asi 💪🏽