DEV Community

Cover image for Cómo gestionar el estado de Terraform en AWS
Jorge Tovar for AWS Community Builders

Posted on

Cómo gestionar el estado de Terraform en AWS

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

Terraform 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:

  1. 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" {}
Enter fullscreen mode Exit fullscreen mode
  1. Luego, se define un bloque de variables locales. En este caso, se define una variable local llamada principal_arns. Esta variable se asignará a var.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 datos aws_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]
}
Enter fullscreen mode Exit fullscreen mode
  1. 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 variable local.namespace, y en la política de asunción, se especifica qué entidades (en este caso, los ARNs almacenados en local.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
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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]
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode

Recursos del Remote state 💾

  1. aws_s3_bucket: Crea un bucket (depósito) de S3 en AWS. Se le da un nombre basado en local.namespace y se especifica si el bucket debe permitir la eliminación forzada o no, utilizando el valor de var.force_destroy_state. También se le asigna una etiqueta ResourceGroup basada en local.namespace.

  2. 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 en aws_kms_key.kms_key.arn.

  3. aws_s3_bucket_versioning: Habilita la versión del bucket de S3 que se creó previamente. La versióning_configuration con status = "Enabled" indica que el versionado de objetos está habilitado en el bucket.

  4. 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.

  5. aws_dynamodb_table: Crea una tabla de DynamoDB en AWS. La tabla se nombra con base en local.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 etiqueta ResourceGroup basada en local.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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 referencia aws_s3_bucket.state_bucket.bucket, donde aws_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 referencia data.aws_region.current.name, donde data.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 referencia aws_iam_role.iam_role.arn, donde aws_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 referencia aws_dynamodb_table.state_lock_table.name, donde aws_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.

  1. 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.

  1. 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
|   |   |-- ...
Enter fullscreen mode Exit fullscreen mode

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                      
}
Enter fullscreen mode Exit fullscreen mode

Happy coding! 🎉

If you enjoyed the articles, visit my blog at jorgetovar.dev.

Top comments (1)

Collapse
 
mgcenteno profile image
Mariano Gabriel Centeno

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 💪🏽