API TypeScript Smithy
Smithy est un langage de définition d’interface indépendant du protocole pour créer des API de manière pilotée par modèle.
Le générateur d’API TypeScript Smithy crée une nouvelle API utilisant Smithy pour la définition du service, et le Smithy TypeScript Server SDK pour l’implémentation. Le générateur fournit une infrastructure en tant que code CDK ou Terraform pour déployer votre service sur AWS Lambda, exposé via une API REST AWS API Gateway. Il offre un développement d’API type-safe avec génération automatique de code à partir des modèles Smithy. Le gestionnaire généré utilise AWS Lambda Powertools pour TypeScript pour l’observabilité, incluant la journalisation, le traçage AWS X-Ray et les métriques CloudWatch
Utilisation
Section intitulée « Utilisation »Générer une API TypeScript Smithy
Section intitulée « Générer une API TypeScript Smithy »Vous pouvez générer une nouvelle API TypeScript Smithy de deux manières :
Exécuter ce générateur@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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#api - Remplissez les paramètres requis
- Cliquez sur
Generate
Composez votre commande10
Requis
framework = smithy
nameRequisstringLe nom de l'API (requis). Utilisé pour générer les noms de classes et les chemins de fichiers.
frameworkenumPar défaut:trpcLe framework d'API à utiliser.
trpcsmithyintegrationPatternenumPar défaut:isolatedComment les intégrations API Gateway sont générées pour l'API. Choisissez entre isolated (par défaut) et shared.
isolatedsharedauthenumPar défaut:iamLa méthode utilisée pour s'authentifier auprès de votre API. Choisissez entre iam (par défaut), cognito ou custom.
iamcognitocustomdirectorystringPar défaut:packagesLe répertoire dans lequel stocker l'application.
iacenumPar défaut:inheritLe fournisseur IaC préféré. Par défaut, celui-ci est hérité de votre sélection initiale.
inheritcdkterraforminfraenumPar défaut:rest-lambdaLe type d'infrastructure à utiliser pour déployer cette API.
rest-lambdanonenamespacestringframework = smithyL'espace de noms pour l'API Smithy (applicable uniquement pour le framework smithy). Par défaut, correspond à la portée de votre monorepo
subDirectorystringLe sous-répertoire dans lequel le projet est placé. Par défaut, il s'agit du nom du projet.
preferInstallDependenciesbooleanPar défaut:trueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin.
Sortie du générateur
Section intitulée « Sortie du générateur »Le générateur crée deux projets liés dans le répertoire <directory>/<api-name> :
Répertoiremodel/ Projet de modèle Smithy
- project.json Configuration du projet et cibles de build
- smithy-build.json Configuration de build Smithy
- ssdk.rolldown.config.mjs Bundle le TypeScript Server SDK généré
Répertoiresrc/
- main.smithy Définition principale du service
Répertoireoperations/
- echo.smithy Exemple de définition d’opération
Répertoirebackend/ Implémentation backend TypeScript
- package.json Manifeste du projet définissant le nom du package et les dépendances
- project.json Configuration du projet et cibles de build
- rolldown.config.ts Configuration du bundle
- tsconfig.json Configuration TypeScript
- tsconfig.lib.json Configuration TypeScript pour les sources de la bibliothèque
- tsconfig.spec.json Configuration TypeScript pour les tests
- vitest.config.mts Configuration Vitest
Répertoiresrc/
- index.ts Point d’entrée du package
- handler.ts Gestionnaire AWS Lambda
- local-server.ts Serveur de développement local
- service.ts Implémentation du service
- context.ts Définition du contexte du service
Répertoireoperations/
- echo.ts Exemple d’implémentation d’opération
Répertoiregenerated/ SDK TypeScript généré (créé pendant le build)
- …
Infrastructure
Section intitulée « Infrastructure »Puisque ce générateur crée une infrastructure en tant que code basée sur votre iac choisi, il créera un projet dans packages/common qui inclut les constructs CDK ou modules Terraform pertinents.
Le projet d’infrastructure en tant que code commun est structuré comme suit :
Répertoirepackages/common/constructs
Répertoiresrc
Répertoireapp/ Constructs pour l’infrastructure spécifique à un projet/générateur
Répertoireapis/
- <project-name>.ts Construct CDK pour déployer votre API
Répertoirecore/ Constructs génériques réutilisés par les constructs dans
appRépertoireapi/
- rest-api.ts Construct CDK pour déployer une API REST
- utils.ts Utilitaires pour les constructs d’API
- index.ts Point d’entrée exportant les constructs depuis
app
- project.json Cibles de build et configuration du projet
Répertoirepackages/common/terraform
Répertoiresrc
Répertoireapp/ Modules Terraform pour l’infrastructure spécifique à un projet/générateur
Répertoireapis/
Répertoire<project-name>/
- <project-name>.tf Module pour déployer votre API
Répertoirecore/ Modules génériques réutilisés par les modules dans
appRépertoireapi/
Répertoirerest-api/
- rest-api.tf Module pour déployer une API REST
- project.json Cibles de build et configuration du projet
Architecture
Section intitulée « Architecture »L’API Smithy déployée a l’architecture suivante, avec une Web ACL AWS WAFv2 devant l’étape API Gateway :
Implémenter votre API Smithy
Section intitulée « Implémenter votre API Smithy »Définir des opérations dans Smithy
Section intitulée « Définir des opérations dans Smithy »Les opérations sont définies dans des fichiers Smithy au sein du projet de modèle. La définition principale du service se trouve dans 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 ]}Les opérations individuelles sont définies dans des fichiers séparés dans le répertoire 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}Ajout d’une bibliothèque de formes
Section intitulée « Ajout d’une bibliothèque de formes »Si vous avez plusieurs API Smithy qui partagent les mêmes types de données, vous pouvez définir ces types une fois dans une bibliothèque de formes plutôt que de les dupliquer dans chaque modèle. Une bibliothèque de formes est un projet Smithy sans service — juste des formes réutilisables — dont n’importe quel nombre de projets Smithy peuvent dépendre.
Générez-en une avec le générateur smithy#project :
Exécuter ce générateur@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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - smithy#project - Remplissez les paramètres requis
- Cliquez sur
Generate
Composez votre commande7
Requis
Le modèle de votre API peut ensuite référencer ses formes avec use :
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}Consultez le guide du projet Smithy pour savoir comment créer une bibliothèque de formes et la configurer comme dépendance du modèle de votre API.
Implémenter des opérations en TypeScript
Section intitulée « Implémenter des opérations en TypeScript »Les implémentations d’opérations sont situées dans le répertoire src/operations/ du projet backend. Chaque opération est implémentée en utilisant les types générés à partir du TypeScript Server SDK (généré au moment du build à partir de votre modèle 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 };};Les opérations doivent être enregistrées dans la définition du service dans 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};Contexte du service
Section intitulée « Contexte du service »Vous pouvez définir un contexte partagé pour vos opérations dans 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;}Ce contexte est passé à toutes les implémentations d’opérations et peut être utilisé pour partager des ressources comme des connexions de base de données, de la configuration ou des utilitaires de journalisation.
Observabilité avec AWS Lambda Powertools
Section intitulée « Observabilité avec AWS Lambda Powertools »Journalisation
Section intitulée « Journalisation »Le générateur configure la journalisation structurée en utilisant AWS Lambda Powertools avec injection automatique de contexte via le middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez accéder au logger depuis vos implémentations d’opérations via le contexte :
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'); // ...};Le traçage AWS X-Ray est configuré automatiquement via le middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez ajouter des sous-segments personnalisés à vos traces dans vos opérations :
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étriques
Section intitulée « Métriques »Les métriques CloudWatch sont collectées automatiquement pour chaque requête via le middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez ajouter des métriques personnalisées dans vos opérations :
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); // ...};Gestion des erreurs
Section intitulée « Gestion des erreurs »Smithy fournit une gestion des erreurs intégrée. Vous pouvez définir des erreurs personnalisées dans votre modèle Smithy :
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}Et les enregistrer dans votre opération/service :
operation MyOperation { ... errors: [InvalidRequestError]}Puis les lever dans votre implémentation 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 */ };};Accéder à l’utilisateur appelant
Section intitulée « Accéder à l’utilisateur appelant »Lorsque votre API est protégée par authentification, vos opérations ont souvent besoin de savoir qui appelle. L’approche recommandée est de résoudre l’identité de l’appelant une fois dans le gestionnaire et de la transmettre via le contexte du service pour consommation par des opérations spécifiques.
Nous modéliserons le cas non autorisé comme une erreur Smithy afin qu’elle se sérialise en une réponse 403 appropriée. Ajoutez-la à votre modèle, par exemple dans model/src/operations/errors.smithy, et référencez-la sur toute opération qui nécessite une identité :
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Tout d’abord, exposez l’identité résolue sur le contexte du service dans src/context.ts. Nous la fournissons en tant que fonction afin que l’UnauthorizedError soit levée depuis une opération (où le Server SDK la sérialise en un 403), plutôt que depuis le gestionnaire :
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>;}Ensuite, écrivez le résolveur dans src/identity.ts. Il lève UnauthorizedError lorsque l’appelant ne peut pas être déterminé. L’implémentation dépend de votre méthode auth sélectionnée :
Pour l’authentification IAM, nous recherchons l’appelant dans Cognito en utilisant le sub extrait de l’événement API Gateway. La recherche utilise le client Cognito Identity Provider, qui n’est pas une dépendance d’un backend Smithy généré, donc installez-le d’abord dans le projet 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! };};Avec auth: 'cognito', l’autorisateur Cognito User Pools d’API Gateway vérifie le JWT que l’appelant fournit dans l’en-tête Authorization et place les revendications vérifiées sur l’événement à 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 };};Ensuite, connectez le résolveur au contexte dans 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 est un champ requis sur ServiceContext, et comme la mise en garde ci-dessus le note, le contexte est construit dans les deux points d’entrée — donc src/local-server.ts en a également besoin. Il n’y a pas d’autorisateur API Gateway devant le serveur local, donc fournissez une identité factice pour le développement local :
const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: async () => ({ sub: 'local', username: 'local' }),});Nous pouvons maintenant utiliser l’identité résolue dans une opération, par exemple dans 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}` };};Build et génération de code
Section intitulée « Build et génération de code »Le projet de modèle Smithy utilise la CLI Smithy pour construire les artefacts Smithy et générer le TypeScript Server SDK :
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>Sur macOS et Linux, la CLI est résolue par mise, que le build récupère à la demande, donc il n’y a rien à installer — elle télécharge et met en cache la version épinglée la première fois que vous construisez.
Ce processus :
- Compile le modèle Smithy et le valide
- Génère la spécification OpenAPI à partir du modèle Smithy
- Crée le TypeScript Server SDK avec des interfaces d’opération type-safe
- Produit les artefacts de build dans
dist/<model-project>/build/
Le projet backend copie automatiquement le SDK généré pendant la compilation :
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Build sur Windows
Section intitulée « Build sur Windows »mise ne publie aucun package Windows sur npm, donc sur Windows la CLI Smithy est un prérequis que vous installez vous-même. Installez-la une fois en suivant le guide d’installation de la CLI Smithy (par exemple winget install smithy ou scoop install smithy), et assurez-vous que smithy est dans votre PATH. Un projet Smithy généré sur Windows exécute smithy directement plutôt que via mise.
Alternativement, développez dans WSL, où le build exécute le chemin Linux et mise résout la CLI pour vous — rien à installer.
Un projet généré sur Windows commit une cible compile qui invoque smithy directement, donc toute autre personne travaillant dessus — y compris sur macOS ou Linux — a besoin de la CLI Smithy dans son PATH également. Pour que ces machines résolvent la CLI via mise à la place, basculez la cible vers la commande mise comme décrit ci-dessous.
Choisir comment la CLI est résolue
Section intitulée « Choisir comment la CLI est résolue »macOS et Linux résolvent la CLI via mise et Windows utilise une CLI installée globalement, mais vous pouvez choisir l’une ou l’autre sur n’importe quelle plateforme en modifiant la commande de la cible compile dans le project.json du projet de modèle.
Pour utiliser une CLI Smithy installée globalement au lieu de mise, remplacez le préfixe mise par un simple smithy :
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}Pour revenir à mise résolvant la CLI, restaurez le préfixe npx -y mise@<version> exec smithy@<version> --.
Cible Bundle
Section intitulée « Cible Bundle »Le générateur configure automatiquement une cible bundle qui utilise Rolldown pour créer un package de déploiement :
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configuration de Rolldown se trouve dans rolldown.config.ts, avec une entrée par bundle à générer. Rolldown gère la création de plusieurs bundles en parallèle s’ils sont définis.
Développement local
Section intitulée « Développement local »Le générateur configure un serveur de développement local avec rechargement à chaud :
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Déployer votre API Smithy
Section intitulée « Déployer votre API Smithy »Le générateur crée une infrastructure CDK ou Terraform basée sur votre iac sélectionné.
Le construct CDK pour déployer votre API se trouve dans le dossier 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(), }); }}Cela configure :
- Une fonction AWS Lambda pour le service Smithy
- Une API REST API Gateway comme déclencheur de fonction
- Des rôles IAM et des permissions
- Un groupe de logs CloudWatch
- Une configuration de traçage X-Ray
Les modules Terraform pour déployer votre API se trouvent dans le dossier common/terraform.
Le module API stocke son zip de déploiement Lambda dans un bucket S3 d’actifs partagé — consultez le guide d’infrastructure Terraform pour plus de détails. Instanciez le module core/asset-bucket une fois par déploiement et transmettez sa sortie bucket_name dans chaque module API / Lambda via l’entrée 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}Cela configure :
- Une fonction AWS Lambda qui sert l’API Smithy
- Une API REST API Gateway comme déclencheur de fonction
- Des rôles IAM et des permissions
- Un groupe de logs CloudWatch
- Une configuration de traçage X-Ray
- Une configuration CORS
Le module Terraform fournit plusieurs sorties :
# 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}Pour les API REST, le construct généré associe par défaut une Web ACL AWS WAFv2 à l’étape API Gateway. La Web ACL utilise l’ensemble de règles par défaut géré par AWS (AWSManagedRulesCommonRuleSet et AWSManagedRulesKnownBadInputsRuleSet), offrant une protection contre les exploits web courants, y compris le Top 10 OWASP. Les journaux de requêtes WAF sont écrits dans un groupe CloudWatch Logs.
Vous pouvez modifier le construct rest-api généré pour ajouter, supprimer ou ajuster des règles (par exemple, pour ajouter des règles basées sur le taux ou des groupes de règles gérées supplémentaires).
Pour désactiver cette fonctionnalité (par exemple, pour attacher votre propre Web ACL), définissez enableWaf sur false :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Pour désactiver cette fonctionnalité (par exemple, pour attacher votre propre Web ACL), définissez enable_waf sur false :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Journalisation des accès
Section intitulée « Journalisation des accès »Pour les API REST, l’infrastructure générée active la journalisation des accès par défaut, écrivant une ligne JSON structurée par requête dans un groupe CloudWatch Logs dédié. Le groupe de logs est chiffré avec une clé KMS gérée par le client et conservé pendant un an.
API Gateway écrit les logs d’accès en utilisant un rôle CloudWatch Logs au niveau du compte. Ce rôle est configuré sur le paramètre AWS::ApiGateway::Account, qui est un singleton par région par compte — il n’y a qu’un seul rôle pour chaque API REST dans la région. Pour gérer cela en toute sécurité à travers plusieurs stacks déployées indépendamment, l’infrastructure générée :
- Crée un rôle CloudWatch Logs partagé et le configure sur le compte uniquement lorsqu’aucun rôle fonctionnel n’est déjà défini, de sorte que les déploiements n’écrasent jamais un rôle appartenant à une autre stack.
- Laisse le paramètre du compte intact lors du démontage, de sorte que la destruction d’une stack ne désactive jamais la journalisation pour d’autres API REST dans la région.
Le rôle du compte est géré par le construct ApiGatewayAccount, un singleton à portée de stack résolu via ApiGatewayAccount.ensure(scope). Le stage de chaque API REST en dépend, et le rôle est configuré par une ressource personnalisée basée sur Lambda.
Le format du log d’accès est défini par le construct RestApi que votre API étend. Pour le personnaliser, passez deployOptions à super dans le fichier généré packages/common/constructs/src/app/apis/my-api.ts, en conservant le tracingEnabled que le construct définit déjà :
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat est importé depuis aws-cdk-lib/aws-apigateway. Tout ce que vous ne définissez pas conserve la valeur par défaut du construct — un format JSON avec les champs standard.
Le rôle du compte est géré par le module core/api/api-gateway-account, qui est instancié par le module API généré. Il configure le compte de manière idempotente et n’est jamais réinitialisé lors d’un terraform destroy.
Vous pouvez personnaliser le format du log d’accès en modifiant le bloc access_log_settings sur la ressource aws_api_gateway_stage dans le module API généré.
Intégrations
Section intitulée « Intégrations »Les constructs CDK d’API REST/HTTP sont configurés pour fournir une interface type-safe pour définir des intégrations pour chacune de vos opérations.
Intégrations par défaut
Section intitulée « Intégrations par défaut »Vous pouvez utiliser la méthode statique defaultIntegrations pour utiliser le modèle par défaut, qui définit une fonction AWS Lambda individuelle pour chaque opération :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Le module généré définit déjà les intégrations par défaut pour le modèle avec lequel l’API a été générée, donc aucune configuration supplémentaire n’est nécessaire :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}Avec le modèle isolated par défaut, cela crée une fonction Lambda par opération.
Accès aux intégrations
Section intitulée « Accès aux intégrations »Vous pouvez accéder aux fonctions AWS Lambda sous-jacentes via la propriété integrations du construct API, de manière type-safe. Par exemple, si votre API définit une opération nommée sayHello et que vous devez ajouter des permissions à cette fonction, vous pouvez le faire comme suit :
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 votre API utilise le modèle shared, le Lambda de routeur partagé est exposé comme api.integrations.$router :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Notez que $router n’est plus disponible si vous remplacez chaque opération via withOverrides, puisqu’aucune opération n’utilise plus l’intégration de routeur par défaut.
Avec le modèle isolated, les sorties du module sont des maps indexées par nom d’opération, vous pouvez donc accéder aux ressources d’une seule opération. Par exemple, pour accorder des permissions supplémentaires à la fonction Lambda d’une opération :
# 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/*" } ] })}Pour accorder les mêmes permissions à toutes les opérations, itérez sur la sortie 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/*" } ] })}Le module expose également lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids et lambda_log_group_names comme des maps indexées par nom d’opération. Avec le modèle shared, les sorties singulières équivalentes (lambda_execution_role_name, lambda_function_name, …) sont exposées à la place, puisqu’il n’y a qu’une seule fonction.
Les permissions dont chaque opération a besoin sont mieux passées au module, qui les applique au rôle de chaque fonction :
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/*"] } ]}Personnalisation des options par défaut
Section intitulée « Personnalisation des options par défaut »Si vous souhaitez personnaliser les options utilisées lors de la création de la fonction Lambda pour chaque intégration par défaut, vous pouvez utiliser la méthode withDefaultOptions. Par exemple, si vous souhaitez que toutes vos fonctions Lambda résident dans un Vpc :
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configuration VPC est déjà prise en charge par le module généré — définissez enable_vpc avec vpc_id et subnet_ids, et le module déploie chaque fonction Lambda dans votre VPC derrière un groupe de sécurité partagé qu’il crée pour vous :
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}Pour les options que le module n’expose pas, modifiez directement la ressource aws_lambda_function dans le module Terraform généré. Avec le modèle isolated, cette ressource unique est déclarée for_each = local.operations, donc une modification s’applique à toutes les opérations.
Personnalisation des options par opération
Section intitulée « Personnalisation des options par opération »Pour personnaliser les options utilisées pour créer l’intégration par défaut pour des opérations spécifiques (sans affecter les autres), vous pouvez utiliser la méthode withOperationOptions. Par exemple, si vous souhaitez augmenter le timeout de la fonction Lambda pour une seule opération :
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({ ... }));Les options que vous spécifiez sont fusionnées avec les options d’intégration par défaut (et toutes les options définies via withDefaultOptions). Notez que vous ne pouvez pas spécifier d’options pour les opérations que vous avez remplacées via withOverrides, car celles-ci n’utilisent plus l’intégration par défaut.
Vous rencontrerez une erreur de type si la même opération est ciblée à la fois par withOperationOptions et withOverrides, quel que soit l’ordre dans lequel vous les appelez.
Avec le modèle isolated, la ressource de fonction Lambda est déjà par opération, donc les options peuvent varier selon le nom de l’opération. Par exemple, pour donner à une opération un timeout plus long, modifiez la ressource aws_lambda_function dans le module généré :
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}Remplacement des intégrations
Section intitulée « Remplacement des intégrations »Vous pouvez également remplacer les intégrations pour des opérations spécifiques en utilisant la méthode withOverrides. Chaque remplacement doit spécifier une propriété integration qui est typée selon le construct d’intégration CDK approprié pour l’API HTTP ou REST. La méthode withOverrides est également type-safe. Par exemple, si vous souhaitez remplacer une API getDocumentation pour pointer vers de la documentation hébergée par un site web externe, vous pourriez le faire comme suit :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Vous remarquerez également que l’intégration remplacée n’a plus de propriété handler lors de l’accès via api.integrations.getDocumentation.
Vous pouvez ajouter des propriétés supplémentaires à une intégration qui seront également typées en conséquence, permettant à d’autres types d’intégration d’être abstraits tout en restant type-safe, par exemple si vous avez créé une intégration S3 pour une API REST et souhaitez plus tard référencer le bucket pour une opération particulière, vous pouvez le faire comme suit :
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(...);Pour pointer une opération spécifique vers un type d’intégration différent, excluez-la du for_each par défaut et déclarez son intégration séparément. Par exemple, pour servir getDocumentation depuis un site web externe :
# 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}"}Remplacement des autorisateurs
Section intitulée « Remplacement des autorisateurs »Vous pouvez également fournir des options dans votre intégration pour remplacer des options de méthode particulières telles que les autorisateurs, par exemple si vous souhaitez utiliser l’authentification Cognito pour votre opération 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’autorisation est définie sur la route (API HTTP) ou la méthode (API REST) pour chaque opération, elle peut donc varier selon le nom de l’opération. Par exemple, pour laisser une opération non authentifiée sur une API HTTP :
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Pour une API REST authentifiée par IAM, ajoutez également une déclaration de politique de ressource autorisant l’accès non authentifié au chemin de cette opération.
Intégrations explicites
Section intitulée « Intégrations explicites »Si vous préférez, vous pouvez choisir de ne pas utiliser les intégrations par défaut et d’en fournir directement une pour chaque opération. Ceci est utile si, par exemple, chaque opération doit utiliser un type d’intégration différent ou si vous souhaitez recevoir une erreur de type lors de l’ajout de nouvelles opérations :
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Remplacez le for_each utilisé par le modèle isolated par des instanciations explicites des fonctions Lambda, intégrations et permissions pour chaque opération.
Modèle d’intégration
Section intitulée « Modèle d’intégration »Les API générées prennent en charge deux modèles d’intégration :
isolatedcrée une fonction Lambda par opération. C’est l’option par défaut et recommandée pour les API.sharedcrée un seul Lambda de routeur par défaut et le réutilise pour chaque opération sauf si vous remplacez des intégrations spécifiques.
isolated vous donne des permissions et une configuration plus granulaires par opération, ainsi qu’une meilleure séparation pour les logs et les traces. shared réduit la probabilité de rencontrer des démarrages à froid pour les API à faible utilisation.
Le modèle d’intégration peut être changé à tout moment dans CDK en mettant à jour votre construct API. Par exemple, définir pattern à 'shared' crée une seule fonction au lieu d’une par intégration :
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Contrairement à CDK, le modèle d’intégration est intégré dans le module généré. Pour changer le modèle d’intégration :
- Supprimez le module API précédemment généré dans
packages/common/terraform/src/app/apis - Relancez le générateur qui a créé votre API avec l’autre modèle d’intégration (par exemple
--integrationPattern=shared)
Avec le modèle isolated, le module lit les opérations depuis un fichier généré :
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Ce fichier est généré à partir de votre API, vous n’avez donc pas besoin de le modifier manuellement. L’ajout d’une opération à votre code d’application API ajoute la route et la fonction lambda lors du prochain déploiement. Il est .gitignored par défaut ; supprimez l’entrée si vous préférez le versionner.
Limite de profondeur de chemin de l’API REST Terraform
Section intitulée « Limite de profondeur de chemin de l’API REST Terraform »Génération de code
Section intitulée « Génération de code »Puisque les opérations sont définies dans Smithy, nous utilisons la génération de code pour fournir des métadonnées au construct CDK pour des intégrations type-safe.
Une cible generate:<ApiName>-metadata est ajoutée au project.json des constructs communs pour faciliter cette génération de code, qui émet un fichier tel que packages/common/constructs/src/generated/my-api/metadata.gen.ts. Puisque cela est généré au moment du build, il est ignoré dans le contrôle de version.
Accorder l’accès (IAM uniquement)
Section intitulée « Accorder l’accès (IAM uniquement) »Si vous avez sélectionné l’authentification IAM, vous pouvez utiliser la méthode grantInvokeAccess pour accorder l’accès à votre 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}Invoquer votre API Smithy
Section intitulée « Invoquer votre API Smithy »Pour invoquer votre API depuis un site web React, vous pouvez utiliser le générateur connection, qui fournit une génération de client type-safe à partir de votre modèle Smithy.
Connexions
Section intitulée « Connexions »Utilisez le générateur connection pour intégrer ce projet avec d’autres dans votre espace de travail. Les connexions suivantes impliquent ce projet :