Módulo 3: Infrastructure As Code For An Ai Endpoint

5. Manos a la obra: `terraform plan` de la infraestructura de IA completa

Descripción

Las lecciones 3 y 4 construyeron dos piezas por separado: el módulo bedrock-guardrail y BedrockManifestExtractorRole. Esta lección las junta, dentro de bedrock.tf, y corre terraform plan sobre el proyecto completo de andes-cargo-infra/ — todo lo que las tres guías anteriores de este ecosistema ya construyeron, más las dos piezas nuevas de este módulo. El resultado es real, ejecutado en este entorno, sin LocalStack corriendo, sin cuenta AWS.

Conexión con el módulo

Esta es la lección que cumple, literalmente, la promesa de la lección 1: un plan completo de infraestructura de IA, con un número real de recursos a crear, sin ningún endpoint de red respondiendo. Es también la última lección de este módulo antes de que la lección 6 marque, con la misma honestidad, dónde ese mismo plan deja de poder convertirse en un apply real en este laboratorio.


Paso 1 — bedrock.tf, la versión completa de este módulo

El borrador del Módulo 2, lección 5 —un solo aws_bedrock_guardrail con un único filtro, escrito únicamente para probar Infracost— queda reemplazado por la versión completa que las lecciones 3 y 4 construyeron:

# 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",
    ]

    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
}

Nota que este bedrock.tf agregó una segunda entidad PII (PHONE, además de EMAIL) respecto al ejemplo aislado de la lección 3 — la lista completa de dos entidades que Andes Cargo necesita proteger en un manifiesto de texto libre real, no solo la de un ejemplo mínimo.


Paso 2 — terraform fmt y terraform validate, sobre el proyecto completo

cd andes-cargo-infra/
terraform fmt -check -recursive
echo "exit: $?"
terraform validate

Qué esperar (literal — ejecutado en este entorno, sobre andes-cargo-infra/ completo):

exit: 0
Success! The configuration is valid.

Sin salida de fmt -check — el proyecto entero, incluidos los archivos heredados de las tres guías anteriores y los dos nuevos de este módulo, ya está formateado correctamente. validate confirma, de nuevo, que todo el HCL —viejo y nuevo— es sintácticamente correcto y coincide con el schema de ambos providers instalados.


Paso 3 — terraform plan, el proyecto completo

terraform init -input=false
terraform plan -input=false -no-color -out=tfplan

Qué esperar (literal — ejecutado en este entorno, sin LocalStack corriendo, sin cuenta AWS):

Initializing modules...
- bedrock_manifest_extractor_role in modules/iam-role
- manifest_extractor_guardrail in modules/bedrock-guardrail

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Reusing previous version of hashicorp/archive from the dependency lock file
- Using previously-installed hashicorp/aws v6.60.0
- Using previously-installed hashicorp/archive v2.8.0

Terraform has been successfully initialized!

El bloque completo de terraform plan es largo —diecisiete recursos, contando todo lo heredado de terraform-and-iac-guide, aws-serverless-and-containers-guide y cloud-security-and-guardrails-guide—. Esta lección muestra el resumen y los dos bloques nuevos completos; el resto son los mismos recursos ya confirmados, sin cambios, en los módulos anteriores de esas guías:

  # module.bedrock_manifest_extractor_role.aws_iam_role.this will be created
  + resource "aws_iam_role" "this" {
      + name = "BedrockManifestExtractorRole"
      + assume_role_policy = jsonencode({
          + Statement = [{
              + Action    = "sts:AssumeRole"
              + Effect    = "Allow"
              + Principal = { Service = "lambda.amazonaws.com" }
            }]
          + Version = "2012-10-17"
        })
      ...
    }

  # module.bedrock_manifest_extractor_role.aws_iam_role_policy.this will be created
  + resource "aws_iam_role_policy" "this" {
      + name = "BedrockManifestExtractorRole-policy"
      + policy = jsonencode({
          + Statement = [{
              + Action   = "bedrock:InvokeModel"
              + Effect   = "Allow"
              + Resource = "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0"
              + Sid      = "InvokeManifestExtractionModelOnly"
            }]
          + Version = "2012-10-17"
        })
      ...
    }

  # module.manifest_extractor_guardrail.aws_bedrock_guardrail.this will be created
  + resource "aws_bedrock_guardrail" "this" {
      + 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."
      + region                    = "us-east-1"
      ...

      + content_policy_config {
          + filters_config {
              + input_strength  = "HIGH"
              + output_strength = "NONE"
              + type            = "PROMPT_ATTACK"
            }
        }

      + sensitive_information_policy_config {
          + pii_entities_config {
              + action = "ANONYMIZE"
              + type   = "EMAIL"
            }
          + pii_entities_config {
              + action = "ANONYMIZE"
              + type   = "PHONE"
            }
        }
    }

  # (14 recursos adicionales, heredados de terraform-and-iac-guide, aws-serverless-
  #  and-containers-guide y cloud-security-and-guardrails-guide, sin cambios en
  #  este módulo: aws_dynamodb_table.shipments, aws_lambda_function.process_shipment_manifest,
  #  aws_s3_bucket.this, module.shipment_docs_bucket.*, module.lambda_manifest_processor_role.*,
  #  module.app_server_role.*, module.github_oidc.*, aws_s3_bucket_notification.shipment_docs_trigger,
  #  aws_lambda_permission.allow_s3_invoke, data.aws_iam_policy_document.lambda_dynamodb_write)

