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

2. Qué recursos de Terraform existen para Bedrock

Descripción

La lección 1 confirmó por qué Terraform puede declarar Bedrock sin necesitar una API real. Esta lección confirma qué, exactamente, hay para declarar — no de memoria, no de una búsqueda genérica, sino extraído directamente del schema del provider hashicorp/aws ya instalado en este entorno, con el comando diseñado exactamente para esto: terraform providers schema -json. El resultado de esa extracción es la fuente literal del resto de este módulo y del Módulo 4 completo.

Conexión con el módulo

El módulo bedrock-guardrail que la lección 3 construye, y el rol de mínimo privilegio de la lección 4, usan únicamente argumentos que aparecen en el schema extraído aquí. Cuando el Módulo 4 declare las seis políticas completas del guardrail, va a citar la misma fuente, extendida — nunca un argumento inventado ni copiado de una versión anterior de la documentación.


Analogía: el catálogo del fabricante, no el folleto de ventas

Un folleto de ventas de un fabricante de muebles te dice, en términos generales, "este armario tiene compartimentos ajustables". El catálogo técnico del fabricante —el que usa el instalador— te dice exactamente cuántos tornillos, de qué medida, en qué posición, con qué tolerancia. terraform providers schema -json es el catálogo técnico del provider hashicorp/aws: no una descripción conceptual de "Bedrock tiene guardrails", sino el nombre exacto de cada argumento, si es obligatorio u opcional, y cómo se anida cada bloque — la única fuente que un main.tf real puede usar sin adivinar.


Paso 1 — El panorama completo: cuántos recursos de Bedrock existen en el provider

Antes de enfocarse en los tres recursos que este módulo usa, vale la pena ver la escala real del catálogo. El siguiente comando extrae el schema completo del provider ya instalado (hashicorp/aws v6.60.0, confirmado desde el Módulo 2) y cuenta cuántos recursos tienen bedrock en su nombre:

terraform providers schema -json > schema.json
python3 -c "
import json
data = json.load(open('schema.json'))
aws = data['provider_schemas']['registry.terraform.io/hashicorp/aws']
bedrock = sorted(k for k in aws['resource_schemas'] if 'bedrock' in k)
print(len(bedrock), 'recursos con \'bedrock\' en el nombre')
for r in bedrock[:10]:
    print(' -', r)
print('...')
"

Qué esperar (literal — extraído del provider real instalado en este entorno):

39 recursos con 'bedrock' en el nombre
 - aws_bedrock_custom_model
 - aws_bedrock_evaluation_job
 - aws_bedrock_foundation_model_agreement
 - aws_bedrock_guardrail
 - aws_bedrock_guardrail_version
 - aws_bedrock_inference_profile
 - aws_bedrock_model_invocation_logging_configuration
 - aws_bedrock_provisioned_model_throughput
 - aws_bedrock_use_case_for_model_access
...

Treinta y nueve recursos — el catálogo completo incluye desde recursos para fine-tuning de modelos propios (aws_bedrock_custom_model) hasta la familia entera de aws_bedrockagent_* y aws_bedrockagentcore_* para construir agentes con Bedrock AgentCore. Esta guía no usa casi ninguno de esos 39 — la frontera con AI Engineering (Módulo 1, lección 4) es exactamente la razón: agentes, knowledge bases, custom models pertenecen al territorio de construir la aplicación de IA, no de operar su infraestructura. Los tres que sí importan aquí son los que gobiernan un guardrail y una capacidad de inferencia gestionada — la infraestructura alrededor del modelo, no el modelo ni la aplicación en sí.


Paso 2 — aws_bedrock_guardrail: el recurso central de este módulo

Extraído del mismo schema.json, con el bloque completo (no resumido — cada nombre de argumento, cada anidamiento, exactamente como el provider lo expone):

aws_bedrock_guardrail
======================
blocked_input_messaging:   string  [required]
blocked_outputs_messaging: string  [required]
name:                      string  [required]
description:               string  [optional, computed]
kms_key_arn:                string  [optional]
tags:                      map(string) [optional]

created_at:    string [computed]
guardrail_arn: string [computed]
guardrail_id:  string [computed]
region:        string [optional, computed]
status:        string [computed]
tags_all:      map(string) [computed]
updated_at:    string [computed]
version:       string [computed]

