Si trabajas con AWS a escala, conoces el problema aunque no lo llames por su nombre. Alguien define un estándar de etiquetado en una wiki, en un Confluence, en un PDF que circula por Slack. Y los recursos siguen apareciendo sin Proyecto, sin CentroDeCosto, sin Environment. No por mala fe, sino porque el estándar vive en un documento y la infraestructura vive en el código, y entre los dos no hay nada que los obligue a coincidir.

El costo de esa brecha no es teórico. Sin etiquetas consistentes los reportes de costos dejan de cuadrar, el control de acceso basado en atributos (ABAC) no puede aplicar permisos correctamente, y la automatización (backups, parches, respuesta a incidentes) simplemente salta los recursos que no logra identificar. El tag que falta no se nota el día que lo olvidas. Se nota tres meses después, cuando intentas explicar una factura o aislar un recurso comprometido y descubres que no sabes de quién es.

La pregunta de fondo siempre fue la misma: ¿en qué momento se detecta el recurso sin etiquetar? Y durante mucho tiempo la respuesta fue “después”. Después del apply, con AWS Config marcando el recurso como no conforme. Después, con un script que recorre la cuenta y manda un reporte. Después, con una política de remediación que corre cuando el daño ya está hecho. Detectar “después” significa que el recurso no conforme ya existe, ya costó, ya rompió el reporte.

Desde la versión 6.22 del AWS provider de Terraform hay una respuesta distinta: antes. Antes de crear el recurso, durante el plan.

TLDR

El AWS provider 6.22+ puede validar tus recursos contra las tag policies de tu organización durante terraform plan y terraform apply. Si una organizational tag policy exige el tag Proyecto en un recurso y tu configuración no lo tiene, el plan falla con un error antes de tocar nada en AWS. Se activa con una línea en el bloque del provider:

provider "aws" {
  tag_policy_compliance = "error"
}

Es opt-in (no afecta configuraciones existentes hasta que lo enciendes) y mueve la validación de etiquetado de “después del apply” a “durante el plan”, que es donde el error cuesta segundos en lugar de una auditoría.

Las dos piezas

Esta solución junta dos cosas que mucha gente usa por separado, o que ni conoce: las tag policies de AWS Organizations y la idea de shift-left. El valor está en cómo se combinan, así que vale la pena entender cada una con su implementación al lado.

Pieza 1: las Organization Tag Policies (el qué se exige)

Si administras varias cuentas de AWS bajo una organización, AWS Organizations te deja definir reglas a nivel central y aplicarlas hacia abajo. Una de esas reglas es la tag policy: declaras que ciertos tags son obligatorios para ciertos tipos de recurso, la aplicas a una cuenta o a una OU entera, y a partir de ahí AWS conoce cuáles son las etiquetas requeridas para esos recursos. El estándar deja de ser un documento y pasa a ser una política que vive en la organización.

La tag policy se define como un recurso aws_organizations_policy de tipo TAG_POLICY. La pieza clave es report_required_tag_for, donde declaras para qué tipos de recurso ese tag es obligatorio. El siguiente ejemplo exige el tag Proyecto en los buckets de S3:

resource "aws_organizations_policy" "example" {
  name = "tag-policy-example"
  content = jsonencode({
    "tags" : {
      "Proyecto" : {
        "tag_key" : {
          "@@assign" : "Proyecto"
        },
        "report_required_tag_for" : {
          "@@assign" : [
            "s3:bucket"
          ]
        }
      }
    }
  })
  type = "TAG_POLICY"
}

Luego adjuntas la política al target que quieras, por ejemplo la raíz de tu organización:

data "aws_organizations_organization" "current" {}

resource "aws_organizations_policy_attachment" "example" {
  policy_id = aws_organizations_policy.example.id
  target_id = data.aws_organizations_organization.current.roots[0].id
}

Para aplicarla a una sola cuenta, usas su ID en target_id:

resource "aws_organizations_policy_attachment" "example" {
  policy_id = aws_organizations_policy.example.id
  target_id = "123456789012" # ID de la cuenta destino
}

Y para cualquier otro recurso

El ejemplo usa S3, pero el mecanismo es el mismo para todo. La pieza que define el alcance es report_required_tag_for, donde listas los tipos de recurso para los que ese tag es obligatorio, con el formato servicio:tipo-de-recurso del lado de AWS. s3:bucket mapea al recurso aws_s3_bucket de Terraform, igual que logs:log-group mapea a aws_cloudwatch_log_group, dynamodb:table a aws_dynamodb_table, lambda:function a aws_lambda_function, y así. Para exigir el tag en varios tipos de recurso, los listas todos en el mismo @@assign:

"report_required_tag_for" : {
  "@@assign" : [
    "s3:bucket",
    "dynamodb:table",
    "lambda:function"
  ]
}

Si no quieres enumerar recurso por recurso, AWS soporta el comodín ALL_SUPPORTED por servicio: s3:ALL_SUPPORTED, ec2:ALL_SUPPORTED, rds:ALL_SUPPORTED cubren todos los tipos de recurso soportados de ese servicio en una sola línea. Lo que no existe es un comodín global de “todos los recursos de la cuenta”: el alcance se define por servicio, y solo aplica a los tipos que soportan esta capacidad, no a absolutamente todo lo que existe en AWS. Lo que sí cubre toda tu organización es el otro lado de la ecuación, el target_id del attachment: apuntándolo al root o a una OU, la misma política baja a todas las cuentas que tengas debajo.

Y ojo con un detalle: sin report_required_tag_for, la tag policy solo estandariza el nombre del key o los valores permitidos, pero no obliga a que el tag esté presente. Es ese campo, y no la tag policy en general, el que hace que el tag sea requerido y que ListRequiredTags (y por lo tanto el provider) lo vea.

La correspondencia completa entre identificadores de tag policy y recursos de Terraform está en la guía oficial de Tag Policy Compliance. Y si necesitas más de un tag obligatorio, por ejemplo Proyecto y CentroDeCosto, cada uno es un bloque más dentro de tags, con su propio conjunto de tipos de recurso.

¿Y si creas el recurso sin el tag?

Acá viene el detalle que mucha gente asume al revés. Con la tag policy en su lugar, defines este bucket sin el tag Proyecto y corres terraform apply:

resource "aws_s3_bucket" "example" {
}
aws_s3_bucket.example: Creating...
aws_s3_bucket.example: Creation complete after 2s [id=required-tags-demo]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

El bucket se creó. Sin el tag. La tag policy, por sí sola, no impidió nada. La capacidad de “required tag key” declara qué tags son obligatorios y los expone para que algo los valide, pero no rechaza la operación a nivel de la API. El recurso no conforme aparece recién después, marcado en el reporte de compliance de la tag policy, o cuando AWS Config o un script de auditoría recorren la cuenta y lo encuentran.

Es decir: la primera pieza define el estándar, pero por sí sola te devuelve al “después” del que hablábamos en la intro. Para que el estándar se haga cumplir antes de crear el recurso falta la segunda pieza.

Pieza 2: el shift-left (el cuándo te enteras)

Shift-left es una idea simple: mover la verificación lo más temprano posible en el flujo de trabajo. En lugar de descubrir el problema en producción, lo descubres en el código, antes de aplicar nada. Es la misma lógica de correr los tests en el PR en vez de el día del pase.

Aplicado al etiquetado, el shift-left significa que la validación de la tag policy no espera al apply. Corre en el plan, que es lo que ejecuta tu CI cuando alguien abre un PR. Así que el recurso sin el tag obligatorio hace fallar el plan, el check del PR queda en rojo, y con branch protection ese PR no se puede mergear hasta que se corrija. El problema queda atrapado en la revisión, antes de tocar la rama principal y mucho antes del pase. Te enteraste en el plan, cuando corregir cuesta agregar una línea y no abrir un ticket.

