Corrección de errores de dependencia circular de CloudFormation en AWS Amplify Gen 2.
RESUMEN
Cuando tu función Lambda necesita acceso a Cognito Y se usa como controlador de datos, usa resourceGroupName: "auth" en tu definición de función para evitar dependencias circulares.
El Problema
Si estás construyendo con AWS Amplify Gen 2 y creando funciones Lambda que interactúan con Cognito (para gestión de usuarios, operaciones de grupo, etc.) mientras las usas como resolvers de GraphQL, probablemente hayas encontrado este error frustrante:
[ERROR] [CloudformationStackCircularDependencyError]
The CloudFormation deployment failed due to circular dependency found
between nested stacks [storage0YH5D, auth17956DE9, data74490FG5R, function164FR5G3]
Esto sucede porque Amplify Gen 2 organiza los recursos en stacks CloudFormation anidados separados:
- Auth stack - Cognito User Pool, grupos, triggers.
- Data stack - AppSync API, tablas DynamoDB, resolvers.
- Function stack - Funciones Lambda (por defecto).
- Storage stack - Buckets S3.
Cuando tu función necesita recursos de múltiples stacks, CloudFormation no puede determinar el orden de despliegue, creando una dependencia circular.
Escenario del mundo real
Estaba construyendo un panel de administración para gestión de roles de usuarios. Las funciones Lambda necesitaban:
- Llamar a APIs de Cognito (AdminAddUserToGroup, ListUsersInGroup, etc.).
- Ser usadas como controladores de mutaciones/consultas de GraphQL en el esquema de datos.
Así es como se veía mi configuración inicial:
// amplify/functions/add-user-to-group/resource.ts
export const addUserToGroup = defineFunction({
name: "add-user-to-group",
entry: "./handler.ts",
});
// amplify/auth/resource.ts
export const auth = defineAuth({
// ...
access: (allow) => [
allow.resource(addUserToGroup).to(["addUserToGroup", "listUsers"]),
],
});
// amplify/data/resource.ts - using the function as a handler
addUserToGroup: a
.mutation()
.handler(a.handler.function(addUserToGroup))
.authorization((allow) => [allow.group("ADMINS")]),
Resultado: Error de dependencia circular. El stack de auth importa la función, el stack de datos importa la función, y el stack de funciones depende de ambos 💥.
La solución: resourceGroupName
Amplify Gen 2 proporciona una propiedad simple pero poderosa: resourceGroupName. Esto le dice a Amplify en qué stack anidado colocar tu función.
La corrección
// amplify/functions/add-user-to-group/resource.ts
export const addUserToGroup = defineFunction({
name: "add-user-to-group",
entry: "./handler.ts",
resourceGroupName: "auth", // ← This is the magic line
});
Al colocar la función en el stack auth, rompemos la dependencia circular:
- La función es ahora parte de el stack de auth, no una dependencia separada.
- El stack de datos aún puede referenciarla como controlador.
- Los permisos de auth se otorgan dentro del mismo stack.
Ejemplo completo de trabajo 1
1. Definición de función
// amplify/functions/add-user-to-group/resource.ts
import { defineFunction } from "@aws-amplify/backend";
export const addUserToGroup = defineFunction({
name: "add-user-to-group",
entry: "./handler.ts",
timeoutSeconds: 30,
memoryMB: 256,
resourceGroupName: "auth", // Place in auth stack
});
2. Recurso de auth con otorgamiento de acceso
// amplify/auth/resource.ts
import { defineAuth } from "@aws-amplify/backend";
import { addUserToGroup } from "../functions/add-user-to-group/resource";
export const auth = defineAuth({
loginWith: { email: true },
groups: ["ADMINS", "EDITORS"],
access: (allow) => [
allow.resource(addUserToGroup).to(["addUserToGroup", "listUsers"]),
],
});
3. Esquema de datos usando la función
// amplify/data/resource.ts
import { addUserToGroup } from "../functions/add-user-to-group/resource";
const schema = a.schema({
addUserToGroup: a
.mutation()
.arguments({
email: a.string().required(),
groupName: a.string().required(),
})
.returns(
a.customType({
success: a.boolean(),
message: a.string(),
})
)
.handler(a.handler.function(addUserToGroup))
.authorization((allow) => [allow.group("ADMINS")]),
});
4. Controlador Lambda
// amplify/functions/add-user-to-group/handler.ts
import {
CognitoIdentityProviderClient,
AdminAddUserToGroupCommand,
ListUsersCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import { env } from "$amplify/env/add-user-to-group";
const cognitoClient = new CognitoIdentityProviderClient({});
export const handler = async (event) => {
const { email, groupName } = event.arguments;
// Look up user by email
const listUsersResponse = await cognitoClient.send(
new ListUsersCommand({
UserPoolId: env.AMPLIFY_AUTH_USERPOOL_ID, // Auto-injected by Amplify
Filter: `email = "${email}"`,
})
);
const username = listUsersResponse.Users?.[0]?.Username;
// Add to group
await cognitoClient.send(
new AdminAddUserToGroupCommand({
UserPoolId: env.AMPLIFY_AUTH_USERPOOL_ID,
Username: username,
GroupName: groupName,
})
);
return { success: true, message: `User added to ${groupName}` };
};
5. No olvides el package.json
// amplify/functions/add-user-to-group/package.json
{
"name": "add-user-to-group",
"type": "module",
"dependencies": {
"@aws-sdk/client-cognito-identity-provider": "^3.0.0"
}
}
¡Ejecuta npm install en el directorio de la función!
Ejemplo 2: Dependencia circular de Storage + Data
Otro escenario común es cuando una función Lambda necesita acceder tanto a Storage (S3) como a Data (DynamoDB). Esto ocurre frecuentemente con trabajos de limpieza programados, pipelines de procesamiento de archivos, o cualquier función que gestione archivos mientras rastrea estado en una base de datos.
El escenario
Estaba construyendo una función de limpieza programada que:
- Consulta DynamoDB para encontrar sesiones de grabación de voz antiguas
- Elimina los archivos de audio asociados de S3
- Actualiza los registros de sesión para limpiar las referencias de grabación
La configuración inicial (rota)
Mi primer intento usó allow.resource() en la definición de storage:
// amplify/storage/resource.ts
import { defineStorage } from "@aws-amplify/backend";
import { recordingCleanup } from "../functions/recording-cleanup/resource";
export const storage = defineStorage({
name: "my-app-storage",
access: (allow) => ({
"audio/*": [
allow.authenticated.to(["read", "write"]),
allow.resource(recordingCleanup).to(["read", "delete"]), // ← Problem!
],
}),
});
// amplify/functions/recording-cleanup/resource.ts
import { defineFunction } from "@aws-amplify/backend";
export const recordingCleanup = defineFunction({
name: "recording-cleanup",
entry: "./handler.ts",
schedule: "every week",
resourceGroupName: "data", // Function needs DynamoDB access
});
// amplify/backend.ts - granting DynamoDB permissions
backend.recordingCleanup.resources.lambda.addToRolePolicy(
new PolicyStatement({
actions: ["dynamodb:Query", "dynamodb:Scan", "dynamodb:UpdateItem"],
resources: [backend.data.resources.tables["Session"].tableArn],
})
);
Resultado: Error de dependencia circular entre los stacks storage y data 💥
Por qué falla
La cadena de dependencia crea un bucle:
- Storage importa la función
recordingCleanup→ storage depende de la función - Function está en el stack
datay referencia tablas de datos enbackend.ts→ el stack data está involucrado - backend.ts referencia
backend.storagepara la función → data depende de storage
CloudFormation no puede resolver esta referencia circular.
La corrección: elige el stack primario, otorga el secundario vía CDK
La solución es remover la dependencia de importación de storage y otorgar permisos de S3 vía CDK:
// amplify/storage/resource.ts - NO function import
import { defineStorage } from "@aws-amplify/backend";
export const storage = defineStorage({
name: "my-app-storage",
access: (allow) => ({
"audio/*": [
allow.authenticated.to(["read", "write"]),
// Function access granted via CDK in backend.ts
],
}),
});
// amplify/functions/recording-cleanup/resource.ts
import { defineFunction } from "@aws-amplify/backend";
export const recordingCleanup = defineFunction({
name: "recording-cleanup",
entry: "./handler.ts",
schedule: "every week",
resourceGroupName: "data", // Keep in data stack (primary resource)
});
// amplify/backend.ts - grant BOTH permissions via CDK
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
// DynamoDB permissions
backend.recordingCleanup.resources.lambda.addToRolePolicy(
new PolicyStatement({
actions: ["dynamodb:Query", "dynamodb:Scan", "dynamodb:UpdateItem"],
resources: [
backend.data.resources.tables["Session"].tableArn,
`${backend.data.resources.tables["Session"].tableArn}/index/*`,
],
})
);
// S3 permissions (instead of allow.resource())
backend.recordingCleanup.resources.lambda.addToRolePolicy(
new PolicyStatement({
actions: ["s3:DeleteObject", "s3:GetObject", "s3:ListBucket"],
resources: [
backend.storage.resources.bucket.bucketArn,
`${backend.storage.resources.bucket.bucketArn}/*`,
],
})
);
// Pass bucket name as environment variable
(
backend.recordingCleanup.resources
.lambda as import("aws-cdk-lib/aws-lambda").Function
).addEnvironment(
"STORAGE_BUCKET_NAME",
backend.storage.resources.bucket.bucketName
);
// amplify/functions/recording-cleanup/handler.ts
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
const s3Client = new S3Client({});
const BUCKET_NAME = process.env.STORAGE_BUCKET_NAME!;
export const handler = async () => {
// Query DynamoDB for old sessions...
// Delete from S3 using BUCKET_NAME...
};
Por qué funciona
Al remover la importación allow.resource() de storage:
- Storage ya no depende de la definición de la función
- La función permanece en el stack
data(su recurso primario) - Los permisos de S3 fluyen en una dirección: stack data → recursos storage
- ¡Sin dependencia circular!
Actualización de guía de decisión
| Función necesita... | resourceGroupName | Otorgar acceso vía |
|---|---|---|
| Operaciones de Cognito | "auth" | Callback auth.access |
| API de datos / AppSync | "data" | Schema .authorization() |
| Solo Storage (S3) | "storage" | storage.access con allow.resource() |
| Both Data AND Storage | "data" | Data vía schema, S3 vía CDK |
| Auth AND Data handler | "auth" | Auth vía callback access |
Conclusión clave
Cuando tu función necesita múltiples tipos de recursos, elige el stack primario y otorga permisos secundarios vía CDK. El patrón allow.resource() es conveniente pero crea dependencias de importación que pueden causar referencias circulares. addToRolePolicy() de CDK crea dependencias IAM unidireccionales que CloudFormation puede resolver.
Guía de decisión
| Tu función necesita... | Usa resourceGroupName | Por qué |
|---|---|---|
| Operaciones de Cognito | "auth" | La función se une al stack auth, obtiene permisos vía callback access |
| API de datos / AppSync | "data" | La función se une al stack data, obtiene permisos vía schema authorization |
| Solo Storage (S3) | "storage" | storage.access con allow.resource() |
| Tanto Cognito como handler de datos | "auth" | Prioriza auth ya que los permisos de Cognito requieren el callback access |
| Tanto Data como Storage | "data" | Data vía schema, S3 vía CDK |
| Trigger de auth (pre-signup, etc.) | "auth" | Debe estar en el mismo stack que el recurso auth |
| Solo acceso a S3 | (default) | Usa políticas IAM en backend.ts |
Errores comunes a evitar
❌ No otorgues políticas IAM manualmente cuando el callback access funciona
// DON'T DO THIS for auth operations
backend.myFunction.resources.lambda.addToRolePolicy(
new PolicyStatement({
actions: ["cognito-idp:AdminAddUserToGroup"],
resources: [userPoolArn],
})
);
✅ Sí usa el callback access nativo de Amplify
// DO THIS instead
export const auth = defineAuth({
access: (allow) => [allow.resource(myFunction).to(["addUserToGroup"])],
});
El callback access:
- Automáticamente inyecta la variable de entorno
AMPLIFY_AUTH_USERPOOL_ID. - Otorga permisos IAM de menor privilegio.
- Funciona sin problemas con
resourceGroupName: "auth".
Consejos de depuración
-
Verifica tus imports - Asegúrate de que auth/resource.ts y data/resource.ts estén importando desde el mismo archivo de definición de función.
-
Verifica resourceGroupName - El valor debe ser exactamente
"auth"o"data"(strings en minúsculas). -
Limpia la sandbox - A veces necesitas eliminar y recrear:
npx ampx sandbox delete && npx ampx sandbox. -
Verifica CloudFormation - En AWS Console, mira los stacks anidados para ver dónde terminaron los recursos.
Prevención de este error con documentos de dirección de Kiro
Después de golpear este error múltiples veces, decidí crear un documento de dirección usando Kiro - un IDE potenciado por IA que soporta orientación contextual a través de archivos de dirección.
¿Qué son los documentos de dirección?
Los documentos de dirección son archivos markdown que proporcionan orientación consciente del contexto a los asistentes de IA. Viven en tu directorio .kiro/steering/ y pueden ser:
- Siempre incluidos - Aplicados a cada interacción de IA.
- Emparejados por archivo - Solo incluidos cuando se trabaja en archivos específicos.
- Manual - Incluidos bajo demanda vía claves de contexto.
Creación de un documento de dirección de prevención de dependencia circular
Creé .kiro/steering/amplify-circular-dependency.md con emparejamiento de archivos:
---
inclusion: fileMatch
fileMatchPattern: "amplify/**/*.ts"
---
# Amplify Gen 2 Circular Dependency Prevention
## Checklist Before Creating Lambda Functions
- [ ] Does this function need Cognito access? → Use `resourceGroupName: "auth"`
- [ ] Does this function need Data API access? → Use `resourceGroupName: "data"`
- [ ] Is this function used as a data handler AND needs Cognito? → Use `resourceGroupName: "auth"`
## Decision Matrix
| Function needs... | resourceGroupName | Grant access via |
| ------------------ | ----------------- | ------------------------- |
| Cognito operations | `"auth"` | `auth.access` callback |
| Data API / AppSync | `"data"` | Schema `.authorization()` |
| Storage (S3) only | (default) | `backend.ts` IAM policy |
| Auth trigger | `"auth"` | `auth.triggers` |
Cómo funciona
Ahora, cuando trabajo (o el asistente de IA trabaja) en cualquier archivo del directorio amplify/, Kiro automáticamente incluye esta orientación. La IA recuerda:
- Verificar si la función necesita acceso a Cognito.
- Usar el
resourceGroupNamecorrecto. - Otorgar permisos vía el mecanismo apropiado.
Esto ha eliminado completamente los errores de dependencia circular de mi flujo de trabajo. La IA detecta el problema antes de que suceda, no después de un despliegue fallido.
Por qué importa
- Prevención proactiva - Detecta problemas antes de que causen fallas de despliegue.
- Compartición de conocimiento del equipo - Los nuevos miembros del equipo obtienen la misma orientación.
- Desarrollo asistido por IA - La IA aprende los patrones de tu proyecto.
- Documentación viva - Los documentos de dirección evolucionan con tu base de código.
Consulta Kiro para agregar documentos de dirección a tus propios proyectos y prevenir errores recurrentes como este.
Conclusión
La propiedad resourceGroupName es una herramienta simple pero esencial para gestionar backends complejos de Amplify Gen 2. Cuando tus funciones Lambda necesitan interactuar con múltiples recursos de Amplify (auth, data, storage), piensa en cuál stack pertenecen para evitar dependencias circulares.
Regla general: Si tu función necesita acceso a Cognito, colócala en el stack auth. Si necesita acceso a Data API, colócala en el stack data. Si necesita ambos, auth generalmente gana porque los permisos de Cognito requieren el callback access.
Y si quieres prevenir este error de que nunca vuelva a suceder, usa documentos de dirección de Kiro para codificar este conocimiento directamente en tu flujo de trabajo de desarrollo.