content_policy_config { }  (bloque, nesting=list)
  tier_config: list(object) [optional, computed]
  filters_config { }  (bloque, nesting=set)
    type:            string [required]   -- ej. PROMPT_ATTACK, HATE, SEXUAL
    input_strength:  string [required]   -- NONE | LOW | MEDIUM | HIGH
    output_strength: string [required]
    input_action, output_action:   string [optional]
    input_enabled, output_enabled: bool   [optional]
    input_modalities, output_modalities: set(string) [optional]

sensitive_information_policy_config { }  (bloque, nesting=list)
  pii_entities_config { }  (bloque, nesting=list)
    type:   string [required]   -- ej. EMAIL, PHONE, NAME
    action: string [required]   -- BLOCK | ANONYMIZE
    input_action, output_action:   string [optional, computed]
    input_enabled, output_enabled: bool   [optional, computed]
  regexes_config { }  (bloque, nesting=list)
    name:    string [required]
    pattern: string [required]
    action:  string [required]
    description: string [optional, computed]
    input_action, output_action:   string [optional, computed]
    input_enabled, output_enabled: bool   [optional, computed]

topic_policy_config { }  (bloque, nesting=list)
  tier_config: list(object) [optional, computed]
  topics_config { }  (bloque, nesting=list)
    name:       string [required]
    definition: string [required]
    type:       string [required]   -- DENY
    examples:   list(string) [optional, computed]

contextual_grounding_policy_config { }  (bloque, nesting=list)
  filters_config { }  (bloque, nesting=list)
    type:      string [required]   -- GROUNDING | RELEVANCE
    threshold: number [required]

word_policy_config { }  (bloque, nesting=list)
  managed_word_lists_config { }  (bloque, nesting=list)
    type: string [required]   -- ej. PROFANITY
    input_action, output_action:   string [optional]
    input_enabled, output_enabled: bool   [optional]
  words_config { }  (bloque, nesting=list)
    text: string [required]
    input_action, output_action:   string [optional]
    input_enabled, output_enabled: bool   [optional]

cross_region_config { }  (bloque, nesting=list)
  guardrail_profile_identifier: string [required]

timeouts { }  (bloque, nesting=single)
  create, update, delete: string [optional]

Cinco políticas —contenido, información sensible, temas, grounding contextual, palabras— más un bloque de región cruzada, todas como bloques anidados de un mismo recurso. Fíjate en algo importante para el Módulo 4: casi todos los argumentos dentro de las políticas son optional, no required — el único requisito estructural es que, si declaras un bloque de política (por ejemplo content_policy_config), los campos dentro de él (type, input_strength, output_strength en filters_config) sí son obligatorios. Esto es lo que permite que este módulo (lección 3) declare solo dos de las cinco políticas —contenido e información sensible— sin que Terraform se queje de que faltan las otras tres: el bloque entero de una política que no se declara, simplemente, no existe en el plan.


Paso 3 — Los otros dos: aws_bedrock_guardrail_version y aws_bedrock_provisioned_model_throughput

aws_bedrock_guardrail_version
==============================
guardrail_arn: string [required]
description:   string [optional]
skip_destroy:  bool   [optional]
region:        string [optional, computed]
version:       string [computed]
timeouts { }  (bloque, nesting=single)
  create, delete: string [optional]

Este recurso publica una versión inmutable de un guardrail ya declarado — la diferencia entre editar el guardrail "en vivo" (el borrador, DRAFT, que cambia cada vez que aplicas aws_bedrock_guardrail) y fijar una versión numerada específica para que una aplicación en producción la referencie sin que un cambio futuro al guardrail la afecte sin aviso. Esta guía no lo declara —el guardrail de Andes Cargo se queda en DRAFT durante todo este laboratorio, una decisión honesta para un sistema que todavía no tiene una invocación real a la cual versionar—, pero forma parte del panorama completo del provider, y el patrón (guardrail_arn como input, version como output computado) es el mismo que cualquier flujo de versionado de recurso ya visto en terraform-and-iac-guide.

aws_bedrock_provisioned_model_throughput
==========================================
model_arn:               string [required]
model_units:              number [required]
provisioned_model_name:  string [required]
commitment_duration:     string [optional]   -- NONE | ONE_MONTH | SIX_MONTHS
tags:                    map(string) [optional]
region:                  string [optional, computed]

provisioned_model_arn: string [computed]
tags_all:              map(string) [computed]
timeouts { }  (bloque, nesting=single)
  create: string [optional]

Este es el recurso que declararía Provisioned Throughput — capacidad de inferencia reservada, facturada por hora sin importar cuánto se use, la alternativa a on-demand que el Módulo 2, lección 2 ya nombró y el Módulo 6, lección 7 retoma con números reales. Fíjate en model_units, un número entero: es la unidad de capacidad reservada de AWS, no tokens ni invocaciones — reservar más model units aumenta el throughput garantizado, a un costo fijo por hora, exactamente el modelo "prendido = cuesta" que el Módulo 2, lección 1 contrastó contra el costo por token de on-demand. Esta guía tampoco lo declara: Andes Cargo, al volumen que GENAI-COST-PROFILE.md proyecta ($0.00–$0.42/mes), no tiene ningún caso de negocio para capacidad reservada — el Módulo 6, lección 7 explica exactamente el punto de volumen donde esa decisión cambiaría.


Cómo se extrajo este esquema, para que puedas repetirlo

El comando completo, en la raíz de andes-cargo-infra/ (con terraform init ya corrido, para que el provider esté instalado):

terraform providers schema -json > schema.json

Qué esperar (literal): un único archivo JSON de aproximadamente 19 MB — el schema completo de todos los providers usados en el proyecto (hashicorp/aws y hashicorp/archive), con todos sus recursos, no solo Bedrock. El tamaño es la prueba de la escala real del provider: miles de recursos y data sources, de los cuales esta guía usa, en total —contando las siete guías hermanas—, menos de veinte.

import json
data = json.load(open("schema.json"))
aws = data["provider_schemas"]["registry.terraform.io/hashicorp/aws"]
block = aws["resource_schemas"]["aws_bedrock_guardrail"]["block"]
print(sorted(block["block_types"].keys()))

Qué esperar (literal):

['content_policy_config', 'contextual_grounding_policy_config', 'cross_region_config', 'sensitive_information_policy_config', 'timeouts', 'topic_policy_config', 'word_policy_config']

Siete bloques anidados de primer nivel — la misma lista que el Paso 2 mostró completa, extraída aquí en tres líneas de Python contra el mismo archivo. Este es el método exacto que produjo cada tabla de esta lección: nunca una copia de la documentación web (que puede quedar desactualizada respecto al provider realmente instalado), siempre el schema.json del entorno donde el HCL se va a validate.


Errores comunes

Copiar un ejemplo de HCL de un tutorial en línea sin confirmar que el argumento existe en el schema instalado (de fuente no verificada). Qué pasa: un tutorial —o una versión anterior de la documentación de AWS— usa un nombre de argumento que cambió, o que nunca existió en el provider hashicorp/aws, sino en un fork o una versión distinta. Cómo detectarlo: terraform validate falla con Unsupported argument o Blocks of type "X" are not expected here. Cómo corregirlo: contra el schema.json extraído en el Paso 3 de esta lección, grep el nombre exacto del bloque o argumento — si no aparece ahí, no existe en la versión del provider que tienes instalada, sin importar cuántos tutoriales lo mencionen.

Asumir que las cinco políticas de aws_bedrock_guardrail son mutuamente excluyentes, o que hay que declarar las cinco (de lectura incompleta del schema). Qué pasa: alguien, viendo el Paso 2, concluye que un guardrail válido necesita las cinco políticas presentes. Cómo detectarlo: si dudas en declarar un guardrail con solo dos políticas, pensando que Terraform lo va a rechazar. Cómo corregirlo: los cinco bloques de política son, cada uno, optional a nivel del recurso completo —el schema no marca ninguno como obligatorio—; declarar solo content_policy_config y sensitive_information_policy_config, como hace el módulo de la lección 3, es un guardrail perfectamente válido. El Módulo 4 agrega los tres restantes porque el caso de Andes Cargo los necesita, no porque el schema lo exija.

Confundir aws_bedrock_guardrail_version con "la versión del provider" o "la versión del guardrail en el sentido de historial de cambios de Git" (de ambigüedad de la palabra "versión"). Qué pasa: alguien lee el nombre del recurso y asume que gestiona algo relacionado con control de versiones de código. Cómo detectarlo: si tu expectativa de aws_bedrock_guardrail_version es que registra un historial tipo Git. Cómo corregirlo: es un concepto específico de la API de Bedrock — congela el guardrail DRAFT actual en un número de versión inmutable (1, 2, 3...) que una aplicación puede referenciar de forma estable, para que un cambio posterior al DRAFT no afecte, sin aviso, a algo ya en producción. No tiene relación con el versionado del propio código Terraform (eso lo maneja Git, fuera de este recurso).


Ejercicios

Ejercicio 1 — Localiza, sin mirar esta lección, si aws_bedrock_guardrail acepta un argumento llamado guardrail_name. Usando el patrón del Paso 3 de esta lección (terraform providers schema -json + un filtro de Python o grep), confirma tú mismo si ese nombre de argumento existe.

