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

4. Manos a la obra: `BedrockManifestExtractorRole`, mínimo privilegio

Descripción

Un guardrail controla lo que el modelo puede leer y decir. El rol de esta lección controla quién puede invocar el modelo, y a cuál — una capa distinta, tan necesaria como la primera. BedrockManifestExtractorRole es el rol que extract-shipment-manifest-fields asume para llamar bedrock:InvokeModel, acotado, con terraform validate real, al ARN de un único modelo — nunca al servicio completo. El código de esta lección corrió de verdad; el apply contra IAM y la lectura con awslocal quedan marcados con precisión, con la razón exacta de por qué en este entorno específico.

Conexión con el módulo

Este es el rol que el Módulo 5, lección 3 verifica con una política Rego dedicada (bedrock-least-privilege.rego), y el mismo que la lección 7 de este módulo retoma para trazar la frontera exacta entre lo que sí se puede aplicar en este laboratorio y lo que no. Todo lo que sigue —guardrails, gate de seguridad, gate de costo— asume que este rol, no bedrock:*, es la única puerta de entrada al modelo.


Analogía: la llave de una sala, no la llave maestra del edificio

Un edificio de oficinas le da a un empleado nuevo una llave que abre exactamente la sala donde trabaja — no la llave maestra que abre todas las puertas del edificio, aunque esa llave maestra sea, técnicamente, "más simple de administrar" (una sola llave para todo, nada que memorizar sobre quién necesita qué). La razón de dar la llave acotada, no la maestra, no es desconfianza hacia ese empleado específico: es que cuando una llave se pierde, se clona, o se usa desde una cuenta comprometida, el daño posible está limitado a exactamente lo que esa llave abre. bedrock:InvokeModel sobre el ARN de un modelo específico es la llave de una sala. bedrock:* —o bedrock:InvokeModel sobre "*", cualquier ARN— es la llave maestra: si algo sale mal con las credenciales de extract-shipment-manifest-fields, la diferencia entre esas dos políticas es la diferencia entre "puede invocar Nova Lite" y "puede invocar, gestionar, borrar o reconfigurar cualquier recurso de Bedrock en la cuenta entera".


Paso 1 — El modelo ARN exacto, y por qué importa que sea exacto

GENAI-COST-PROFILE.md (Módulo 2, sección 2) ya fijó la decisión: Amazon Nova Lite (amazon.nova-lite-v1:0), on-demand. El ARN de un modelo base de Bedrock sigue un formato fijo, sin cuenta AWS en la ruta —porque los modelos base son un recurso compartido de AWS, no algo que cada cuenta posee individualmente—:

arn:aws:bedrock:{region}::foundation-model/{model-id}

Para Andes Cargo, en us-east-1, con el modelo elegido:

arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0

Esto se declara como un local, no un valor repetido a mano en cada lugar que lo necesite — la misma disciplina de locals.tf que andes-cargo-infra/ ya sigue para common_tags desde el Módulo 1 de terraform-and-iac-guide:

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 this lesson builds.
  manifest_extractor_model_arn = "arn:aws:bedrock:${var.aws_region}::foundation-model/amazon.nova-lite-v1:0"
}

var.aws_region ya existe en variables.tf desde terraform-and-iac-guide, con default = "us-east-1" — se reusa aquí, sin duplicar el valor.


Paso 2 — El rol, con modules/iam-role/ heredado, sin reescribirlo

modules/iam-role/ ya existe, construido en terraform-and-iac-guide Módulo 5, lección 7, y ya usado dos veces en andes-cargo-infra/ (LambdaManifestProcessorRole, AppServerRole). Este rol es el tercer uso, sin ningún cambio al módulo mismo — la prueba de que el molde generaliza de verdad a un dominio que no existía cuando se escribió:

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
}

