Cómo gestionar el estado de Terraform en AWS
Tags: terraform, aws, cloud, devops
Published: 2023-07-26 18:43:10

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://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpmz8r4uec47n2vsvyx2.png)

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.

```hcl
data "aws_caller_identity" "current" {}
```

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

```hcl
locals {
  principal_arns = var.principal_arns != null ? var.principal_arns : [data.aws_caller_identity.current.arn]
}
```

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

```hcl
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
  }
}
```

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

```hcl
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]
  }
}
```

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

```hcl
resource "aws_iam_policy" "iam_policy" {
  name   = "${local.namespace}-tf-policy"
  path   = "/"
  policy = data.aws_iam_policy_document.policy_doc.json
}
```

6. Finalmente, se adjunta la política creada al rol de IAM, utilizando el recurso `aws_iam_role_policy_attachment`.

```hcl
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 💾

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

```hcl

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

2. **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.

```hcl
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! 🎉

- [LinkedIn](https://www.linkedin.com/in/%F0%9F%91%A8%E2%80%8D%F0%9F%8F%AB-jorge-tovar-71847669/)
- [Twitter](https://twitter.com/jorgetovar621)
- [GitHub](https://github.com/jorgetovar)

If you enjoyed the articles, visit my blog at [jorgetovar.dev](https://jorgetovar.dev).