Módulo 2: Federated Identity And Least Privilege Iam

4. Manos a la obra: creando el IAM OIDC Identity Provider

Descripción

Esta es la lección donde el "en quién confío" de la lección 3 deja de ser un diagrama y se convierte en HCL real, validado por el motor real de Terraform. Vas a crear modules/oidc-provider/ —el primer directorio genuinamente nuevo de esta guía— con un único recurso: aws_iam_openid_connect_provider. terraform fmt, init, validate y plan corren de verdad, contra el Terraform 1.15.8 y el provider hashicorp/aws ~> 6.0 de este entorno — sin necesitar, todavía, ningún LocalStack corriendo.

Conexión con el módulo

La lección 3 te dio el vocabulario exacto: el identity provider es el registro, dentro de IAM, de que esta cuenta confía en tokens firmados por token.actions.githubusercontent.com. Esta lección declara ese registro. La lección 5 extiende este mismo módulo con la segunda pieza —el rol y su trust policy—, así que el diseño de modules/oidc-provider/ desde esta lección ya anticipa esa extensión: nada de lo que escribas aquí necesita reescribirse después, solo crece.


Paso 1 — La estructura del módulo, antes del contenido

  andes-cargo-infra/
  └── modules/
      └── oidc-provider/
          ├── main.tf         ← el recurso
          ├── variables.tf    ← sus inputs
          └── outputs.tf      ← lo que expone hacia el root module

El mismo patrón de tres archivos que ya conoces de modules/s3-bucket/ y modules/iam-role/ en terraform-and-iac-guide — esta guía no inventa una convención nueva.


Paso 2 — modules/oidc-provider/variables.tf

variable "thumbprint_list" {
  description = "SHA-1 thumbprints of the GitHub Actions OIDC issuer's TLS certificate chain."
  type        = list(string)
  default     = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

variable "tags" {
  description = "Tags applied to the OIDC provider and the role this module creates."
  type        = map(string)
  default     = {}
}

Dos variables, ambas con default — a diferencia de modules/iam-role/, donde role_name y las dos políticas eran obligatorias porque un rol sin ellas no tiene sentido operativo. Aquí, thumbprint_list tiene un valor por defecto razonable (el thumbprint real y vigente del certificado de token.actions.githubusercontent.com, el mismo que ya viste citado en cicd-and-gitops-on-aws-guide M4.5) porque casi ningún proyecto necesita cambiarlo — dejarlo como default evita que cada llamada al módulo tenga que repetir un valor hexadecimal que rara vez cambia, sin impedir que alguien lo sobrescriba si AWS o GitHub alguna vez rotan ese certificado.


Paso 3 — modules/oidc-provider/main.tf

resource "aws_iam_openid_connect_provider" "github_actions" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = var.thumbprint_list

  tags = var.tags
}

Cuatro argumentos, cada uno con una razón concreta, ya explicada en la lección 3: url es el iss exacto que un JWT de GitHub Actions declara; client_id_list es la audiencia que este provider acepta —sts.amazonaws.com, el mismo valor que la trust policy de la lección 5 va a comparar contra aud—; thumbprint_list es la huella criptográfica del certificado TLS del emisor, la pieza que le permite a AWS confirmar que está hablando con el servidor real de GitHub y no con un impostor; tags, el mismo bloque de tags de todo el proyecto, heredado de local.common_tags.


Paso 4 — modules/oidc-provider/outputs.tf

output "provider_arn" {
  description = "ARN of the GitHub Actions OIDC identity provider."
  value       = aws_iam_openid_connect_provider.github_actions.arn
}

Un único output por ahora — el ARN del provider, la pieza exacta que la trust policy de la lección 5 va a necesitar como Principal.Federated. La lección 5 agrega más outputs a este mismo archivo, sin tocar este.


Paso 5 — Llamando al módulo desde la raíz

En andes-cargo-infra/, crea oidc.tf:

module "github_oidc" {
  source = "./modules/oidc-provider"

  tags = local.common_tags
}

Sin ningún input obligatorio todavía —thumbprint_list usa su default, y el rol de la lección 5 (que sí va a pedir inputs obligatorios) todavía no existe en el módulo—. Es, a propósito, la llamada más simple posible: confirma que el módulo funciona antes de agregarle la pieza que sí necesita datos específicos de Andes Cargo.


Paso 6 — fmt, init, validate: el motor real, sin ningún LocalStack corriendo

terraform fmt -recursive
terraform init

Qué esperar (literal, ejecutado para escribir esta lección — Terraform 1.15.8, provider hashicorp/aws resuelto a 6.60.0 dentro del rango ~> 6.0):

Initializing the backend...

Initializing modules...
- github_oidc in modules/oidc-provider

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 6.0"...
- Installing hashicorp/aws v6.60.0...
- Installed hashicorp/aws v6.60.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

Fíjate en la segunda línea: Initializing modules... - github_oidc in modules/oidc-provider — Terraform confirma, antes de resolver ningún provider, que encontró y puede leer el módulo que acabas de escribir. Mismo v6.60.0 que ya viste resuelto en terraform-and-iac-guide para este mismo rango ~> 6.0 — el pin de versión sigue produciendo el resultado esperado en este ecosistema.

terraform validate

Qué esperar (literal):

Success! The configuration is valid.

Paso 7 — El plan, literal, ejecutado para escribir esta lección

terraform plan

Qué esperar (literal, ejecutado para escribir esta lección):

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:

  # module.github_oidc.aws_iam_openid_connect_provider.github_actions will be created
  + resource "aws_iam_openid_connect_provider" "github_actions" {
      + arn             = (known after apply)
      + client_id_list  = [
          + "sts.amazonaws.com",
        ]
      + id              = (known after apply)
      + tags            = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
      + tags_all        = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
      + thumbprint_list = [
          + "6938fd4d98bab03faadb97b34396831e3780aea1",
        ]
      + url             = "https://token.actions.githubusercontent.com"
    }

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

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Un solo recurso a crear, exactamente lo que esperas la primera vez que declaras un módulo. arn e id están (known after apply) — AWS asigna el ARN de un identity provider recién en el momento de crearlo, no antes—, pero url, client_id_list, y thumbprint_list ya están resueltos en el plan, porque no dependen de ninguna llamada de red: son literales que tú mismo escribiste.


Paso 8 — Aplicando y verificando (representativo)

Qué esperar (representativo) — este entorno de escritura no tiene un LOCALSTACK_AUTH_TOKEN exportado, así que el contenedor de LocalStack no arranca aquí (Could not connect to the endpoint URL), el mismo límite exacto que ya viste en el Módulo 1, lección 4. Lo que sigue está reconstruido campo por campo del plan real de arriba:

