Día 99 - Attach IAM Policy for DynamoDB Access (IAM role + trust policy + attachment + least privilege)
Problema / Desafío
Crear una tabla DynamoDB segura con control de acceso fino vía IAM, para acceso restringido desde servicios AWS de confianza. Con Terraform:
- DynamoDB table
datacenter-tablecon configuración mínima - IAM role
datacenter-roleque podrá acceder a la tabla - IAM policy
datacenter-readonly-policyque otorgue acceso solo lectura (GetItem,Scan,Query) a esa tabla específica, y adjuntarla al role main.tfpara table + role + policy (no otro.tfde recursos)variables.tf:KKE_TABLE_NAME,KKE_ROLE_NAME,KKE_POLICY_NAMEoutputs.tf:kke_dynamodb_table,kke_iam_role_name,kke_iam_policy_name- Definir los valores en
terraform.tfvars terraform plandebe devolver "No changes" antes de entregar
Directorio /home/bob/terraform.
Cierra el arco de IAM: el Día 97 creó una policy suelta (nunca adjuntada,
attachment_count = 0). Hoy aparecen las piezas que faltaban — un role con su trust policy, y el attachment que conecta policy ↔ role — más least privilege real (scope al ARN de la tabla).
Conceptos clave
DynamoDB — tabla NoSQL con configuración mínima
DynamoDB es la base NoSQL gestionada de AWS (clave-valor / documentos). Lo mínimo para una tabla:
resource "aws_dynamodb_table" "this" {
name = var.KKE_TABLE_NAME
billing_mode = "PAY_PER_REQUEST" # ← minimal: sin read/write capacity
hash_key = "id"
attribute {
name = "id"
type = "S" # S=String, N=Number, B=Binary
}
}
| Campo | Función |
|---|---|
hash_key |
La partition key (clave primaria); obligatoria |
attribute |
Declara el tipo de cada key usada (solo las keys, no todos los campos) |
billing_mode |
PAY_PER_REQUEST (on-demand, sin capacity) o PROVISIONED (con capacity) |
Con
PROVISIONEDhabría que declararread_capacity/write_capacity.PAY_PER_REQUESTes lo más simple y encaja con "minimal configuration". DynamoDB es schemaless salvo las keys: solo se declara elattributede las claves, no de los demás campos.
IAM role vs policy suelta (Día 97) — el role necesita una trust policy
Un role es una identidad que otras entidades asumen temporalmente (un servicio AWS, una instancia EC2, otra cuenta). A diferencia de un user, no tiene credenciales fijas. Todo role necesita una trust policy (assume_role_policy): define quién puede asumirlo.
resource "aws_iam_role" "this" {
name = var.KKE_ROLE_NAME
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ec2.amazonaws.com" } # ← quién puede "ponerse" el role
}]
})
}
★ Las dos policies de un role — dos preguntas distintas
Este es el concepto central del día. Un role tiene dos tipos de policy que responden cosas diferentes:
| Policy | Responde | Dónde vive |
|---|---|---|
Trust policy (assume_role_policy) |
¿quién puede asumir el role? | Dentro del aws_iam_role |
| Permission policy (adjunta) | ¿qué puede hacer el role? | aws_iam_policy + attachment |
sts:AssumeRole en la trust policy = "el principal indicado puede ponerse este sombrero". La policy adjunta = "lo que el sombrero permite hacer". Son independientes: se puede poder asumir un role que no permite nada, o tener permisos definidos que nadie puede asumir.
El attachment — la pieza que faltaba en el Día 97
La aws_iam_policy por sí sola no hace nada (Día 97: attachment_count = 0). El recurso de attachment la conecta a un role:
resource "aws_iam_role_policy_attachment" "this" {
role = aws_iam_role.this.name
policy_arn = aws_iam_policy.this.arn
}
| Recurso de attachment | Adjunta una policy a… |
|---|---|
aws_iam_role_policy_attachment |
un role (hoy) |
aws_iam_user_policy_attachment |
un user |
aws_iam_group_policy_attachment |
un group |
aws_iam_policy_attachment |
varios a la vez (evitar; conflictos) |
★ Least privilege — scope al Resource, no solo al Action
El requisito pide "solo lectura" y "restringido a esa tabla específica". Eso son dos dimensiones:
- Action limitado a lectura:
dynamodb:GetItem,dynamodb:Scan,dynamodb:Query(noPutItem,DeleteItem, nidynamodb:*). - Resource limitado a la tabla: el ARN de
datacenter-table, no"*".
Contraste directo con el Día 97, donde ec2:Describe* iba con Resource = "*" (las acciones Describe no soportan resource-level). Aquí las acciones de DynamoDB sí son resource-level, así que el least privilege exige apuntar al ARN concreto. Referenciar aws_dynamodb_table.this.arn además crea la dependencia (la tabla se crea antes que la policy).
terraform.tfvars — valores separados de las declaraciones
El Día 98 usó default en cada variable. Hoy el lab pide los valores en terraform.tfvars, un archivo que Terraform carga automáticamente:
# variables.tf → DECLARA (nombre, tipo) | terraform.tfvars → ASIGNA (valor)
variable "KKE_TABLE_NAME" { type = string } | KKE_TABLE_NAME = "datacenter-table"
Precedencia de valores (de mayor a menor): -var en CLI → *.tfvars → default en la declaración. Separar declaración (variables.tf) de valores (terraform.tfvars) permite el mismo código con distintos valores por entorno (dev/prod), y mantener terraform.tfvars fuera de git si trae datos sensibles.
Pasos
- Abrir terminal en
/home/bob/terraform variables.tf(las tresKKE_*, sin default)terraform.tfvars(los valores)main.tf(table + role + policy + attachment)outputs.tf(los treskke_*)terraform init→plan→apply→yes- Re-correr
terraform plan→ confirmar "No changes"
Comandos / Código
variables.tf
variable "KKE_TABLE_NAME" {
description = "Name of the DynamoDB table"
type = string
}
variable "KKE_ROLE_NAME" {
description = "Name of the IAM role"
type = string
}
variable "KKE_POLICY_NAME" {
description = "Name of the IAM policy"
type = string
}
terraform.tfvars
KKE_TABLE_NAME = "datacenter-table"
KKE_ROLE_NAME = "datacenter-role"
KKE_POLICY_NAME = "datacenter-readonly-policy"
outputs.tf
output "kke_dynamodb_table" {
description = "DynamoDB table name"
value = aws_dynamodb_table.this.name
}
output "kke_iam_role_name" {
description = "IAM role name"
value = aws_iam_role.this.name
}
output "kke_iam_policy_name" {
description = "IAM policy name"
value = aws_iam_policy.this.name
}
main.tf
provider "aws" {
region = "us-east-1"
}
# --- DynamoDB table (minimal) ---
resource "aws_dynamodb_table" "this" {
name = var.KKE_TABLE_NAME
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
# --- IAM role + trust policy (quién puede asumirlo) ---
resource "aws_iam_role" "this" {
name = var.KKE_ROLE_NAME
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
}
# --- IAM policy: read-only, scoped a la tabla ---
resource "aws_iam_policy" "this" {
name = var.KKE_POLICY_NAME
description = "Read-only access to the datacenter DynamoDB table"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query",
]
Resource = aws_dynamodb_table.this.arn # scoped a la tabla, NO "*"
}]
})
}
# --- Attachment: conecta la policy al role ---
resource "aws_iam_role_policy_attachment" "this" {
role = aws_iam_role.this.name
policy_arn = aws_iam_policy.this.arn
}
Ejecutar y verificar idempotencia
Output esperado del apply (resumen):
Plan: 4 to add, 0 to change, 0 to destroy.
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
kke_dynamodb_table = "datacenter-table"
kke_iam_role_name = "datacenter-role"
kke_iam_policy_name = "datacenter-readonly-policy"
Verificación:
terraform state list
# aws_dynamodb_table.this
# aws_iam_policy.this
# aws_iam_role.this
# aws_iam_role_policy_attachment.this
# La policy adjunta al role
aws iam list-attached-role-policies --role-name datacenter-role
# Debe listar datacenter-readonly-policy
# El documento de la policy (confirmar actions + Resource = ARN de la tabla)
terraform state show aws_iam_policy.this
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
plan muestra cambios tras el apply |
Drift, o un valor que no se estabiliza | El requisito pide plan limpio; investigar el diff |
| La policy permite más de lo pedido | dynamodb:* o Resource = "*" en vez de scope |
Solo GetItem/Scan/Query + Resource = aws_dynamodb_table.this.arn |
MalformedPolicyDocument |
JSON de la policy mal armado | Usar jsonencode({...}) (valida estructura) |
| El role no puede ser asumido | Falta o está mal la assume_role_policy (trust) |
Trust policy con sts:AssumeRole + el Principal correcto |
| La policy no aplica al role | Falta el aws_iam_role_policy_attachment |
Agregar el attachment (policy suelta no hace nada, como Día 97) |
no value for required variable |
Falta el valor en terraform.tfvars (variables sin default) |
Definir las tres KKE_* en terraform.tfvars |
attribute ... not defined en la tabla |
Se usó un hash_key sin su bloque attribute |
Declarar el attribute de cada key usada |
Conexión con días anteriores
- Día 97 (IAM policy suelta): hoy se completa el arco — ahí la policy quedó sin adjuntar (
attachment_count = 0); hoy aparecen el role y el attachment que la conectan. Además, least privilege real:Resource= ARN de la tabla, no"*". - Día 98 (multi-file, variables, outputs, idempotencia): misma estructura de proyecto; hoy se suma
terraform.tfvars(valores separados de las declaraciones) y mismo requisito deplanlimpio. - Día 96 (grafo de dependencias):
Resource = aws_dynamodb_table.this.arnypolicy_arn = aws_iam_policy.this.arncrean el orden table → policy → attachment implícitamente. - Ansible (Día 90 ACLs): el mismo principio de "acceso fino a un recurso específico" — allá ACLs POSIX sobre un archivo, hoy IAM least-privilege sobre una tabla.