Ver solución

No existe. El argumento correcto, confirmado en el Paso 2 de esta lección y verificable con python3 -c "import json; d=json.load(open('schema.json')); print(sorted(d['provider_schemas']['registry.terraform.io/hashicorp/aws']['resource_schemas']['aws_bedrock_guardrail']['block']['attributes'].keys()))", es simplemente name, no guardrail_name. Este es exactamente el tipo de error que un tutorial desactualizado, o una comparación mental con otro recurso (aws_dynamodb_table sí usa name para la tabla, pero otros recursos de AWS a veces usan un prefijo con el nombre del servicio) puede introducir sin querer.

Ejercicio 2 — Explica por qué aws_bedrock_provisioned_model_throughput tiene model_units como número, no como texto libre. ¿Qué te dice esto sobre cómo AWS modela la capacidad reservada de un modelo, comparado con cómo Bedrock modela el costo on-demand (por token)?

Ver solución

model_units es un número entero porque representa una cantidad discreta de capacidad reservada —cuántas "unidades" de throughput garantizado estás comprando por hora, un concepto de aprovisionamiento fijo, igual en estructura a "cuántas instancias EC2" o "cuánta capacidad de lectura/escritura reservada en DynamoDB"—. Contrasta esto con el modelo on-demand de Bedrock, donde no hay ninguna "unidad" que reservar por adelantado: el costo emerge de tokens de entrada y salida reales, medidos después del hecho, sin ningún número que declarar en Terraform. La existencia misma de model_units como argumento numérico confirma, en el nivel del schema, la distinción que el Módulo 2 entero desarrolló en prosa: on-demand no tiene una "cantidad" que aprovisionar, Provisioned Throughput sí.

Ejercicio 3 — Predice qué pasaría si intentaras declarar content_policy_config sin ningún filters_config dentro. Basándote en el schema del Paso 2 (filters_config como bloque de nesting=set dentro de content_policy_config), ¿sería un plan válido, o Terraform lo rechazaría?

Ver solución

Sería un plan válido — un content_policy_config {} vacío, sin ningún filters_config adentro, es HCL sintácticamente correcto: el bloque en sí no tiene argumentos required a su propio nivel (tier_config es optional, computed), y filters_config es un bloque repetible (nesting=set), no un argumento obligatorio del bloque padre. El resultado práctico sería un guardrail con una política de contenido declarada pero sin ningún filtro real activo dentro de ella —probablemente no lo que se quiso lograr, pero no un error de Terraform. Esto es exactamente el tipo de caso donde terraform validate (que solo confirma tipos y estructura) no sustituye una revisión humana de que el contenido declarado tiene sentido para el caso de uso real.


Resumen y siguiente paso

Esta lección extrajo, con terraform providers schema -json contra el provider hashicorp/aws v6.60.0 ya instalado, el panorama completo de los recursos de Bedrock: 39 recursos en total, de los cuales esta guía usa tres. Viste el schema completo de aws_bedrock_guardrail —cinco políticas, todas opcionales a nivel de recurso, cada una con sus propios campos obligatorios adentro—, y el de los otros dos recursos nombrados (aws_bedrock_guardrail_version, aws_bedrock_provisioned_model_throughput), ninguno de los cuales esta guía declara, por razones de alcance explícitas en cada caso.

Antes de avanzar deberías poder: nombrar los tres recursos de Bedrock que esta guía usa, sin mirar la lección; explicar por qué ninguna de las cinco políticas de aws_bedrock_guardrail es obligatoria a nivel de recurso; y repetir, tú mismo, el comando de extracción del schema contra cualquier provider instalado.

La lección 3 pone este schema a trabajar por primera vez: modules/bedrock-guardrail/, el molde real, validado de forma aislada, exactamente como terraform-and-iac-guide ya construyó modules/s3-bucket/ y modules/iam-role/.

Recursos

  1. Terraform Registry — aws_bedrock_guardrail — documentación oficial del recurso central de este módulo.
  2. Terraform Registry — aws_bedrock_guardrail_version — documentación oficial.
  3. Terraform Registry — aws_bedrock_provisioned_model_throughput — documentación oficial.
  4. Terraform Docs — Command: providers schema — referencia oficial del comando usado en esta lección para extraer el schema real.
  5. terraform-and-iac-guide, Módulo 5 — la convención de módulos (modules/s3-bucket/, modules/iam-role/) que la lección 3 sigue.