Fíjate en la política de confianza (bedrock_manifest_extractor_trust): el principal que puede asumir este rol es lambda.amazonaws.com, no Bedrock. Esto no es un error — BedrockManifestExtractorRole es el rol de ejecución de la función Lambda extract-shipment-manifest-fields (heredada de aws-serverless-and-containers-guide), no un rol que Bedrock asume por sí mismo. La función Lambda asume este rol al arrancar, y desde ahí, con esas credenciales temporales, llama bedrock:InvokeModel — el mismo patrón exacto que LambdaManifestProcessorRole ya usa para que process-shipment-manifest pueda leer de S3 y escribir en DynamoDB.


Paso 3 — terraform validate, real, sobre el rol completo

terraform validate

Qué esperar (literal — ejecutado en este entorno mientras se escribía esta lección, sobre andes-cargo-infra/ completo, incluidos este rol y el módulo de la lección 3):

Success! The configuration is valid.

terraform validate no distingue "valida solo esto" de "valida todo el proyecto" — siempre revisa el proyecto completo. Para confirmar, de forma aislada, que este rol específico produce el plan correcto sin arrastrar el resto de andes-cargo-infra/, usa -target, la misma bandera que la lección 5 de este módulo explica con detalle:

terraform plan -input=false -no-color -target=module.bedrock_manifest_extractor_role

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

  # 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"
            }
        )
      ... (otros atributos "known after apply")
    }

  # 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"
            }
        )
      ... (otros atributos "known after apply")
    }

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

Dos recursos —el rol y su política inline—, y el punto central de esta lección visible, literal, en la salida: "Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0", no "bedrock:*", no "*". Este es exactamente el JSON que se instalaría en IAM si este plan se aplicara de verdad.


Paso 4 — La frontera: qué es real aquí, qué es representativo, y por qué exactamente

Esto es el punto donde esta lección tiene que ser precisa, no aproximada. IAM, como servicio, está confirmado en el plan gratuito Hobby de LocalStack desde aws-core-services-guide — nada de eso cambió. En una máquina con LocalStack corriendo y un token de Hobby real exportado, los siguientes dos comandos correrían de verdad y producirían resultados reales:

tflocal apply -target=module.bedrock_manifest_extractor_role -auto-approve
awslocal iam get-role-policy --role-name BedrockManifestExtractorRole --policy-name BedrockManifestExtractorRole-policy

En este entorno de escritura específico, ninguno de los dos comandos de arriba corrió. No porque IAM esté fuera de Hobby —no lo está—, sino por la misma razón, ya confirmada por ejecución directa, que el Módulo 1, lección 7 de esta guía documentó con el exit code 55 real: este sandbox no tiene un LOCALSTACK_AUTH_TOKEN exportado, así que el contenedor de LocalStack no llega a arrancar para ningún servicio — ni Bedrock (fuera de Hobby de cualquier forma), ni IAM (dentro de Hobby, pero igual bloqueado aquí por la falta de token).

   LA FRONTERA EXACTA DE ESTA LECCIÓN

   terraform validate / terraform plan
        │
        │  Corren REAL, sin excepción -- confirmado arriba,
        │  Plan: 2 to add, sin LocalStack, sin token.
        ▼
   tflocal apply -target=module.bedrock_manifest_extractor_role
        │
        │  En TU máquina, con LocalStack + token de Hobby real:
        │  correría de verdad -- IAM SÍ está en Hobby.
        │
        │  En ESTE sandbox de escritura: no corrió -- exit code 55,
        │  el mismo límite de "sin token" que el Módulo 1, lección 7
        │  ya confirmó, sin relación con qué plan de LocalStack
        │  incluye qué servicio.
        ▼
   awslocal iam get-role-policy ...
        │
        │  En TU máquina: devolvería el JSON real de la política,
        │  idéntico al que "terraform plan" ya mostró arriba.
        │
        │  En ESTE sandbox: representativo -- construido con
        │  precisión sobre el JSON real que el plan ya produjo,
        │  nunca inventado.

Qué esperar (representativo — construido, con precisión, sobre el JSON exacto que terraform plan ya mostró en el Paso 3, nunca invocado de verdad en este entorno):

{
    "RoleName": "BedrockManifestExtractorRole",
    "PolicyName": "BedrockManifestExtractorRole-policy",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "InvokeManifestExtractionModelOnly",
                "Effect": "Allow",
                "Action": "bedrock:InvokeModel",
                "Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0"
            }
        ]
    }
}

La lección 7 de este módulo vuelve sobre esta misma frontera con más profundidad —incluido qué pasaría si intentaras aplicar el guardrail junto al rol en el mismo comando—; aquí basta con la distinción exacta: el límite de este rol específico es de este entorno de escritura, no del servicio. Cualquiera con un token de Hobby gratis, en su propia máquina, corre estos dos comandos de verdad.


Errores comunes

Escribir resources = ["arn:aws:bedrock:*:*:foundation-model/*"] o resources = ["*"], pensando que "ya lo voy a acotar después" (de mínimo privilegio pospuesto). Qué pasa: alguien, para "que funcione primero", usa un comodín amplio con la intención de restringirlo en una iteración futura que nunca llega. Cómo detectarlo: si tu terraform plan muestra "Resource": "*" en la política de BedrockManifestExtractorRole, en vez del ARN específico de esta lección. Cómo corregirlo: el Módulo 5, lección 3 construye una política Rego (bedrock-least-privilege.rego) que hace exactamente esta verificación de forma automática y bloquea un plan con un comodín así antes de que llegue a apply — pero la disciplina correcta es escribir el ARN específico desde el primer borrador, no confiar en que un gate posterior lo va a atrapar.

Confundir la política de confianza (assume_role_policy, quién puede asumir el rol) con la política de permisos (permissions_policy_json, qué puede hacer una vez que lo asumió) (de los dos documentos JSON de un rol IAM). Qué pasa: alguien intenta poner bedrock:InvokeModel dentro del data.aws_iam_policy_document.bedrock_manifest_extractor_trust, o intenta poner lambda.amazonaws.com como recurso dentro de la política de permisos. Cómo detectarlo: un terraform validate que sí pasa (ambos documentos son JSON de política válido, sintácticamente), pero un rol que, aplicado de verdad, no le daría a Lambda el permiso correcto, o que no dejaría que nadie lo asuma. Cómo corregirlo: la política de confianza contesta "¿quién puede convertirse en este rol?" (aquí, el servicio Lambda); la política de permisos contesta "¿qué puede hacer una vez que ya es este rol?" (aquí, invocar exactamente un modelo). Son dos preguntas distintas, con dos documentos JSON distintos, exactamente como ya viste con LambdaManifestProcessorRole en iam.tf.

Pensar que, porque IAM está en Hobby, este rol SÍ se pudo aplicar de verdad en este sandbox (de generalizar "el servicio está disponible" a "el comando corrió aquí"). Qué pasa: alguien lee que IAM es un servicio de Hobby y concluye que esta lección debió haber podido correr tflocal apply de verdad. Cómo detectarlo: si tu pregunta es "¿por qué la lección dice representativo si IAM sí es gratis?". Cómo corregirlo: son dos preguntas independientes, la misma distinción que el Módulo 1, lección 7 ya hizo para Bedrock —ahí, con dos obstáculos independientes (token del sandbox, plan de LocalStack)—. Aquí solo hay un obstáculo, pero es real: el sandbox de escritura de esta guía específicamente no tiene un LOCALSTACK_AUTH_TOKEN exportado, así que ningún servicio —ni siquiera los que sí están en Hobby— arranca en este entorno. Con tu propio token, gratis, registrado, este mismo rol sí se aplica y se lee de verdad.


Ejercicios

Ejercicio 1 — Reescribe, de memoria, el ARN completo del modelo que BedrockManifestExtractorRole puede invocar. Sin mirar esta lección, escribe el ARN exacto, incluida la región y el formato de dos puntos dobles antes de foundation-model/.

Ver solución

arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0. Los dos puntos dobles (::) marcan la ausencia deliberada de un ID de cuenta en la ruta —los modelos base de Bedrock no pertenecen a ninguna cuenta individual, son un recurso compartido de AWS, a diferencia de, por ejemplo, el ARN de una tabla DynamoDB o un bucket S3, que sí incluyen el ID de cuenta del propietario.

Ejercicio 2 — Explica por qué el principal de la política de confianza es lambda.amazonaws.com y no bedrock.amazonaws.com. Un colega, viendo el nombre BedrockManifestExtractorRole, asume que Bedrock mismo debería ser el principal que asume este rol. Corrígelo.

Ver solución

El nombre del rol describe para qué se usa (extraer manifiestos vía Bedrock), no quién lo asume. Quien asume este rol es la función Lambda extract-shipment-manifest-fields —el mismo patrón que LambdaManifestProcessorRole ya usa para process-shipment-manifest—; una vez que Lambda asume el rol, usa las credenciales temporales resultantes para llamar bedrock:InvokeModel como parte de su propio código. Bedrock nunca "asume" el rol de nadie en este flujo; es el servicio al que el código, ya corriendo con las credenciales del rol, hace una llamada saliente. Confundir esto llevaría a poner bedrock.amazonaws.com como principal, lo cual dejaría el rol sin nadie que realmente pueda usarlo desde Lambda.

Ejercicio 3 — Predice qué mostraría terraform plan -target=module.bedrock_manifest_extractor_role si, por error, alguien agregara un segundo modelo al ARN scope, así: resources = [local.manifest_extractor_model_arn, "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-pro-v1:0"]. ¿Seguiría siendo terraform validate capaz de detectar que esto viola el principio de mínimo privilegio?

Ver solución

terraform validate no lo detectaría — sintácticamente, una lista con dos ARN válidos es HCL perfectamente correcto, y el plan simplemente mostraría dos entradas en el arreglo "Resource" en vez de una. Mínimo privilegio no es una propiedad de tipos ni de sintaxis que validate pueda verificar por definición —es una decisión de alcance de negocio ("¿cuántos modelos necesita invocar esta función, de verdad?")—. Esta es exactamente la razón por la que el Módulo 5, lección 3 construye una política Rego dedicada (bedrock-least-privilege.rego): un chequeo separado, escrito a propósito para esta pregunta específica, que un terraform validate genérico nunca podría contestar por sí solo.


Resumen y siguiente paso

En esta lección construiste BedrockManifestExtractorRole reusando modules/iam-role/ sin ningún cambio al molde, con una política de permisos que acota bedrock:InvokeModel al ARN de un único modelo — Amazon Nova Lite, la decisión ya fijada por GENAI-COST-PROFILE.md. Confirmaste terraform validate y un plan targeteado, ambos reales, con Plan: 2 to add y el JSON exacto de la política visible, literal, en la salida. Trazaste la frontera exacta de este rol: IAM sí es un servicio de Hobby, pero este sandbox de escritura específico no tiene un token de LocalStack exportado, así que el apply y la lectura con awslocal quedan representativos aquí — no por el servicio, por el entorno.

Antes de avanzar deberías poder: escribir de memoria el formato de un ARN de modelo base de Bedrock; explicar la diferencia entre política de confianza y política de permisos; y explicar por qué terraform validate nunca podría, por sí solo, detectar una violación de mínimo privilegio de negocio.

La lección 5 junta este rol con el módulo de la lección 3 en un solo terraform plan — la infraestructura de IA completa de Andes Cargo, planeada de punta a punta, con el número real de recursos a crear.

Recursos

  1. Terraform Registry — aws_iam_role_policy — referencia oficial de la política inline que este rol usa.
  2. AWS Docs — Amazon Bedrock foundation model ARNs — formato oficial del ARN citado en esta lección.
  3. cloud-security-and-guardrails-guide, Módulo 2 — la disciplina de mínimo privilegio de IAM que esta lección aplica por primera vez a un modelo, no a un servicio.
  4. Módulo 1, lección 7 de esta guía (07-hands-on-the-honest-attempt-against-bedrock-on-localstack.md) — la fuente del hallazgo del exit code 55, reusado aquí para explicar por qué el apply de esta lección es representativo.