Esto es lo que habilitó la versión 6.22 del AWS provider. La misma política de antes, el mismo bucket sin tags, pero ahora activas la validación con una sola línea en el bloque del provider:

provider "aws" {
  tag_policy_compliance = "error"
}

Con eso encendido, ese bucket sin Proyecto ya no llega al apply. Al correr terraform plan contra una cuenta dentro del target, el plan termina en error:

Error: Missing Required Tags - An organizational tag policy requires the following tags for aws_s3_bucket: [Proyecto]

  with aws_s3_bucket.example,
  on main.tf line 23, in resource "aws_s3_bucket" "example":
  23: resource "aws_s3_bucket" "example" {

El error te dice el tipo de recurso afectado y qué tag falta. Se resuelve agregando el tag, ya sea en el argumento tags del recurso o en el default_tags del provider (que es la forma habitual de aplicar etiquetas a todos los recursos de una configuración).

Compara este resultado con el de la Pieza 1. Antes, el mismo bucket sin tag te devolvía un “Apply complete!” y el problema aparecía después, en un reporte. Ahora el plan termina en error y nunca llega al apply. Misma política, mismo recurso, la única diferencia es una línea en el provider, y el recurso no conforme deja de existir por completo.

Ese argumento acepta tres valores: error detiene el plan, warning te avisa pero deja pasar, y disabled lo apaga. Y no tiene por qué vivir en el código: la variable de entorno TF_AWS_TAG_POLICY_COMPLIANCE hace exactamente lo mismo. Eso te deja activar o desactivar la validación sin tocar la configuración. Si configuras la variable de entorno y el argumento del provider a la vez, el argumento del provider tiene precedencia.

Cómo funciona por debajo (y por qué importa)

Vale la pena entender tres detalles antes de encenderlo en producción.

Necesitas el permiso ListRequiredTags. El principal que ejecuta Terraform tiene que poder llamar a la API ListRequiredTags, que es de donde el provider obtiene las etiquetas requeridas vigentes. Sin ese permiso la validación no puede correr.

No valida todo, valida lo que cambia. La validación de etiquetas requeridas corre durante las operaciones de creación, y durante las de actualización solo cuando los tags cambian. Modificar un recurso existente sin tocar sus etiquetas no dispara la validación. Es una decisión deliberada: no te bloquea un cambio no relacionado solo porque un recurso preexistente quedó fuera de norma.

Corre incluso con -refresh=false. La validación está implementada en la capa de “interceptor” del provider (un CustomizeDiff para los recursos basados en SDK V2, un ModifyPlan para los del Plugin Framework), y esos interceptores se ejecutan aunque pases -refresh=false al plan o al apply. La validación ocurre aunque el recurso remoto no se refresque antes.

Cómo lo encenderías sin romperle el día a nadie

Encender error de golpe en una organización con configuraciones existentes es la forma más rápida de que el equipo desactive la feature en lugar de arreglar los tags. Un mejor camino, si recién introduces tag policies: empieza en warning, deja que los equipos descubran qué flujos no cumplen, y solo después sube a error. Así le das a cada equipo margen para ordenarse antes de que el plan empiece a bloquear.

Lo que realmente cambia

Lo que me parece valioso de esta feature no es la línea de configuración. Es que cierra una brecha que arrastramos por años: la distancia entre el momento en que se define una regla y el momento en que alguien la cumple sin querer romperla. Durante mucho tiempo “etiquetar bien” fue una intención que dependía de la disciplina de cada persona y se verificaba tarde, cuando corregir ya costaba un ticket o una factura.

Ahora la regla y la verificación viven en el mismo lugar donde vive la infraestructura, el código, y se chequean antes de aplicar. Una línea en el provider, y el plan se encarga del resto.