Plan: 17 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bedrock_manifest_extractor_role_arn = (known after apply)
  + bucket_name_in_use                  = "andes-cargo-shipment-docs"
  + environment_in_use                  = "dev"
  + manifest_extractor_guardrail_arn    = (known after apply)
  + table_name_in_use                   = "Shipments"

Saved the plan to: tfplan

Plan: 17 to add, 0 to change, 0 to destroy. — diecisiete, no dos ni tres: el proyecto completo, no solo la infraestructura nueva de este módulo. Es el mismo número que verías tú, en tu propia máquina, corriendo estos mismos comandos contra el mismo HCL, sin LocalStack corriendo — la prueba directa de que este plan no dependió, en ningún momento, de una conexión de red hacia AWS o hacia LocalStack.


Paso 4 — Aislando solo lo nuevo, con -target

Diecisiete recursos es un plan útil para confirmar que nada se rompió, pero mezcla lo nuevo de este módulo con lo ya construido. Para ver, de forma limpia, exactamente cuántos recursos agrega este módulo específicamente, usa -target — la misma bandera que la lección 4 ya introdujo, ahora sobre ambas piezas a la vez:

terraform plan -input=false -no-color \
  -target=module.manifest_extractor_guardrail \
  -target=module.bedrock_manifest_extractor_role \
  -out=tfplan-ai-only

Qué esperar (literal — ejecutado en este entorno):

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bedrock_manifest_extractor_role_arn = (known after apply)
  + manifest_extractor_guardrail_arn    = (known after apply)

Warning: Resource targeting is in effect

You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.

The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.

Tres recursos exactos — verifícalo tú mismo, con terraform show -json y un filtro que enumere cada uno por su dirección completa:

terraform show -json tfplan-ai-only | python3 -c "
import json, sys
data = json.load(sys.stdin)
for rc in data['resource_changes']:
    print(rc['address'], '->', rc['change']['actions'])
"

Qué esperar (literal):

module.bedrock_manifest_extractor_role.aws_iam_role.this -> ['create']
module.bedrock_manifest_extractor_role.aws_iam_role_policy.this -> ['create']
module.manifest_extractor_guardrail.aws_bedrock_guardrail.this -> ['create']

Un recurso de cada tipo, cada uno con exactamente una acción (create) — literalmente "1 to add" por recurso, tres veces, sumando el Plan: 3 to add de arriba. Fíjate en la advertencia de Terraform sobre -target: es real, y vale la pena tomarla en serio fuera de un contexto de aprendizaje — -target es una herramienta de diagnóstico, útil aquí para aislar exactamente qué agrega este módulo, pero no el flujo normal de trabajo con este proyecto. El plan sin -target del Paso 3 —los diecisiete recursos completos— es el que de verdad se aplicaría en un flujo de CI/CD real, el mismo que cicd-and-gitops-on-aws-guide ya construyó.


Errores comunes

Usar -target como el flujo normal de trabajo, en vez de una herramienta de diagnóstico puntual (de mal hábito adquirido en esta lección). Qué pasa: alguien, después de ver lo cómodo que es aislar "solo lo nuevo" con -target en esta lección, empieza a usarlo por rutina en cada plan de andes-cargo-infra/. Cómo detectarlo: si tu flujo habitual de trabajo con este proyecto siempre incluye una o más banderas -target. Cómo corregirlo: Terraform mismo lo advierte en la salida de esta lección — "not for routine use". Usarlo como hábito esconde cambios reales que otros recursos del proyecto podrían necesitar (por ejemplo, si algo dependiera de un output nuevo), exactamente el riesgo que la advertencia nombra. Resérvalo para diagnóstico puntual, como en esta lección; el flujo real de aplicación pasa por el plan completo, sin -target, revisado en un pull request — el patrón que cicd-and-gitops-on-aws-guide ya estableció.

Interpretar Plan: 17 to add como si esta lección hubiera creado diecisiete recursos nuevos (de no distinguir "heredado" de "nuevo"). Qué pasa: alguien, viendo el número 17, asume que este módulo construyó diecisiete piezas de infraestructura. Cómo detectarlo: si tu resumen de este módulo dice "creamos 17 recursos". Cómo corregirlo: el Paso 4 de esta lección existe exactamente para esta distinción — solo tres de esos diecisiete son nuevos de este módulo (el guardrail, el rol, su política inline); los otros catorce ya existían, sin cambio, desde las tres guías hermanas anteriores. El plan completo del Paso 3 confirma que nada de lo heredado se rompió al agregar bedrock.tf; el plan aislado del Paso 4 confirma exactamente cuánto agregó este módulo específicamente.

