Smithy TypeScript API
Smithy è un linguaggio di definizione di interfacce indipendente dal protocollo per la creazione di API in modo model-driven.
Il generatore Smithy TypeScript API crea una nuova API utilizzando Smithy per la definizione del servizio e il Smithy TypeScript Server SDK per l’implementazione. Il generatore fornisce infrastruttura come codice CDK o Terraform per distribuire il servizio su AWS Lambda, esposto tramite un’API REST di AWS API Gateway. Fornisce sviluppo API type-safe con generazione automatica del codice dai modelli Smithy. L’handler generato utilizza AWS Lambda Powertools for TypeScript per l’osservabilità, inclusi logging, tracing AWS X-Ray e CloudWatch Metrics
Utilizzo
Sezione intitolata “Utilizzo”Generare una Smithy TypeScript API
Sezione intitolata “Generare una Smithy TypeScript API”Puoi generare una nuova Smithy TypeScript API in due modi:
Esegui questo generatore@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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#api - Compila i parametri richiesti
- Clicca su
Generate
Componi il tuo comando10
Obbligatorio
framework = smithy
Opzioni
Sezione intitolata “Opzioni”nameObbligatoriostringIl nome dell'API (obbligatorio). Utilizzato per generare i nomi delle classi e i percorsi dei file.
frameworkenumPredefinito:trpcIl framework API da utilizzare.
trpcsmithyintegrationPatternenumPredefinito:isolatedCome vengono generate le integrazioni API Gateway per l'API. Scegli tra isolated (predefinito) e shared.
isolatedsharedauthenumPredefinito:iamIl metodo utilizzato per autenticare con la tua API. Scegli tra iam (predefinito), cognito o custom.
iamcognitocustomdirectorystringPredefinito:packagesLa directory in cui memorizzare l'applicazione.
iacenumPredefinito:inheritIl provider IaC preferito. Per impostazione predefinita viene ereditato dalla selezione iniziale.
inheritcdkterraforminfraenumPredefinito:rest-lambdaIl tipo di infrastruttura da utilizzare per distribuire questa API.
rest-lambdanonenamespacestringframework = smithyIl namespace per l'API Smithy (applicabile solo per il framework smithy). Il valore predefinito è lo scope del monorepo
subDirectorystringLa sottodirectory in cui viene posizionato il progetto. Per impostazione predefinita corrisponde al nome del progetto.
preferInstallDependenciesbooleanPredefinito:trueSe preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine.
Output del Generatore
Sezione intitolata “Output del Generatore”Il generatore crea due progetti correlati nella directory <directory>/<api-name>:
Directorymodel/ Progetto del modello Smithy
- project.json Configurazione del progetto e target di build
- smithy-build.json Configurazione di build Smithy
- ssdk.rolldown.config.mjs Raggruppa il TypeScript Server SDK generato
Directorysrc/
- main.smithy Definizione principale del servizio
Directoryoperations/
- echo.smithy Definizione di operazione di esempio
Directorybackend/ Implementazione backend TypeScript
- package.json Manifest del progetto che definisce il nome del pacchetto e le dipendenze
- project.json Configurazione del progetto e target di build
- rolldown.config.ts Configurazione del bundle
- tsconfig.json Configurazione TypeScript
- tsconfig.lib.json Configurazione TypeScript per i sorgenti della libreria
- tsconfig.spec.json Configurazione TypeScript per i test
- vitest.config.mts Configurazione Vitest
Directorysrc/
- index.ts Punto di ingresso del pacchetto
- handler.ts Handler AWS Lambda
- local-server.ts Server di sviluppo locale
- service.ts Implementazione del servizio
- context.ts Definizione del contesto del servizio
Directoryoperations/
- echo.ts Implementazione di operazione di esempio
Directorygenerated/ SDK TypeScript generato (creato durante la build)
- …
Infrastruttura
Sezione intitolata “Infrastruttura”Poiché questo generatore crea infrastruttura come codice basata sul tuo iac scelto, creerà un progetto in packages/common che include i costrutti CDK o i moduli Terraform pertinenti.
Il progetto comune di infrastruttura come codice è strutturato come segue:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Costrutti per l’infrastruttura specifica di un progetto/generatore
Directoryapis/
- <project-name>.ts Costrutto CDK per distribuire la tua API
Directorycore/ Costrutti generici riutilizzati dai costrutti in
appDirectoryapi/
- rest-api.ts Costrutto CDK per distribuire un’API REST
- utils.ts Utilità per i costrutti API
- index.ts Punto di ingresso che esporta i costrutti da
app
- project.json Target di build e configurazione del progetto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Moduli Terraform per l’infrastruttura specifica di un progetto/generatore
Directoryapis/
Directory<project-name>/
- <project-name>.tf Modulo per distribuire la tua API
Directorycore/ Moduli generici riutilizzati dai moduli in
appDirectoryapi/
Directoryrest-api/
- rest-api.tf Modulo per distribuire un’API REST
- project.json Target di build e configurazione del progetto
Architettura
Sezione intitolata “Architettura”L’API Smithy distribuita ha la seguente architettura, con un Web ACL AWS WAFv2 davanti allo stage API Gateway:
Implementare la tua Smithy API
Sezione intitolata “Implementare la tua Smithy API”Definire Operazioni in Smithy
Sezione intitolata “Definire Operazioni in Smithy”Le operazioni sono definite in file Smithy all’interno del progetto del modello. La definizione principale del servizio è in 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 ]}Le singole operazioni sono definite in file separati nella directory 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}Aggiungere una Libreria di Shape
Sezione intitolata “Aggiungere una Libreria di Shape”Se hai diverse API Smithy che condividono gli stessi tipi di dati, puoi definire quei tipi una volta in una libreria di shape piuttosto che duplicarli in ogni modello. Una libreria di shape è un progetto Smithy senza servizio — solo shape riutilizzabili — su cui qualsiasi numero di progetti Smithy può dipendere.
Generane una con il generatore smithy#project:
Esegui questo generatore@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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - smithy#project - Compila i parametri richiesti
- Clicca su
Generate
Componi il tuo comando7
Obbligatorio
Il modello della tua API può quindi fare riferimento alle sue shape con use:
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}Consulta la guida al progetto Smithy per come creare una libreria di shape e collegarla come dipendenza del modello della tua API.
Implementare Operazioni in TypeScript
Sezione intitolata “Implementare Operazioni in TypeScript”Le implementazioni delle operazioni si trovano nella directory src/operations/ del progetto backend. Ogni operazione è implementata utilizzando i tipi generati dal TypeScript Server SDK (generato al momento della build dal tuo modello 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 };};Le operazioni devono essere registrate nella definizione del servizio in 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};Contesto del Servizio
Sezione intitolata “Contesto del Servizio”Puoi definire un contesto condiviso per le tue operazioni in 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;}Questo contesto viene passato a tutte le implementazioni delle operazioni e può essere utilizzato per condividere risorse come connessioni al database, configurazione o utilità di logging.
Osservabilità con AWS Lambda Powertools
Sezione intitolata “Osservabilità con AWS Lambda Powertools”Logging
Sezione intitolata “Logging”Il generatore configura il logging strutturato utilizzando AWS Lambda Powertools con iniezione automatica del contesto tramite middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi fare riferimento al logger dalle implementazioni delle tue operazioni tramite il contesto:
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'); // ...};Tracing
Sezione intitolata “Tracing”Il tracing AWS X-Ray è configurato automaticamente tramite il middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi aggiungere subsegment personalizzati alle tue tracce nelle tue operazioni:
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(); }};Metriche
Sezione intitolata “Metriche”Le metriche CloudWatch vengono raccolte automaticamente per ogni richiesta tramite il middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi aggiungere metriche personalizzate nelle tue operazioni:
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); // ...};Gestione degli Errori
Sezione intitolata “Gestione degli Errori”Smithy fornisce gestione degli errori integrata. Puoi definire errori personalizzati nel tuo modello Smithy:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}E registrarli alla tua operazione/servizio:
operation MyOperation { ... errors: [InvalidRequestError]}Quindi lanciarli nella tua implementazione 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 */ };};Accedere all’Utente Chiamante
Sezione intitolata “Accedere all’Utente Chiamante”Quando la tua API è protetta da autenticazione, le tue operazioni spesso devono sapere chi sta chiamando. L’approccio consigliato è risolvere l’identità del chiamante una volta nell’handler e passarla attraverso il contesto del servizio per il consumo da parte di operazioni specifiche.
Modelleremo il caso non autorizzato come un errore Smithy in modo che si serializzi in una risposta 403 appropriata. Aggiungilo al tuo modello, ad esempio in model/src/operations/errors.smithy, e fai riferimento ad esso su qualsiasi operazione che richiede l’identità:
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Prima, esponi l’identità risolta sul contesto del servizio in src/context.ts. La forniamo come funzione in modo che l’UnauthorizedError venga lanciato dall’interno di un’operazione (dove il Server SDK lo serializza in un 403), piuttosto che dall’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>;}Successivamente, scrivi il resolver in src/identity.ts. Lancia UnauthorizedError quando il chiamante non può essere determinato. L’implementazione dipende dal tuo metodo auth selezionato:
Per l’autenticazione IAM, cerchiamo il chiamante in Cognito utilizzando il sub estratto dall’evento API Gateway. La ricerca utilizza il client Cognito Identity Provider, che non è una dipendenza di un backend Smithy generato, quindi installalo prima nel progetto 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', l’authorizer Cognito User Pools di API Gateway verifica il JWT che il chiamante fornisce nell’header Authorization e posiziona i claim verificati sull’evento in 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 };};Quindi collega il resolver al contesto in 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 è un campo obbligatorio su ServiceContext, e come nota la cautela sopra il contesto è costruito in entrambi i punti di ingresso — quindi anche src/local-server.ts ne ha bisogno. Non c’è alcun authorizer API Gateway davanti al server locale, quindi fornisci un’identità stub per lo sviluppo locale:
const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: async () => ({ sub: 'local', username: 'local' }),});Ora possiamo utilizzare l’identità risolta in un’operazione, ad esempio in 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}` };};Building e Generazione del Codice
Sezione intitolata “Building e Generazione del Codice”Il progetto del modello Smithy utilizza la Smithy CLI per costruire gli artefatti Smithy e generare il TypeScript Server SDK:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>Su macOS e Linux la CLI viene risolta da mise, che la build recupera su richiesta, quindi non c’è nulla da installare — scarica e memorizza nella cache la versione fissata la prima volta che esegui la build.
Questo processo:
- Compila il modello Smithy e lo valida
- Genera la specifica OpenAPI dal modello Smithy
- Crea il TypeScript Server SDK con interfacce di operazione type-safe
- Produce artefatti di build in
dist/<model-project>/build/
Il progetto backend copia automaticamente l’SDK generato durante la compilazione:
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Building su Windows
Sezione intitolata “Building su Windows”mise non pubblica alcun pacchetto Windows su npm, quindi su Windows la Smithy CLI è un prerequisito che installi tu stesso. Installala una volta seguendo la guida all’installazione della Smithy CLI (ad esempio winget install smithy o scoop install smithy), e assicurati che smithy sia nel tuo PATH. Un progetto Smithy generato su Windows esegue smithy direttamente piuttosto che attraverso mise.
In alternativa, sviluppa all’interno di WSL, dove la build esegue il percorso Linux e mise risolve la CLI per te — nulla da installare.
Un progetto generato su Windows esegue il commit di un target compile che invoca smithy direttamente, quindi chiunque altro ci lavori — incluso su macOS o Linux — ha bisogno della Smithy CLI nel proprio PATH anche. Per far sì che quelle macchine risolvano la CLI attraverso mise invece, cambia il target al comando mise come descritto di seguito.
Scegliere come viene risolta la CLI
Sezione intitolata “Scegliere come viene risolta la CLI”macOS e Linux risolvono la CLI attraverso mise e Windows utilizza una CLI installata globalmente, ma puoi scegliere una delle due su qualsiasi piattaforma modificando il comando del target compile nel project.json del progetto del modello.
Per utilizzare una Smithy CLI installata globalmente invece di mise, sostituisci il prefisso mise con un semplice smithy:
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}Per tornare a far risolvere la CLI da mise, ripristina il prefisso npx -y mise@<version> exec smithy@<version> --.
Target Bundle
Sezione intitolata “Target Bundle”Il generatore configura automaticamente un target bundle che utilizza Rolldown per creare un pacchetto di distribuzione:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configurazione di Rolldown si trova in rolldown.config.ts, con una voce per ogni bundle da generare. Rolldown gestisce la creazione di più bundle in parallelo se definiti.
Sviluppo Locale
Sezione intitolata “Sviluppo Locale”Il generatore configura un server di sviluppo locale con hot reloading:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Distribuire la tua Smithy API
Sezione intitolata “Distribuire la tua Smithy API”Il generatore crea infrastruttura CDK o Terraform in base al tuo iac selezionato.
Il costrutto CDK per distribuire la tua API si trova nella cartella 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(), }); }}Questo configura:
- Una funzione AWS Lambda per il servizio Smithy
- API Gateway REST API come trigger della funzione
- Ruoli IAM e permessi
- Gruppo di log CloudWatch
- Configurazione del tracing X-Ray
I moduli Terraform per distribuire la tua API si trovano nella cartella common/terraform.
Il modulo API prepara il suo zip di distribuzione Lambda in un bucket S3 di asset condiviso — consulta la guida all’infrastruttura Terraform per i dettagli. Istanzia il modulo core/asset-bucket una volta per distribuzione e passa il suo output bucket_name in ogni modulo API / Lambda tramite l’input 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}Questo configura:
- Una funzione AWS Lambda che serve l’API Smithy
- API Gateway REST API come trigger della funzione
- Ruoli IAM e permessi
- Gruppo di log CloudWatch
- Configurazione del tracing X-Ray
- Configurazione CORS
Il modulo Terraform fornisce diversi output:
# 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}Per le REST API, il costrutto generato associa un Web ACL AWS WAFv2 allo stage di API Gateway per impostazione predefinita. Il Web ACL utilizza il set di regole predefinito gestito da AWS (AWSManagedRulesCommonRuleSet e AWSManagedRulesKnownBadInputsRuleSet), fornendo protezione contro exploit web comuni inclusa la OWASP Top 10. I log delle richieste WAF vengono scritti in un gruppo di CloudWatch Logs.
Puoi modificare il costrutto rest-api generato per aggiungere, rimuovere o regolare le regole (ad esempio, per aggiungere regole basate sulla frequenza o gruppi di regole gestite aggiuntivi).
Per disattivare (ad esempio, per collegare il tuo Web ACL), imposta enableWaf su false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Per disattivare (ad esempio, per collegare il tuo Web ACL), imposta enable_waf su false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Logging degli accessi
Sezione intitolata “Logging degli accessi”Per le REST API, l’infrastruttura generata abilita la registrazione degli accessi per impostazione predefinita, scrivendo una riga JSON strutturata per richiesta in un gruppo CloudWatch Logs dedicato. Il gruppo di log è crittografato con una chiave KMS gestita dal cliente e conservato per un anno.
API Gateway scrive i log di accesso utilizzando un ruolo CloudWatch Logs a livello di account. Questo ruolo è configurato sull’impostazione AWS::ApiGateway::Account, che è un singleton per regione per account — esiste un solo ruolo per ogni REST API nella regione. Per gestire questo in modo sicuro tra più stack distribuiti indipendentemente, l’infrastruttura generata:
- Crea un ruolo CloudWatch Logs condiviso e lo configura sull’account solo quando non è già impostato un ruolo funzionante, in modo che le distribuzioni non sovrascrivano mai un ruolo di proprietà di un altro stack.
- Lascia intatta l’impostazione dell’account durante lo smantellamento, in modo che la distruzione di uno stack non disabiliti mai la registrazione per altre REST API nella regione.
Il ruolo dell’account è gestito dal costrutto ApiGatewayAccount, un singleton con ambito stack risolto tramite ApiGatewayAccount.ensure(scope). Ogni stage della REST API dipende da esso e il ruolo è configurato da una risorsa personalizzata supportata da Lambda.
Il formato del log di accesso è impostato dal costrutto RestApi che la tua API estende. Per personalizzarlo, passa deployOptions a super nel file generato packages/common/constructs/src/app/apis/my-api.ts, mantenendo il tracingEnabled che il costrutto già imposta:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat è importato da aws-cdk-lib/aws-apigateway. Tutto ciò che lasci non impostato mantiene il valore predefinito del costrutto — un formato JSON con i campi standard.
Il ruolo dell’account è gestito dal modulo core/api/api-gateway-account, che viene istanziato dal modulo API generato. Configura l’account in modo idempotente e non viene mai reimpostato con terraform destroy.
Puoi personalizzare il formato del log di accesso modificando il blocco access_log_settings sulla risorsa aws_api_gateway_stage nel modulo API generato.
Integrazioni
Sezione intitolata “Integrazioni”I costrutti CDK REST/HTTP API sono configurati per fornire un’interfaccia type-safe per definire integrazioni per ciascuna delle tue operazioni.
Integrazioni Predefinite
Sezione intitolata “Integrazioni Predefinite”Puoi utilizzare il metodo statico defaultIntegrations per utilizzare il pattern predefinito, che definisce una funzione AWS Lambda individuale per ogni operazione:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Il modulo generato definisce già le integrazioni predefinite per il pattern con cui l’API è stata generata, quindi non è necessaria alcuna configurazione aggiuntiva:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}Con il pattern predefinito isolated, questo crea una funzione Lambda per operazione.
Accesso alle Integrazioni
Sezione intitolata “Accesso alle Integrazioni”Puoi accedere alle funzioni AWS Lambda sottostanti tramite la proprietà integrations del costrutto API, in modo type-safe. Ad esempio, se la tua API definisce un’operazione chiamata sayHello e devi aggiungere alcune autorizzazioni a questa funzione, puoi farlo come segue:
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: [...],}));Se la tua API utilizza il pattern shared, il router Lambda condiviso è esposto come api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Nota che $router non è più disponibile se sovrascrivi ogni operazione tramite withOverrides, poiché nessuna operazione rimane ad utilizzare l’integrazione router predefinita.
Con il pattern isolated, gli output del modulo sono mappe indicizzate per nome di operazione, quindi puoi raggiungere le risorse di una singola operazione. Ad esempio, per concedere autorizzazioni extra alla funzione Lambda di un’operazione:
# 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/*" } ] })}Per concedere le stesse autorizzazioni a ogni operazione, itera l’output 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/*" } ] })}Il modulo espone anche lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids e lambda_log_group_names come mappe indicizzate per nome di operazione. Con il pattern shared vengono invece esposti gli output singolari equivalenti (lambda_execution_role_name, lambda_function_name, …), poiché c’è solo una funzione.
Le autorizzazioni di cui ogni operazione ha bisogno è meglio passarle al modulo, che le applica al ruolo di ogni funzione:
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/*"] } ]}Personalizzazione delle Opzioni Predefinite
Sezione intitolata “Personalizzazione delle Opzioni Predefinite”Se desideri personalizzare le opzioni utilizzate durante la creazione della funzione Lambda per ogni integrazione predefinita, puoi utilizzare il metodo withDefaultOptions. Ad esempio, se desideri che tutte le tue funzioni Lambda risiedano in un Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configurazione VPC è già supportata dal modulo generato — imposta enable_vpc insieme a vpc_id e subnet_ids, e il modulo distribuisce ogni funzione Lambda nel tuo VPC dietro un security group condiviso che crea per te:
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}Per opzioni che il modulo non espone, modifica direttamente la risorsa aws_lambda_function nel modulo Terraform generato. Con il pattern isolated quella singola risorsa è dichiarata for_each = local.operations, quindi una modifica lì si applica a ogni operazione.
Personalizzazione delle Opzioni per Operazione
Sezione intitolata “Personalizzazione delle Opzioni per Operazione”Per personalizzare le opzioni utilizzate per creare l’integrazione predefinita per operazioni specifiche (senza influenzare le altre), puoi utilizzare il metodo withOperationOptions. Ad esempio, se desideri aumentare il timeout della funzione Lambda solo per un’operazione:
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({ ... }));Le opzioni che specifichi vengono unite alle opzioni di integrazione predefinite (e a qualsiasi opzione impostata tramite withDefaultOptions). Nota che non puoi specificare opzioni per operazioni che hai sostituito tramite withOverrides, poiché queste non utilizzano più l’integrazione predefinita.
Incontrerai un errore di tipo se la stessa operazione è targetizzata sia da withOperationOptions che da withOverrides, indipendentemente dall’ordine in cui li chiami.
Con il pattern isolated la risorsa della funzione Lambda è già per operazione, quindi le opzioni possono essere variate per nome di operazione. Ad esempio, per dare a un’operazione un timeout più lungo, modifica la risorsa aws_lambda_function nel modulo generato:
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}Override delle Integrazioni
Sezione intitolata “Override delle Integrazioni”Puoi anche sovrascrivere le integrazioni per operazioni specifiche utilizzando il metodo withOverrides. Ogni override deve specificare una proprietà integration che è tipizzata al costrutto di integrazione CDK appropriato per l’API HTTP o REST. Il metodo withOverrides è anche type-safe. Ad esempio, se desideri sovrascrivere un’API getDocumentation per puntare alla documentazione ospitata da un sito web esterno, potresti ottenere questo come segue:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Noterai anche che l’integrazione sovrascritta non ha più una proprietà handler quando vi accedi tramite api.integrations.getDocumentation.
Puoi aggiungere proprietà aggiuntive a un’integrazione che saranno anche tipizzate di conseguenza, consentendo ad altri tipi di integrazione di essere astratti ma rimanere type-safe, ad esempio se hai creato un’integrazione S3 per un’API REST e successivamente desideri fare riferimento al bucket per una particolare operazione, puoi farlo come segue:
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(...);Per puntare un’operazione specifica a un tipo di integrazione diverso, escludila dal for_each predefinito e dichiara la sua integrazione separatamente. Ad esempio, per servire getDocumentation da un sito web esterno:
# 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}"}Override degli Authorizer
Sezione intitolata “Override degli Authorizer”Puoi anche fornire options nella tua integrazione per sovrascrivere particolari opzioni del metodo come gli authorizer, ad esempio se desideri utilizzare l’autenticazione Cognito per la tua operazione 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(),});L’autorizzazione è impostata sulla route (HTTP API) o sul metodo (REST API) per ogni operazione, quindi può essere variata per nome di operazione. Ad esempio, per lasciare un’operazione non autenticata su un’API HTTP:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Per un’API REST autenticata con IAM, aggiungi anche una dichiarazione di policy delle risorse che consente l’accesso non autenticato al percorso di quell’operazione.
Integrazioni Esplicite
Sezione intitolata “Integrazioni Esplicite”Se preferisci, puoi scegliere di non utilizzare le integrazioni predefinite e invece fornirne direttamente una per ogni operazione. Questo è utile se, ad esempio, ogni operazione deve utilizzare un tipo diverso di integrazione o desideri ricevere un errore di tipo quando aggiungi nuove operazioni:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Sostituisci il for_each utilizzato dal pattern isolated con istanziazioni esplicite delle funzioni Lambda, integrazioni e autorizzazioni per ogni operazione.
Pattern di Integrazione
Sezione intitolata “Pattern di Integrazione”Le API generate supportano due pattern di integrazione:
isolatedcrea una funzione Lambda per operazione. Questa è l’opzione predefinita e consigliata per le API.sharedcrea un singolo router Lambda predefinito e lo riutilizza per ogni operazione a meno che non si sovrascrivano integrazioni specifiche.
isolated ti offre autorizzazioni e configurazione più granulari per operazione, oltre a una migliore separazione per log e tracce. shared riduce la probabilità di incontrare cold-start per API a basso utilizzo.
Il pattern di integrazione può essere modificato in qualsiasi momento in CDK aggiornando il tuo costrutto API. Ad esempio, impostando pattern su 'shared' si crea una singola funzione invece di una per integrazione:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}A differenza di CDK, il pattern di integrazione è incorporato nel modulo generato. Per cambiare il pattern di integrazione:
- Elimina il modulo API generato precedentemente in
packages/common/terraform/src/app/apis - Riesegui il generatore che ha creato la tua API con l’altro pattern di integrazione (es.
--integrationPattern=shared)
Con il pattern isolated, il modulo legge le operazioni da un file generato:
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Questo file è generato dalla tua API, quindi non è necessario modificarlo manualmente. Aggiungere un’operazione al codice dell’applicazione API aggiunge la route e la funzione lambda al prossimo deploy. È .gitignored per impostazione predefinita; rimuovi la voce se preferisci includerlo nel controllo versione.
Terraform REST API Path Depth Limit
Sezione intitolata “Terraform REST API Path Depth Limit”Generazione del Codice
Sezione intitolata “Generazione del Codice”Poiché le operazioni sono definite in Smithy, utilizziamo la generazione del codice per fornire metadati al costrutto CDK per integrazioni type-safe.
Un target generate:<ApiName>-metadata viene aggiunto al project.json dei costrutti comuni per facilitare questa generazione del codice, che emette un file come packages/common/constructs/src/generated/my-api/metadata.gen.ts. Poiché questo viene generato al momento della build, viene ignorato nel controllo di versione.
Concedere l’Accesso (Solo IAM)
Sezione intitolata “Concedere l’Accesso (Solo IAM)”Se hai selezionato l’autenticazione IAM, puoi utilizzare il metodo grantInvokeAccess per concedere l’accesso alla tua 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}Invocare la tua Smithy API
Sezione intitolata “Invocare la tua Smithy API”Per invocare la tua API da un sito web React, puoi utilizzare il generatore connection, che fornisce generazione di client type-safe dal tuo modello Smithy.
Connessioni
Sezione intitolata “Connessioni”Utilizza il generatore connection per integrare questo progetto con altri nel tuo workspace. Le seguenti connessioni coinvolgono questo progetto: