API TypeScript de Smithy
Smithy es un lenguaje de definición de interfaz independiente del protocolo para crear APIs de manera orientada a modelos.
El generador de API TypeScript de Smithy crea una nueva API usando Smithy para la definición del servicio, y el Smithy TypeScript Server SDK para la implementación. El generador proporciona infraestructura como código CDK o Terraform para desplegar tu servicio en AWS Lambda, expuesto a través de una API REST de AWS API Gateway. Proporciona desarrollo de API con seguridad de tipos con generación automática de código a partir de modelos Smithy. El handler generado utiliza AWS Lambda Powertools for TypeScript para observabilidad, incluyendo registro, trazado de AWS X-Ray y métricas de CloudWatch
Generar una API TypeScript de Smithy
Sección titulada «Generar una API TypeScript de Smithy»Puedes generar una nueva API TypeScript de Smithy de dos maneras:
Ejecute este generador@aws/nx-plugin:ts#api
pnpm nx g @aws/nx-plugin:ts#api yarn nx g @aws/nx-plugin:ts#api npx nx g @aws/nx-plugin:ts#api bunx nx g @aws/nx-plugin:ts#api- 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 - ts#api - Complete los parámetros requeridos
- Haga clic en
Generate
Construya su comando10
Requerido
framework = smithy
Opciones
Sección titulada «Opciones»nameRequeridostringEl nombre de la API (requerido). Se utiliza para generar nombres de clases y rutas de archivos.
frameworkenumPredeterminado:trpcEl framework de API a utilizar.
trpcsmithyintegrationPatternenumPredeterminado:isolatedCómo se generan las integraciones de API Gateway para la API. Elija entre isolated (predeterminado) y shared.
isolatedsharedauthenumPredeterminado:iamEl método utilizado para autenticar con tu API. Elige entre iam (predeterminado), cognito o custom.
iamcognitocustomdirectorystringPredeterminado:packagesEl directorio donde almacenar la aplicación.
iacenumPredeterminado:inheritEl proveedor de IaC preferido. Por defecto, se hereda de tu selección inicial.
inheritcdkterraforminfraenumPredeterminado:rest-lambdaEl tipo de infraestructura a utilizar para desplegar esta API.
rest-lambdanonenamespacestringframework = smithyEl espacio de nombres para la API Smithy (solo aplicable para el framework smithy). Por defecto es el alcance de tu monorepo
subDirectorystringEl subdirectorio en el que se coloca el proyecto. Por defecto es el nombre del proyecto.
preferInstallDependenciesbooleanPredeterminado:trueSi se prefiere instalar las dependencias después de que se ejecute el generador. Establece en false para diferir la instalación cuando se ejecutan 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); instala una vez al final.
Salida del Generador
Sección titulada «Salida del Generador»El generador crea dos proyectos relacionados en el directorio <directory>/<api-name>:
Directoriomodel/ Proyecto del modelo Smithy
- project.json Configuración del proyecto y objetivos de compilación
- smithy-build.json Configuración de compilación de Smithy
- ssdk.rolldown.config.mjs Empaqueta el TypeScript Server SDK generado
Directoriosrc/
- main.smithy Definición principal del servicio
Directoriooperations/
- echo.smithy Definición de operación de ejemplo
Directoriobackend/ Implementación del backend en TypeScript
- package.json Manifiesto del proyecto que define el nombre del paquete y las dependencias
- project.json Configuración del proyecto y objetivos de compilación
- rolldown.config.ts Configuración del empaquetado
- tsconfig.json Configuración de TypeScript
- tsconfig.lib.json Configuración de TypeScript para las fuentes de la biblioteca
- tsconfig.spec.json Configuración de TypeScript para las pruebas
- vitest.config.mts Configuración de Vitest
Directoriosrc/
- index.ts Punto de entrada del paquete
- handler.ts Handler de AWS Lambda
- local-server.ts Servidor de desarrollo local
- service.ts Implementación del servicio
- context.ts Definición del contexto del servicio
Directoriooperations/
- echo.ts Implementación de operación de ejemplo
Directoriogenerated/ SDK de TypeScript generado (creado durante la compilación)
- …
Infraestructura
Sección titulada «Infraestructura»Dado que este generador crea infraestructura como código basada en tu iac elegido, creará un proyecto en packages/common que incluye los constructos CDK o módulos Terraform relevantes.
El proyecto común de infraestructura como código está estructurado de la siguiente manera:
Directoriopackages/common/constructs
Directoriosrc
Directorioapp/ Constructos para infraestructura específica de un proyecto/generador
Directorioapis/
- <project-name>.ts Constructo CDK para desplegar tu API
Directoriocore/ Constructos genéricos que son reutilizados por constructos en
appDirectorioapi/
- rest-api.ts Constructo CDK para desplegar una API REST
- utils.ts Utilidades para los constructos de API
- index.ts Punto de entrada que exporta constructos desde
app
- project.json Objetivos de compilación y configuración del proyecto
Directoriopackages/common/terraform
Directoriosrc
Directorioapp/ Módulos Terraform para infraestructura específica de un proyecto/generador
Directorioapis/
Directorio<project-name>/
- <project-name>.tf Módulo para desplegar tu API
Directoriocore/ Módulos genéricos que son reutilizados por módulos en
appDirectorioapi/
Directoriorest-api/
- rest-api.tf Módulo para desplegar una API REST
- project.json Objetivos de compilación y configuración del proyecto
Arquitectura
Sección titulada «Arquitectura»La API de Smithy desplegada tiene la siguiente arquitectura, con un Web ACL de AWS WAFv2 frente al stage de API Gateway:
Implementar tu API de Smithy
Sección titulada «Implementar tu API de Smithy»Definir Operaciones en Smithy
Sección titulada «Definir Operaciones en Smithy»Las operaciones se definen en archivos Smithy dentro del proyecto del modelo. La definición principal del servicio está en main.smithy:
$version: "2.0"
namespace your.namespace
use aws.protocols#restJson1use smithy.framework#ValidationException
@title("YourService")@restJson1service YourService { version: "1.0.0" operations: [ Echo, // Add your operations here ] errors: [ ValidationException ]}Las operaciones individuales se definen en archivos separados en el directorio operations/:
$version: "2.0"
namespace your.namespace
@http(method: "POST", uri: "/echo")operation Echo { input: EchoInput output: EchoOutput}
structure EchoInput { @required message: String
foo: Integer bar: String}
structure EchoOutput { @required message: String}Agregar una Biblioteca de Formas
Sección titulada «Agregar una Biblioteca de Formas»Si tienes varias APIs de Smithy que comparten los mismos tipos de datos, puedes definir esos tipos una vez en una biblioteca de formas en lugar de duplicarlos en cada modelo. Una biblioteca de formas es un proyecto Smithy sin servicio — solo formas reutilizables — del cual cualquier número de proyectos Smithy pueden depender.
Genera una con el generador smithy#project:
Ejecute este generador@aws/nx-plugin:smithy#project
pnpm nx g @aws/nx-plugin:smithy#project yarn nx g @aws/nx-plugin:smithy#project npx nx g @aws/nx-plugin:smithy#project bunx nx g @aws/nx-plugin:smithy#project- 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 - smithy#project - Complete los parámetros requeridos
- Haga clic en
Generate
Construya su comando7
Requerido
El modelo de tu API puede entonces referenciar sus formas con use:
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}Consulta la guía del proyecto Smithy para saber cómo crear una biblioteca de formas y conectarla como dependencia del modelo de tu API.
Implementar Operaciones en TypeScript
Sección titulada «Implementar Operaciones en TypeScript»Las implementaciones de operaciones se encuentran en el directorio src/operations/ del proyecto backend. Cada operación se implementa usando los tipos generados del TypeScript Server SDK (generado en tiempo de compilación a partir de tu modelo Smithy).
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input) => { // Your business logic here return { message: `Echo: ${input.message}` // type-safe based on your Smithy model };};Las operaciones deben registrarse en la definición del servicio en src/service.ts:
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';// Import other operations here
// Register operations to the service hereexport const Service: YourServiceService<ServiceContext> = { Echo, // Add other operations here};Contexto del Servicio
Sección titulada «Contexto del Servicio»Puedes definir un contexto compartido para tus operaciones en context.ts:
export interface ServiceContext { // Powertools tracer, logger and metrics are provided by default tracer: Tracer; logger: Logger; metrics: Metrics; // Add shared dependencies, database connections, etc. dbClient: any; userIdentity: string;}Este contexto se pasa a todas las implementaciones de operaciones y puede usarse para compartir recursos como conexiones de base de datos, configuración o utilidades de registro.
Observabilidad con AWS Lambda Powertools
Sección titulada «Observabilidad con AWS Lambda Powertools»Registro
Sección titulada «Registro»El generador configura el registro estructurado usando AWS Lambda Powertools con inyección automática de contexto a través del middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puedes referenciar el logger desde tus implementaciones de operaciones a través del contexto:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Your log message'); // ...};Trazado
Sección titulada «Trazado»El trazado de AWS X-Ray se configura automáticamente a través del middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puedes agregar subsegmentos personalizados a tus trazas en tus operaciones:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { // Creates a new subsegment const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation'); try { // Your logic here } catch (error) { subsegment?.addError(error as Error); throw error; } finally { subsegment?.close(); }};Métricas
Sección titulada «Métricas»Las métricas de CloudWatch se recopilan automáticamente para cada solicitud a través del middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puedes agregar métricas personalizadas en tus operaciones:
import { MetricUnit } from '@aws-lambda-powertools/metrics';import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.metrics.addMetric("CustomMetric", MetricUnit.Count, 1); // ...};Manejo de Errores
Sección titulada «Manejo de Errores»Smithy proporciona manejo de errores integrado. Puedes definir errores personalizados en tu modelo Smithy:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}Y registrarlos en tu operación/servicio:
operation MyOperation { ... errors: [InvalidRequestError]}Luego lanzarlos en tu implementación TypeScript:
import { InvalidRequestError } from '../generated/ssdk/index.js';
export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => { if (!input.requiredField) { throw new InvalidRequestError({ message: "Required field is missing" }); }
return { /* success response */ };};Acceder al Usuario que Llama
Sección titulada «Acceder al Usuario que Llama»Cuando tu API está protegida por autenticación, tus operaciones a menudo necesitan saber quién está llamando. El enfoque recomendado es resolver la identidad del llamador una vez en el handler y pasarla a través del contexto del servicio para su consumo por operaciones específicas.
Modelaremos el caso no autorizado como un error de Smithy para que se serialice a una respuesta 403 adecuada. Agrégalo a tu modelo, por ejemplo en model/src/operations/errors.smithy, y refiérelo en cualquier operación que requiera identidad:
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Primero, expón la identidad resuelta en el contexto del servicio en src/context.ts. La proporcionamos como una función para que el UnauthorizedError se lance desde dentro de una operación (donde el Server SDK lo serializa a un 403), en lugar de desde el handler:
import { Logger } from '@aws-lambda-powertools/logger';import { Metrics } from '@aws-lambda-powertools/metrics';import { Tracer } from '@aws-lambda-powertools/tracer';
export interface Identity { sub: string; username: string;}
/** * Context provided to all operations. */export interface ServiceContext { tracer: Tracer; logger: Logger; metrics: Metrics; getIdentity: () => Promise<Identity>;}A continuación, escribe el resolvedor en src/identity.ts. Lanza UnauthorizedError cuando no se puede determinar el llamador. La implementación depende de tu método auth seleccionado:
Para autenticación IAM, buscamos el llamador en Cognito usando el sub extraído del evento de API Gateway. La búsqueda utiliza el cliente Cognito Identity Provider, que no es una dependencia de un backend Smithy generado, así que instálalo primero en el proyecto backend:
pnpm add @aws-sdk/client-cognito-identity-provider@3.1126.0 --filter my-apiyarn workspace @my-scope/my-api add @aws-sdk/client-cognito-identity-provider@3.1126.0npm install --legacy-peer-deps @aws-sdk/client-cognito-identity-provider@3.1126.0 -w packages/my-api/backendbun add @aws-sdk/client-cognito-identity-provider@3.1126.0 --cwd packages/my-api/backendimport { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
const cognito = new CognitoIdentityProvider();
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const cognitoAuthenticationProvider = event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new UnauthorizedError({ message: 'Unable to determine calling user', }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new UnauthorizedError({ message: `No user found with subjectId ${sub}`, }); }
return { sub, username: Users[0].Username! };};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 las claims verificadas en el evento en event.requestContext.authorizer.claims:
import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const claims = event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username;
if (!sub || !username) { throw new UnauthorizedError({ message: 'Unable to determine calling user', }); }
return { sub, username };};Luego conecta el resolvedor al contexto en src/handler.ts:
import { Service } from './service.js';import { getIdentity } from './identity.js';// ...const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: () => getIdentity(event),});getIdentity es un campo requerido en ServiceContext, y como la advertencia anterior señala, el contexto se construye en ambos puntos de entrada — por lo que src/local-server.ts también lo necesita. No hay un autorizador de API Gateway frente al servidor local, así que proporciona una identidad stub para el desarrollo local:
const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: async () => ({ sub: 'local', username: 'local' }),});Ahora podemos usar la identidad resuelta en una operación, por ejemplo en src/operations/echo.ts:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { const identity = await ctx.getIdentity(); return { message: `${identity.username} says ${input.message}` };};Compilación y Generación de Código
Sección titulada «Compilación y Generación de Código»El proyecto del modelo Smithy usa la CLI de Smithy para compilar los artefactos de Smithy y generar el TypeScript Server SDK:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>En macOS y Linux, la CLI se resuelve mediante mise, que la compilación obtiene bajo demanda, por lo que no hay nada que instalar — descarga y almacena en caché la versión fijada la primera vez que compilas.
Este proceso:
- Compila el modelo Smithy y lo valida
- Genera la especificación OpenAPI a partir del modelo Smithy
- Crea el TypeScript Server SDK con interfaces de operación con seguridad de tipos
- Genera artefactos de compilación en
dist/<model-project>/build/
El proyecto backend copia automáticamente el SDK generado durante la compilación:
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Compilar en Windows
Sección titulada «Compilar en Windows»mise no publica ningún paquete de Windows en npm, por lo que en Windows la CLI de Smithy es un requisito previo que instalas tú mismo. Instálala una vez siguiendo la guía de instalación de la CLI de Smithy (por ejemplo winget install smithy o scoop install smithy), y asegúrate de que smithy esté en tu PATH. Un proyecto Smithy generado en Windows ejecuta smithy directamente en lugar de a través de mise.
Alternativamente, desarrolla dentro de WSL, donde la compilación ejecuta la ruta de Linux y mise resuelve la CLI por ti — nada que instalar.
Un proyecto generado en Windows confirma un objetivo compile que invoca smithy directamente, por lo que cualquier otra persona que trabaje en él — incluyendo en macOS o Linux — necesita la CLI de Smithy en su PATH también. Para que esas máquinas resuelvan la CLI a través de mise en su lugar, cambia el objetivo al comando mise como se describe a continuación.
Elegir cómo se resuelve la CLI
Sección titulada «Elegir cómo se resuelve la CLI»macOS y Linux resuelven la CLI a través de mise y Windows usa una CLI instalada globalmente, pero puedes elegir cualquiera en cualquier plataforma editando el comando del objetivo compile en el project.json del proyecto del modelo.
Para usar una CLI de Smithy instalada globalmente en lugar de mise, reemplaza el prefijo mise con un smithy simple:
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}Para volver a que mise resuelva la CLI, restaura el prefijo npx -y mise@<version> exec smithy@<version> --.
Objetivo Bundle
Sección titulada «Objetivo Bundle»El generador configura automáticamente un target bundle que utiliza Rolldown para crear un paquete de despliegue:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configuración de Rolldown se encuentra en rolldown.config.ts, con una entrada por cada bundle a generar. Rolldown gestiona la creación de múltiples bundles en paralelo si están definidos.
Desarrollo Local
Sección titulada «Desarrollo Local»El generador configura un servidor de desarrollo local con recarga en caliente:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Desplegar tu API de Smithy
Sección titulada «Desplegar tu API de Smithy»El generador crea infraestructura CDK o Terraform basada en tu iac seleccionado.
El constructo CDK para desplegar tu API está en la carpeta common/constructs:
import { MyApi } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the API to your stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}Esto configura:
- Una función AWS Lambda para el servicio Smithy
- API Gateway REST API como disparador de la función
- Roles y permisos de IAM
- Grupo de logs de CloudWatch
- Configuración de trazado de X-Ray
Los módulos Terraform para desplegar tu API están en la carpeta common/terraform.
El módulo de la API almacena su zip de despliegue de 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 a través de 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
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}Esto configura:
- Una función AWS Lambda que sirve la API de Smithy
- API Gateway REST API como disparador de la función
- Roles y permisos de IAM
- Grupo de logs de CloudWatch
- Configuración de trazado de X-Ray
- Configuración de CORS
El módulo Terraform proporciona varias salidas:
# Access the API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Access Lambda function detailsoutput "lambda_function_name" { value = module.my_api.lambda_function_name}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, incluidos los 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 tasa 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 REST APIs, la infraestructura generada habilita el registro de acceso de forma predeterminada, escribiendo una línea JSON estructurada por solicitud en un grupo de CloudWatch Logs dedicado. El grupo de logs está cifrado con una clave KMS administrada por el cliente y se retiene durante un año.
API Gateway escribe los logs de acceso utilizando un rol de CloudWatch Logs a nivel de cuenta. Este rol se configura en la configuración AWS::ApiGateway::Account, que es un singleton por región por cuenta: solo hay un rol para cada REST API en la región. Para gestionar esto de forma segura en múltiples stacks implementados 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 las implementaciones nunca sobrescriben un rol que pertenece a otro stack.
- Deja la configuración de la cuenta intacta durante el desmontaje, por lo que destruir un stack nunca deshabilita el registro para otras REST APIs en la región.
El rol de cuenta es gestionado por el constructo ApiGatewayAccount, un singleton con ámbito de stack resuelto mediante ApiGatewayAccount.ensure(scope). La etapa de cada REST API depende de él, y el rol se configura mediante un recurso personalizado respaldado por Lambda.
El formato del log de acceso es establecido por el constructo RestApi que tu API extiende. Para personalizarlo, pasa deployOptions a super en el archivo generado packages/common/constructs/src/app/apis/my-api.ts, manteniendo el tracingEnabled que el constructo ya establece:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat se importa desde aws-cdk-lib/aws-apigateway. Cualquier cosa que dejes sin configurar mantiene el valor predeterminado del constructo: un formato JSON con los campos estándar.
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 log 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 API REST/HTTP están configurados para proporcionar una interfaz con seguridad de tipos para definir integraciones para cada una de sus operaciones.
Integraciones Predeterminadas
Sección titulada «Integraciones Predeterminadas»Puede usar el método estático defaultIntegrations para hacer uso del patrón predeterminado, que define una función AWS Lambda individual para cada operación:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});El módulo generado ya define las integraciones predeterminadas para el patrón con el que se generó la API, por lo que 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
tags = local.common_tags}Con el patrón isolated predeterminado, esto crea una función Lambda por operación.
Acceso a Integraciones
Sección titulada «Acceso a Integraciones»Puede acceder a las funciones AWS Lambda subyacentes a través de la propiedad integrations del constructo de API, de manera segura en cuanto a tipos. Por ejemplo, si su API define una operación llamada sayHello y necesita agregar algunos permisos a esta función, puede hacerlo de la siguiente manera:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Si su API utiliza el patrón shared, el enrutador 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');Tenga en cuenta que $router ya no está disponible si anula todas las operaciones a través de withOverrides, ya que ninguna operación queda usando la integración de enrutador predeterminada.
Con el patrón isolated, las salidas del módulo son mapas indexados por nombre de operación, por lo que puede acceder a los recursos de una sola operación. Por ejemplo, para otorgar permisos adicionales a la función Lambda de una operación:
# Grant additional permissions to just the sayHello operation's functionresource "aws_iam_role_policy" "say_hello_permissions" { name = "say-hello-additional-permissions" role = module.my_api.lambda_execution_role_names["sayHello"]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Para otorgar los mismos permisos a todas las operaciones, itere la salida operations:
resource "aws_iam_role_policy" "additional_permissions" { for_each = toset(module.my_api.operations)
name = "additional-api-permissions" role = module.my_api.lambda_execution_role_names[each.key]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = "arn:aws:s3:::my-bucket/*" } ] })}El módulo también expone lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids y lambda_log_group_names como mapas indexados por nombre de operación. Con el patrón shared, se exponen en su lugar las salidas singulares equivalentes (lambda_execution_role_name, lambda_function_name, …), ya que solo hay una función.
Los permisos que necesita cada operación es mejor pasarlos al módulo, que los aplica al rol de cada función:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
additional_iam_policy_statements = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = ["arn:aws:s3:::my-bucket/*"] } ]}Personalización de Opciones Predeterminadas
Sección titulada «Personalización de Opciones Predeterminadas»Si desea personalizar las opciones utilizadas al crear la función Lambda para cada integración predeterminada, puede usar el método withDefaultOptions. Por ejemplo, si desea que todas sus funciones Lambda residan en una Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configuración de VPC ya es compatible con el módulo generado — establezca enable_vpc junto con vpc_id y subnet_ids, y el módulo desplegará cada función Lambda en su VPC detrás de un grupo de seguridad compartido que crea para usted:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# VPC configuration enable_vpc = true vpc_id = aws_vpc.main.id subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
tags = local.common_tags}Para opciones que el módulo no expone, edite el recurso aws_lambda_function en el módulo Terraform generado directamente. Con el patrón isolated, ese único recurso se declara for_each = local.operations, por lo que una edición allí se aplica a todas las operaciones.
Personalización de Opciones por Operación
Sección titulada «Personalización de Opciones por Operación»Para personalizar las opciones utilizadas para crear la integración predeterminada para operaciones específicas (sin afectar a las demás), puede usar el método withOperationOptions. Por ejemplo, si desea aumentar el tiempo de espera 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(),});
// The selected operations remain default integrations, so they're still typed accordingly:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));Las opciones que especifique se fusionan con las opciones de integración predeterminadas (y cualquier opción establecida a través de withDefaultOptions). Tenga en cuenta que no puede especificar opciones para operaciones que haya reemplazado a través de withOverrides, ya que estas ya no usan la integración predeterminada.
Encontrará un error de tipo si la misma operación es objetivo tanto de withOperationOptions como de withOverrides, independientemente del orden en que los llame.
Con el patrón isolated, el recurso de función Lambda ya es por operación, por lo que las opciones pueden variar según el nombre de la operación. Por ejemplo, para dar a una operación un tiempo de espera más largo, edite el recurso aws_lambda_function en el módulo generado:
resource "aws_lambda_function" "api_lambda" { for_each = local.operations
# Default to 30 seconds, but allow longer for specific operations timeout = lookup({ sayHello = 60 }, each.key, 30)
# ... rest of configuration}Anulación de Integraciones
Sección titulada «Anulación de Integraciones»También puede anular integraciones para operaciones específicas usando el método withOverrides. Cada anulación 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 seguro en cuanto a tipos. Por ejemplo, si desea anular una API getDocumentation para que apunte a documentación alojada por algún sitio web externo, podría lograrlo de la siguiente manera:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});También notará que la integración anulada ya no tiene una propiedad handler al acceder a ella a través de api.integrations.getDocumentation.
Puede agregar propiedades adicionales a una integración que también se tipará en consecuencia, permitiendo que otros tipos de integración se abstraigan pero permanezcan seguros en cuanto a tipos, por ejemplo, si ha creado una integración S3 para una API REST y luego desea hacer referencia al bucket para una operación en particular, puede hacerlo de la siguiente manera:
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(),});
// Later, perhaps in another file, you can access the bucket property we defined// in a type-safe mannerapi.integrations.getFile.bucket.grantRead(...);Para apuntar una operación específica a un tipo de integración diferente, exclúyala del for_each predeterminado y declare su integración por separado. Por ejemplo, para servir getDocumentation desde un sitio web externo:
# Exclude the overridden operation from the default per-operation resourceslocals { overridden_operations = ["getDocumentation"] default_operations = { for op, details in local.operations : op => details if !contains(local.overridden_operations, op) }}
# Then use local.default_operations in place of local.operations for the# aws_lambda_function, aws_iam_role, aws_apigatewayv2_integration and# aws_lambda_permission resources, and add the override:resource "aws_apigatewayv2_integration" "get_documentation" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET"}
resource "aws_apigatewayv2_route" "get_documentation" { api_id = module.http_api.api_id route_key = local.route_key["getDocumentation"] target = "integrations/${aws_apigatewayv2_integration.get_documentation.id}"}Anulación de Autorizadores
Sección titulada «Anulación de Autorizadores»También puede proporcionar options en su integración para anular opciones de método particulares como autorizadores, por ejemplo, si desea usar autenticación Cognito para su operación getDocumentation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // for REST, or HttpUserPoolAuthorizer for an HTTP API } }, }) .build(),});La autorización se establece en la ruta (HTTP API) o método (REST API) para cada operación, por lo que puede variar según el nombre de la operación. Por ejemplo, para dejar una operación sin autenticación en una HTTP API:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Para una REST API autenticada con IAM, también agregue una declaración de política de recursos que permita el acceso no autenticado a la ruta de esa operación.
Integraciones Explícitas
Sección titulada «Integraciones Explícitas»Si lo prefiere, puede optar por no usar las integraciones predeterminadas y en su lugar proporcionar directamente una para cada operación. Esto es útil si, por ejemplo, cada operación necesita usar un tipo diferente de integración o si desea recibir un error de tipo al agregar nuevas operaciones:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Reemplace el for_each utilizado por el patrón isolated con instanciaciones explícitas de las funciones Lambda, integraciones y permisos para cada operación.
Patrón de Integración
Sección titulada «Patrón de Integración»Las API generadas admiten dos patrones de integración:
isolatedcrea una función Lambda por operación. Esta es la opción predeterminada y recomendada para las API.sharedcrea un único enrutador Lambda predeterminado y lo reutiliza para cada operación a menos que anule integraciones específicas.
isolated le brinda permisos y configuración más detallados por operación, así como una mejor separación para registros y trazas. shared reduce la probabilidad de encontrar arranques en frío para API de bajo uso.
El patrón de integración se puede cambiar en cualquier momento en CDK actualizando su constructo de API. Por ejemplo, establecer pattern en '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', ... }); };}A diferencia de CDK, el patrón de integración está integrado en el módulo generado. Para cambiar el patrón de integración:
- Elimine el módulo de API generado previamente en
packages/common/terraform/src/app/apis - Vuelva a ejecutar el generador que creó su API con el otro patrón de integración (ej.
--integrationPattern=shared)
Con el patrón isolated, el módulo lee las operaciones de un archivo generado:
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Este archivo se genera a partir de su API, por lo que no necesita editarlo manualmente. Agregar una operación al código de aplicación de su API agrega la ruta y la función lambda en el próximo despliegue. Está en .gitignore de forma predeterminada; elimine la entrada si prefiere registrarlo.
Límite de Profundidad de Ruta de REST API en Terraform
Sección titulada «Límite de Profundidad de Ruta de REST API en Terraform»Generación de Código
Sección titulada «Generación de Código»Dado que las operaciones se definen en Smithy, usamos generación de código para proporcionar metadatos al constructo CDK para integraciones con seguridad de tipos.
Se agrega un objetivo 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 compilación, se ignora en el control de versiones.
Conceder Acceso (Solo IAM)
Sección titulada «Conceder Acceso (Solo IAM)»Si seleccionaste autenticación IAM, puedes usar el método grantInvokeAccess para conceder acceso a tu API:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Create an IAM policy to allow invoking the APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy to allow invoking the Smithy API"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attach the policy to an IAM roleresource "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}Invocar tu API de Smithy
Sección titulada «Invocar tu API de Smithy»Para invocar tu API desde un sitio web React, puedes usar el generador connection, que proporciona generación de cliente con seguridad de tipos a partir de tu modelo Smithy.
Conexiones
Sección titulada «Conexiones»Usa el generador connection para integrar este proyecto con otros en tu espacio de trabajo. Las siguientes conexiones involucran este proyecto: