Módulo 6: Finops For Tokens

3. Manos a la obra: `bedrock-budget.rego`, agregada a `cost-policy/`

Descripción

Esta lección escribe el segundo archivo de cost-policy/bedrock-budget.rego, agregado al mismo directorio donde finops-and-cost-guardrails-guide ya dejó required-cost-tags.rego. Nada de eso se toca: bedrock-budget.rego es un archivo nuevo, con package main, hermano de required-cost-tags.rego, y hermano también de policy/ (el directorio de seguridad del Módulo 5). Todo lo que ves aquí corrió de verdad, contra el tfplan.json real del proyecto completo de Andes Cargo — y el resultado, la primera vez que corre, es un FAIL real: bedrock.tf, tal como el Módulo 3 y 4 lo dejaron, todavía no declara el tag que esta política exige.

Conexión con el módulo

La lección 2 estableció el límite exacto: cost-policy/ solo puede evaluar lo que existe en tfplan.json — recursos declarados, nunca invocaciones. aws_bedrock_guardrail sí existe ahí. Esta lección escribe la política que exige, sobre ese recurso específico, el tag Workload=GenAIExtraction — la pieza de asignación de costo que permite, el día en que Bedrock Guardrails empiece a facturar por filtrado de contenido, atribuir ese gasto exactamente a esta carga de trabajo, no a "gasto de IA sin clasificar". La lección 5 cierra el FAIL de esta lección con el HCL corregido.


Analogía: la misma báscula, con una etiqueta nueva que solo esta carga necesita

finops-and-cost-guardrails-guide, Módulo 4, ya comparó required-cost-tags.rego con la báscula del equipaje de un mostrador de aerolínea — no le importa si la maleta contiene algo prohibido, solo si tiene la etiqueta de destino correctamente puesta. bedrock-budget.rego es la misma báscula, en el mismo mostrador, pero con una etiqueta adicional que solo aplica a un tipo específico de carga: piensa en el código de "mercancía frágil" o "carga refrigerada" que algunas maletas necesitan además de la etiqueta de destino estándar. Una maleta sin esa etiqueta específica no es peligrosa —la báscula general (required-cost-tags.rego) ya la dejaría pasar si tiene Project/Environment/CostCenter/Owner—, pero, si contiene algo que necesita ese código adicional y no lo tiene, un sistema de manejo especializado nunca sabrá tratarla distinto. Workload=GenAIExtraction es exactamente ese código adicional: existe únicamente sobre el recurso específico de esta carga de IA, nunca sobre el resto del equipaje de Andes Cargo.


Paso 1 — La regla, en prosa, antes de Rego

La condición que bedrock-budget.rego tiene que detectar, en palabras exactas: cualquier recurso de tipo aws_bedrock_guardrail que se esté creando ("create" en sus acciones), cuyo tag Workload esté ausente, o presente con un valor distinto a "GenAIExtraction". Dos condiciones, no una: a diferencia de required-cost-tags.rego (que solo verifica que la clave de cada tag exista, sin importar su valor), esta política también verifica el valor — porque Workload no es un tag genérico como Project, es un identificador específico que otras piezas de este ecosistema (el M7 de esta guía, cuando calcule SLIs por carga de trabajo) van a usar para filtrar exactamente este recurso, y ningún otro.


Paso 2 — cost-policy/bedrock-budget.rego, completa

# cost-policy/bedrock-budget.rego -- Modulo 6, leccion 3 de genai-on-aws-production-guide.
# Un archivo nuevo en cost-policy/ (finops-and-cost-guardrails-guide, Modulo 4),
# hermano de required-cost-tags.rego -- nada reinstalado, nada reemplazado.
#
# required-cost-tags.rego ya exige cuatro tags genericos (Project, Environment,
# CostCenter, Owner) en todo recurso facturable "clasico" (S3, Lambda, DynamoDB).
# Esta politica agrega una quinta dimension, especifica de la carga de IA: el tag
# Workload, exigido unicamente sobre aws_bedrock_guardrail -- el unico tipo de
# recurso que el Terraform de esta guia declara que es especifico de Bedrock.
# A diferencia de required-cost-tags.rego (que solo verifica que la CLAVE exista),
# esta politica tambien verifica el VALOR exacto -- Workload tiene que ser
# "GenAIExtraction", nunca cualquier string no vacio.
package main