Correr terraform apply (sin -target, sobre el tfplan completo del Paso 3) esperando que los diecisiete recursos se creen de verdad en este entorno (de expectativa hacia adelante, sin haber leído la lección 6 todavía). Qué pasa: alguien, animado por el plan limpio, intenta aplicar de inmediato. Cómo detectarlo: si tu siguiente paso, sin pasar por la lección 6, es terraform apply tfplan. Cómo corregirlo: un plan limpio confirma que el HCL es correcto y que Terraform sabe exactamente qué crear —nunca confirma que el entorno donde se aplicaría está disponible—. La lección 6 documenta, con precisión y cita oficial, exactamente por qué ese apply no llegaría a completarse en este laboratorio específico, para el recurso Bedrock en particular.


Ejercicios

Ejercicio 1 — Explica, sin ejecutar ningún comando, por qué Plan: 17 to add y Plan: 3 to add (con -target) no se contradicen. Un compañero, viendo ambos números en la misma lección, pregunta cuál es "el verdadero". Respóndele.

Ver solución

No se contradicen porque contestan preguntas distintas. Plan: 17 to add, sin -target, es la respuesta a "¿cuántos recursos hacen falta para que andes-cargo-infra/ completo, incluida toda la historia de las guías anteriores, exista de verdad?". Plan: 3 to add, con -target limitado a los dos módulos nuevos, es la respuesta a "¿cuántos recursos agrega, específicamente, la infraestructura de IA de este módulo?". Ambos números son correctos, simultáneamente, porque miden alcances distintos del mismo proyecto — el segundo es un subconjunto exacto del primero, no un resultado alternativo.

Ejercicio 2 — Predice qué pasaría si corrieras terraform plan -target=module.manifest_extractor_guardrail (sin el segundo -target del rol). ¿Cuántos recursos mostraría, y por qué ese número, no otro?

Ver solución

Mostraría Plan: 1 to add — únicamente module.manifest_extractor_guardrail.aws_bedrock_guardrail.this. -target limita el plan exactamente al recurso o módulo nombrado (y a cualquier cosa de la que ese recurso dependa, que en este caso es ninguna otra pieza nueva); sin el segundo -target para module.bedrock_manifest_extractor_role, ese rol y su política inline simplemente no aparecen en el plan, ni como algo a crear ni como algo ignorado con error — Terraform los trata como si no existieran en esta corrida específica.

Ejercicio 3 — Verifica, con terraform show -json y un filtro propio, que ninguno de los 14 recursos "heredados" del Paso 3 aparece marcado con la acción update o delete. Usando el mismo patrón de Python del Paso 4 pero sobre tfplan (el completo, sin -target), confirma que las únicas acciones presentes son create.

Ver solución
terraform show -json tfplan | python3 -c "
import json, sys
data = json.load(sys.stdin)
actions = set()
for rc in data['resource_changes']:
    actions.update(rc['change']['actions'])
print(sorted(actions))
"

El resultado esperado es ['create'] — un único tipo de acción presente en todo el plan, para los diecisiete recursos. Esto confirma, de forma programática (no solo leyendo el resumen 0 to change, 0 to destroy), que agregar bedrock.tf no modificó ni amenazó con destruir ningún recurso heredado de las tres guías anteriores — la definición exacta de una extensión aditiva, la misma promesa que el DISEÑO de esta guía hizo desde el principio.


Resumen y siguiente paso

Esta lección corrió terraform plan sobre andes-cargo-infra/ completo, con la infraestructura de IA de las lecciones 3 y 4 ya integrada en bedrock.tf: Plan: 17 to add, 0 to change, 0 to destroy, ejecutado de verdad, sin LocalStack, sin cuenta AWS. Aislaste, con -target, exactamente los tres recursos nuevos de este módulo (Plan: 3 to add), y verificaste, con terraform show -json, que ninguno de los recursos heredados cambió de estado. Todo lo que ves en esta lección es literal — la salida real de comandos reales, corridos mientras se escribía.

Antes de avanzar deberías poder: explicar la diferencia entre el plan completo y uno aislado con -target; nombrar los tres recursos exactos que este módulo agrega; y explicar por qué un plan limpio nunca confirma, por sí solo, que un apply real vaya a completarse.

La lección 6 marca, con la misma precisión y una cita oficial, exactamente dónde este mismo plan deja de poder convertirse en un apply real en este laboratorio — el único paso de este módulo que no corre de verdad aquí.

Recursos

  1. Terraform Docs — Command: plan — referencia oficial de -target, incluida la advertencia citada en esta lección.
  2. Terraform Docs — JSON Output Format — referencia oficial del formato que terraform show -json produce, usado en el Paso 4.
  3. cicd-and-gitops-on-aws-guide, Módulo 3 — el flujo real de plan-en-PR/apply-en-merge que hace de -target una herramienta de diagnóstico, no el flujo normal.