El misterio de los disparadores de Lambda que desaparecen: una historia de drift de estado en Terraform
Todos hemos estado ahí: empiezas el día sintiéndote bien por terminar el trabajo del sprint anterior, café en mano, listo para abordar algo nuevo. Entonces llega la notificación de Slack que lo cambia todo.
“Oye, ¿puedes investigar por qué un par de Lambdas no se ejecutaron hace dos semanas?”
El misterio era desconcertante: las funciones Lambda programadas habían perdido sus ejecuciones en un día específico, luego reanudaron su funcionamiento normal al día siguiente sin intervención. No eran funciones críticas (esas habrían disparado alertas inmediatas), pero eran lo suficientemente importantes como para que necesitáramos entender qué pasó y evitar que se repitiera.
Trabajando con un compañero, descubrimos que dos funciones habían fallado al ejecutarse: una activada por reglas de EventBridge, otra por eventos de S3. Diferentes disparadores, mismo problema, mismo día. Eso apuntaba a algo sistémico.
Mientras investigábamos la Lambda afectada en la consola, todo parecía normal. Los logs de CloudWatch no mostraban nada inusual. Los eventos de CloudTrail para ese período no revelaron anomalías. Entonces, mientras revisábamos la configuración de la Lambda, notamos algo extraño: el disparador había desaparecido ante nuestros ojos.
Una verificación rápida confirmó nuestra sospecha: un despliegue de producción acababa de completarse. El despliegue automatizado de Terraform desde nuestro pipeline de CI/CD había desconectado los disparadores de alguna manera. La infraestructura estaba ahí: reglas de EventBridge, targets, incluso la función Lambda misma, pero ya no estaban conectados.
La Investigación
Profundizando más, encontramos algo interesante:
- ✅ Las reglas de EventBridge existían y estaban habilitadas
- ✅ Los targets de EventBridge apuntaban a la ARN correcta de Lambda
- ❌ Los disparadores de Lambda no mostraban nada en la consola
- ❌ Las invocaciones manuales desde EventBridge fallaban con errores “not authorized”
Esto apuntaba a una cosa: permisos de Lambda faltantes.
En AWS, tener una regla de EventBridge con un target no es suficiente. También necesitas un recurso aws_lambda_permission que otorgue explícitamente a EventBridge el derecho de invocar tu Lambda. Estos son dos recursos separados:
# La regla y target de EventBridge
resource "aws_cloudwatch_event_rule" "my_rule" {
name = "my-scheduled-rule"
schedule_expression = "cron(0 12 * * ? *)"
}
resource "aws_cloudwatch_event_target" "my_target" {
rule = aws_cloudwatch_event_rule.my_rule.name
arn = aws_lambda_function.my_function.arn
}
# El permiso (¡ESTO faltaba!)
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.my_function.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.my_rule.arn
}
Cuando verificamos la política de recursos de la Lambda:
aws lambda get-policy \
--function-name my_scheduled_lambda \
--region us-west-2
Los permisos efectivamente faltaban. Pero la pregunta permanecía: ¿Por qué?
El Problema de Arquitectura de AWS
Así es como se ve la arquitectura cuando todo está configurado correctamente vs. cuando faltan permisos:
La Causa Raíz
Después de investigar e investigar, descubrimos que esto es un comportamiento conocido en las interacciones AWS/Terraform:
- Cuando AWS elimina una función Lambda, elimina automáticamente todos los permisos asociados
- Esto es comportamiento de AWS, no un bug de Terraform
- Cuando Terraform recrea una función Lambda (durante un despliegue), AWS elimina silenciosamente los permisos
- El archivo de estado de Terraform todavía piensa que los permisos existen (estado obsoleto)
- Los permisos no aparecen en el plan cuando la Lambda es reemplazada
- Solo aparecen como necesitando recreación en la siguiente ejecución de Terraform
Esto crea una ventana peligrosa donde tu infraestructura se ve bien en Terraform, pero en realidad está rota en AWS.
Pero Espera—¿Por Qué Se Estaban Reemplazando las Lambdas?
Aquí es donde la trama se complica. En circunstancias normales, los despliegues de Lambda con solo cambios de código deberían actualizarse in-place, no reemplazar la función. Así que tuvimos que preguntar: ¿qué realmente activó el reemplazo que causó este lío?
Revisando el historial de despliegues y los logs de CloudTrail reveló una historia fascinante de no una, sino dos migraciones arquitectónicas separadas que ambas causaron reemplazos de Lambda.
La Primera Migración: Paquetes Zip a Imágenes de Contenedor
El reemplazo inicial ocurrió durante una migración importante de infraestructura. Estábamos moviéndonos de:
Antes:
- Tipo de paquete:
Zip - Runtime:
nodejs22.x - Dependencias: montaje EFS (
/mnt/efs/node_modules) - Despliegue: Subir archivos zip a S3
Después:
- Tipo de paquete:
Image - Dependencias: Incluidas en imágenes de contenedor
- Despliegue: Push a ECR, referencia URI de imagen
- Node modules: Incluidos en la capa del contenedor (
/opt/nodejs/node_modules)
Este es un cambio que rompe compatibilidad para AWS Lambda. No puedes cambiar el tipo de paquete de Zip a Image in-place: AWS requiere una eliminación y recreación completa. Cuando Terraform ejecutó esta migración:
- Eliminó las funciones Lambda basadas en Zip
- AWS eliminó automáticamente todos los permisos asociados
- Creó nuevas funciones Lambda basadas en Image
- Pero no recreó los permisos en la misma ejecución
Esto era comprensible: era una migración arquitectónica única. La verdadera sorpresa vino después.
El Segundo Problema: La Migración de Refactorización del Módulo
Después de migrar exitosamente a imágenes de contenedor, notamos algo en los logs de despliegue. Durante los primeros despliegues después de la migración, las Lambdas continuaron siendo reemplazadas en lugar de actualizarse in-place.
Los logs de CloudTrail mostraron un patrón claro:
- 15 de octubre: Migración inicial Zip→Image (reemplazos intencionales)
- 22 de octubre: Lambda reemplazada (versión 105)
- 30 de octubre: Lambda reemplazada nuevamente (versión 106)
Comparando los planes de Terraform entre estas ejecuciones reveló lo que estaba pasando. Los planes mostraron recursos cambiando entre:
- module.my_lambda.aws_lambda_function.this_image[0] (will be destroyed)
+ module.my_lambda.aws_lambda_function.this (will be created)
La causa raíz: Durante la migración Zip→Image, nuestro módulo Lambda también estaba siendo refactorizado de un patrón de doble recurso a un diseño más limpio de recurso único. El módulo antiguo tenía:
# Estructura del módulo antiguo (usado durante la migración)
resource "aws_lambda_function" "this" {
count = var.package_type == "Zip" ? 1 : 0
# Configuración del paquete Zip
}
resource "aws_lambda_function" "this_image" {
count = var.package_type == "Image" ? 1 : 0
# Configuración de imagen de contenedor
}
El nuevo módulo usa un único recurso más limpio:
# Estructura del módulo nuevo (actual)
resource "aws_lambda_function" "this" {
package_type = "Image"
image_uri = var.image_uri
# Un único recurso maneja todo
}
La secuencia de migración fue:
- Primer despliegue (15 oct): Migrado de Zip a Image, creando recursos
this_image[0] - Actualización del módulo: Refactorizado para usar un único recurso
this - Despliegues subsecuentes (22, 30 oct): Terraform migrando estado de
this_image[0]athis
Durante estos despliegues transicionales:
- Terraform vio
this_image[0]en el archivo de estado - El código actual definía
this - Terraform destruyó
this_image[0], creóthis - AWS eliminó todos los permisos cuando la Lambda fue eliminada
- Los permisos no fueron recreados en la misma ejecución
Después de algunos ciclos de despliegue, Terraform completó la migración de estado automáticamente, y los planes subsecuentes mostraron el comportamiento correcto: actualizaciones in-place.
La Lección
Lo que parecía ser un simple problema de drift de permisos era en realidad una tormenta perfecta de cambios:
- Comportamiento de AWS: Eliminación automática de permisos cuando las Lambdas son eliminadas
- Migración planificada: Tipo de paquete Zip→Image requiriendo reemplazo
- Refactorización del módulo: Migración de estado de patrón de doble recurso a patrón de recurso único
- Período transicional: Múltiples despliegues necesarios para reconciliar completamente el estado
La solución replace_triggered_by no solo arregló el drift de permisos inmediato sino que también nos protegió durante el período de migración de estado. Aún más importante, evitará este problema si alguna vez necesitamos reemplazar Lambdas nuevamente por cualquier razón (cambios de VPC, etc.).
La lección más grande: las migraciones importantes de infraestructura rara vez ocurren de forma aislada. Cuando múltiples cambios se combinan, tener patrones de infraestructura defensivos como replace_triggered_by se vuelve crítico.
Por Qué Terraform No Detecta Esto
El problema es que los recursos aws_lambda_permission no detectan automáticamente cuando la función Lambda a la que hacen referencia ha sido recreada. Aunque el permiso referencia la Lambda, Terraform los trata como recursos independientes durante la operación de reemplazo.
Esto es lo que sucede durante un despliegue típico de Lambda:
Terraform Plan:
- aws_lambda_function.this will be replaced
(image_uri changed)
Terraform Apply:
1. Eliminar Lambda antigua → AWS elimina permisos automáticamente
2. Crear nueva Lambda → ¡Éxito!
3. Terraform verifica permisos... el estado dice que existen ✓
Siguiente Ejecución de Terraform:
- aws_lambda_permission.eventbridge[0] will be created
(drift detectado - permiso faltante en AWS)
¿Notas el retraso de una ejecución? Ese es el problema.
La Línea de Tiempo del State Drift
Este diagrama de secuencia ilustra exactamente cómo ocurre el drift de estado:
La Solución: replace_triggered_by
Terraform 1.2 introdujo un meta-argumento de lifecycle llamado replace_triggered_by específicamente para manejar esta clase de problemas. Fuerza a Terraform a recrear un recurso siempre que otro recurso sea reemplazado.
Así es como lo implementamos:
Para Permisos Dentro del Módulo Lambda
resource "aws_lambda_permission" "eventbridge_execution_allowed" {
count = var.eventbridge_execution_allowed_arns != null ? length(var.eventbridge_execution_allowed_arns) : 0
statement_id = "AllowExecutionFromEventBridge_${count.index}"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.this.function_name
principal = "events.amazonaws.com"
source_arn = var.eventbridge_execution_allowed_arns[count.index]
# Esto fuerza a Terraform a recrear permisos cuando Lambda cambia
lifecycle {
replace_triggered_by = [
aws_lambda_function.this
]
}
}
El Problema del Límite del Módulo
Sin embargo, nos topamos con un problema con las Lambdas activadas por S3. Teníamos algunos permisos definidos fuera del módulo Lambda:
# En la configuración principal de terraform (fuera del módulo)
resource "aws_lambda_permission" "s3_invoke" {
function_name = module.my_lambda.lambda_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.my_bucket.arn
lifecycle {
replace_triggered_by = [
module.my_lambda.aws_lambda_function.this # ❌ ¡Esto no funciona!
]
}
}
El problema: replace_triggered_by solo puede referenciar recursos directos, no outputs de módulos. Incluso si expones el recurso Lambda como un output, no puedes usarlo en replace_triggered_by a través de límites de módulos.
Visualizando el Problema del Límite del Módulo
La Solución Final: Mover Permisos al Módulo
Resolvimos esto moviendo todos los permisos al módulo Lambda:
Paso 1: Agregar un parámetro opcional para buckets S3
# modules/lambda/variable.tf
variable "s3_execution_allowed_arns" {
description = "Lista de ARNs de buckets S3 permitidos para invocar esta Lambda"
type = list(string)
default = null
}
Paso 2: Crear permisos S3 dentro del módulo
# modules/lambda/main.tf
resource "aws_lambda_permission" "s3_execution_allowed" {
count = var.s3_execution_allowed_arns != null ? length(var.s3_execution_allowed_arns) : 0
statement_id = "AllowExecutionFromS3Bucket_${count.index}"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.this.function_name
principal = "s3.amazonaws.com"
source_arn = var.s3_execution_allowed_arns[count.index]
lifecycle {
replace_triggered_by = [
aws_lambda_function.this
]
}
}
Paso 3: Actualizar configuraciones Lambda para usar el parámetro del módulo
module "my_lambda" {
source = "./modules/lambda"
# ... otros parámetros ...
s3_execution_allowed_arns = [aws_s3_bucket.my_bucket.arn]
}
# Eliminar completamente el recurso aws_lambda_permission externo
Arquitectura Antes y Después
Los Resultados
Después de implementar esta solución:
- Todos los permisos de Lambda ahora están co-localizados con el recurso Lambda
replace_triggered_byfunciona correctamente ya que todo está en el mismo módulo- No más drift de estado - los permisos se recrean en la misma ejecución que la Lambda
- Patrón consistente - permisos de EventBridge, API Gateway y S3 todos manejados de la misma manera
Cuando ejecutamos terraform plan y la Lambda necesita reemplazo, ahora vemos:
Terraform will perform the following actions:
# module.my_lambda.aws_lambda_function.this will be replaced
# module.my_lambda.aws_lambda_permission.eventbridge_execution_allowed[0] will be replaced
# module.my_lambda.aws_lambda_permission.s3_execution_allowed[0] will be replaced
¡Todo en el mismo plan! No más retraso de una ejecución, no más disparadores faltantes.
Puntos Clave
AWS elimina automáticamente los permisos de Lambda cuando la Lambda es eliminada - esto es por diseño, no un bug
Terraform no siempre detecta esta eliminación durante el plan de reemplazo - solo aparece en la siguiente ejecución
replace_triggered_byes la solución correcta - pero solo funciona dentro del mismo módulo/configuraciónLos límites de módulos importan - no puedes usar
replace_triggered_bya través de límites de módulos, incluso con outputsCo-localizar recursos dependientes - mantener recursos estrechamente acoplados (como Lambdas y sus permisos) en el mismo módulo
¿Qué Pasa con Versiones Antiguas de Terraform?
Si estás atascado en Terraform < 1.2, tienes algunas opciones, aunque ninguna es tan limpia como replace_triggered_by:
- Documentar el comportamiento: Aceptar el retraso de una ejecución y asegurarte de que tu equipo sepa ejecutar apply dos veces después de reemplazos de Lambda
- Tainting manual: Usar
terraform tainten recursos de permisos cuando sabes que una Lambda será reemplazada - Scripts wrapper: Crear automatización que maneje el proceso de apply de dos pasos
- Usar Terraform Cloud: Las características de detección de drift pueden ayudar a detectar estos problemas
Dicho esto, si puedes actualizar a Terraform 1.2+, vale la pena solo por esta característica.
Monitoreo y Prevención
Después de este incidente, también configuramos alarmas de CloudWatch para detectar problemas de permisos más rápido. Ahora monitoreamos fallos de invocación de Lambda y comparamos conteos esperados vs reales de disparadores de EventBridge. No evitará el problema, pero al menos sabremos inmediatamente si algo sale mal.
Referencias
- Documentación de
replace_triggered_byde Terraform - Stack Overflow: Recreación de permisos Lambda causando downtime
- Stack Overflow: Permiso Lambda reemplazado en cada apply
Conclusión
La solución replace_triggered_by es infraestructura defensiva: protege no solo contra este problema específico, sino contra cualquier escenario futuro donde las Lambdas necesiten ser reemplazadas. Dado lo frecuente que evoluciona la infraestructura (cambios de VPC, actualizaciones de runtime, migraciones de tipo de paquete), esa tranquilidad vale la pena.
Si estás gestionando funciones Lambda con Terraform y disparadores de EventBridge o S3, implementa este patrón antes de encontrarte con problemas de drift. Tu yo futuro te lo agradecerá.