ai_workload_resource_types := {"aws_bedrock_guardrail"}

required_workload_tag_value := "GenAIExtraction"

# Regla 1: falta el tag Workload por completo.
deny contains msg if {
    some rc in input.resource_changes
    rc.type in ai_workload_resource_types
    "create" in rc.change.actions
    tags := object.get(rc.change.after, "tags", {})
    not tags.Workload
    msg := sprintf(
        "%s: missing required cost allocation tag \"Workload\" (AI workload resource type %q) -- add Workload = %q so any future Bedrock-driven cost can be attributed to this workload",
        [rc.address, rc.type, required_workload_tag_value],
    )
}

# Regla 2: el tag Workload existe, pero con un valor distinto al esperado.
deny contains msg if {
    some rc in input.resource_changes
    rc.type in ai_workload_resource_types
    "create" in rc.change.actions
    tags := object.get(rc.change.after, "tags", {})
    tags.Workload
    tags.Workload != required_workload_tag_value
    msg := sprintf(
        "%s: tag \"Workload\" is %q, expected %q -- this workload's cost would be misattributed in Cost Explorer",
        [rc.address, tags.Workload, required_workload_tag_value],
    )
}

Pieza por pieza, todo ya reconocible de required-cost-tags.rego (finops-and-cost-guardrails-guide, Módulo 4, lección 4) y de bedrock-least-privilege.rego (Módulo 5, lección 3, de esta guía):

  • ai_workload_resource_types := {"aws_bedrock_guardrail"}" — un conjunto de un solo elemento, deliberadamente pequeño: la lección 2 ya confirmó que este es el único tipo de recurso que el Terraform de esta guía declara que es específico de Bedrock, distinto de los recursos IAM (que no facturan) y de los tres recursos "clásicos" que required-cost-tags.rego ya cubre.
  • required_workload_tag_value := "GenAIExtraction" — el valor exacto, declarado una sola vez, reusado en ambas reglas y en ambos mensajes.
  • deny contains msg if { ... } — Rego v1, la misma sintaxis obligatoria desde conftest v0.57.0+ que finops-and-cost-guardrails-guide Módulo 4, lección 4 ya verificó con su propio rego_parse_error reproducido a propósito.
  • tags.Workload (Regla 2) — a diferencia de not tags[tag] (que verifica ausencia), tags.Workload en un contexto de verdad verifica presencia con un valor no vacío — la condición exacta que hace que la Regla 2 solo dispare cuando el tag existe pero está mal, nunca cuando está ausente (ese caso ya lo cubre la Regla 1, por separado).
  • Dos reglas independientes, no una condición compuesta — la misma disciplina que bedrock-least-privilege.rego (Módulo 5, lección 3) ya estableció para sus dos vectores (bedrock:* y Resource: "*"): cada mensaje de FAIL señala, con precisión, cuál de los dos problemas ocurrió, sin obligar a quien lo lee a adivinar.

Paso 3 — Confirmando que cost-policy/ y policy/ siguen sin compartir nada

Antes de correr la política, verifica en vivo la garantía que la lección 1 de este módulo prometió — cero archivos compartidos entre los dos directorios de reglas:

ls policy/ cost-policy/
comm -12 <(ls policy/ | sort) <(ls cost-policy/ | sort)

Qué esperar (literal — ejecutado para escribir esta lección):

policy/:
bedrock-least-privilege.rego
least-privilege-iam.rego
no-destroy-shipments.rego
no-public-buckets.rego

