Módulo 3: Infrastructure As Code For An Ai Endpoint
8. Proyecto: la infraestructura de IA de Andes Cargo, declarada
Descripción
Las siete lecciones anteriores construyeron, por partes, la infraestructura completa de la primera carga de IA generativa de Andes Cargo: el mecanismo que hace posible declararla sin una API real (lección 1), el panorama de recursos disponibles (lección 2), el módulo reusable (lección 3), el rol de mínimo privilegio (lección 4), el plan completo (lección 5), y la frontera exacta de lo que se puede aplicar de verdad, con dos razones distintas para dos recursos distintos (lecciones 6 y 7). Este proyecto final entrega el repositorio completo, bedrock.tf y modules/bedrock-guardrail/ en su forma final, con el ledger de honestidad que cierra el módulo con más peso ejecutado de toda esta guía.
Conexión con el módulo
Este es el segundo entregable con formato de ADR que esta guía produce —el primero fue ADR-001-llm-as-escalation-path.md—, pero esta vez el entregable es código, no un documento: el HCL mismo, validado y planeado de verdad, es la evidencia de que la decisión de arquitectura del Módulo 1 ya tiene infraestructura real detrás. El Módulo 4 extiende exactamente estos mismos archivos —nunca los reescribe— con las tres políticas de guardrail restantes.
Paso 1 — bedrock.tf, la versión final de este módulo
# bedrock.tf -- Andes Cargo's first generative AI workload. ADR-001 (Module 1)
# fixed the LLM as an escalation path, not the default; this file is the exact
# infrastructure that decision requires -- nothing more. Module 2 drafted a single
# aws_bedrock_guardrail resource here just to test Infracost against it (see
# GENAI-COST-PROFILE.md, section 6). Module 3 replaces that draft with the real
# module (modules/bedrock-guardrail/, lesson 3) and adds BedrockManifestExtractorRole,
# the least-privilege role extract-shipment-manifest-fields assumes to call
# bedrock:InvokeModel (lesson 4). Module 4 extends the guardrail with the remaining
# policies (topic, contextual grounding, word filters); this file does not build
# those yet.
module "manifest_extractor_guardrail" {
source = "./modules/bedrock-guardrail"
name = "andes-cargo-manifest-extractor-guardrail"
blocked_input_messaging = "This input is not allowed due to content policy violations."
blocked_outputs_messaging = "This output is not allowed due to content policy violations."
content_filters = [
{
type = "PROMPT_ATTACK"
input_strength = "HIGH"
output_strength = "NONE"
}
]
pii_entities = [
{
type = "EMAIL"
action = "ANONYMIZE"
},
{
type = "PHONE"
action = "ANONYMIZE"
}
]
tags = local.common_tags
}
data "aws_iam_policy_document" "bedrock_manifest_extractor_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
data "aws_iam_policy_document" "bedrock_manifest_extractor_permissions" {
statement {
sid = "InvokeManifestExtractionModelOnly"
effect = "Allow"
actions = [
"bedrock:InvokeModel",
]
# Scoped to exactly one model ARN -- never "bedrock:*", never a wildcard ARN:
# Module 5, lesson 3 tests this exact statement against bedrock-least-privilege.rego.
resources = [
local.manifest_extractor_model_arn,
]
}
}
module "bedrock_manifest_extractor_role" {
source = "./modules/iam-role"
role_name = "BedrockManifestExtractorRole"
trust_policy_json = data.aws_iam_policy_document.bedrock_manifest_extractor_trust.json
permissions_policy_json = data.aws_iam_policy_document.bedrock_manifest_extractor_permissions.json
tags = local.common_tags
}
Y el local que fija el ARN del modelo, agregado a locals.tf junto a common_tags:
locals {
common_tags = {
Project = "andes-cargo"
Environment = var.environment
ManagedBy = "terraform"
}
# The exact Bedrock model BedrockManifestExtractorRole is allowed to invoke.
# GENAI-COST-PROFILE.md (Module 2, section 2) chose Amazon Nova Lite. Scoping
# bedrock:InvokeModel to this single ARN -- never "bedrock:*", never a wildcard
# ARN -- is the least-privilege decision Module 3, lesson 4 builds.
manifest_extractor_model_arn = "arn:aws:bedrock:${var.aws_region}::foundation-model/amazon.nova-lite-v1:0"
}
Paso 2 — modules/bedrock-guardrail/, el molde completo
# modules/bedrock-guardrail/variables.tf
variable "name" {
description = "Name of the Bedrock guardrail."
type = string
}
variable "blocked_input_messaging" {
description = "Message returned to the caller when an input is blocked by the guardrail."
type = string
}
variable "blocked_outputs_messaging" {
description = "Message returned to the caller when a model output is blocked by the guardrail."
type = string
}
variable "content_filters" {
description = "Content policy filters (e.g. PROMPT_ATTACK, HATE, SEXUAL). Each entry sets input/output detection strength."
type = list(object({
type = string
input_strength = string
output_strength = string
}))
default = []
}
variable "pii_entities" {
description = "PII entity types to detect in the sensitive information policy, with the action to take on each (BLOCK or ANONYMIZE)."
type = list(object({
type = string
action = string
}))
default = []
}
variable "tags" {
description = "Tags applied to the guardrail."
type = map(string)
default = {}
}
# modules/bedrock-guardrail/main.tf
resource "aws_bedrock_guardrail" "this" {
name = var.name
blocked_input_messaging = var.blocked_input_messaging
blocked_outputs_messaging = var.blocked_outputs_messaging
dynamic "content_policy_config" {
for_each = length(var.content_filters) > 0 ? [1] : []
content {
dynamic "filters_config" {
for_each = var.content_filters
content {
type = filters_config.value.type
input_strength = filters_config.value.input_strength
output_strength = filters_config.value.output_strength
}
}
}
}
dynamic "sensitive_information_policy_config" {
for_each = length(var.pii_entities) > 0 ? [1] : []
content {
dynamic "pii_entities_config" {
for_each = var.pii_entities
content {
type = pii_entities_config.value.type
action = pii_entities_config.value.action
}
}
}
}
tags = var.tags
}
# modules/bedrock-guardrail/outputs.tf
output "guardrail_arn" {
description = "ARN of the created guardrail."
value = aws_bedrock_guardrail.this.guardrail_arn
}
output "guardrail_id" {
description = "ID of the created guardrail."
value = aws_bedrock_guardrail.this.guardrail_id
}
output "name" {
description = "Name of the created guardrail."
value = aws_bedrock_guardrail.this.name
}
Paso 3 — Verificando el repositorio, de punta a punta
cd andes-cargo-infra/
terraform fmt -check -recursive
echo "fmt exit: $?"
terraform validate
terraform plan -input=false -no-color -out=tfplan-m3-final
Qué esperar (literal — corrido de verdad, sin LocalStack, sin cuenta AWS, en este entorno, para cerrar este módulo):
fmt exit: 0
Success! The configuration is valid.
Plan: 17 to add, 0 to change, 0 to destroy.
Y, aislando solo lo que este módulo agregó, con el mismo comando -target de la lección 5:
terraform show -json tfplan-m3-final | python3 -c "
import json, sys
data = json.load(sys.stdin)
new = [rc['address'] for rc in data['resource_changes']
if 'manifest_extractor' in rc['address']]
print(len(new), 'recursos nuevos de este módulo:')
for addr in sorted(new):
print(' -', addr)
"
Qué esperar (literal):
3 recursos nuevos de este módulo:
- module.bedrock_manifest_extractor_role.aws_iam_role.this
- module.bedrock_manifest_extractor_role.aws_iam_role_policy.this
- module.manifest_extractor_guardrail.aws_bedrock_guardrail.this
Paso 4 — El ledger de honestidad de este módulo
Igual que ADR-001 (Módulo 1) y GENAI-COST-PROFILE.md (Módulo 2) cerraron sus módulos con un documento explícito de qué es qué, este proyecto cierra con la misma disciplina, en una sola tabla:
| Pieza | Comando | Estado | Razón exacta |
|---|---|---|---|
| Panorama del provider | terraform providers schema -json | Ejecutado | Corre localmente contra el provider ya instalado; cero red |
modules/bedrock-guardrail/ | terraform fmt / validate (aislado) | Ejecutado | Módulo nuevo, sin dependencia de LocalStack ni cuenta AWS |
BedrockManifestExtractorRole | terraform validate / plan -target | Ejecutado | Recurso nuevo; plan nunca necesita leer del state |
bedrock.tf completo | terraform plan (proyecto completo) | Ejecutado | Plan: 17 to add, 0 to change, 0 to destroy, sin red |
BedrockManifestExtractorRole | tflocal apply / awslocal iam get-role-policy | Representativo | IAM sí es Hobby, pero este sandbox no tiene LOCALSTACK_AUTH_TOKEN (límite del entorno, resoluble con un token real) |
aws_bedrock_guardrail | tflocal apply | Representativo | Bedrock es "Included in Plans: Ultimate" — ningún token de Hobby lo resuelve, en ninguna máquina (límite del servicio) |
La distinción entre las dos últimas filas —ambas representativas, por razones distintas— es, precisamente, el argumento completo de la lección 7: no toda etiqueta "representativo" de esta guía significa lo mismo, y confundirlas escondería información real y útil para cualquiera que replique este trabajo en su propia máquina.
Errores comunes
Copiar bedrock.tf y modules/bedrock-guardrail/ a un proyecto nuevo sin locals.tf ni variables.tf (de olvidar las dependencias silenciosas). Qué pasa: alguien copia solo los dos archivos nuevos de este módulo, sin local.manifest_extractor_model_arn, local.common_tags ni var.aws_region, y obtiene un error de referencia no definida. Cómo detectarlo: terraform validate falla con Reference to undeclared local value o Reference to undeclared input variable. Cómo corregirlo: bedrock.tf depende de tres piezas que viven en otros archivos del mismo proyecto —locals.tf (los dos local usados), variables.tf (var.aws_region, ya con default = "us-east-1" desde terraform-and-iac-guide)—; cópialas junto con bedrock.tf, o declara sus equivalentes en el proyecto nuevo.
Presentar este proyecto como "la infraestructura completa de Andes Cargo para GenAI" sin aclarar que el guardrail solo tiene dos de las cinco políticas posibles (de alcance incompleto sin declararlo). Qué pasa: alguien, en una entrevista o en un README, describe el guardrail de este módulo como "el guardrail completo de la aplicación". Cómo detectarlo: si tu descripción de andes-cargo-manifest-extractor-guardrail no menciona que faltan temas denegados, grounding contextual y filtros de palabras. Cómo corregirlo: el comentario del encabezado de bedrock.tf ya lo dice explícitamente —"Module 4 extends the guardrail with the remaining policies... this file does not build those yet"—; repite esa misma honestidad al describir el trabajo, la misma disciplina que sostiene toda esta guía.
No verificar Plan: 0 to change, 0 to destroy antes de considerar el módulo cerrado (de dar por sentado que "17 to add" es suficiente confirmación). Qué pasa: alguien revisa solo el número de recursos a agregar, sin confirmar que nada existente cambió o se destruyó. Cómo detectarlo: si tu verificación de este proyecto se detiene en "vi 17 to add" sin leer el resto de la línea. Cómo corregirlo: los tres números —to add, to change, to destroy— importan juntos. 0 to destroy es la confirmación de que agregar la infraestructura de IA no puso en riesgo, ni por accidente, un solo recurso ya construido por las tres guías anteriores — exactamente la promesa aditiva que el diseño de esta guía hizo desde el principio.
Ejercicios
Ejercicio 1 — Verifica, tú mismo, que el ledger de honestidad de esta lección coincide exactamente con lo que las lecciones 1 a 7 de este módulo ya demostraron. Repasa la tabla del Paso 4 fila por fila y confirma contra qué lección específica se sostiene cada afirmación.
Ver solución
Fila 1 (panorama del provider) → lección 2. Fila 2 (modules/bedrock-guardrail/) → lección 3. Fila 3 (BedrockManifestExtractorRole, validate/plan) → lección 4. Fila 4 (bedrock.tf completo) → lección 5. Fila 5 (rol IAM, representativo por el entorno) → lecciones 4 y 7. Fila 6 (guardrail, representativo por el servicio) → lección 6. Si alguna fila no encuentra su lección exacta, revisa que no se haya introducido una afirmación nueva sin evidencia — la misma disciplina de trazabilidad que ADR-001 y GENAI-COST-PROFILE.md ya exigieron de sí mismos.
Ejercicio 2 — Explica, a un entrevistador técnico hipotético, por qué este módulo se describe como "el más ejecutado de la guía" a pesar de que ninguno de los dos recursos de bedrock.tf se aplicó de verdad en este entorno. Responde en dos o tres frases, sin repetir la palabra "representativo" más de una vez.
Ver solución
Una respuesta completa suena, más o menos, así: "'Ejecutado' en este módulo no se refiere a apply —se refiere a fmt, validate y plan, los tres comandos que confirman que la infraestructura declarada es correcta y completa, y los tres corrieron de verdad, con salida literal, sobre cada pieza de este módulo, sin excepción. Un plan limpio con diecisiete recursos y cero cambios inesperados es una señal real de calidad de ingeniería, no un sustituto de haber creado los recursos —y esta guía nunca finge que lo sea—; el límite de aplicar contra Bedrock es del servicio mismo (solo el plan de pago más alto de LocalStack lo incluye), no una limitación del código ni de cómo se escribió esta guía."
Ejercicio 3 — Predice qué cambiaría en este proyecto si, en el Módulo 4, se agregaran las tres políticas restantes del guardrail (temas, grounding contextual, palabras). Basándote en la estructura de modules/bedrock-guardrail/ de esta lección, ¿el archivo bedrock.tf necesitaría cambiar, o solo el módulo?
Ver solución
Ambos necesitarían cambiar, pero de forma proporcional a lo que cada uno ya hace. modules/bedrock-guardrail/variables.tf y main.tf necesitarían tres variables nuevas (denied_topics, grounding_filters, word_filters o nombres equivalentes) y tres bloques dynamic nuevos, siguiendo exactamente el mismo patrón de doble anidamiento que content_policy_config y sensitive_information_policy_config ya establecen — ningún cambio a lo que ya funciona, solo adición. bedrock.tf, por su parte, solo necesitaría pasar los nuevos argumentos a la llamada module "manifest_extractor_guardrail" ya existente —ni el rol IAM, ni la estructura del archivo, cambiarían—. Es exactamente el mismo tipo de crecimiento sin ruptura que el Ejercicio 3 de la lección 3 de este módulo ya anticipó.
Resumen y siguiente paso
Este proyecto final del Módulo 3 entregó bedrock.tf y modules/bedrock-guardrail/ en su forma completa: un guardrail de Bedrock con dos políticas activas (contenido, información sensible) declarado a través de un módulo reusable con bloques dynamic, y BedrockManifestExtractorRole, con bedrock:InvokeModel acotado a un único ARN de modelo. Confirmaste, de nuevo, fmt limpio, validate exitoso y Plan: 17 to add, 0 to change, 0 to destroy sobre el proyecto completo — y cerraste con un ledger de honestidad de seis filas que distingue, con precisión, cuatro piezas ejecutadas de verdad y dos representativas, cada una con su propia razón exacta, nunca una etiqueta genérica.
Antes de avanzar deberías poder: recitar, de memoria, los tres recursos exactos que este módulo agrega a andes-cargo-infra/; explicar la diferencia entre un límite representativo "del entorno" y uno "del servicio", con un ejemplo de cada uno tomado de este mismo proyecto; y defender, frente a cualquier pregunta, por qué "el módulo más ejecutado de la guía" es una afirmación honesta, no una exageración de mercadeo.
Con bedrock.tf y modules/bedrock-guardrail/ declarados, validados y planeados, el Módulo 3 completo —ocho lecciones, desde el mecanismo que hace posible validate/plan sin red hasta este proyecto— queda atrás. El Módulo 4 retoma exactamente estos mismos archivos y les agrega lo que todavía falta: las tres políticas restantes del guardrail, y la pregunta que ningún validate puede contestar por sí solo — por qué un guardrail gestionado, por completo que esté, nunca es, por sí solo, suficiente defensa.
Recursos
terraform-and-iac-guide, Módulo 5, lección 8 (08-project-andes-cargos-module-library.md) — el mismo patrón de proyecto de cierre de módulo, aplicado a los primeros dos módulos de Andes Cargo.ADR-001-llm-as-escalation-path.md(Módulo 1, lección 8 de esta guía) — la fuente de la fila que asigna a este módulo la responsabilidad de declarar la infraestructura del camino de escalamiento.- Terraform Registry —
aws_bedrock_guardrail— documentación oficial del recurso central de este proyecto. - LocalStack Docs — Bedrock — la fuente de la última fila del ledger de honestidad, verificada de nuevo en la lección 6 de este módulo.
- Este módulo, lecciones 1 a 7 — la fuente completa de cada afirmación del ledger de honestidad del Paso 4.