tflocal apply -auto-approve
module.github_oidc.aws_iam_openid_connect_provider.github_actions: Creating...
module.github_oidc.aws_iam_openid_connect_provider.github_actions: Creation complete after 1s [id=arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
awslocal iam list-open-id-connect-providers

Qué esperar (representativo):

{
    "OpenIDConnectProviderList": [
        {
            "Arn": "arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com"
        }
    ]
}

Un identity provider, con un ARN que sigue el patrón arn:aws:iam::<cuenta>:oidc-provider/<url-sin-esquema> — fíjate que el ARN no incluye https://, solo el dominio, aunque el url que declaraste en el HCL sí lo llevaba. Es un detalle real del formato de este recurso, no un error de transcripción.

Para ver el detalle completo, no solo el ARN:

awslocal iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com

Qué esperar (representativo — CreateDate es variable, el resto es fijo para este HCL exacto):

{
    "Url": "token.actions.githubusercontent.com",
    "ClientIDList": ["sts.amazonaws.com"],
    "ThumbprintList": ["6938fd4d98bab03faadb97b34396831e3780aea1"],
    "CreateDate": "2026-08-13T10:12:04+00:00",
    "Tags": [
        {"Key": "Project", "Value": "andes-cargo"},
        {"Key": "Environment", "Value": "dev"},
        {"Key": "ManagedBy", "Value": "terraform"}
    ]
}

Otra vez, Url sin el esquema https:// — este es el comportamiento documentado de la API de IAM para este recurso específico, confirmado contra la documentación oficial: el campo Url de la respuesta nunca incluye el protocolo, aunque el argumento de creación sí lo exija.


Errores comunes

Escribir url sin el esquema https:// en el HCL, "porque la respuesta de la API tampoco lo lleva" (de confusión de dirección). Qué pasa: alguien, habiendo visto ya la respuesta de get-open-id-connect-provider sin https://, escribe url = "token.actions.githubusercontent.com" en el recurso, sin el esquema. Cómo detectarlo: terraform plan falla con un error de validación del provider (url debe ser una URL completa con esquema) antes de llegar siquiera a intentar nada contra AWS. Cómo corregirlo: el argumento de creación (url en el HCL) exige el esquema completo (https://token.actions.githubusercontent.com); es la respuesta de la API, después de creado, la que lo omite. Son dos formatos distintos del mismo dato, en dos direcciones distintas del mismo recurso.

Declarar un segundo aws_iam_openid_connect_provider para el mismo emisor, por proyecto o por rol (de diseño). Qué pasa: alguien, más adelante, necesita un segundo rol para un segundo repositorio, y declara un segundo identity provider completo en vez de reutilizar el que ya existe. Cómo detectarlo: si tu plan intenta crear un aws_iam_openid_connect_provider con la misma url que uno ya existente, AWS rechaza la creación —EntityAlreadyExists— porque un identity provider para un emisor dado es único por cuenta, no por rol ni por proyecto. Cómo corregirlo: un identity provider se declara una sola vez por cuenta y por emisor; múltiples roles, para múltiples repositorios, referencian el mismo identity provider en su Principal.Federated — exactamente el patrón que la lección 5 construye sobre este mismo módulo.

Olvidar terraform init después de crear el módulo por primera vez. Qué pasa: alguien escribe los tres archivos de modules/oidc-provider/ y corre terraform plan directamente, sin init primero. Cómo detectarlo: el error Module not installed — Terraform necesita registrar el módulo nuevo en su árbol de dependencias antes de poder planearlo, exactamente el mismo comportamiento que ya viste con modules/iam-role/ y modules/s3-bucket/ en terraform-and-iac-guide. Cómo corregirlo: cualquier módulo nuevo, o cualquier cambio a la ruta source de uno existente, exige un terraform init antes del siguiente plan.


Ejercicios

Ejercicio 1 — Explica por qué arn está (known after apply) pero url no. Sin volver a mirar el plan de esta lección, explica en dos o tres frases por qué Terraform puede mostrar el valor exacto de url en el plan, pero no el de arn.

Ver solución

url es un valor que escribiste directamente en el HCL —un literal, conocido antes de que exista cualquier comunicación con AWS—, así que Terraform puede mostrarlo tal cual en el plan, sin necesitar ninguna llamada de red. arn, en cambio, es un atributo que AWS asigna en el momento de crear el recurso —incluye la cuenta y sigue un formato que depende de la respuesta real de la API—, así que Terraform no tiene forma de conocerlo hasta después del apply. Es la misma distinción que ya viste con RoleId/CreateDate en terraform-and-iac-guide: cualquier valor generado por AWS, nunca por ti, aparece como (known after apply).

Ejercicio 2 — Predice qué pasaría si client_id_list estuviera vacío. Si, por error, alguien declarara client_id_list = [] en vez de ["sts.amazonaws.com"], ¿qué le pasaría, conceptualmente, a cualquier intento posterior de asumir un rol vía este identity provider?

Ver solución

Fallaría, sin importar cuán bien configurada estuviera la trust policy del rol —client_id_list define qué audiencias (aud) acepta el identity provider en absoluto; un client_id_list vacío significa que ningún token, sin importar su aud, sería aceptado por este provider. Es un chequeo que ocurre en el identity provider mismo, antes incluso de que la trust policy del rol evalúe su propia condición sobre aud — dos capas de verificación distintas, la primera del lado del provider, la segunda del lado del rol, ambas necesarias.

Ejercicio 3 — Verifica de memoria la estructura completa de modules/oidc-provider/ al cerrar esta lección. Sin volver a mirar, describe los tres archivos de este módulo y qué contiene cada uno en este punto exacto de la guía.

Ver solución

main.tf — un único recurso, aws_iam_openid_connect_provider.github_actions, con url, client_id_list, thumbprint_list y tags. variables.tf — dos variables, thumbprint_list y tags, ambas con default. outputs.tf — un único output, provider_arn. Si recordaste los tres archivos y el contenido exacto de cada uno, tienes clara la base sobre la que la lección 5 va a construir, sin reescribir nada de lo que ya existe aquí.


Resumen y siguiente paso

En esta lección construiste modules/oidc-provider/, el primer directorio genuinamente nuevo de esta guía, con un único recurso declarado: aws_iam_openid_connect_provider. Corriste fmt/init/validate/plan de verdad, contra Terraform 1.15.8 y el provider hashicorp/aws resuelto a 6.60.0 — sin ningún LocalStack corriendo, exactamente igual que cualquier otro plan de este ecosistema. Confirmaste (representativo) que el apply produce el identity provider esperado, con un ARN que omite el esquema de la URL, un detalle real del formato de este recurso específico.

Antes de avanzar deberías poder: escribir de memoria el main.tf completo de este módulo; explicar por qué client_id_list y thumbprint_list cumplen roles distintos, aunque ambos sean listas de strings; y explicar por qué un identity provider se declara una sola vez por cuenta, no una vez por rol.

La lección 5 extiende este mismo módulo con la segunda pieza del flujo: el rol que un pipeline de GitHub Actions puede asumir, con una trust policy condicionada, con precisión de repositorio y rama, sobre el claim sub del JWT.

Recursos

  1. AWS Docs — Creating OpenID Connect (OIDC) identity providers — documentación oficial completa del recurso declarado en esta lección.
  2. Terraform Registry — aws_iam_openid_connect_provider — referencia completa del recurso HCL de esta lección.
  3. AWS CLI — iam list-open-id-connect-providers — referencia completa del comando de verificación del Paso 8.
  4. AWS CLI — iam get-open-id-connect-provider — referencia completa del comando que muestra el detalle del provider, incluido el formato de Url sin esquema.
  5. cicd-and-gitops-on-aws-guide, Módulo 4, lección 5 — la fuente original del thumbprint_list usado como default en esta lección.