cost-policy/:
bedrock-budget.rego
required-cost-tags.rego

comm -12 no imprime nada — la salida vacía es la confirmación: ningún nombre de archivo aparece en las dos listas a la vez. Cuatro archivos de seguridad, dos de costo, ocho nombres en total, cero superposición. bedrock-budget.rego y bedrock-least-privilege.rego viven en directorios distintos aunque compartan la palabra "bedrock" en el nombre — una coincidencia de nomenclatura, nunca de contenido ni de propósito.


Paso 4 — FAIL, contra el estado real de bedrock.tf

tfplan.json es el mismo archivo que el Módulo 5 de esta guía ya generó: terraform show -json tfplan > tfplan.json sobre el proyecto completo, diecisiete recursos, sin LocalStack, sin cuenta AWS.

conftest test tfplan.json -p cost-policy/bedrock-budget.rego

Qué esperar (literal — ejecutado para escribir esta lección):

FAIL - tfplan.json - main - module.manifest_extractor_guardrail.aws_bedrock_guardrail.this: missing required cost allocation tag "Workload" (AI workload resource type "aws_bedrock_guardrail") -- add Workload = "GenAIExtraction" so any future Bedrock-driven cost can be attributed to this workload

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

Este FAIL es el resultado correcto y esperado, no un error de esta lección: bedrock.tf, tal como el Módulo 3 y 4 lo dejaron, declara tags = local.common_tags sobre manifest_extractor_guardrailProject, Environment, ManagedBy—, y common_tags nunca incluyó Workload, porque esa dimensión es nueva de este módulo. La Regla 1 dispara, con precisión: nombra el recurso exacto (module.manifest_extractor_guardrail.aws_bedrock_guardrail.this), el tipo exacto, y el motivo exacto. La Regla 2 —la que revisa un valor incorrecto cuando el tag sí existe— no dispara, y por la misma razón que ya viste en el Módulo 5, lección 3, Paso 3: como el tag no existe en absoluto, la condición tags.Workload de la Regla 2 nunca se cumple, así que esa regla cuenta como passed — no porque el tag esté bien, sino porque la pregunta que esa regla contesta no aplica todavía a este caso.


Paso 5 — Confirmando el segundo vector por separado: un valor incorrecto

El Paso 4 probó la ausencia total del tag — la Regla 1. Vale la pena confirmar, con su propio FAIL independiente, que la Regla 2 también dispara cuando el tag existe pero está mal, no solo cuando falta por completo:

# bedrock.tf -- cambio propuesto, SOLO para esta prueba; revertido antes de seguir
module "manifest_extractor_guardrail" {
  source = "./modules/bedrock-guardrail"
  # ... (resto de argumentos sin cambios)
  tags = merge(local.common_tags, { Workload = "wrong-value" })
}
terraform plan -input=false -no-color -out=tfplan-wrong-value
terraform show -json tfplan-wrong-value > tfplan-wrong-value.json
conftest test tfplan-wrong-value.json -p cost-policy/bedrock-budget.rego

Qué esperar (literal — ejecutado para escribir esta lección):

FAIL - tfplan-wrong-value.json - main - module.manifest_extractor_guardrail.aws_bedrock_guardrail.this: tag "Workload" is "wrong-value", expected "GenAIExtraction" -- this workload's cost would be misattributed in Cost Explorer

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

Este es el caso opuesto al Paso 4: aquí la Regla 1 pasa (tags.Workload sí existe, así que not tags.Workload es falso), y la Regla 2 dispara con su propio mensaje, distinto y específico. Ningún mensaje se confunde con el otro — cada uno señala, con precisión, cuál de los dos ejes de esta política falló. Revierte este cambio de prueba antes de seguir:

rm tfplan-wrong-value tfplan-wrong-value.json

Lo que esta lección deja pendiente, a propósito

