FastAPI
FastAPI es un framework para construir APIs en Python.
El generador de FastAPI crea una nueva FastAPI con configuración de infraestructura usando AWS CDK o Terraform. El backend generado utiliza AWS Lambda para despliegue serverless, expuesto a través de una API de AWS API Gateway. Configura AWS Lambda Powertools para observabilidad, incluyendo registro, trazado con AWS X-Ray y métricas de CloudWatch.
Generar una API FastAPI
Sección titulada «Generar una API FastAPI»Puedes generar una nueva API FastAPI de dos maneras:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#api - Complete los parámetros requeridos
- framework: fastapi
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#api --framework=fastapiyarn nx g @aws/nx-plugin:py#api --framework=fastapinpx nx g @aws/nx-plugin:py#api --framework=fastapibunx nx g @aws/nx-plugin:py#api --framework=fastapiTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runyarn nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runnpx nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runbunx nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runOpciones
Sección titulada «Opciones»| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
| name Requerido | string | - | Nombre del proyecto de API a generar |
| framework | fastapi | fastapi | El framework de API a utilizar. |
| integrationPattern | isolated | shared | isolated | Cómo se generan las integraciones de API Gateway para la API. Elija entre isolated (predeterminado) y shared. |
| auth | iam | cognito | custom | iam | El método utilizado para autenticar con tu API. Elige entre iam (predeterminado), cognito o custom. |
| directory | string | packages | El directorio donde almacenar la aplicación. |
| subDirectory | string | - | El subdirectorio en el que se coloca el proyecto. Por defecto, este es el nombre del proyecto. |
| iac | inherit | cdk | terraform | inherit | El proveedor de IaC preferido. Por defecto, se hereda de tu selección inicial. |
| moduleName | string | - | Nombre del módulo Python |
| infra | rest-lambda | http-lambda | none | rest-lambda | El tipo de infraestructura a utilizar para desplegar esta API. |
| preferInstallDependencies | boolean | true | Si se prefiere instalar las dependencias después de que se ejecute el generador. Establecer en false para diferir la instalación al procesar múltiples generadores en lote (la instalación aún se ejecuta si es necesario para que los generadores subsecuentes puedan calcular el grafo de proyectos de Nx); instalar una vez al final. |
Salida del generador
Sección titulada «Salida del generador»El generador creará la siguiente estructura de proyecto en el directorio <directory>/<api-name>:
- project.json Configuración del proyecto y objetivos de build
- pyproject.toml Configuración del proyecto Python y dependencias
- run.sh Script de arranque de Lambda Web Adapter para iniciar la app FastAPI mediante uvicorn
Directorio<module_name>
- __init__.py Inicialización del módulo
- init.py Configura la aplicación FastAPI y el middleware de powertools
- main.py Implementación de la API
Directorioscripts
- generate_open_api.py Script para generar un esquema OpenAPI desde la app FastAPI
Infraestructura
Sección titulada «Infraestructura»Dado que este generador proporciona infraestructura como código basada en tu proveedor de iacProvider seleccionado, creará un proyecto en packages/common que incluye los constructos CDK o módulos de Terraform correspondientes.
El proyecto común de infraestructura como código tiene la siguiente estructura:
Directoriopackages/common/constructs
Directoriosrc
Directorioapp/ Constructos para infraestructura específica de un proyecto/generador
- …
Directoriocore/ Constructos genéricos reutilizados por los constructos en
app- …
- index.ts Punto de entrada que exporta los constructos de
app
- project.json Objetivos de compilación y configuración del proyecto
Directoriopackages/common/terraform
Directoriosrc
Directorioapp/ Módulos de Terraform para infraestructura específica de un proyecto/generador
- …
Directoriocore/ Módulos genéricos reutilizados por los módulos en
app- …
- project.json Objetivos de compilación y configuración del proyecto
Para implementar tu API, se generan los siguientes archivos:
Directoriopackages/common/constructs/src
Directorioapp
Directorioapis
- <project-name>.ts Constructo de CDK para implementar tu API
Directoriocore
Directorioapi
- http-api.ts Constructo de CDK para implementar una API HTTP (si seleccionaste implementar una API HTTP)
- rest-api.ts Constructo de CDK para implementar una API REST (si seleccionaste implementar una API REST)
- utils.ts Utilidades para los constructos de API
Directoriopackages/common/terraform/src
Directorioapp
Directorioapis
Directorio<project-name>
- <project-name>.tf Módulo para implementar tu API
Directoriocore
Directorioapi
Directoriohttp-api
- http-api.tf Módulo para implementar una API HTTP (si seleccionaste implementar una API HTTP)
Directoriorest-api
- rest-api.tf Módulo para implementar una API REST (si seleccionaste implementar una API REST)
Arquitectura
Sección titulada «Arquitectura»La aplicación desplegada tiene la siguiente arquitectura:
Las REST APIs incluyen una Web ACL de AWS WAFv2 frente al stage de API Gateway con el conjunto de reglas predeterminado administrado por AWS habilitado.
Las HTTP APIs no soportan WAF directamente — si necesitas protección WAF, elige REST API en su lugar o coloca la HTTP API detrás de una distribución de CloudFront.
Implementando tu API FastAPI
Sección titulada «Implementando tu API FastAPI»La implementación principal de la API está en main.py. Aquí es donde defines las rutas de tu API y sus implementaciones. Aquí un ejemplo:
from pydantic import BaseModelfrom .init import app, tracer
class Item(BaseModel): name: str
@app.get("/items/{item_id}")@tracer.capture_methoddef get_item(item_id: int) -> Item: return Item(name=...)
@app.post("/items")@tracer.capture_methoddef create_item(item: Item): return ...El generador configura automáticamente varias características:
- Integración de AWS Lambda Powertools para observabilidad
- Middleware de manejo de errores
- Correlación de solicitudes/respuestas
- Recolección de métricas
- Despliegue en AWS Lambda mediante Lambda Web Adapter con uvicorn
- Streaming con tipos seguros (solo REST API)
Observabilidad con AWS Lambda Powertools
Sección titulada «Observabilidad con AWS Lambda Powertools»Registro
Sección titulada «Registro»El generador configura registro estructurado usando AWS Lambda Powertools. Puedes acceder al logger en tus manejadores de rutas:
from .init import app, logger
@app.get("/items/{item_id}")def read_item(item_id: int): logger.info("Fetching item", extra={"item_id": item_id}) return {"item_id": item_id}El logger incluye automáticamente:
- IDs de correlación para trazado de solicitudes
- Ruta y método de la solicitud
- Información del contexto de Lambda
- Indicadores de inicio en frío
Seguimiento
Sección titulada «Seguimiento»El trazado con AWS X-Ray se configura automáticamente. Puedes añadir subsegmentos personalizados a tus trazas:
from .init import app, tracer
@app.get("/items/{item_id}")@tracer.capture_methoddef read_item(item_id: int): # Crea un nuevo subsegmento with tracer.provider.in_subsegment("fetch-item-details"): # Tu lógica aquí return {"item_id": item_id}Métricas
Sección titulada «Métricas»Las métricas de CloudWatch se recopilan automáticamente para cada solicitud. Puedes añadir métricas personalizadas:
from .init import app, metricsfrom aws_lambda_powertools.metrics import MetricUnit
@app.get("/items/{item_id}")def read_item(item_id: int): metrics.add_metric(name="ItemViewed", unit=MetricUnit.Count, value=1) return {"item_id": item_id}Las métricas por defecto incluyen:
- Conteo de solicitudes
- Conteos de éxito/fallo
- Métricas de inicio en frío
- Métricas por ruta
Manejo de errores
Sección titulada «Manejo de errores»El generador incluye manejo de errores completo:
from fastapi import HTTPException
@app.get("/items/{item_id}")def read_item(item_id: int): if item_id < 0: raise HTTPException(status_code=400, detail="Item ID must be positive") return {"item_id": item_id}Las excepciones no manejadas son capturadas por el middleware y:
- Registran la excepción completa con stack trace
- Registran una métrica de fallo
- Devuelven una respuesta segura 500 al cliente
- Preservan el ID de correlación
Accediendo al usuario que llama
Sección titulada «Accediendo al usuario que llama»Cuando tu API está protegida por autenticación, tus manejadores de rutas a menudo necesitan saber quién está llamando. La FastAPI generada se ejecuta dentro de AWS Lambda mediante el Lambda Web Adapter, que reenvía el contexto de solicitud de API Gateway como JSON en el encabezado x-amzn-request-context. Puedes leerlo desde el Request de FastAPI para extraer la identidad del llamador.
Como ejemplo, agreguemos un endpoint /me que devuelve detalles sobre el usuario que llama. Implementaremos la extracción como una dependencia de FastAPI para que pueda reutilizarse en todas las rutas. La forma del contexto de solicitud — y por lo tanto cómo extraes la identidad — depende tanto de tu método auth seleccionado como de si desplegaste una REST API o HTTP API.
Para autenticación IAM, buscamos al llamador en Cognito usando el sub extraído del contexto de solicitud de API Gateway. Crea identity.py junto a main.py:
import jsonimport osfrom typing import Annotated
from boto3 import clientfrom fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
cognito = client("cognito-idp")
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # El Lambda Web Adapter reenvía el contexto de solicitud de API Gateway como JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) provider = request_context.get("identity", {}).get("cognitoAuthenticationProvider")
sub = provider.split(":")[-1] if provider else None if not sub: raise HTTPException(status_code=403, detail="Unable to determine calling user")
users = cognito.list_users( # Asume que el id del user pool está configurado en el entorno lambda UserPoolId=os.environ["USER_POOL_ID"], Limit=1, Filter=f'sub="{sub}"', ).get("Users", [])
if len(users) != 1: raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
return Identity(sub=sub, username=users[0]["Username"])
CurrentUser = Annotated[Identity, Depends(get_identity)]import jsonimport osfrom typing import Annotated
from boto3 import clientfrom fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
cognito = client("cognito-idp")
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # El Lambda Web Adapter reenvía el contexto de solicitud de API Gateway como JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) amr = ( request_context.get("authorizer", {}) .get("iam", {}) .get("cognitoIdentity", {}) .get("amr", []) ) sign_in = next((s for s in amr if ":CognitoSignIn:" in s), None) sub = sign_in.split(":")[-1] if sign_in else None
if not sub: raise HTTPException(status_code=403, detail="Unable to determine calling user")
users = cognito.list_users( # Asume que el id del user pool está configurado en el entorno lambda UserPoolId=os.environ["USER_POOL_ID"], Limit=1, Filter=f'sub="{sub}"', ).get("Users", [])
if len(users) != 1: raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
return Identity(sub=sub, username=users[0]["Username"])
CurrentUser = Annotated[Identity, Depends(get_identity)]Con auth: 'cognito', el autorizador de Cognito User Pools de API Gateway verifica el JWT que el llamador proporciona en el encabezado Authorization y coloca los claims verificados en el contexto de solicitud.
Crea identity.py junto a main.py:
import jsonfrom typing import Annotated
from fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # El Lambda Web Adapter reenvía el contexto de solicitud de API Gateway como JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) claims = request_context.get("authorizer", {}).get("claims", {})
sub = claims.get("sub") username = claims.get("username")
if not sub or not username: raise HTTPException(status_code=403, detail="Unable to determine calling user")
return Identity(sub=sub, username=username)
CurrentUser = Annotated[Identity, Depends(get_identity)]Las HTTP APIs usan un autorizador JWT que coloca los claims verificados bajo authorizer.jwt.claims:
import jsonfrom typing import Annotated
from fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # El Lambda Web Adapter reenvía el contexto de solicitud de API Gateway como JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) claims = request_context.get("authorizer", {}).get("jwt", {}).get("claims", {})
sub = claims.get("sub") username = claims.get("username")
if not sub or not username: raise HTTPException(status_code=403, detail="Unable to determine calling user")
return Identity(sub=sub, username=username)
CurrentUser = Annotated[Identity, Depends(get_identity)]Luego puedes inyectar la dependencia CurrentUser en cualquier ruta que necesite la identidad del llamador:
from .identity import CurrentUser, Identityfrom .init import app, tracer
@app.get("/me")@tracer.capture_methoddef me(identity: CurrentUser) -> Identity: return identityStreaming
Sección titulada «Streaming»La FastAPI generada soporta respuestas en streaming de forma predeterminada cuando se usa una REST API. La infraestructura está configurada para usar el AWS Lambda Web Adapter para ejecutar tu FastAPI mediante uvicorn dentro de Lambda, con ResponseTransferMode.STREAM en API Gateway para todas las operaciones de REST API, lo que permite que el streaming funcione junto con operaciones que no son de streaming.
Usando JsonStreamingResponse
Sección titulada «Usando JsonStreamingResponse»El init.py generado exporta una clase JsonStreamingResponse que proporciona streaming con tipos seguros y generación adecuada del esquema OpenAPI. Esto asegura que el generador connection pueda producir métodos de cliente de streaming correctamente tipados.
from pydantic import BaseModelfrom .init import app, JsonStreamingResponse
class Chunk(BaseModel): message: str
async def generate_chunks(): for i in range(100): yield Chunk(message=f"This is chunk {i}")
@app.post( "/stream", response_class=JsonStreamingResponse, responses={200: JsonStreamingResponse.openapi_response(Chunk, "Stream of chunks")},)async def my_stream() -> JsonStreamingResponse: return JsonStreamingResponse(generate_chunks())La clase JsonStreamingResponse:
- Serializa modelos Pydantic al formato JSON Lines (
application/jsonl) - Proporciona un helper
openapi_responseque genera el esquema OpenAPI correcto conitemSchema, permitiendo que el generadorconnectionproduzca métodos de cliente de streaming con tipos seguros
Consumo
Sección titulada «Consumo»Para consumir un stream de respuestas, puedes usar el generador connection que provee un método tipado para iterar sobre los fragmentos transmitidos.
Desplegando tu API FastAPI
Sección titulada «Desplegando tu API FastAPI»El generador de FastAPI crea código de infraestructura CDK o Terraform según tu iacProvider seleccionado. Puedes usarlo para desplegar tu API FastAPI.
El constructo CDK para desplegar tu API está en la carpeta common/constructs. Puedes usarlo en una aplicación CDK:
import { MyApi } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Añadir la API al stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}Esto configura:
- Una función AWS Lambda por cada operación en la aplicación FastAPI
- API Gateway HTTP/REST API como trigger de la función
- Roles y permisos IAM
- Grupo de logs de CloudWatch
- Configuración de trazado X-Ray
- Namespace de métricas CloudWatch
Los módulos Terraform para desplegar tu API están en la carpeta common/terraform. Puedes usarlos en una configuración Terraform.
El módulo de API almacena su zip de despliegue Lambda en un bucket S3 de activos compartido — consulta la guía de infraestructura Terraform para más detalles. Instancia el módulo core/asset-bucket una vez por despliegue y pasa su salida bucket_name a cada módulo de API / Lambda mediante la entrada asset_bucket_name:
module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Variables de entorno para la función Lambda env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Políticas IAM adicionales si es necesario additional_iam_policy_statements = [ # Añade los permisos adicionales que tu API necesite ]
tags = local.common_tags}Esto configura:
- Una función Lambda que sirve todas las rutas de FastAPI
- API Gateway HTTP/REST API como trigger de la función
- Roles y permisos IAM
- Grupo de logs CloudWatch
- Configuración de trazado X-Ray
- Configuración CORS
El módulo Terraform provee varios outputs que puedes usar:
# Acceder al endpoint de la APIoutput "api_url" { value = module.my_api.stage_invoke_url}
# Acceder a los detalles de la función Lambdaoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Acceder al rol IAM para conceder permisos adicionalesoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}Puedes personalizar la configuración CORS pasando variables al módulo:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Configuración CORS personalizada cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}Para las API REST, el constructo generado asocia una Web ACL de AWS WAFv2 con la etapa de API Gateway de forma predeterminada. La Web ACL utiliza el conjunto de reglas predeterminado administrado por AWS (AWSManagedRulesCommonRuleSet y AWSManagedRulesKnownBadInputsRuleSet), proporcionando protección contra exploits web comunes, incluido el OWASP Top 10. Los registros de solicitudes de WAF se escriben en un grupo de CloudWatch Logs.
Puedes editar el constructo rest-api generado para agregar, eliminar o ajustar reglas (por ejemplo, para agregar reglas basadas en tasas o grupos de reglas administradas adicionales).
Para optar por no participar (por ejemplo, para adjuntar tu propia Web ACL), establece enableWaf en false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Para optar por no participar (por ejemplo, para adjuntar tu propia Web ACL), establece enable_waf en false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Registro de acceso
Sección titulada «Registro de acceso»Para las APIs REST, la infraestructura generada habilita el registro de acceso por defecto, escribiendo una línea JSON estructurada por solicitud en un grupo de CloudWatch Logs dedicado. El grupo de registros está cifrado con una clave KMS administrada por el cliente y se conserva durante un año.
API Gateway escribe registros de acceso utilizando un rol de CloudWatch Logs a nivel de cuenta. Este rol se configura en el ajuste AWS::ApiGateway::Account, que es un singleton por región por cuenta — solo hay un rol para cada API REST en la región. Para gestionar esto de forma segura a través de múltiples stacks desplegados de forma independiente, la infraestructura generada:
- Crea un rol compartido de CloudWatch Logs y lo configura en la cuenta solo cuando no hay un rol funcional ya establecido, por lo que los despliegues nunca sobrescriben un rol que pertenece a otro stack.
- Deja el ajuste de la cuenta intacto durante el desmontaje, por lo que destruir un stack nunca deshabilita el registro para otras APIs REST en la región.
El rol de cuenta es gestionado por el constructo ApiGatewayAccount, un singleton con ámbito de stack resuelto a través de ApiGatewayAccount.ensure(scope). Cada etapa de la API REST depende de él, y el rol se configura mediante un recurso personalizado respaldado por Lambda.
Puedes personalizar el formato del registro de acceso pasando deployOptions al construir tu API:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), deployOptions: { accessLogFormat: AccessLogFormat.clf(), },});El rol de cuenta es gestionado por el módulo core/api/api-gateway-account, que es instanciado por el módulo de API generado. Configura la cuenta de forma idempotente y nunca se restablece con terraform destroy.
Puedes personalizar el formato del registro de acceso editando el bloque access_log_settings en el recurso aws_api_gateway_stage en el módulo de API generado.
Integraciones
Sección titulada «Integraciones»Los constructos CDK de la API REST/HTTP están configurados para proporcionar una interfaz type-safe que define integraciones para cada una de tus operaciones.
Los constructos CDK proporcionan soporte completo de integración con seguridad de tipos como se describe a continuación.
Integraciones por defecto
Sección titulada «Integraciones por defecto»Puedes usar el método estático defaultIntegrations para utilizar el patrón por defecto, que define una función AWS Lambda individual para cada operación:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Los módulos de Terraform usan automáticamente el patrón router con una única función Lambda. No se necesita configuración adicional:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# El módulo crea automáticamente una única función Lambda # que maneja todas las operaciones de la API tags = local.common_tags}Accediendo a las integraciones
Sección titulada «Accediendo a las integraciones»Puedes acceder a las funciones AWS Lambda subyacentes a través de la propiedad integrations del constructo de la API, de manera type-safe. Por ejemplo, si tu API define una operación llamada sayHello y necesitas agregar permisos a esta función, puedes hacerlo así:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello está tipado según las operaciones definidas en tu APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Si tu API usa el patrón shared, el router Lambda compartido se expone como api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Con el patrón router de Terraform, solo hay una función Lambda. Puedes acceder a ella a través de los outputs del módulo:
# Otorgar permisos adicionales a la única función Lambdaresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Personalizando opciones por defecto
Sección titulada «Personalizando opciones por defecto»Si deseas personalizar las opciones usadas al crear la función Lambda para cada integración por defecto, puedes usar el método withDefaultOptions. Por ejemplo, si quieres que todas tus funciones Lambda residan en una VPC:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});Para personalizar opciones como configuración de VPC, necesitas editar el módulo de Terraform generado. Por ejemplo, para agregar soporte de VPC a todas las funciones Lambda:
# Agregar variables de VPCvariable "vpc_subnet_ids" { description = "Lista de IDs de subredes VPC para la función Lambda" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "Lista de IDs de grupos de seguridad VPC para la función Lambda" type = list(string) default = []}
# Actualizar el recurso de función Lambdaresource "aws_lambda_function" "api_lambda" { # ... configuración existente ...
# Agregar configuración VPC vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}Luego usar el módulo con configuración VPC:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Configuración VPC vpc_subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] vpc_security_group_ids = [aws_security_group.lambda_sg.id]
tags = local.common_tags}Personalizando opciones por operación
Sección titulada «Personalizando opciones por operación»Para personalizar las opciones usadas al crear la integración por defecto para operaciones específicas (sin afectar a las demás), puedes usar el método withOperationOptions. Por ejemplo, si deseas aumentar el timeout de la función Lambda para solo una operación:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// Las operaciones seleccionadas siguen siendo integraciones por defecto, por lo que siguen tipadas en consecuencia:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));Las opciones que especificas se fusionan con las opciones de integración por defecto (y cualquier opción establecida mediante withDefaultOptions). Ten en cuenta que no puedes especificar opciones para operaciones que hayas reemplazado mediante withOverrides, ya que estas ya no usan la integración por defecto.
Encontrarás un error de tipo si la misma operación es objetivo tanto de withOperationOptions como de withOverrides, independientemente del orden en que los llames.
Para personalizar opciones para operaciones específicas con Terraform, necesitas editar el módulo de Terraform generado para configurar funciones Lambda individuales por operación (consulta la sección Integraciones Explícitas abajo).
Sobrescribiendo integraciones
Sección titulada «Sobrescribiendo integraciones»También puedes sobrescribir integraciones para operaciones específicas usando el método withOverrides. Cada override debe especificar una propiedad integration que está tipada al constructo de integración CDK apropiado para la API HTTP o REST. El método withOverrides también es type-safe. Por ejemplo, si quieres sobrescribir una API getDocumentation para apuntar a documentación alojada en un sitio externo:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Notarás que la integración sobrescrita ya no tiene una propiedad handler cuando se accede a través de api.integrations.getDocumentation.
Puedes agregar propiedades adicionales a una integración que también estarán tipadas, permitiendo abstraer otros tipos de integración manteniendo el type-safe. Por ejemplo, si creaste una integración S3 para una API REST y luego quieres referenciar el bucket para una operación particular:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Más tarde, quizás en otro archivo, puedes acceder a la propiedad bucket que definimos// de manera type-safeapi.integrations.getFile.bucket.grantRead(...);Sobrescribiendo autorizadores
Sección titulada «Sobrescribiendo autorizadores»También puedes proveer options en tu integración para sobrescribir opciones de método específicas como autorizadores. Por ejemplo, si deseas usar autenticación con Cognito para tu operación getDocumentation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // para REST, o HttpUserPoolAuthorizer para HTTP API } }, }) .build(),});Integraciones explícitas
Sección titulada «Integraciones explícitas»Si prefieres, puedes no usar las integraciones por defecto y proveer directamente una para cada operación. Esto es útil si, por ejemplo, cada operación necesita usar un tipo diferente de integración o quieres recibir un error de tipo al agregar nuevas operaciones:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Para integraciones explícitas por operación con Terraform, debes modificar el módulo específico de la aplicación generado para reemplazar la integración proxy por defecto con integraciones específicas para cada operación.
Edita packages/common/terraform/src/app/apis/my-api/my-api.tf:
- Eliminar las rutas proxy por defecto (ej.
resource "aws_apigatewayv2_route" "proxy_routes") - Reemplazar la única función Lambda con funciones individuales para cada operación
- Crear integraciones y rutas específicas para cada operación, reusando el mismo paquete ZIP:
# Eliminar la función Lambda única por defecto resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... resto de la configuración }
# Eliminar la integración proxy por defecto resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... resto de la configuración }
# Eliminar las rutas proxy por defecto resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... resto de la configuración }
# Agregar funciones Lambda individuales para cada operación usando el mismo bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Agregar integraciones específicas para cada operación resource "aws_apigatewayv2_integration" "say_hello_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.say_hello_handler.invoke_arn payload_format_version = "2.0" timeout_milliseconds = 30000 }
resource "aws_apigatewayv2_integration" "get_documentation_integration" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET" }
# Agregar rutas específicas para cada operación resource "aws_apigatewayv2_route" "say_hello_route" { api_id = module.http_api.api_id route_key = "POST /sayHello" target = "integrations/${aws_apigatewayv2_integration.say_hello_integration.id}" authorization_type = "AWS_IAM" }
resource "aws_apigatewayv2_route" "get_documentation_route" { api_id = module.http_api.api_id route_key = "GET /documentation" target = "integrations/${aws_apigatewayv2_integration.get_documentation_integration.id}" authorization_type = "NONE" }
# Agregar permisos Lambda para cada función resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }# Eliminar la función Lambda única por defecto resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... resto de la configuración }
# Eliminar la integración proxy por defecto resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... resto de la configuración }
# Eliminar las rutas proxy por defecto resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... resto de la configuración }
# Agregar funciones Lambda individuales para cada operación usando el mismo bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Agregar recursos y métodos específicos para cada operación resource "aws_api_gateway_resource" "say_hello_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = "POST" authorization = "AWS_IAM" }
resource "aws_api_gateway_integration" "say_hello_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = aws_api_gateway_method.say_hello_method.http_method
integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.say_hello_handler.invoke_arn }
resource "aws_api_gateway_resource" "get_documentation_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "documentation" }
resource "aws_api_gateway_method" "get_documentation_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = "GET" authorization = "NONE" }
resource "aws_api_gateway_integration" "get_documentation_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = aws_api_gateway_method.get_documentation_method.http_method
integration_http_method = "GET" type = "HTTP" uri = "https://example.com/documentation" }
# Actualizar deployment para depender de nuevas integraciones~ resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = module.rest_api.api_id
depends_on = [ aws_api_gateway_integration.lambda_integration, aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ]
lifecycle { create_before_destroy = true }
triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ])) } }
# Agregar permisos Lambda para cada función resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }Patrón de Integración
Sección titulada «Patrón de Integración»Los constructos de API CDK generados admiten dos patrones de integración:
isolatedcrea una función Lambda por operación. Este es el valor por defecto para las APIs generadas.sharedcrea un único router Lambda por defecto y lo reutiliza para cada operación a menos que sobrescribas integraciones específicas.
isolated te brinda permisos y configuración más granulares por operación. shared reduce la proliferación de funciones Lambda e integraciones de API Gateway mientras aún permite sobrescritas selectivas.
Por ejemplo, establecer pattern a 'shared' crea una única función en lugar de una por integración:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Los módulos de Terraform usan automáticamente el patrón router - este es el enfoque por defecto y único soportado. El módulo generado crea una única función Lambda que maneja todas las operaciones de la API.
Simplemente instancia el módulo por defecto para obtener el patrón router:
# Patrón router por defecto - única función Lambda para todas las operacionesmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Una única función Lambda maneja todas las operaciones automáticamente tags = local.common_tags}Generación de código
Sección titulada «Generación de código»Dado que las operaciones en FastAPI se definen en Python y la infraestructura CDK en TypeScript, instrumentamos generación de código para proveer metadatos al constructo CDK y proporcionar una interfaz tipada para las integraciones.
Se añade un target generate:<ApiName>-metadata al project.json de los constructos comunes para facilitar esta generación de código, que emite un archivo como packages/common/constructs/src/generated/my-api/metadata.gen.ts. Dado que esto se genera en tiempo de build, se ignora en control de versiones.
Concediendo acceso (solo IAM)
Sección titulada «Concediendo acceso (solo IAM)»Si seleccionaste autenticación IAM, puedes usar el método grantInvokeAccess para conceder acceso a tu API:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Crear una política IAM para permitir invocar la APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Política para permitir invocar la FastAPI"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Adjuntar la política a un rol IAM (por ejemplo, para usuarios autenticados)resource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}
# O adjuntar a un rol existente por nombreresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}Los outputs clave del módulo API que puedes usar para políticas IAM son:
module.my_api.api_execution_arn- Para conceder permisos execute-api:Invokemodule.my_api.api_arn- El ARN de API Gatewaymodule.my_api.lambda_function_arn- El ARN de la función Lambda
Desarrollo local
Sección titulada «Desarrollo local»El generador configura un servidor de desarrollo local que puedes ejecutar con:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiEsto inicia un servidor de desarrollo FastAPI local con:
- Recarga automática al cambiar el código
- Documentación interactiva de la API en
/docso/redoc - Esquema OpenAPI en
/openapi.json
Invocando tu API FastAPI
Sección titulada «Invocando tu API FastAPI»Para invocar tu API desde un sitio React, puedes usar el generador connection.
Conexiones
Sección titulada «Conexiones»Usa el generador connection para integrar este proyecto con otros en tu workspace. Las siguientes conexiones involucran este proyecto: