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:

PiezaComandoEstadoRazón exacta
Panorama del providerterraform providers schema -jsonEjecutadoCorre localmente contra el provider ya instalado; cero red
modules/bedrock-guardrail/terraform fmt / validate (aislado)EjecutadoMódulo nuevo, sin dependencia de LocalStack ni cuenta AWS
BedrockManifestExtractorRoleterraform validate / plan -targetEjecutadoRecurso nuevo; plan nunca necesita leer del state
bedrock.tf completoterraform plan (proyecto completo)EjecutadoPlan: 17 to add, 0 to change, 0 to destroy, sin red
BedrockManifestExtractorRoletflocal apply / awslocal iam get-role-policyRepresentativoIAM sí es Hobby, pero este sandbox no tiene LOCALSTACK_AUTH_TOKEN (límite del entorno, resoluble con un token real)
aws_bedrock_guardrailtflocal applyRepresentativoBedrock 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

  1. 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.
  2. 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.
  3. Terraform Registry — aws_bedrock_guardrail — documentación oficial del recurso central de este proyecto.
  4. 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.
  5. Este módulo, lecciones 1 a 7 — la fuente completa de cada afirmación del ledger de honestidad del Paso 4.