El FAIL del Paso 4 no se corrige en esta lección — se corrige en la lección 5, que agrega Workload = "GenAIExtraction" al HCL real. Esta lección termina, deliberadamente, con cost-policy/ mostrando un hallazgo real y sin resolver, exactamente la misma secuencia que finops-and-cost-guardrails-guide Módulo 4 ya siguió: la lección 4 de esa guía escribió la política, la lección 5 la corrió contra un plan sin tags (FAIL), la lección 6 tagueó el HCL real (PASS). Esta lección es el equivalente de esas dos primeras; la lección 5 de este módulo es el equivalente de la tercera.


Errores comunes

Escribir una sola regla deny que combine ausencia y valor incorrecto, en vez de dos reglas independientes (de simplificación prematura). Qué pasa: alguien intenta condensar la lógica en una sola condición (not tags.Workload or tags.Workload != required_workload_tag_value). Cómo detectarlo: si tu política produce un solo mensaje genérico en vez de dos mensajes específicos según cuál de los dos problemas ocurrió. Cómo corregirlo: los Pasos 4 y 5 de esta lección demuestran, con evidencia ejecutada, por qué separarlas importa — la misma disciplina que bedrock-least-privilege.rego (Módulo 5, lección 3) ya estableció para sus propios dos vectores independientes.

Esperar que el FAIL del Paso 4 sea un error de esta lección, y buscar qué está "roto" en bedrock-budget.rego (de mala lectura del resultado). Qué pasa: alguien ve FAIL y asume que la política tiene un bug. Cómo detectarlo: si tu reacción al FAIL del Paso 4 es revisar la sintaxis de la política en vez de revisar el HCL. Cómo corregirlo: el FAIL es correcto — bedrock.tf de verdad no tiene el tag todavía. La política está haciendo exactamente su trabajo: encontrar un gasto sin clasificar antes de que llegue a producción. La lección 5 es donde se corrige el HCL, no la política.

Olvidar restaurar bedrock.tf después del Paso 5, y arrastrar el valor de prueba ("wrong-value") a la lección siguiente (de higiene del proyecto). Qué pasa: alguien corre el escenario del Paso 5, ve el FAIL esperado, y sigue adelante sin revertir el cambio de prueba. Cómo detectarlo: si conftest test tfplan.json -p cost-policy/bedrock-budget.rego sigue mostrando el mensaje de "wrong-value" en vez de "missing" al empezar la lección 5. Cómo corregirlo: bedrock.tf debe quedar exactamente como el Módulo 4 lo dejó (con tags = local.common_tags, sin Workload en absoluto) antes de avanzar — el mismo FAIL original del Paso 4, listo para que la lección 5 lo corrija de verdad.


Ejercicios

Ejercicio 1 — Explica por qué ai_workload_resource_types tiene un solo elemento hoy, y qué pasaría si Andes Cargo agregara aws_bedrock_provisioned_model_throughput en el futuro (Módulo 6, lección 2, Ejercicio 3). ¿Qué línea exacta de bedrock-budget.rego cambiarías?

Ver solución

Cambiarías únicamente la declaración del conjunto: ai_workload_resource_types := {"aws_bedrock_guardrail", "aws_bedrock_provisioned_model_throughput"}. Ninguna otra línea de la política necesita cambiar — ambas reglas deny ya iteran sobre rc.type in ai_workload_resource_types, así que agregar un tipo de recurso al conjunto extiende automáticamente la protección a cualquier recurso de ese tipo nuevo, sin reescribir la lógica de las reglas. La misma propiedad de extensión incremental que el Ejercicio 1 del Módulo 5, lección 3 de esta guía ya practicó.

Ejercicio 2 — Predice el resultado exacto de conftest test tfplan.json -p cost-policy/ (el directorio completo, no solo bedrock-budget.rego) contra el estado actual, antes de que la lección 5 corrija el HCL. ¿Cuántos tests en total, cuántos pasan, cuántos fallan?

Ver solución

required-cost-tags.rego aporta una regla (evaluada contra los tres recursos facturables clásicos, que ya tienen sus cuatro tags completos desde antes de este módulo), y bedrock-budget.rego aporta dos reglas (la Regla 1 y la Regla 2 de esta lección). Contra el estado del Paso 4 —Workload completamente ausente—, el resultado esperado es 3 tests, 2 passed, 0 warnings, 1 failure, 0 exceptions: la regla de required-cost-tags.rego pasa (los tags que sí exige ya están completos), la Regla 1 de bedrock-budget.rego falla (el FAIL de esta lección), y la Regla 2 pasa (vacuamente, porque el tag ni siquiera existe).

Ejercicio 3 — Un compañero propone escribir Workload como parte de common_tags directamente, en vez de crear un local nuevo para la lección 5. Sin mirar la lección 5 todavía, explica, basándote en el alcance de common_tags que ya conoces del Módulo 3, por qué esa propuesta sería un error.

Ver solución

common_tags (declarado en locals.tf desde el Módulo 1) se aplica a todos los recursos de Andes Cargo — el bucket S3, la tabla DynamoDB, la función Lambda determinista, además del guardrail de esta guía. Agregar Workload = "GenAIExtraction" directamente a common_tags etiquetaría, incorrectamente, cada uno de esos recursos "clásicos" como parte de la carga de IA generativa, aunque ninguno de ellos tenga nada que ver con Bedrock. La solución correcta —que la lección 5 construye— es un local nuevo, específico, aplicado únicamente a los recursos que sí pertenecen a esta carga: exactamente la misma disciplina de alcance que la lección 2 de finops-and-cost-guardrails-guide Módulo 4 ya estableció para CostCenter/Owner, reservados solo para recursos facturables, nunca aplicados "por si acaso" a todo el proyecto.


Resumen y siguiente paso

Esta lección escribió cost-policy/bedrock-budget.rego, un segundo archivo agregado a cost-policy/, con dos reglas deny independientes que exigen el tag Workload=GenAIExtraction sobre aws_bedrock_guardrail — el único recurso que el Terraform de esta guía declara que es específico de Bedrock. Confirmaste, con comm -12 real, que cost-policy/ y policy/ siguen sin compartir ningún archivo. Corriste la política contra el tfplan.json real y obtuviste un FAIL genuino (2 tests, 1 passed, 1 failure) — el hallazgo correcto, porque bedrock.tf todavía no declara ese tag. Confirmaste, además, que el segundo vector de la política (un valor de tag incorrecto) dispara de forma independiente, con su propio mensaje preciso.

Antes de avanzar deberías poder: escribir de memoria las dos reglas de esta política; explicar por qué el FAIL de esta lección es el resultado correcto, no un error; y correr comm -12 para confirmar el aislamiento entre policy/ y cost-policy/ sin ayuda.

La lección 4 se aleja de Rego por un momento y extiende bedrock_cost_estimate.py (Módulo 2) con un umbral de presupuesto — el segundo mecanismo de este módulo, fuera del tfplan.json por completo.

Recursos

  1. Open Policy Agent — Policy Reference, Rego v1 — referencia oficial de la sintaxis contains/if y de la evaluación de valores en un mapa.
  2. Conftest — Documentación oficial — referencia de conftest test contra un archivo único (-p cost-policy/bedrock-budget.rego) y contra un directorio completo.
  3. finops-and-cost-guardrails-guide, Módulo 4, lecciones 4 y 5 — el origen exacto de required-cost-tags.rego y del patrón "escribir la política, correrla contra un plan real, ver el FAIL honesto" que esta lección reaplica.
  4. Este curso, Módulo 5, lección 3 — el precedente directo de "un archivo nuevo en un directorio heredado, dos reglas independientes, PASS/FAIL real", aplicado ahí a seguridad.