diff --git a/.agents/skills/aws-sdk-js-v3-usage/SKILL.md b/.agents/skills/aws-sdk-js-v3-usage/SKILL.md new file mode 100644 index 0000000..2762a3e --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/SKILL.md @@ -0,0 +1,254 @@ +--- +name: aws-sdk-js-v3-usage +description: | + AWS SDK for JavaScript v3 development patterns. Use when writing JavaScript or TypeScript code that uses AWS services via @aws-sdk/* packages (aws-sdk-js-v3), or when asked about schemas, runtime validation, serialization, or code generation in the context of the JS/TS AWS SDK. +--- + +> Do not use emojis in any code, comments, or output when this skill is active. + +# AWS SDK for JavaScript v3 + +## Package Structure + +- `@aws-sdk/client-*` — one per service, generated by [smithy-typescript](https://github.com/awslabs/smithy-typescript); one-to-one with AWS services and operations +- `@aws-sdk/lib-*` — higher-level helpers (e.g. `lib-dynamodb`, `lib-storage`) +- `@aws-sdk/*` (no prefix) — utility packages (mostly internal; don't import deep paths) + +Always import from the package root: + +```js +import { S3Client } from "@aws-sdk/client-s3"; // correct +// NOT: import { S3Client } from "@aws-sdk/client-s3/dist-cjs/S3Client" +``` + +## Two Client Styles + +**Bare-bones** (preferred — smaller bundle): + +```js +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +const client = new S3Client({ region: "us-east-1" }); +const output = await client.send(new GetObjectCommand({ Bucket: "b", Key: "k" })); +``` + +**Aggregated** (v2-style but NOT v2, larger bundle): + +```js +import { S3 } from "@aws-sdk/client-s3"; +const client = new S3({ region: "us-east-1" }); +const output = await client.getObject({ Bucket: "b", Key: "k" }); +``` + +## Client Configuration + +No global config in v3 — pass config to each client. `region` is always required; set it explicitly or via `AWS_REGION` env var. + +```js +const config = { region: "us-east-1", maxAttempts: 5 }; +const s3 = new S3Client(config); +const dynamo = new DynamoDBClient(config); +``` + +**Do not read or mutate `client.config` after instantiation** — it is a resolved form (e.g. `region` becomes an async function). See `references/effective-practices.md`. + +For HTTP handler (`NodeHttpHandler` from `@smithy/node-http-handler`), retry strategy, endpoint details, logging, FIPS, dual-stack, protocol selection, and S3-specific options → see `references/clients.md`. + +## Credentials + +All providers from `@aws-sdk/credential-providers`. Credentials are lazy and cached per client until ~5 min before expiry. + +```js +// Default chain (env → ini → IMDS/ECS) — use in most Node.js apps +const client = new S3Client({ credentials: fromNodeProviderChain() }); + +// Assume role (NOTE: fromTemporaryCredentials is correct for STS AssumeRole) +const client = new S3Client({ + credentials: fromTemporaryCredentials({ params: { RoleArn: "arn:aws:iam::123456789012:role/MyRole" } }), +}); + +// Named profile +const client = new S3Client({ profile: "my-profile" }); +``` + +Share credentials and socket pool across multi-region clients: + +```js +const east = new S3Client({ region: "us-east-1" }); +const { credentials, requestHandler } = east.config; +const west = new S3Client({ region: "us-west-2", credentials, requestHandler }); +``` + +For all providers (Cognito, SSO, web identity, custom chains, STS region priority) → see `references/credentials.md`. + +## Streams (e.g. S3 GetObject Body) + +**Always read or discard streaming responses** — unread streams leave sockets open (socket exhaustion): + +```js +const { Body } = await client.send(new GetObjectCommand({ Bucket: "b", Key: "k" })); +const str = await Body.transformToString(); // read as string +const bytes = await Body.transformToByteArray(); // read as Uint8Array +// or discard: +await (Body.destroy?.() ?? Body.cancel?.()); +``` + +Streams can only be read once. + +## Paginators + +Use `paginate*` functions instead of manual token handling: + +```js +import { DynamoDBClient, paginateListTables } from "@aws-sdk/client-dynamodb"; + +const client = new DynamoDBClient({}); + +const tableNames = []; +for await (const page of paginateListTables({ client }, {})) { + // page contains a single paginated output. + tableNames.push(...page.TableNames); +} +``` + +## DynamoDB DocumentClient + +Use `@aws-sdk/lib-dynamodb` to work with native JS types instead of AttributeValues: + +```js +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb"; + +const client = DynamoDBDocumentClient.from(new DynamoDBClient({})); +await client.send(new PutCommand({ TableName: "T", Item: { id: "1", name: "Alice" } })); +const { Item } = await client.send(new GetCommand({ TableName: "T", Key: { id: "1" } })); +``` + +For marshall options, large numbers (NumberValue), pagination, and aggregated client → see `references/dynamodb.md`. + +## S3: Presigned URLs, Multipart Upload, Waiters + +```js +// Presigned GET URL +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +const url = await getSignedUrl(client, new GetObjectCommand({ Bucket: "b", Key: "k" }), { expiresIn: 3600 }); + +// Multipart upload (large files / streams) +import { Upload } from "@aws-sdk/lib-storage"; +const upload = new Upload({ client, params: { Bucket: "b", Key: "k", Body: stream } }); +await upload.done(); + +// Waiters +import { waitUntilObjectExists } from "@aws-sdk/client-s3"; +await waitUntilObjectExists({ client, maxWaitTime: 120 }, { Bucket: "b", Key: "k" }); +``` + +For presigned POST, signed headers, waiter options → see `references/s3.md`. + +## Error Handling + +```js +import { S3ServiceException } from "@aws-sdk/client-s3"; + +try { + await client.send(new GetObjectCommand({ Bucket: "b", Key: "k" })); +} catch (e) { + if (e?.$metadata) { + // SDK service error — has $metadata.httpStatusCode, e.name, e.$response + console.error(e.name, e.$metadata.httpStatusCode); + } +} +``` + +Check `e.name` or `instanceof` for specific error types. See `references/error-handling.md` for full patterns. + +For **runtime validation, serialization to non-default formats, or questions about what schemas are** in jsv3 → see `references/schemas.md`. + +## Performance: Parallel Workloads + +```js +// Configure maxSockets to match your parallel batch size +const client = new S3Client({ + requestHandler: { httpsAgent: { maxSockets: 50 } }, + cacheMiddleware: true, // skip if using custom middleware +}); +``` + +**Streaming deadlock warning**: with limited sockets, don't `await` the request and stream body separately — chain them. See `references/performance.md`. + +## Middleware + +Add custom logic to all commands on a client: + +```js +client.middlewareStack.add( + (next, context) => async (args) => { + console.log(context.commandName, args.input); + const result = await next(args); + return result; + }, + { name: "MyMiddleware", step: "build", override: true } +); +``` + +Steps (in order): `initialize` → `serialize` → `build` → `finalizeRequest` → `deserialize` + +## Abort Controller + +```js +const { AbortController } = require("@aws-sdk/abort-controller"); +const { S3Client, CreateBucketCommand } = require("@aws-sdk/client-s3"); + +const abortController = new AbortController(); +const client = new S3Client(clientParams); + +const requestPromise = client.send(new CreateBucketCommand(commandParams), { + abortSignal: abortController.signal, +}); + +// The request will not be created if abortSignal is already aborted. +// The request will be destroyed if abortSignal is aborted before response is returned. +abortController.abort(); + +// This will fail with "AbortError" as abortSignal is aborted. +await requestPromise; +``` + +## Lambda Best Practices + +Initialize clients **outside** the handler (container reuse), make API calls **inside**. For one-time async setup, use a lazy init flag inside the handler: + +```js +import { S3Client } from "@aws-sdk/client-s3"; + +const client = new S3Client({}); // outside — reused across invocations + +let ready = false; +export const handler = async (event) => { + if (!ready) { await prepare(); ready = true; } // lazy one-time setup inside handler + // ... API calls here +}; +``` + +See `references/lambda.md` for Lambda layers and versioning. + +## Node.js Version Requirements + +- v3.968.0+ requires Node.js >= 20 +- v3.723.0+ requires Node.js >= 18 + +## TypeScript + +Response fields are typed as `T | undefined` by default. Use `AssertiveClient` from `@smithy/types` to remove `| undefined`, or `NodeJsClient` / `BrowserClient` to narrow streaming blob types. See `references/typescript.md`. + +## SigV4a (S3 Multi-Region Access Points) + +S3 MRAP and certain other features require SigV4a. You must install and side-effect-import exactly one of: + +- `@aws-sdk/signature-v4-crt` — Node.js only, better performance +- `@aws-sdk/signature-v4a` — Node.js + browsers, pure JS + +```js +import "@aws-sdk/signature-v4a"; // side-effect only — no exported values needed +``` + +See `references/sigv4a.md` for full details and MRAP ARN format. diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/clients.md b/.agents/skills/aws-sdk-js-v3-usage/references/clients.md new file mode 100644 index 0000000..e202adb --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/clients.md @@ -0,0 +1,143 @@ +# Client Configuration Reference + +## Request Handler (HTTP) + +### Node.js (shorthand, v3.521.0+) + +```js +const client = new S3Client({ + requestHandler: { + requestTimeout: 15_000, // ms to receive response + connectionTimeout: 6_000, // ms to establish connection + httpsAgent: { keepAlive: true, maxSockets: 50 }, + }, +}); +``` + +### Node.js (explicit) + +```js +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import https from "node:https"; + +const client = new S3Client({ + requestHandler: new NodeHttpHandler({ + httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 200 }), + requestTimeout: 15_000, + connectionTimeout: 6_000, + }), +}); +``` + +Default `maxSockets` is 50 per client. Socket exhaustion warning: + +```text +@smithy/node-http-handler:WARN - socket usage at capacity=N and M additional requests are enqueued. +``` + +### Browser + +```js +import { FetchHttpHandler } from "@aws-sdk/config/requestHandler"; +const client = new S3Client({ requestHandler: new FetchHttpHandler({ requestTimeout: 30_000 }) }); +``` + +XHR (for upload progress events): + +```js +import { XhrHttpHandler } from "@aws-sdk/xhr-http-handler"; +const handler = new XhrHttpHandler({ requestTimeout: 30_000 }); +handler.on(XhrHttpHandler.EVENTS.UPLOAD_PROGRESS, (event) => { ... }); +const client = new S3Client({ requestHandler: handler }); +``` + +## Retry Strategy + +```js +// Simple: set max attempts +new S3Client({ maxAttempts: 5 }); + +// Custom backoff +import { ConfiguredRetryStrategy } from "@aws-sdk/config/retryStrategy"; +new S3Client({ + retryStrategy: new ConfiguredRetryStrategy(5, (attempt) => 500 + attempt * 1_000), +}); + +// Adaptive (rate-limiting) +new S3Client({ retryMode: "ADAPTIVE" }); +``` + +When `retryStrategy` is set, `retryMode` and `maxAttempts` are ignored. + +## Logging + +```js +// Enable SDK logging (suppress trace/debug) +new S3Client({ + logger: { ...console, debug() {}, trace() {} }, +}); +``` + +For full request/response logging, use middleware (see SKILL.md Middleware section). + +## Endpoint + +```js +// Custom endpoint (e.g. local mock) +new S3Client({ endpoint: "http://localhost:8888" }); +``` + +## FIPS / Dual-stack + +```js +new S3Client({ useFipsEndpoint: true }); +new S3Client({ useDualstackEndpoint: true }); +``` + +## Retrieving the Endpoint Without Making a Request + +**This interface is not public/stable.** Do not use in production, or verify it on every SDK version upgrade. + +```ts +import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { getEndpointFromInstructions } from "@smithy/middleware-endpoint"; + +const client = new S3Client({ region: "us-east-1" }); + +/** @internal do not directly use in production. */ +const endpoint = await getEndpointFromInstructions( + { Key: "foo", Bucket: "bar" }, // 1. command input + GetObjectCommand, // 2. Command class + client.config // 3. client config +); +``` + +## Protocol Selection (v3.953.0+) + +Most services support only one protocol. CloudWatch and SQS support multiple: + +```js +import { AwsJson1_0Protocol, AwsSmithyRpcV2CborProtocol } from "@aws-sdk/core/protocols"; + +new CloudWatch({ protocol: AwsJson1_0Protocol }); // default +new CloudWatch({ protocol: AwsSmithyRpcV2CborProtocol }); // CBOR +``` + +## Middleware Caching + +```js +// Cache middleware stack per client+command — reduces per-request overhead. +// Do not use if you modify the middleware stack after requests begin. +new S3Client({ cacheMiddleware: true }); +``` + +## S3-Specific Options + +```js +// Retry with corrected region on 301 redirect (use only if bucket region is unknown) +new S3Client({ followRegionRedirects: true }); +``` + +## Schemas (v3.953.0+) + +See `references/schemas.md`. diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/credentials.md b/.agents/skills/aws-sdk-js-v3-usage/references/credentials.md new file mode 100644 index 0000000..da17b88 --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/credentials.md @@ -0,0 +1,117 @@ +# Credentials Reference + +All providers from `@aws-sdk/credential-providers`. + +## Provider Quick Reference + +| Provider | Use case | +|---|---| +| `fromNodeProviderChain()` | Default Node.js chain (env → ini → IMDS/ECS) | +| `fromEnv()` | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` env vars | +| `fromIni()` | `~/.aws/credentials` / `~/.aws/config` profiles | +| `fromTemporaryCredentials()` | STS AssumeRole | +| `fromWebToken()` | STS AssumeRoleWithWebIdentity (OIDC) | +| `fromTokenFile()` | OIDC token file (EKS IRSA) — reads `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | +| `fromSSO()` | AWS IAM Identity Center (SSO) | +| `fromCognitoIdentityPool()` | Browser/mobile — Cognito Identity Pool | +| `fromInstanceMetadata()` | EC2 instance profile (IMDSv1/v2) | +| `fromContainerMetadata()` | ECS task role | +| `fromHttp()` | Custom HTTP credential endpoint | +| `createCredentialChain()` | Custom fallback chain | + +## Assume Role (STS) + +```js +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + credentials: fromTemporaryCredentials({ + params: { + RoleArn: "arn:aws:iam::123456789012:role/MyRole", + RoleSessionName: "my-session", // optional, auto-generated if omitted + DurationSeconds: 3600, // optional + }, + // clientConfig: { region: "us-east-1" } // override STS region if needed + }), +}); +``` + +Chained role assumption: + +```js +credentials: fromTemporaryCredentials({ + masterCredentials: fromTemporaryCredentials({ + params: { RoleArn: "arn:aws:iam::123456789012:role/RoleA" }, + }), + params: { RoleArn: "arn:aws:iam::123456789012:role/RoleB" }, +}) +``` + +## Named Profile + +```js +// Simplest — sets profile for both client config and credentials +const client = new S3Client({ profile: "my-profile" }); + +// Explicit — credentials only +import { fromIni } from "@aws-sdk/credential-providers"; +const client = new S3Client({ credentials: fromIni({ profile: "my-profile" }) }); +``` + +## Web Identity / OIDC (fromWebToken) + +```js +import { fromWebToken } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + credentials: fromWebToken({ + roleArn: "arn:aws:iam::123456789012:role/MyRole", + webIdentityToken: await getTokenFromIdP(), + roleSessionName: "session", // optional + }), +}); +``` + +## Cognito Identity Pool (browser/mobile) + +```js +import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + region: "us-east-1", + credentials: fromCognitoIdentityPool({ + identityPoolId: "us-east-1:1699ebc0-7900-4099-b910-2df94f52a030", + logins: { "accounts.google.com": googleIdToken }, // optional, for authenticated identities + }), +}); +``` + +## Custom Chain + +```js +import { createCredentialChain, fromEnv, fromIni } from "@aws-sdk/credential-providers"; + +const client = new S3Client({ + credentials: createCredentialChain(fromEnv(), fromIni({ profile: "fallback" })), +}); +``` + +## STS Region Priority + +When a credential provider uses STS internally, region is resolved in this order: + +1. `clientConfig.region` passed to the provider +2. Profile region — if resolving from config file, this beats `AWS_REGION` +3. Outer client's region +4. `AWS_REGION` env var +5. Profile region — if *not* resolving from config file, this is lower than `AWS_REGION` +6. `us-east-1` fallback + +To pin the STS region explicitly: + +```js +fromTemporaryCredentials({ + params: { RoleArn: "..." }, + clientConfig: { region: "us-east-1" }, +}) +``` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/dynamodb.md b/.agents/skills/aws-sdk-js-v3-usage/references/dynamodb.md new file mode 100644 index 0000000..966df3c --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/dynamodb.md @@ -0,0 +1,104 @@ +# DynamoDB Reference + +## DocumentClient (lib-dynamodb) + +`@aws-sdk/lib-dynamodb` marshals native JS types to/from DynamoDB AttributeValues automatically. + +```js +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, DeleteCommand } from "@aws-sdk/lib-dynamodb"; + +const client = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" })); + +// Put +await client.send(new PutCommand({ TableName: "MyTable", Item: { id: "1", name: "Alice", age: 30 } })); + +// Get +const { Item } = await client.send(new GetCommand({ TableName: "MyTable", Key: { id: "1" } })); + +// Query +const { Items } = await client.send(new QueryCommand({ + TableName: "MyTable", + KeyConditionExpression: "id = :id", + ExpressionAttributeValues: { ":id": "1" }, +})); + +// Delete +await client.send(new DeleteCommand({ TableName: "MyTable", Key: { id: "1" } })); +``` + +## Type Mapping + +| JS type | DynamoDB type | +|---|---| +| string | S | +| number / bigint / NumberValue | N | +| boolean | BOOL | +| null | NULL | +| Array | L | +| Object | M | +| Uint8Array / Buffer / Blob / File... | B | +| Set\ | SS | +| Set\ / Set\ / Set\ | NS | +| Set\ / Set\... | BS | + +## Marshall Options + +```js +const client = DynamoDBDocumentClient.from(new DynamoDBClient({}), { + marshallOptions: { + removeUndefinedValues: true, // strip undefined from objects/arrays + convertEmptyValues: false, // convert "" / empty sets to null + convertClassInstanceToMap: false, + allowImpreciseNumbers: false, // true = allow numbers > MAX_SAFE_INTEGER (loses precision) + }, + unmarshallOptions: { + wrapNumbers: false, // true = return NumberValue instead of JS number + }, +}); +``` + +## Large Numbers + +Numbers exceeding `Number.MAX_SAFE_INTEGER` throw by default. Use `NumberValue` for precision: + +```js +import { NumberValue, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; + +await client.send(new PutCommand({ + TableName: "MyTable", + Item: { id: "1", bigNum: NumberValue.from("1000000000000000000000.000000001") }, +})); +``` + +Custom unmarshalling with BigInt: + +```js +const client = DynamoDBDocumentClient.from(new DynamoDBClient({}), { + unmarshallOptions: { wrapNumbers: (str) => BigInt(str) }, +}); +``` + +## Pagination (Scan / Query) + +```js +import { paginateScan } from "@aws-sdk/lib-dynamodb"; + +for await (const page of paginateScan({ client }, { TableName: "MyTable", Limit: 100 })) { + console.log(page.Items); +} +``` + +## Aggregated (full) Client + +```js +import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"; + +const doc = DynamoDBDocument.from(new DynamoDBClient({})); +await doc.put({ TableName: "MyTable", Item: { id: "1" } }); +await doc.get({ TableName: "MyTable", Key: { id: "1" } }); +``` + +## Destroy + +`ddbDocClient.destroy()` is a no-op. Call `destroy()` on the underlying `DynamoDBClient`. diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/effective-practices.md b/.agents/skills/aws-sdk-js-v3-usage/references/effective-practices.md new file mode 100644 index 0000000..58eaef3 --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/effective-practices.md @@ -0,0 +1,81 @@ +# Effective Practices Reference + +## Client Reuse + +Create one client per region+credentials combination. Don't create clients inside loops: + +```js +// WRONG: +for (const item of items) { + const client = new S3Client({ region, credentials }); + await client.send(new PutObjectCommand(item)); +} + +// OK: +const client = new S3Client({ region, credentials }); +for (const item of items) { + await client.send(new PutObjectCommand(item)); +} +``` + +## Don't Read or Mutate `client.config` + +`client.config` is a resolved form — `region` becomes `async () => "us-east-1"`, credentials are wrapped, etc. Reading or writing it directly will cause errors: + +```js +// WRONG: — throws "config.region is not a function" +client.config.region = "us-west-2"; + +// WRONG: — throws "client.config.endpoint is not a function" +const endpoint = await client.config.endpoint(); +``` + +To use multiple regions, create separate clients (share credentials to avoid duplicate resolution): + +```js +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; +const creds = fromTemporaryCredentials({ params: { RoleArn: "..." } }); +const east = new S3Client({ region: "us-east-1", credentials: creds }); +const west = new S3Client({ region: "us-west-2", credentials: creds }); +``` + +To get the resolved endpoint for a specific operation: + +```js +import { getEndpointFromInstructions } from "@smithy/middleware-endpoint"; +const endpoint = await getEndpointFromInstructions( + { Bucket, Key }, + GetObjectCommand, + { region: "us-west-2", useDualstackEndpoint: false, useFipsEndpoint: false } +); +console.log(endpoint.url.toString()); +``` + +## Always Read or Discard Streaming Responses + +Unread streams hold sockets open → socket exhaustion / memory leak: + +```js +const { Body } = await client.send(new GetObjectCommand({ Bucket, Key })); + +// OK: read +const bytes = await Body.transformToByteArray(); + +// OK: pipe +await client.send(new PutObjectCommand({ Bucket: dest, Key, Body })); + +// OK: discard +await (Body.destroy?.() ?? Body.cancel?.()); + +// WRONG: — socket stays open +// (no action on Body) +``` + +## Cross-Region Connection Timeouts (Node.js 20+) + +For cross-region requests that hit `ETIMEDOUT` / `AggregateError`: + +```js +import net from "node:net"; +net.setDefaultAutoSelectFamilyAttemptTimeout(500); // default is 250ms +``` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/error-handling.md b/.agents/skills/aws-sdk-js-v3-usage/references/error-handling.md new file mode 100644 index 0000000..48e526d --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/error-handling.md @@ -0,0 +1,59 @@ +# Error Handling Reference + +## Service Errors + +Non-2xx responses are thrown as JavaScript `Error`s with SDK-specific fields: + +```js +try { + await client.send(new CreateFunctionCommand({ ... })); +} catch (e) { + if (e?.$metadata) { + // e.name — error code string (e.g. "ResourceNotFoundException") + // e.$metadata.httpStatusCode — HTTP status + // e.$response — raw HTTP response object + // e.$responseBodyText — set when SDK fails to parse the error body (unexpected format) + console.error(e.name, e.$metadata.httpStatusCode); + } +} +``` + +## Checking Specific Error Types + +By name or `instanceof` (both safe — SDK overrides `Symbol.hasInstance`): + +```js +import { NoSuchKeyException } from "@aws-sdk/client-s3"; + +if (e.name === "NoSuchKeyException") { ... } +if (e instanceof NoSuchKeyException) { ... } +``` + +## Unparseable Error Bodies + +If the error body can't be parsed (e.g. a proxy returned HTML), the message will say: +> "Deserialization error: to see the raw response, inspect the hidden field {error}.$response" + +Inspect with: + +```js +if (e.$responseBodyText) console.debug(e.$responseBodyText); +``` + +## TypeScript: Version Mismatch Compilation Error + +If you see: + +```console +error TS2345: Argument of type 'X' is not assignable to parameter of type 'Y' + 'A' is assignable to the constraint of type 'B', but 'B' could be instantiated with a different subtype +``` + +This is caused by mismatched `@smithy/types` / `@aws-sdk/types` versions across clients. Fix by pinning all `@aws-sdk/client-*` packages to the same version range: + +```json +{ + "@aws-sdk/client-s3": "<=3.800.0", + "@aws-sdk/client-dynamodb": "<=3.800.0" +} +``` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/lambda.md b/.agents/skills/aws-sdk-js-v3-usage/references/lambda.md new file mode 100644 index 0000000..6797706 --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/lambda.md @@ -0,0 +1,72 @@ +# Lambda Reference + +## SDK Version in Lambda Runtimes + +Lambda bundles a specific SDK version — not the latest. To control the version, bundle the SDK with your function or use a Lambda layer. + +Check the installed version: + +```js +const pkg = require("@aws-sdk/client-s3/package.json"); +exports.handler = () => JSON.stringify(pkg); +``` + +## Creating a Lambda Layer + +```json +// package.json for layer content +{ + "dependencies": { + "@aws-sdk/client-s3": "<=3.750.0", + "@aws-sdk/client-dynamodb": "<=3.750.0" + } +} +``` + +Run `npm install`, then zip as: + +```text +layer_content.zip +└ nodejs/node_modules/@aws-sdk/... +``` + +Deploy: + +```js +import { Lambda } from "@aws-sdk/client-lambda"; +import fs from "node:fs"; + +const lambda = new Lambda(); +await lambda.publishLayerVersion({ + LayerName: "my-sdk-layer", + Content: { ZipFile: fs.readFileSync("./layer_content.zip") }, + CompatibleRuntimes: ["nodejs20.x", "nodejs22.x"], + CompatibleArchitectures: ["x86_64", "arm64"], +}); +``` + +## One-Time Async Initialization + +Don't call async setup outside the handler — signed requests may expire during provisioned concurrency pre-warming. Use a lazy flag inside the handler instead: + +```js +// WRONG: risky — network requests may be frozen pre-flight +const ready = prepare(); +export const handler = async (event) => { await ready; ... }; + +// OK: lazy init inside handler +let client = null; +export const handler = async (event) => { + if (!client) client = await prepare(); + return client.getItem({ ... }); +}; +``` + +SDK clients themselves (no async setup) are safe to initialize outside the handler: + +```js +const s3 = new S3Client({}); // OK: outside handler — reused across invocations +export const handler = async (event) => { + return s3.send(new GetObjectCommand({ ... })); +}; +``` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/performance.md b/.agents/skills/aws-sdk-js-v3-usage/references/performance.md new file mode 100644 index 0000000..772c68c --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/performance.md @@ -0,0 +1,68 @@ +# Performance Reference + +## Parallel Workloads (Node.js) + +### Socket Configuration + +Set `maxSockets` to match your parallel batch size: + +```js +import { NodeHttpHandler } from "@aws-sdk/config/requestHandler"; +import { Agent } from "node:https"; + +const client = new S3Client({ + cacheMiddleware: true, // cache middleware resolution — only if not adding custom middleware + requestHandler: new NodeHttpHandler({ + httpsAgent: new Agent({ keepAlive: true, maxSockets: 50 }), + }), +}); + +// Shorthand (v3.521.0+): +const client = new S3Client({ + requestHandler: { requestTimeout: 3_000, httpsAgent: { maxSockets: 50 } }, +}); +``` + +Too few sockets → queuing slowdown. Too many → new socket overhead + risk of `EMFILE` (too many open files). + +### Sharing Credentials and Socket Pool + +```js +const primary = new S3Client({ region: "us-east-1" }); +const { credentials, requestHandler } = primary.config; +const secondary = new S3Client({ region: "us-west-2", credentials, requestHandler }); +``` + +### Streaming Deadlock + +With limited sockets, don't `await` the request before setting up stream consumption: + +```js +// WRONG: deadlock with maxSockets: 1 +const responses = await Promise.all([ + s3.getObject({ Bucket, Key: "1" }), + s3.getObject({ Bucket, Key: "2" }), +]); +await Promise.all(responses.map((r) => r.Body.transformToByteArray())); + +// OK: chain stream handling before awaiting +const responses = [s3.getObject({ Bucket, Key: "1" }), s3.getObject({ Bucket, Key: "2" })]; +const objects = responses.map((get) => get.Body.transformToByteArray()); +await Promise.all(objects); +``` + +### Batch Upload Example + +```js +const BATCH_SIZE = 100; +const client = new S3Client({ requestHandler: { httpsAgent: { maxSockets: 100 } } }); + +const promises = []; +while (files.length) { + promises.push(...files.splice(0, BATCH_SIZE).map((f) => + client.send(new PutObjectCommand({ Bucket: "b", Key: f.name, Body: f.contents })) + )); + await Promise.all(promises); + promises.length = 0; +} +``` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/s3.md b/.agents/skills/aws-sdk-js-v3-usage/references/s3.md new file mode 100644 index 0000000..4737a93 --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/s3.md @@ -0,0 +1,87 @@ +# S3 Reference + +## Presigned URLs (GET / PUT) + +```js +import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +const client = new S3Client({ region: "us-east-1" }); + +// GET — default expiry 900s +const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: "b", Key: "k" }), { expiresIn: 3600 }); + +// PUT +const putUrl = await getSignedUrl(client, new PutObjectCommand({ Bucket: "b", Key: "k" }), { expiresIn: 3600 }); +``` + +Signing non-x-amz headers (e.g. Content-Type): + +```js +const url = await getSignedUrl(client, new PutObjectCommand({ Bucket: "b", Key: "k", ContentType: "image/png" }), { + signableHeaders: new Set(["content-type"]), + expiresIn: 3600, +}); +``` + +Signing x-amz-* headers (must use `unhoistableHeaders`): + +```js +const url = await getSignedUrl(client, new PutObjectCommand({ Bucket: "b", Key: "k", ChecksumSHA256: sha }), { + unhoistableHeaders: new Set(["x-amz-checksum-sha256"]), + expiresIn: 3600, +}); +``` + +## Presigned POST (browser file upload) + +```js +import { createPresignedPost } from "@aws-sdk/s3-presigned-post"; + +const { url, fields } = await createPresignedPost(client, { + Bucket: "b", + Key: "uploads/${filename}", // ${filename} replaced by browser + Expires: 600, + Conditions: [["content-length-range", 0, 10485760]], + Fields: { acl: "bucket-owner-full-control" }, +}); +// Use url + fields in an HTML
or FormData POST +``` + +## Multipart Upload (lib-storage) + +Use `@aws-sdk/lib-storage` for large files, streams, or unknown-size bodies: + +```js +import { Upload } from "@aws-sdk/lib-storage"; +import { S3Client } from "@aws-sdk/client-s3"; + +const upload = new Upload({ + client: new S3Client({}), + params: { Bucket: "b", Key: "k", Body: readableStream }, + queueSize: 4, // parallel part uploads (default 4) + partSize: 5 * 1024 * 1024, // min 5MB per part + leavePartsOnError: false, +}); + +upload.on("httpUploadProgress", (progress) => console.log(progress)); +await upload.done(); +``` + +## Waiters + +```js +import { S3Client } from "@aws-sdk/client-s3"; +import { waitUntilBucketExists, waitUntilObjectExists } from "@aws-sdk/client-s3"; + +const client = new S3Client({}); + +await waitUntilBucketExists({ client, maxWaitTime: 60 }, { Bucket: "my-bucket" }); +await waitUntilObjectExists({ client, maxWaitTime: 120 }, { Bucket: "my-bucket", Key: "my-key" }); +``` + +Available S3 waiters: `waitUntilBucketExists`, `waitUntilBucketNotExists`, `waitUntilObjectExists`, `waitUntilObjectNotExists`. + +Waiter config: `maxWaitTime` (seconds, required), `minDelay` (default 5s), `maxDelay` (default 120s). + +Other services export their own `waitUntil*` functions from the same client package. diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/schemas.md b/.agents/skills/aws-sdk-js-v3-usage/references/schemas.md new file mode 100644 index 0000000..a12992b --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/schemas.md @@ -0,0 +1,38 @@ +# Schemas Reference (v3.953.0+) + +Schemas are runtime objects that describe the data structures of modeled shapes. Used internally by the SDK for serialization/deserialization, and available for runtime validation or serialization to non-default formats. Not needed for basic SDK usage. + +Each exported interface has a corresponding schema suffixed with `$`: + +```ts +import { type PutBucketAclRequest, PutBucketAclRequest$ } from "@aws-sdk/client-s3"; +``` + +## Use case 1: Runtime validation + +```ts +import { NormalizedSchema } from "@smithy/core/schema"; + +const $ = NormalizedSchema.of(PutBucketAclRequest$); +// Use $.isStringSchema(), $.isStructSchema(), $.structIterator(), etc. +// to walk the schema and validate an object at runtime. +``` + +Useful when accepting unknown user input. Note: schemas do not include required-field or numeric-range constraints (by design — the SDK favors server-side validation). + +## Use case 2: Serialization to non-default formats + +```ts +import { JsonCodec } from "@aws-sdk/core/protocols"; +import { PutItemInput$ } from "@aws-sdk/client-dynamodb"; + +const codec = new JsonCodec({ timestampFormat: { useTrait: true, default: 7 }, jsonName: false }); +const serializer = codec.createSerializer(); +serializer.write(PutItemInput$, myData); +const json = serializer.flush(); // serialize DynamoDB input to JSON string + +const deserializer = codec.createDeserializer(); +const result = await deserializer.read(PutItemInput$, json); +``` + +A schema is required (rather than dynamic heuristics) because serialized representations can be ambiguous — e.g. a number could be a timestamp, a base64 string could be a `Uint8Array`. CBOR is also supported via `CborCodec` from `@smithy/core/cbor`. diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/sigv4a.md b/.agents/skills/aws-sdk-js-v3-usage/references/sigv4a.md new file mode 100644 index 0000000..cb6ddfe --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/sigv4a.md @@ -0,0 +1,51 @@ +# SigV4a and S3 Multi-Region Access Points + +SigV4a (multi-region signing) is required for: + +- S3 Multi-Region Access Points (MRAP) +- S3 Object Integrity with certain checksum types +- CloudFront KeyValueStore + +Without it you get: `Neither CRT nor JS SigV4a implementation is available.` + +## Two implementations — pick one + +### Option A: CRT (Node.js only, better performance) + +```bash +npm install @aws-sdk/signature-v4-crt +``` + +```js +import "@aws-sdk/signature-v4-crt"; // side-effect import only — registers itself +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; + +const client = new S3Client({ region: "us-east-1" }); +await client.send(new PutObjectCommand({ + Bucket: "arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap", + Key: "my-key", + Body: "hello", +})); +``` + +### Option B: JavaScript / non-CRT (Node.js + browsers) + +```bash +npm install @aws-sdk/signature-v4a +``` + +```js +import "@aws-sdk/signature-v4a"; // side-effect import only — registers itself +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; + +const client = new S3Client({ region: "us-east-1" }); +// same usage as above +``` + +## Key rules + +- The import is a **side-effect only** — do not use any exported values. Just `import "..."`. +- Do NOT install both. If both are present, CRT takes precedence. +- CRT version does not work in browsers. Use JS version for browser environments. +- JS version in browsers is not recommended due to large bundle size. +- The MRAP bucket ARN format: `arn:aws:s3:::accesspoint/.mrap` diff --git a/.agents/skills/aws-sdk-js-v3-usage/references/typescript.md b/.agents/skills/aws-sdk-js-v3-usage/references/typescript.md new file mode 100644 index 0000000..2bef23f --- /dev/null +++ b/.agents/skills/aws-sdk-js-v3-usage/references/typescript.md @@ -0,0 +1,31 @@ +# TypeScript Reference + +## Remove `| undefined` from Response Structures + +SDK response fields are typed as `T | undefined` by default. To opt out of this for a client: + +```ts +import { S3Client } from "@aws-sdk/client-s3"; +import type { AssertiveClient } from "@smithy/types"; + +const client = new S3Client({}) as AssertiveClient; +// Response fields are no longer unioned with undefined +``` + +See `@smithy/types` docs for `AssertiveClient` and `UncheckedClient` (skips all runtime checks). + +## Narrow Streaming Blob Types + +`GetObjectCommand` Body is typed as a union because the runtime type depends on the request handler (Node.js vs browser). To narrow it: + +```ts +import { S3Client } from "@aws-sdk/client-s3"; +import type { NodeJsClient } from "@smithy/types"; + +const client = new NodeJsClient(new S3Client({})); +// Body is now typed as NodeJsRuntimeStreamingBlob (Readable) instead of a union +``` + +## Minimum TypeScript Version + +No official minimum. Use a recent version. The SDK's own TypeScript version is in the root `package.json` of the aws-sdk-js-v3 repo. diff --git a/.agents/skills/aws-serverless/SKILL.md b/.agents/skills/aws-serverless/SKILL.md new file mode 100644 index 0000000..45506a6 --- /dev/null +++ b/.agents/skills/aws-serverless/SKILL.md @@ -0,0 +1,51 @@ +--- +name: aws-serverless +description: Builds, deploys, manages, debugs, configures, and optimizes serverless applications on AWS using Lambda, API Gateway, Step Functions, EventBridge, and SAM/CDK. Covers cold starts, CORS debugging, event source mappings, troubleshooting, concurrency, SnapStart, Powertools, function URLs, EventBridge Scheduler, Lambda layers, and production readiness. Triggers on mentions of Lambda, API Gateway, Step Functions, SAM templates, CDK serverless stacks, DynamoDB stream triggers, SQS event sources, cold starts, timeouts, 502/504 errors, throttling, concurrency, CORS, Powertools, or any event-driven architecture on AWS, even without the word "serverless." Does not apply to EC2, ECS/Fargate containers, or Amplify hosting. +version: 1 +metadata: + service: [lambda, api-gateway, step-functions, eventbridge, dynamodb, sqs, sns, s3, kinesis] + task: [build, deploy, debug, optimize] + persona: [developer, devops] + workload: [serverless] +--- + +# AWS Serverless +## Overview + +Domain expertise for building serverless applications on AWS. Covers Lambda configuration, API Gateway debugging, Step Functions orchestration, EventBridge patterns, event source mappings, concurrency tuning, cold start optimization, deployment with SAM/CDK, production readiness, and troubleshooting across all serverless services. + +**Works best with** the [AWS MCP server](https://docs.aws.amazon.com/aws-mcp/) — enables running CLI commands, querying CloudWatch, and validating configurations directly. All guidance also works with standard AWS CLI access. + +**Note:** Reference files contain specific runtime versions, quota values, and feature matrices that may change. When precision matters (e.g., deploying to production, choosing a runtime, or checking a quota), confirm values against current AWS documentation rather than relying solely on the values in these files. + +## Routing + +| User need | Action | +|-----------|--------| +| Building a new serverless app | Read [architecture.md](references/architecture.md) for pattern selection, then [deployment.md](references/deployment.md) for SAM/CDK templates | +| Debugging an error | Read [troubleshooting.md](references/troubleshooting.md) — starts with the 5 most common fixes | +| Optimizing performance or cost | Read [lambda.md](references/lambda.md) for cold starts and memory tuning, [production.md](references/production.md) for readiness checklist | +| Configuring event sources (SQS, DDB Streams, SNS) | Read [event-sources.md](references/event-sources.md) | +| Step Functions, EventBridge, or orchestration | Read [orchestration.md](references/orchestration.md) | +| Concurrency configuration | Read [concurrency.md](references/concurrency.md) | +| API Gateway setup | Read [api-gateway.md](references/api-gateway.md) | +| Common anti-patterns | Read the anti-patterns section in [production.md](references/production.md) | +| Starting with Powertools | Use [powertools-handler.py](assets/powertools-handler.py) as a template | +| Lambda Managed Instances, LMI, capacity providers, EC2-backed Lambda, PerExecutionEnvironmentMaxConcurrency | Use the **aws-lambda-managed-instances** skill instead | +| Durable functions, durable execution, checkpoint-and-replay | Use the **aws-lambda-durable-functions** skill instead | +| Firecracker microVMs, strong tenant isolation, sandboxed/untrusted code execution, long-lived sessions, suspend/resume, port-listening servers, snapshot-resumable compute | Use the **aws-lambda-microvms** skill instead | +| Spans multiple areas | Read the most specific reference first, then consult others as needed | + +## Files + +| File | Content | +|------|---------| +| [lambda.md](references/lambda.md) | Runtime, memory/CPU, cold starts, SnapStart, layers, containers | +| [api-gateway.md](references/api-gateway.md) | REST vs HTTP API, stages, auth, throttling, mapping | +| [event-sources.md](references/event-sources.md) | SQS, DDB Streams, SNS, S3, Kinesis triggers | +| [orchestration.md](references/orchestration.md) | Step Functions, EventBridge rules/pipes/scheduler | +| [concurrency.md](references/concurrency.md) | Reserved vs provisioned, scaling, ESM concurrency | +| [architecture.md](references/architecture.md) | Patterns, reference architectures, service selection | +| [deployment.md](references/deployment.md) | SAM/CDK resource types, globals, fast iteration | +| [production.md](references/production.md) | Readiness checklist, observability, anti-patterns | +| [troubleshooting.md](references/troubleshooting.md) | Error → cause → fix for all serverless services | diff --git a/.agents/skills/aws-serverless/assets/powertools-handler.py b/.agents/skills/aws-serverless/assets/powertools-handler.py new file mode 100644 index 0000000..f58cbf2 --- /dev/null +++ b/.agents/skills/aws-serverless/assets/powertools-handler.py @@ -0,0 +1,49 @@ +"""Lambda handler with Powertools Logger, Tracer, Metrics, and Idempotency wired.""" + +import json + +from aws_lambda_powertools import Logger, Metrics, Tracer +from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + IdempotencyConfig, + idempotent, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() +tracer = Tracer() +metrics = Metrics() +persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + +# Idempotency key: "body" deduplicates identical payloads. +config = IdempotencyConfig(event_key_jmespath="body") + + +# Set log_event=True only in non-production environments; +# events may contain auth tokens, cookies, or PII. +@logger.inject_lambda_context(log_event=False) +@tracer.capture_lambda_handler +@metrics.log_metrics(capture_cold_start_metric=True) +@idempotent(config=config, persistence_store=persistence) +def handler(event: dict, context: LambdaContext) -> dict: + logger.info("Processing request") + + result = process(event) + + metrics.add_metric(name="RequestsProcessed", unit=MetricUnit.Count, value=1) + + return { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "https://your-domain.example", # Replace with your domain + }, + "body": json.dumps(result), + } + + +@tracer.capture_method +def process(event: dict) -> dict: + """Replace with your business logic.""" + return {"message": "success"} diff --git a/.agents/skills/aws-serverless/references/api-gateway.md b/.agents/skills/aws-serverless/references/api-gateway.md new file mode 100644 index 0000000..24f1f63 --- /dev/null +++ b/.agents/skills/aws-serverless/references/api-gateway.md @@ -0,0 +1,553 @@ +# API Gateway Reference + +Quick-reference for REST API, HTTP API, WebSocket API — debugging, configuration, and quotas. + +## Contents + +- [REST vs HTTP API Comparison](#rest-vs-http-api-comparison) +- [CORS Debugging](#cors-debugging) +- [Lambda Authorizers](#lambda-authorizers) +- [Throttling and Quotas](#throttling-and-quotas) +- [WebSocket APIs](#websocket-apis) +- [502/504 Debugging](#502504-debugging) + +--- + +## REST vs HTTP API Comparison + +### Decision Tree + +``` +Need any of these? → REST API + ├── API keys / usage plans / per-client throttling + ├── Request validation (built-in) + ├── Request/response body transformation (VTL) + ├── Caching (built-in) + ├── Private API endpoints + ├── Edge-optimized endpoints + ├── Canary deployments + ├── Execution logs / X-Ray tracing + ├── Resource policies + ├── Mock integrations + └── Response streaming + +None of the above? → HTTP API (lower latency, simpler) +``` + +### Feature Comparison + +| Feature | REST API | HTTP API | +|---|---|---| +| **Latency** | Higher | Lower | +| **Endpoint types** | Edge, Regional, Private | Regional only | +| **AWS WAF** | Yes | No | +| **API keys / usage plans** | Yes | No | +| **Per-client throttling** | Yes | No | +| **Request validation** | Yes | No | +| **Body transformation (VTL)** | Yes | No | +| **Parameter mapping** | Yes | Yes | +| **Caching (built-in)** | Yes | No | +| **Custom domains** | Yes | Yes | +| **Lambda authorizers** | Yes (TOKEN + REQUEST) | Yes (REQUEST only) | +| **JWT authorizers (native)** | No | Yes | +| **IAM auth** | Yes | Yes | +| **Cognito (native)** | Yes | Yes (via JWT) | +| **Resource policies** | Yes | No | +| **Mutual TLS** | Yes | Yes | +| **CORS setup** | Manual OPTIONS method | Built-in config | +| **Automatic deployments** | No | Yes | +| **Canary deployments** | Yes | No | +| **Custom gateway responses** | Yes | No | +| **Execution logs** | Yes | No | +| **Access logs (CloudWatch)** | Yes | Yes | +| **Access logs (Firehose)** | Yes | No | +| **X-Ray tracing** | Yes | No | +| **Mock integrations** | Yes | No | +| **Private integrations (NLB)** | Yes | Yes | +| **Private integrations (ALB)** | Yes | Yes | +| **Private integrations (Cloud Map)** | No | Yes | +| **Response streaming** | Yes | No | +| **Console test invocations** | Yes | No | +| **Integration timeout** | 50ms–29s (configurable) | 30s hard max | +| **Payload size** | 10 MB | 10 MB | + +> **REST API streaming caveats:** Response streaming via REST API proxy integration does not support built-in caching, response transforms (VTL), or WAF inspection of streamed content. Idle timeouts apply, and a 2 MBps bandwidth cap applies after the first 10 MB (Function URLs apply the cap after 6 MB). + +--- + +## CORS Debugging + +### Proxy vs Non-Proxy + +| Aspect | Proxy integration | Non-proxy integration | +|---|---|---| +| Who returns CORS headers? | **Your Lambda function** | **API Gateway** (method response) | +| OPTIONS method needed? | Yes (or use mock) | Yes (mock integration) | +| Where to configure? | In your code | In API Gateway console/IaC | + +### Debugging Flowchart + +``` +"Cross-Origin Request Blocked"? +│ +├─ YES → Which integration type? +│ │ +│ ├─ PROXY → Lambda MUST return CORS headers +│ │ ├─ Access-Control-Allow-Origin +│ │ ├─ Access-Control-Allow-Methods +│ │ └─ Access-Control-Allow-Headers +│ │ +│ └─ NON-PROXY → Configure in API Gateway: +│ ├─ Create OPTIONS method (mock integration) +│ ├─ Add 200 response with CORS headers +│ └─ Add CORS headers to actual method responses +│ +├─ OPTIONS returning 200? +│ ├─ NO → OPTIONS method missing or misconfigured +│ └─ YES → Check actual method response headers +│ +└─ 502 on OPTIONS? + └─ Binary media types set to */* → fix below +``` + +### Common CORS Mistakes + +| # | Mistake | Fix | +|---|---|---| +| 1 | No CORS headers in Lambda (proxy integration) | Add headers to every Lambda response | +| 2 | Missing OPTIONS method (REST API, non-proxy) | Create OPTIONS with mock integration | +| 3 | Binary media types `*/*` breaks OPTIONS | Set `contentHandling: CONVERT_TO_TEXT` on OPTIONS | +| 4 | `Allow-Origin: *` with `credentials: include` | Specify exact origin, not wildcard | +| 5 | Not redeploying API after CORS changes | Redeploy the stage | +| 6 | Missing `Allow-Headers` for custom headers | List all headers the client sends | +| 7 | Gateway 4XX/5XX responses lack CORS headers | Add CORS headers to gateway responses | + +### Lambda CORS Headers — Python + +```python +def handler(event, context): + return { + "statusCode": 200, + "headers": { + "Access-Control-Allow-Origin": "https://example.com", + "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE", + "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token", + }, + "body": json.dumps({"message": "success"}), + } +``` + +### Lambda CORS Headers — TypeScript + +```typescript +export const handler = async (event: any) => ({ + statusCode: 200, + headers: { + "Access-Control-Allow-Origin": "https://example.com", + "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE", + "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token", + }, + body: JSON.stringify({ message: "success" }), +}); +``` + +### Binary Media Types `*/*` Fix + +```bash +# Fix OPTIONS integration request +aws apigateway update-integration \ + --rest-api-id API_ID --resource-id RES_ID \ + --http-method OPTIONS \ + --patch-operations op='replace',path='/contentHandling',value='CONVERT_TO_TEXT' + +# Fix OPTIONS integration response +aws apigateway update-integration-response \ + --rest-api-id API_ID --resource-id RES_ID \ + --http-method OPTIONS --status-code 200 \ + --patch-operations op='replace',path='/contentHandling',value='CONVERT_TO_TEXT' +``` + +--- + +## Lambda Authorizers + +### TOKEN vs REQUEST Authorizer + +| Feature | TOKEN | REQUEST | +|---|---|---| +| Identity source | Single header (bearer token) | Headers, query strings, stage vars, `$context` | +| Cache key | Token header value | All specified identity sources | +| Token validation regex | Yes | No | +| Fine-grained policies | Limited | Yes (multiple sources) | +| Available on | REST API only | REST API + HTTP API | +| **Recommendation** | Legacy | **Preferred** | + +> **Use REQUEST authorizers for new APIs.** TOKEN is legacy. + +### Caching Behavior + +| Setting | Detail | +|---|---| +| Default TTL | 300 seconds | +| Range | 0 (disabled) – 3600 seconds | +| Cache key (TOKEN) | Header value from token source | +| Cache key (REQUEST) | All specified identity sources combined | +| **Critical** | Cached policy applies to **ALL methods/resources** | + +If any specified identity source is missing/null/empty → 401 returned **without** invoking Lambda. + +### REQUEST Authorizer — Python + +```python +def lambda_handler(event, context): + token = event["headers"].get("Authorization", "") + is_authorized = verify_token(token) # Your auth logic + + return { + "principalId": "user", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Effect": "Allow" if is_authorized else "Deny", + "Resource": event["methodArn"], + }], + }, + "context": {"userId": "user", "scope": "read:items"}, + } +``` + +### REQUEST Authorizer — TypeScript + +```typescript +import { APIGatewayAuthorizerResult, APIGatewayRequestAuthorizerEvent } from "aws-lambda"; + +export const handler = async ( + event: APIGatewayRequestAuthorizerEvent +): Promise => { + const token = event.headers?.Authorization ?? ""; + const isAuthorized = verifyToken(token); // Your auth logic + + return { + principalId: "user", + policyDocument: { + Version: "2012-10-17", + Statement: [{ + Action: "execute-api:Invoke", + Effect: isAuthorized ? "Allow" : "Deny", + Resource: event.methodArn, + }], + }, + context: { userId: "user", scope: "read:items" }, + }; +}; +``` + +### HTTP API JWT Authorizer (Native — No Lambda) + +No Lambda function needed. Configure directly on the API: + +```yaml +# SAM / CloudFormation +MyHttpApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + DefaultAuthorizer: MyJwtAuth + Authorizers: + MyJwtAuth: + AuthorizationScopes: + - read:items + IdentitySource: $request.header.Authorization + JwtConfiguration: + issuer: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_abc123 + audience: + - my-client-id +``` + +Supports any OIDC-compliant IdP (Cognito, Auth0, Okta, etc.). + +--- + +## Throttling and Quotas + +### Throttling Hierarchy (Applied in Order) + +``` +Most specific → Least specific: + +1. Per-client / per-method (usage plan + API key) ← REST only +2. Per-method (stage method settings) +3. Account-level (all APIs in account/Region) +4. AWS Regional (hard limit, not changeable) +``` + +### Token Bucket Algorithm + +- Tokens added at steady-state rate (RPS) +- Bucket holds up to burst capacity +- Each request = 1 token +- Empty bucket → `429 Too Many Requests` +- Burst allows temporary spikes above steady-state + +### Account-Level Defaults + +| Quota | Default | Adjustable? | +|---|---|---| +| Steady-state RPS (per Region) | 10,000 | Yes | +| Burst capacity | 5,000 | Set by AWS based on RPS | +| Smaller Regions (Cape Town, Milan, Jakarta…) | 2,500 RPS / 1,250 burst | Yes | + +### REST API Quotas + +| Resource | Default | Adjustable? | +|---|---|---| +| Integration timeout | 50ms–29s (default 29s) | Yes (Regional/private only) | +| Payload size | 10 MB | No | +| Header value size | 10,240 bytes | No | +| Cache TTL | 0–3600s | No | +| Resources per API | 300 | Yes | +| Stages per API | 10 | Yes | +| API keys per account | 10,000 | No | +| Usage plans per account | 300 | Yes | +| Custom domains per Region | 120 | Yes | +| Mapping template size | 300 KB | No | + +### HTTP API Quotas + +| Resource | Default | Adjustable? | +|---|---|---| +| Integration timeout | 30s max | No | +| Payload size | 10 MB | No | +| Routes per API | 300 | Yes | +| Stages per API | 10 | Yes | +| Integrations per API | 300 | No | +| Custom domains per Region | 120 | Yes | +| VPC links per Region | 10 | Yes | + +### Usage Plans (REST API Only) + +- Per-client rate limits (RPS) and burst limits via API keys +- Daily/weekly/monthly quotas per key +- Method-level throttling within a plan (e.g., `GET /pets` = 100 RPS) + +### Client-Side 429 Handling + +- Exponential backoff with jitter +- Respect `Retry-After` header +- Client-side rate limiting to stay under known limits + +--- + +## WebSocket APIs + +### Route Architecture + +``` +Client connects → $connect (auth, store connectionId) +Client sends msg → route selection → custom route or $default +Server pushes data → @connections API (POST to connectionId) +Client disconnects → $disconnect (cleanup connectionId) +``` + +### Route Selection + +- Expression: `$request.body.action` (routes on JSON `action` field) +- Non-JSON messages → always `$default` + +### Predefined Routes + +| Route | When | Required? | Notes | +|---|---|---|---| +| `$connect` | Connection initiated | No | Auth here; connection pending until integration completes | +| `$disconnect` | Connection closed | No | Best-effort; connection already closed | +| `$default` | No matching route / non-JSON | No | Catch-all fallback | + +### Connection Management — Python + +```python +import boto3, json + +dynamodb = boto3.resource("dynamodb") +table = dynamodb.Table("WebSocketConnections") + +def connect_handler(event, context): + table.put_item(Item={"connectionId": event["requestContext"]["connectionId"]}) + return {"statusCode": 200, "body": "Connected"} + +def send_to_client(endpoint_url, connection_id, data): + client = boto3.client("apigatewaymanagementapi", endpoint_url=endpoint_url) + client.post_to_connection( + ConnectionId=connection_id, + Data=json.dumps(data).encode("utf-8"), + ) +``` + +### Connection Management — TypeScript + +```typescript +import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; + +async function sendToClient(endpoint: string, connectionId: string, data: object) { + const client = new ApiGatewayManagementApiClient({ endpoint }); + await client.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: Buffer.from(JSON.stringify(data)), + })); +} +``` + +### WebSocket Quotas + +| Resource | Limit | +|---|---| +| Idle connection timeout | 10 minutes | +| Max connection duration | 2 hours | +| Message payload | 128 KB (hard limit) | + +### WebSocket Close Codes + +| Code | Meaning | +|---|---| +| 1001 | Idle timeout or max duration exceeded | +| 1003 | Unsupported binary media type | +| 1005 | No status code present (reserved, not sent on wire) | +| 1006 | Abnormal closure — no close frame received | +| 1008 | Throttled (too many requests) | +| 1009 | Message exceeds size limit | +| 1011 | Internal server error | +| 1012 | Service restart | + +--- + +## 502/504 Debugging + +### 502 Bad Gateway — Flowchart + +``` +502 Bad Gateway +│ +├─ Lambda proxy integration? +│ └─ YES → Check response format (most common cause): +│ ├─ statusCode: integer (string is coerced, missing defaults to 200) +│ ├─ headers: object with string values +│ ├─ body: string (JSON.stringify, not raw object) +│ └─ Unhandled exception? → Check CloudWatch Logs +│ +├─ Lambda authorizer? +│ ├─ Must return valid IAM policy format +│ ├─ Check authorizer Lambda logs +│ └─ Authorizer timeout is separate from integration timeout +│ +├─ HTTP integration? +│ ├─ Backend reachable from API Gateway? +│ ├─ Valid HTTP response from backend? +│ └─ VPC link healthy? (private integration) +│ +└─ Other causes: + ├─ Payload > 10 MB + ├─ Binary media types */* (breaks OPTIONS) + └─ Stage variable → wrong Lambda alias +``` + +### Correct Lambda Response Format + +The **most common cause of 502** is an incorrect response format in Lambda proxy integrations. + +**Python — Correct:** + +```python +def handler(event, context): + return { + "isBase64Encoded": False, # boolean + "statusCode": 200, # integer, NOT string + "headers": { # object with string values + "Content-Type": "application/json", + }, + "body": json.dumps({"key": "val"}) # MUST be string + } +``` + +**TypeScript — Correct:** + +```typescript +export const handler = async (event: any) => ({ + isBase64Encoded: false, + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key: "val" }), // MUST be string +}); +``` + +**Common mistakes -> 502:** + +```python +return {"statusCode": 200, "body": {"key": "val"}} # body not a string -> 502 +return "just a string" # not a JSON object -> 502 +# Note: string statusCode ("200") and missing statusCode are silently handled (no 502) +``` + +### 504 Timeout — Flowchart + +``` +504 Endpoint Request Timed Out +│ +├─ Step 1: Enable CloudWatch logging +│ ├─ REST: execution logs + access logs +│ ├─ HTTP: access logs only +│ └─ Include: $context.integrationLatency, $context.integration.status +│ +├─ Step 2: Identify timeout source +│ ├─ REST API: integration timeout configurable 50ms–29s +│ ├─ HTTP API: 30s max (can be lowered, cannot be raised) +│ └─ Was integration invoked? +│ ├─ NO → Transient network failure; retry +│ └─ YES → Backend too slow +│ +├─ Step 3: Reduce integration runtime +│ ├─ Move non-critical work to async (SQS, Step Functions) +│ ├─ Increase Lambda memory (faster CPU) +│ ├─ Provisioned concurrency (eliminate cold starts) +│ └─ Check downstream dependencies (DB, external APIs) +│ +└─ Step 4: Increase timeout (REST only) + ├─ Request via Service Quotas console + ├─ Update integration timeout value AND redeploy + └─ Note: may reduce account throttle quota +``` + +### CloudWatch Insights Queries + +**Find all 5xx errors:** + +``` +fields @timestamp, @message, @logStream +| filter status >= 500 and status < 600 +| sort @timestamp desc +| display @timestamp, httpMethod, resourcePath, status, requestId +``` + +**Find timeout errors:** + +``` +fields @timestamp, @message +| filter @message like "Execution failed due to a timeout error" +| sort @timestamp desc +``` + +**Find slow integrations (>10s):** + +``` +fields @timestamp, integrationLatency, status, resourcePath +| filter integrationLatency > 10000 +| sort integrationLatency desc +``` + +### Automated Troubleshooting + +**AWSSupport-TroubleshootAPIGatewayHttpErrors** — Systems Manager runbook: + +- Validates API, resource, operation, and stage +- Analyzes CloudWatch logs automatically +- Requires: `apigateway:GET`, `logs:GetQueryResults`, `logs:StartQuery`, `ssm:*` +- Available in Systems Manager console → Automation diff --git a/.agents/skills/aws-serverless/references/architecture.md b/.agents/skills/aws-serverless/references/architecture.md new file mode 100644 index 0000000..a85e656 --- /dev/null +++ b/.agents/skills/aws-serverless/references/architecture.md @@ -0,0 +1,262 @@ +# Serverless Architecture Patterns + +Reference architectures, pattern selection flowcharts, and service selection tables for common serverless workloads. + +## Contents + +- [Pattern selection flowchart](#pattern-selection-flowchart) +- [REST/HTTP API pattern](#resthttp-api-pattern) +- [Event processing pattern](#event-processing-pattern) +- [Orchestration pattern](#orchestration-pattern) +- [Real-time streaming pattern](#real-time-streaming-pattern) +- [Async fan-out pattern](#async-fan-out-pattern) +- [Scheduled jobs pattern](#scheduled-jobs-pattern) +- [Choosing between patterns](#choosing-between-patterns) + +--- + +## Pattern selection flowchart + +``` +What are you building? +│ +├── Synchronous request/response API? +│ └── REST/HTTP API pattern +│ +├── Processing events from a queue/stream/database? +│ └── Event processing pattern +│ +├── Multi-step workflow with branching/error handling? +│ └── Orchestration pattern +│ +├── Real-time bidirectional communication or LLM streaming? +│ └── Real-time streaming pattern +│ +├── One event triggers multiple independent consumers? +│ └── Async fan-out pattern +│ +└── Recurring task on a schedule? + └── Scheduled jobs pattern +``` + +--- + +## REST/HTTP API pattern + +``` +Client → API Gateway (HTTP API) → Lambda → DynamoDB + → S3 (binary storage) +``` + +**When:** CRUD APIs, mobile/web backends, microservices. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| API type | HTTP API (simpler) | REST API if you need WAF, caching, request validation, API keys | +| Auth | JWT authorizer (HTTP API native) | Cognito (REST: native Cognito authorizer; HTTP: JWT authorizer), Lambda authorizer (custom logic) | +| Database | DynamoDB (on-demand) | RDS Proxy + RDS if relational data needed | +| File storage | S3 with presigned URLs | Direct upload via API Gateway (10 MB limit) | +| Function pattern | One function per route | Lambdalith if team prefers Express/FastAPI style | + +**Key constraints:** + +- HTTP API: 30s hard timeout, no WAF, no caching, 10 MB payload +- REST API: 29s default timeout (adjustable for Regional/private APIs), 10 MB payload + +--- + +## Event processing pattern + +``` +Event source → SQS → Lambda → DynamoDB / S3 + ↓ + DLQ (failed messages) +``` + +**When:** Async workloads, decoupled producers/consumers, batch processing, file processing. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| Buffer | SQS standard queue | SQS FIFO if ordering matters (10 msg batch limit) | +| Trigger | SQS event source mapping | S3 event notification → Lambda (file uploads) | +| Change data capture | DynamoDB Streams → Lambda | EventBridge Pipes → Lambda (no ESM needed) | +| Stream ingestion | SQS (simpler) | Kinesis (ordered replay, multiple consumers, high-throughput) | +| Error handling | SQS redrive policy (DLQ) | On-failure destination (SQS/SNS/S3) for streams | +| Concurrency control | MaximumConcurrency on ESM | Reserved concurrency on function | +| Batch processing | ReportBatchItemFailures | Powertools Batch Processor utility | + +**Key constraints:** + +- SQS visibility timeout ≥ 6× function timeout +- MaximumConcurrency and Provisioned Mode are mutually exclusive on same ESM +- Enable partial batch failure reporting to avoid reprocessing successful messages +- SQS event filtering automatically deletes unmatched messages (permanently — not sent to DLQ) + +**S3 trigger constraints:** + +- Recursive invocation risk: never write output to the same bucket/prefix that triggers the function +- No native DLQ on S3 notifications — use Lambda async invocation DLQ instead +- Use prefix/suffix filtering to limit which objects trigger the function +- Consider EventBridge for S3 instead of S3 notifications (richer filtering, multiple targets) + +**DynamoDB Streams constraints:** + +- Max 2 Lambda consumers per stream shard (use EventBridge Pipes for more) +- 24-hour stream retention — records expire and cannot be replayed after that +- Ordering guaranteed per partition key, not globally + +--- + +## Orchestration pattern + +``` +Trigger → Step Functions → Lambda (validate) + → Choice (route by status) + → Parallel (fan-out) + → Lambda (aggregate) → DynamoDB +``` + +**When:** Multi-step workflows, saga transactions, approval chains, data pipelines, AI agent loops. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| Workflow type | Standard (exactly-once, up to 1 year) | Express (<5 min, high-volume; async=at-least-once, sync=at-most-once) | +| Simple data transforms | JSONata (inline, no Lambda needed) | Lambda task (complex logic) | +| Service calls | Direct SDK integration (200+ services) | Lambda intermediary (only if business logic needed) | +| Human approval | .waitForTaskToken | Lambda durable functions waitForCallback | +| AI agent loops | Step Functions + Bedrock | Lambda durable functions (code-first, checkpointed) | +| Error handling | Retry + Catch in ASL | Lambda durable functions try/catch in code | + +**Key constraints:** + +- 256 KB payload limit between states — use S3 for large data +- Express: no .sync, no .waitForTaskToken, no Distributed Map, no Activities +- 25,000 execution history entries (Standard) — split long workflows into child executions +- Prefer direct SDK integrations over Lambda intermediary functions to reduce latency + +--- + +## Real-time streaming pattern + +``` +Client ←→ API Gateway WebSocket ←→ Lambda → DynamoDB (connections) + → Bedrock (LLM responses) +``` + +Or for LLM token streaming: + +``` +Client → Lambda Function URL (streaming) → Bedrock ConverseStream +``` + +**When:** Chat apps, live dashboards, notifications, LLM token streaming, multiplayer games. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| Bidirectional | API Gateway WebSocket | AppSync subscriptions (GraphQL) | +| LLM streaming | Lambda Function URL + ConverseStream | REST API proxy with STREAM mode | +| Connection state | DynamoDB (connectionId → metadata, enable TTL to clean up stale connections after 2-hour max duration) | ElastiCache (higher throughput) | +| Auth | $connect route authorizer | Cognito + custom auth in Lambda | + +**Key constraints:** + +- WebSocket: 10 min idle timeout, 2 hour max connection, 128 KB message (hard limit) +- Function URL streaming: 200 MB limit, 2 MBps after first 6 MB, Node.js native support +- Function URLs **MUST** use `AWS_IAM` auth type. For CloudFront integration, use Origin Access Control (OAC) to sign requests — do not set auth to `NONE`. If `NONE` is unavoidable for other reasons, authentication **MUST** be enforced at the edge (e.g., CloudFront + Lambda@Edge). No native JWT/Cognito support. + +--- + +## Async fan-out pattern + +``` +Producer → EventBridge → Rule A → Lambda (process) + → Rule B → Step Functions (workflow) + → Rule C → SQS → Lambda (batch) +``` + +**When:** One event triggers multiple independent actions, event-driven microservices, cross-service communication. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| Event router | EventBridge (content-based routing) | SNS (simpler fan-out, attribute/body filtering) | +| Point-to-point | EventBridge Pipes (source→target, no Lambda intermediary) | SQS → Lambda ESM | +| Schema management | EventBridge Schema Registry + Discovery | Manual schema documentation | +| Cross-account | EventBridge cross-account rules | SNS cross-account subscriptions | +| Scheduling | EventBridge Scheduler (cron/rate) | EventBridge rules (simpler but less flexible) | + +**Key constraints:** + +- Use dedicated event bus per application domain (not the default bus) +- EventBridge Pipes eliminates Lambda intermediary functions for source→target integrations +- Be precise with event patterns — overly broad patterns risk loops +- Configure DLQs on all targets + +--- + +## Scheduled jobs pattern + +``` +EventBridge Scheduler → Lambda (task) + → Step Functions (complex workflow) +``` + +**When:** Cron jobs, periodic data sync, report generation, cleanup tasks. + +**Service selection:** + +| Decision | Default | Alternative | +|---|---|---| +| Scheduler | EventBridge Scheduler (flexible, one-time + recurring) | EventBridge rules with schedule expression (simpler) | +| Short task (<15 min) | Lambda directly | — | +| Long task (>15 min) | Step Functions (up to 1 year) | Lambda durable functions | +| High frequency (<1 min) | Not supported natively | SQS delay queue + Lambda | + +**Key constraints:** + +- Minimum schedule interval: 1 minute +- Lambda max timeout: 15 minutes — use Step Functions for longer +- Always make scheduled Lambda idempotent (scheduler guarantees at-least-once) +- Use EventBridge Scheduler over EventBridge rules for new projects (more features, flexible time windows) + +--- + +## Choosing between patterns + +Most real applications combine multiple patterns: + +``` + ┌─ HTTP API ─── Lambda ─── DynamoDB +Client ─── CloudFront ─┤ + └─ WebSocket ── Lambda ─── DynamoDB + │ + ▼ + EventBridge + ┌────┼────┐ + ▼ ▼ ▼ + SQS SFN Lambda + │ │ + ▼ ▼ + Lambda Bedrock +``` + +**Common combinations:** + +| Application | Patterns used | +|---|---| +| SaaS API backend | REST API + Event processing + Scheduled jobs | +| E-commerce | REST API + Orchestration (order saga) + Fan-out (notifications) | +| Data pipeline | Scheduled jobs + Event processing + Orchestration | +| AI chatbot | Real-time streaming + Orchestration (agent loop) | +| IoT processing | Event processing + Fan-out + Scheduled jobs (aggregation) | + +**Begin with a single pattern and add more as requirements grow.** A CRUD API with DynamoDB covers most initial implementations. Add event processing when you need async work. Add orchestration when you need multi-step workflows. Add fan-out when you need cross-service communication. diff --git a/.agents/skills/aws-serverless/references/concurrency.md b/.agents/skills/aws-serverless/references/concurrency.md new file mode 100644 index 0000000..82072e4 --- /dev/null +++ b/.agents/skills/aws-serverless/references/concurrency.md @@ -0,0 +1,200 @@ +# Lambda Concurrency Controls + +Four concurrency controls operate at different levels, solve different problems, and have complex interactions. + +## Contents + +- [The 4 concurrency types](#the-4-concurrency-types) +- [Interaction matrix](#interaction-matrix) +- [Decision scenarios](#decision-scenarios) +- [Account limits and scaling](#account-limits-and-scaling) +- [Common mistakes](#common-mistakes) +- [SnapStart interaction](#snapstart-interaction) +- [SAM/CDK examples](#samcdk-property-reference) + +--- + +## The 4 concurrency types + +### 1. Reserved Concurrency +Sets the **maximum** concurrent instances for a function and **reserves** that capacity from the account pool so no other function can consume it. + +- **Scope:** Function. +- Reserve 400 → function always gets up to 400, never more. Others share the rest. +- Setting to **0** completely throttles the function (emergency shutoff). +- Use for: protecting critical functions, capping to protect downstream, emergency shutoff. + +### 2. Provisioned Concurrency +Pre-initializes execution environments so they are **ready before requests arrive**. + +- **Scope:** Published version or alias (**NOT** `$LATEST`). +- **Allocation rate:** Up to 6,000 environments per minute when provisioning. +- Configure 100 on alias `PROD` → first 100 concurrent requests get sub-10ms startup. + Request 101+ spills to on-demand with cold starts. +- **Account-level RPS quota**: RPS = 10 × account concurrency. For example, 1,000 account concurrency → 10,000 RPS cap across all functions. This is an account-level quota, not a per-instance throughput cap. Per-instance throughput = 1 / function duration. +- Combine with **Application Auto Scaling** (target ~70% utilization). +- Use for: user-facing APIs, functions with heavy init (ML models, DB pools). + +### 3. Maximum Concurrency +Limits how many concurrent instances a **specific SQS event source mapping (ESM)** can invoke. + +- **Scope:** Per ESM. **Range:** 2–1,000. **Sources:** SQS only. +- Does **not** reserve anything — other triggers can still consume function concurrency. +- Use for: multiple SQS queues on one function, rate-limiting a specific queue. + +### 4. Provisioned Mode — ESM (Kafka 2024, SQS 2025) +Allocates **dedicated event pollers** for an SQS or Kafka ESM with configurable min/max. + +- **Scope:** Per ESM. +- Standard mode: ~5 pollers, +300/min, max 1,250 invokes. Provisioned mode: you control + min/max pollers. Each handles up to 1 MB/s, 10 concurrent invokes. +- Use for: high-throughput SQS/Kafka, spiky traffic where standard ramp-up is too slow. + +--- + +## Interaction matrix + +| Combination | OK? | Notes | +|-------------|:---:|-------| +| Reserved + Provisioned | Yes | Provisioned ≤ Reserved | +| Reserved + Max Concurrency (ESM) | Yes | Reserved ≥ Σ(max concurrency across ESMs) | +| Reserved + Provisioned Mode (ESM) | Yes | Independent layers | +| Provisioned + Max Concurrency (ESM) | Yes | Different layers | +| Provisioned + Provisioned Mode (ESM) | Yes | Warms envs vs warms pollers | +| **Max Concurrency + Provisioned Mode (same ESM)** | No | **Mutually exclusive** | +| **Provisioned Concurrency + SnapStart** | No | **Mutually exclusive** | + +**Key rules:** Account limit is the hard ceiling. Reserved carves from the pool — Lambda +always keeps **100 unreserved**. Provisioned ≤ Reserved when both set. Max Concurrency is +advisory to the ESM, not the function. + +``` +┌──────────────────────────────────────────────────────┐ +│ ACCOUNT: 1,000 concurrency │ +│ ┌─────────────────┐ ┌───────────────────────────┐ │ +│ │ RESERVED (400) │ │ UNRESERVED POOL (600) │ │ +│ │ ┌─────────────┐ │ │ Shared by all others │ │ +│ │ │PROVISIONED │ │ │ Must keep ≥100 always │ │ +│ │ │(200 warm) │ │ └───────────────────────────┘ │ +│ │ └─────────────┘ │ │ +│ │ + 200 on-demand │ ESM LAYER (per mapping): │ +│ └─────────────────┘ Max Concurrency — OR — │ +│ Provisioned Mode (not both) │ +└──────────────────────────────────────────────────────┘ +``` + +--- + +## Decision scenarios + +| Scenario | Reserved | Provisioned | Max Conc (ESM) | Prov Mode (ESM) | +|----------|:--------:|:-----------:|:--------------:|:---------------:| +| Protect critical API from starvation | Yes | — | — | — | +| Cap function to protect downstream DB | Yes | — | — | — | +| Eliminate cold starts for user-facing API | Optional | Yes | — | — | +| Multiple SQS queues, prevent hogging | Yes | — | Yes | — | +| High-throughput SQS, low-latency | Optional | Optional | — | Yes | +| Kafka ESM with spiky traffic | — | — | — | Yes | +| Predictable daily traffic | — | Yes+AutoScale | — | — | +| Emergency shutoff | Yes (=0) | — | — | — | +| Java/.NET heavy init | — | Yes or SnapStart | — | — | + +**A — Checkout API:** Reserved=200 + Provisioned=150 + Auto Scaling for peak. +**B — 3 SQS queues → 1 function:** Reserved=300, Max Concurrency=100 per ESM. +**C — Kafka stream (spiky):** Provisioned Mode min=5, max=50 pollers. +**D — Batch job:** Reserved=50, no provisioned. + +--- + +## Account limits and scaling + +| Quota | Default | Adjustable? | +|-------|---------|:-----------:| +| Account concurrency | 1,000 / Region | Yes | +| Reservable concurrency | Account − 100 | Scales | +| RPS limit | 10 × concurrency | Scales | +| Scaling rate | 1,000 envs / 10s / function | No | + +Scaling is per-function, continuously refilled, unused capacity does not accumulate. +~50 seconds to reach 5,000 concurrency from zero. + +**At the limit:** Sync → 429. Async → retries up to 6h then DLQ. Streams → polling +throttled, messages stay in source. + +**RPS constraint:** A 50ms function at 20,000 RPS needs only 1,000 concurrency but the RPS +limit (10×1,000=10,000) throttles it. Request account concurrency = 2,000. + +```bash +aws service-quotas request-service-quota-increase \ + --service-code lambda --quota-code L-B99A9384 --desired-value 5000 +``` + +--- + +## Common mistakes + +1. **Reserved set to 0** — Blocks ALL invocations (429 TooManyRequestsException). Sometimes + set during an incident and not restored. If a function is throttled at low traffic, check + this first. + +2. **Reserved too low** — Reserve 50, need 80 → throttled at 51 even with spare account + capacity. Fix: monitor `ConcurrentExecutions`, set above peak + buffer. + +3. **Starving other functions** — Reserve 800/1,000 → others share 200. Reserved is + subtracted even when unused. Fix: be conservative. + +4. **Provisioned without auto scaling** — Paying for idle envs off-peak, spilling on-peak. + Fix: Auto Scaling targeting ~70% `ProvisionedConcurrencyUtilization`. + +5. **Provisioned on `$LATEST`** — Doesn't work. Fix: publish a version, create an alias. + +6. **Max concurrency > reserved** — ESM tries 100, function caps at 50. Fix: ensure + `reserved ≥ Σ(max concurrency across ESMs)`. + +7. **Confusing ESM max with reserved** — Max concurrency doesn't reserve anything. API + Gateway can still consume all concurrency. Fix: use reserved on the function. + +8. **Both ESM controls on same ESM** — Mutually exclusive; API rejects it. Fix: choose one. + +9. **Forgetting 100-unit buffer** — Max reservable = account limit − 100. + +10. **Not tracking ClaimedAccountConcurrency** — Provisioned counts against account limit + even when idle. Monitor the metric. + +--- + +## SnapStart interaction + +| Aspect | SnapStart | Provisioned Concurrency | +|--------|-----------|------------------------| +| Cold start | Seconds → sub-second | Seconds → ~0 | +| Runtimes | Java 11+, Python 3.12+, .NET 8+ | All | +| Scales with traffic | Yes (snapshot restore) | Only up to provisioned count | + +> **SnapStart and Provisioned Concurrency are mutually exclusive on the same function.** + +``` +Is runtime Java 11+, Python 3.12+, or .NET 8+? +├─ No → Provisioned Concurrency +└─ Yes + ├─ Need guaranteed <50ms on EVERY request? → Provisioned Concurrency + ├─ Need EFS or >512MB ephemeral storage? → Provisioned Concurrency + └─ Otherwise → SnapStart first; if P99 still too high, switch to Provisioned Concurrency (they cannot coexist) +``` + +Limitations: no EFS, no >512MB ephemeral, no container images, must handle uniqueness, +re-validate network connections on restore. + +--- + +## SAM/CDK property reference + +| Concurrency type | SAM property | CDK property | +|---|---|---| +| Reserved | `ReservedConcurrentExecutions: 100` | `reservedConcurrentExecutions: 100` | +| Provisioned | `AutoPublishAlias: live` + `ProvisionedConcurrencyConfig.ProvisionedConcurrentExecutions: 50` | `new lambda.Alias({ provisionedConcurrentExecutions: 50 })` — must use alias, not `$LATEST` | +| Maximum Concurrency (ESM) | `ScalingConfig.MaximumConcurrency: 50` | `maxConcurrency: 50` on `EventSourceMapping` | +| Provisioned Mode (ESM) | `ProvisionedPollerConfig.MinimumPollers` / `MaximumPollers` | `provisionedPollerConfig: { minimumPollers, maximumPollers }` on `EventSourceMapping` | +| SnapStart | `SnapStart.ApplyOn: PublishedVersions` + `AutoPublishAlias` | `snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS` | + +Auto scaling for Provisioned Concurrency: `alias.addAutoScaling({ minCapacity, maxCapacity })` then `scaling.scaleOnUtilization({ utilizationTarget: 0.7 })`. diff --git a/.agents/skills/aws-serverless/references/deployment.md b/.agents/skills/aws-serverless/references/deployment.md new file mode 100644 index 0000000..dd412ef --- /dev/null +++ b/.agents/skills/aws-serverless/references/deployment.md @@ -0,0 +1,94 @@ +# Deployment Reference + +Serverless-specific deployment patterns, resource types, and fast iteration tools. + +## Contents + +- [SAM resource types](#sam-resource-types) +- [SAM Globals section](#sam-globals-section) +- [CDK serverless constructs](#cdk-serverless-constructs) +- [Fast iteration](#fast-iteration) + +--- + +## SAM resource types + +SAM templates extend CloudFormation with `Transform: AWS::Serverless-2016-10-31`. Only `Transform` and `Resources` are required. + +| Resource Type | Purpose | +|---|---| +| `AWS::Serverless::Function` | Lambda + IAM role + event source mappings | +| `AWS::Serverless::HttpApi` | HTTP API (API Gateway v2) — recommended | +| `AWS::Serverless::Api` | REST API (v1) — WAF, usage plans, request validation | +| `AWS::Serverless::SimpleTable` | DynamoDB with minimal config | +| `AWS::Serverless::LayerVersion` | Lambda layer | +| `AWS::Serverless::StateMachine` | Step Functions state machine | +| `AWS::Serverless::Connector` | Simplified permissions between resources | +| `AWS::Serverless::Application` | Nested serverless application (SAR or local) | +| `AWS::Serverless::GraphQLApi` | AppSync GraphQL API | +| `AWS::Serverless::WebSocketApi` | WebSocket API (API Gateway v2) | +| `AWS::Serverless::CapacityProvider` | Lambda Managed Instances on customer-owned EC2 | + +--- + +## SAM Globals section + +Eliminates duplication across functions/APIs. Supported types: `Function`, `Api`, `HttpApi`, `SimpleTable`, `StateMachine`, `CapacityProvider`. + +**Override rules:** + +| Type | Behavior | +|---|---| +| Primitives (string, number, boolean) | Resource value **replaces** global | +| Maps (dictionaries) | **Merged** — resource keys override matching global keys | +| Lists (arrays) | Global entries **prepended** to resource entries | + +--- + +## CDK serverless constructs + +Prefer L2 constructs — they provide sensible defaults and least-privilege IAM via `grant*` methods. + +| Construct | Module | Use for | +|---|---|---| +| `NodejsFunction` | `aws-cdk-lib/aws-lambda-nodejs` | Node.js/TypeScript — bundles with esbuild automatically | +| `PythonFunction` | `@aws-cdk/aws-lambda-python-alpha` | Python — requires Docker for bundling | +| `HttpApi` | `aws-cdk-lib/aws-apigatewayv2` | HTTP API with CORS, JWT auth | +| `HttpLambdaIntegration` | `aws-cdk-lib/aws-apigatewayv2-integrations` | Connect Lambda to HttpApi | + +--- + +## Fast iteration + +Both tools are **development-only** — they bypass CloudFormation safety and introduce drift. Use `sam deploy` or CI/CD for production. + +### SAM Accelerate + +```bash +sam sync --watch --stack-name my-stack # Watch mode — auto-syncs on save +sam sync --code --watch --stack-name my-stack # Code-only (minimal sync time) +sam sync --code --resource-id MyFunction --watch --stack-name my-stack # Single function +``` + +Code changes sync via service APIs in seconds. Infrastructure changes trigger CloudFormation (slower, automatic). + +### CDK hotswap / watch + +```bash +cdk deploy --hotswap # Direct resource update, skips non-hotswappable +cdk deploy --hotswap-fallback # Hotswap with CloudFormation fallback +cdk watch # Watch mode (hotswap + file watching) +``` + +Hotswap supports: Lambda code/config/versions/aliases, Step Functions definitions, ECS images, S3 deployments, CodeBuild projects, AppSync resolvers/functions/schemas. + +### Comparison + +| Feature | SAM Sync | CDK Hotswap | +|---|---|---| +| Watch mode | `sam sync --watch` | `cdk watch` | +| Code-only sync | `sam sync --code` | `cdk deploy --hotswap` | +| Fallback to full deploy | Automatic | `--hotswap-fallback` | +| Selective resource sync | `--resource-id` | Not supported | +| Code change speed | Seconds | Seconds | +| Production safe | **No** | **No** | diff --git a/.agents/skills/aws-serverless/references/event-sources.md b/.agents/skills/aws-serverless/references/event-sources.md new file mode 100644 index 0000000..cb7bf94 --- /dev/null +++ b/.agents/skills/aws-serverless/references/event-sources.md @@ -0,0 +1,484 @@ +# Lambda Event Sources Reference + +Quick reference for Lambda event source mappings (ESMs), direct triggers, filtering, and error handling. + +## Contents + +- [SQS event source mapping](#sqs-event-source-mapping) +- [DynamoDB Streams triggers](#dynamodb-streams-triggers) +- [SNS subscriptions](#sns-subscriptions) +- [Event filtering](#event-filtering) +- [Partial batch failure reporting](#partial-batch-failure-reporting) +- [Error handling strategies](#error-handling-strategies) + +--- + +## SQS event source mapping + +Lambda polls SQS using long polling and invokes your function **synchronously** with a batch of messages. + +### Configuration parameters + +| Parameter | Default | Range / Notes | +|-----------|---------|---------------| +| `BatchSize` | 10 | Standard: max 10,000. FIFO: max 10 | +| `MaximumBatchingWindowInSeconds` | 0 | 0–300. Not supported for FIFO. Requires ≥ 1s when BatchSize > 10 | +| `MaximumConcurrency` | — | 2–1,000. Per-ESM concurrency cap | +| `ProvisionedPollerConfig.MinimumPollers` | 2 | 2–200 | +| `ProvisionedPollerConfig.MaximumPollers` | 200 | 2–2,000 | +| `FilterCriteria` | — | Filters on `body` key only | +| `FunctionResponseTypes` | — | Set to `ReportBatchItemFailures` | + +> **MaximumConcurrency and Provisioned Mode are mutually exclusive.** You cannot set both on the same ESM. + +### Batching behavior + +Lambda invokes when **any** condition is met: + +1. Batching window expires +2. Batch size reached +3. Payload reaches 6 MB + +### Scaling behavior + +**Standard queues:** + +- Starts with **5** concurrent invocations +- Scales up by **300/min** +- Default maximum: **1,250** concurrent invocations +- Provisioned mode: up to **20,000** (scales 3× faster at 1,000/min) + +**FIFO queues:** + +- Concurrency capped by the **lower** of: number of message group IDs or `MaximumConcurrency` +- Messages delivered in order per message group ID + +### Error handling + +- Use the **SQS redrive policy** (native dead-letter queue (DLQ) on the queue) — not an ESM-level DLQ +- Set visibility timeout to **≥ 6× function timeout** to prevent premature retry +- On function error, entire batch becomes visible again after visibility timeout +- On throttle, Lambda backs off; messages reappear after visibility timeout + +### SAM template + +```yaml +MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs22.x + Events: + SQSEvent: + Type: SQS + Properties: + Queue: !GetAtt MyQueue.Arn + BatchSize: 10 + MaximumBatchingWindowInSeconds: 5 + FunctionResponseTypes: + - ReportBatchItemFailures + ScalingConfig: + MaximumConcurrency: 50 + FilterCriteria: + Filters: + - Pattern: '{"body": {"status": ["PENDING"]}}' +``` + +### CDK example + +```typescript +import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; + +const dlq = new sqs.Queue(this, 'DLQ'); +const queue = new sqs.Queue(this, 'MyQueue', { + visibilityTimeout: Duration.seconds(300), // 6× function timeout + deadLetterQueue: { queue: dlq, maxReceiveCount: 3 }, +}); + +fn.addEventSource(new SqsEventSource(queue, { + batchSize: 10, + maxBatchingWindow: Duration.seconds(5), + reportBatchItemFailures: true, + maxConcurrency: 50, +})); +``` + +--- + +## DynamoDB Streams triggers + +Lambda polls DynamoDB stream shards at **4 times per second**. Invokes synchronously with in-order processing at the partition-key level. + +### Configuration parameters + +| Parameter | Default | Range / Notes | +|-----------|---------|---------------| +| `BatchSize` | 100 | Max 10,000 | +| `MaximumBatchingWindowInSeconds` | 0 | 0–300 | +| `StartingPosition` | — | `TRIM_HORIZON` (recommended) or `LATEST` | +| `ParallelizationFactor` | 1 | 1–10. Concurrent batches per shard | +| `BisectBatchOnFunctionError` | false | Split failed batch in half | +| `MaximumRetryAttempts` | -1 (infinite) | 0–10,000 | +| `MaximumRecordAgeInSeconds` | -1 (infinite) | -1 to 604,800 (7 days) | +| `DestinationConfig.OnFailure` | — | SQS, SNS, S3, or Kafka topic | +| `FilterCriteria` | — | Filters on `dynamodb` key and metadata fields (e.g., `eventName`) | +| `FunctionResponseTypes` | — | `ReportBatchItemFailures` | +| `TumblingWindowInSeconds` | — | 0–900 for stateful aggregation | + +### Key behaviors + +- **TRIM_HORIZON** recommended — `LATEST` may miss events during ESM creation +- **Max 2 Lambda readers per shard** (single-region tables). Global tables: limit to 1 +- **ParallelizationFactor**: 100 shards × factor 10 = up to 1,000 concurrent invocations. Order maintained at partition-key level +- **BisectBatchOnFunctionError** does NOT consume retry quota +- DynamoDB stream retention is **24 hours** — a poison record can block a shard for that entire window without retry limits + +### SAM template + +```yaml +MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs22.x + Events: + DDBStream: + Type: DynamoDB + Properties: + Stream: !GetAtt MyTable.StreamArn + StartingPosition: TRIM_HORIZON + BatchSize: 100 + MaximumBatchingWindowInSeconds: 5 + ParallelizationFactor: 5 + BisectBatchOnFunctionError: true + MaximumRetryAttempts: 3 + MaximumRecordAgeInSeconds: 3600 + FunctionResponseTypes: + - ReportBatchItemFailures + DestinationConfig: + OnFailure: + Destination: !GetAtt FailureQueue.Arn + FilterCriteria: + Filters: + - Pattern: '{"eventName": ["INSERT"]}' +``` + +### CDK example + +```typescript +import { DynamoEventSource, SqsDlq } from 'aws-cdk-lib/aws-lambda-event-sources'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; + +const table = new dynamodb.Table(this, 'MyTable', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, +}); + +fn.addEventSource(new DynamoEventSource(table, { + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + batchSize: 100, + maxBatchingWindow: Duration.seconds(5), + parallelizationFactor: 5, + bisectBatchOnError: true, + retryAttempts: 3, + maxRecordAge: Duration.hours(1), + reportBatchItemFailures: true, + onFailure: new SqsDlq(dlq), +})); +``` + +--- + +## SNS subscriptions + +SNS invokes Lambda **asynchronously** — it is a **direct trigger, NOT an event source mapping**. No polling involved; SNS pushes events to Lambda. + +### Key characteristics + +- **Standard topics only** (not FIFO) +- At-least-once delivery — make functions idempotent +- SNS retries at increasing intervals over several hours if Lambda is unreachable +- Cross-account subscriptions supported + +### Filter policies + +Filter policies are managed by **SNS** (not Lambda `FilterCriteria`). Set `FilterPolicyScope` to control what is filtered: + +| Scope | Filters on | +|-------|-----------| +| `MessageAttributes` (default) | SNS message attributes | +| `MessageBody` | JSON body content | + +```json +{ + "event_type": ["order_placed"], + "price_usd": [{"numeric": [">=", 100]}], + "store": [{"anything-but": "test_store"}] +} +``` + +### SAM template + +```yaml +ProcessorFunction: + Type: AWS::Serverless::Function + Properties: + Handler: processor.handler + Runtime: nodejs22.x + Events: + SNSEvent: + Type: SNS + Properties: + Topic: !Ref MyTopic + FilterPolicy: + event_type: + - order_placed + FilterPolicyScope: MessageAttributes +``` + +### CDK example + +```typescript +import * as sns from 'aws-cdk-lib/aws-sns'; +import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; + +topic.addSubscription(new subscriptions.LambdaSubscription(fn, { + filterPolicy: { + event_type: sns.SubscriptionFilter.stringFilter({ + allowlist: ['order_placed'], + }), + price: sns.SubscriptionFilter.numericFilter({ + greaterThanOrEqualTo: 100, + }), + }, +})); +``` + +--- + +## Event filtering + +Lambda `FilterCriteria` applies to event source mappings only (not SNS or other push triggers). + +### Supported sources and filter keys + +| Source | Filter key | Notes | +|--------|-----------|-------| +| SQS | `body` | Unmatched messages **automatically deleted** | +| DynamoDB Streams | `dynamodb` and metadata fields | Does **NOT** support numeric operators | +| Kinesis | `data` | Base64-decoded before filtering | +| MSK / Kafka | `value` | — | +| Amazon MQ | `data` | — | + +### Filter rules + +- Up to **5 filters** per ESM (can request increase to 10) +- Multiple filters are **ORed** — record matches if any filter matches +- Fields within a single filter are **ANDed** + +### Filter rule operators + +| Operator | Syntax | Example | +|----------|--------|---------| +| Equals | `["value"]` | `"City": ["Seattle"]` | +| Equals (ignore case) | `[{"equals-ignore-case": "value"}]` | `"City": [{"equals-ignore-case": "seattle"}]` | +| Null | `[null]` | `"UserID": [null]` | +| Empty | `[""]` | `"Name": [""]` | +| Not | `[{"anything-but": ["value"]}]` | `"Weather": [{"anything-but": ["Raining"]}]` | +| Numeric equals | `[{"numeric": ["=", 100]}]` | `"Price": [{"numeric": ["=", 100]}]` | +| Numeric range | `[{"numeric": [">", 10, "<=", 20]}]` | `"Price": [{"numeric": [">", 10, "<=", 20]}]` | +| Exists | `[{"exists": true}]` | `"Field": [{"exists": true}]` | +| Prefix | `[{"prefix": "us-"}]` | `"Region": [{"prefix": "us-"}]` | +| Suffix | `[{"suffix": ".png"}]` | `"FileName": [{"suffix": ".png"}]` | +| Or (fields) | `"$or": [{...}, {...}]` | `"$or": [{"City": ["NY"]}, {"Day": ["Mon"]}]` | + +> **DynamoDB filtering does NOT support numeric operators.** Numbers are stored as strings in the DynamoDB JSON record. + +### Body/data format matching + +| Incoming format | Filter format | Result | +|----------------|---------------|--------| +| Plain string | Plain string | Filters normally | +| Plain string | Valid JSON | Lambda drops the message | +| Valid JSON | Plain string | Lambda drops the message | +| Valid JSON | Valid JSON | Filters normally | + +### Filter examples + +```yaml +# SQS — filter on body field +FilterCriteria: + Filters: + - Pattern: '{"body": {"RequestCode": ["BBBB"]}}' + +# DynamoDB — INSERT events only +FilterCriteria: + Filters: + - Pattern: '{"eventName": ["INSERT"]}' + +# DynamoDB — filter by NewImage attribute +FilterCriteria: + Filters: + - Pattern: '{"dynamodb": {"NewImage": {"status": {"S": ["ACTIVE"]}}}}' + +# Kinesis — filter decoded data +FilterCriteria: + Filters: + - Pattern: '{"data": {"status": ["ACTIVE"]}}' +``` + +--- + +## Partial batch failure reporting + +Enable by setting `FunctionResponseTypes` to `["ReportBatchItemFailures"]`. + +### SQS — return failed messageId values + +```javascript +export const handler = async (event) => { + const batchItemFailures = []; + for (const record of event.Records) { + try { + await processMessage(record); + } catch (error) { + batchItemFailures.push({ itemIdentifier: record.messageId }); + } + } + return { batchItemFailures }; +}; +``` + +### Streams — return failed SequenceNumber values + +For DynamoDB Streams and Kinesis, Lambda uses the **lowest sequence number** as the checkpoint and retries everything from that point. + +```javascript +export const handler = async (event) => { + for (const record of event.Records) { + try { + await processRecord(record); + } catch (e) { + return { + batchItemFailures: [ + { itemIdentifier: record.dynamodb.SequenceNumber }, + // Kinesis: { itemIdentifier: record.kinesis.sequenceNumber } + ], + }; + } + } + return { batchItemFailures: [] }; +}; +``` + +### Python with Powertools Batch Processor + +```python +from aws_lambda_powertools.utilities.batch import ( + BatchProcessor, EventType, process_partial_response, +) + +processor = BatchProcessor(event_type=EventType.SQS) + +def record_handler(record): + payload = record.body + # process payload... + +def lambda_handler(event, context): + return process_partial_response( + event=event, record_handler=record_handler, + processor=processor, context=context, + ) +``` + +### FIFO queue behavior + +- **Stop processing after the first failure** +- Return all failed and unprocessed messages in `batchItemFailures` +- This preserves message ordering within the group + +### Success/failure conditions + +| Response | Interpretation | +|----------|---------------| +| Empty `batchItemFailures` list | Complete success | +| Null `batchItemFailures` or empty `EventResponse` | Complete success | +| `itemIdentifier` is empty string or null | **Complete failure** (entire batch retried) | +| Bad key name in `itemIdentifier` | **Complete failure** | +| Unhandled exception | **Complete failure** | + +### Interaction with BisectBatchOnFunctionError (streams) + +- Function **errors** (unhandled exception): `BisectBatchOnFunctionError` splits the batch in half for retry. `ReportBatchItemFailures` has no effect since no response was returned. +- Function **succeeds** with `batchItemFailures`: Lambda checkpoints at the lowest failed sequence number and retries from that point. If `BisectBatchOnFunctionError` is also enabled, the batch is bisected at the returned sequence number. + +--- + +## Error handling strategies + +### SQS + +| Strategy | Configuration | When to use | +|----------|--------------|-------------| +| SQS redrive policy (DLQ) | `maxReceiveCount` on the queue | Always — catches poison messages | +| Partial batch failures | `ReportBatchItemFailures` | Batches with mix of good/bad messages | +| Visibility timeout | Set to ≥ 6× function timeout | Always — prevents premature retry | +| MaximumConcurrency | `ScalingConfig` on ESM | Protect downstream resources | + +### DynamoDB Streams / Kinesis + +| Strategy | Configuration | When to use | +|----------|--------------|-------------| +| BisectBatchOnFunctionError | `true` | Isolate bad records in large batches | +| Partial batch failures | `ReportBatchItemFailures` | Avoid reprocessing successful records | +| Maximum retry attempts | `MaximumRetryAttempts` | Limit retries to prevent shard blocking | +| Maximum record age | `MaximumRecordAgeInSeconds` | Skip stale records | +| On-failure destination | `DestinationConfig.OnFailure` | Capture failed records for analysis | +| Parallelization factor | `ParallelizationFactor` | Reduce blast radius per shard | + +### ESM (polling) vs direct trigger (push) + +| Aspect | ESM (SQS, DDB, Kinesis) | Async push (SNS, S3) | Sync push (API Gateway) | +|--------|--------------------------|----------------------|-------------------------| +| Invocation | Synchronous (Lambda polls) | Asynchronous (service pushes) | Synchronous (service pushes) | +| Batching | Yes (configurable) | No (single event) | No (single event) | +| Event filtering | Lambda `FilterCriteria` | SNS filter policies (SNS-managed) | N/A | +| Error handling | Partial batch, bisect, retry config | 2 automatic retries, DLQ/destination | Error returned directly to caller, no automatic retry | +| Ordering | Supported (streams, FIFO) | Not guaranteed | N/A (request/response) | + +### Concurrency formulas + +``` +SQS (default): min(1250, MaximumConcurrency, ReservedConcurrency) +SQS (provisioned): MaximumPollers × 10 +DDB/Kinesis: number_of_shards × ParallelizationFactor +``` + +### Idempotency + +All event sources deliver at least once — duplicates can occur. Use Powertools idempotency utility: + +```python +from aws_lambda_powertools.utilities.batch import ( + BatchProcessor, EventType, process_partial_response, +) +from aws_lambda_powertools.utilities.idempotency import ( + IdempotencyConfig, DynamoDBPersistenceLayer, idempotent_function, +) + +processor = BatchProcessor(event_type=EventType.SQS) +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") +config = IdempotencyConfig(event_key_jmespath="messageId") + +@idempotent_function(config=config, persistence_store=persistence_layer, data_keyword_argument="record") +def record_handler(record): + # process record... + pass + +def lambda_handler(event, context): + return process_partial_response( + event=event, record_handler=record_handler, + processor=processor, context=context, + ) +``` diff --git a/.agents/skills/aws-serverless/references/lambda.md b/.agents/skills/aws-serverless/references/lambda.md new file mode 100644 index 0000000..c389230 --- /dev/null +++ b/.agents/skills/aws-serverless/references/lambda.md @@ -0,0 +1,548 @@ +# AWS Lambda Reference + +Specific values, limits, constraints, and code that complement general Lambda knowledge. + +## Contents + +- [Cold Start Optimization](#cold-start-optimization) +- [Packaging](#packaging) +- [Memory and Timeout Tuning](#memory-and-timeout-tuning) +- [VPC Connectivity](#vpc-connectivity) +- [Execution Roles](#execution-roles) +- [Runtime Lifecycle](#runtime-lifecycle) +- [Powertools for AWS Lambda](#powertools-for-aws-lambda) + +--- + +## Cold Start Optimization + +### SnapStart + +Snapshots the initialized execution environment (Firecracker microVM memory + disk) and restores from cache instead of cold-booting. + +**Supported runtimes:** Java 11+, Python 3.12+, .NET 8+ +**NOT supported:** Node.js, Ruby, container images, OS-only runtimes + +**Constraints:** + +- Mutually exclusive with Provisioned Concurrency +- Mutually exclusive with Amazon EFS +- Ephemeral storage must be ≤ 512 MB +- Only works on published versions (not `$LATEST`) +- Java: no additional SnapStart overhead +- Python/.NET: caching charge (based on memory, minimum 3 hours) + per-restore charge + +**Restoration considerations:** + +- Generate unique IDs/secrets in the handler, not during init (snapshot reuse) +- Re-establish network connections in the handler (connections are stale after restore) +- Refresh cached timestamps/credentials in the handler + +**CDK example (Python):** + +```python +from aws_cdk import aws_lambda as lambda_ + +fn = lambda_.Function(self, "MyFunction", + runtime=lambda_.Runtime.PYTHON_3_13, + handler="index.handler", + code=lambda_.Code.from_asset("lambda"), + snap_start=lambda_.SnapStartConf.ON_PUBLISHED_VERSIONS, +) +version = fn.current_version +``` + +### Provisioned Concurrency + +Pre-initializes execution environments that stay warm permanently. + +- A single instance handles one concurrent request at a time; throughput per instance = 1 / function duration +- Account-level RPS quota: 10 × total concurrency (applies across all invocations, not per instance) +- Supports auto-scaling via Application Auto Scaling +- Lambda can scale beyond provisioned count using on-demand instances +- **Paid even when idle** — disable in dev/staging + +```typescript +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('lambda'), +}); + +const version = fn.currentVersion; +const alias = new lambda.Alias(this, 'ProdAlias', { + aliasName: 'prod', + version, + provisionedConcurrentExecutions: 10, +}); +``` + +### Graviton (arm64) + +- **Up to 34% better price-performance** compared to x86 (per AWS) +- Supported for all Lambda managed runtimes +- Set `architecture: lambda_.Architecture.ARM_64` in CDK + +### Strategy Selection + +| Scenario | Strategy | +|---|---| +| Java/Python/.NET with heavy init | SnapStart | +| Strict <50ms cold start | Provisioned Concurrency | +| Tolerant of occasional cold starts | On-demand + minimize package | +| Predictable traffic | Provisioned Concurrency + auto-scaling | +| General optimization | arm64 (Graviton) | + +--- + +## Packaging + +### Decision Tree + +``` +Need > 250 MB uncompressed? + └─ YES → Container image (up to 10 GB) + └─ NO + ├─ Sharing deps across multiple functions? + │ └─ YES → Lambda layers + └─ NO + ├─ Simple function, few deps → .zip + └─ Native binaries, complex build → Container image +``` + +### Size Limits + +| Package Type | Limit | +|---|---| +| .zip compressed | 50 MB | +| .zip uncompressed (including layers) | 250 MB | +| Container image | 10 GB | +| Layers per function | 5 | + +### Layer Paths by Runtime + +| Runtime | Layer Path | +|---|---| +| Python | `python/` or `python/lib/python3.x/site-packages/` | +| Node.js | `nodejs/node_modules/` | +| Java | `java/lib/` | +| Ruby | `ruby/gems/3.4.0/` or `ruby/lib/` | +| All runtimes | `bin/` (PATH), `lib/` (LD_LIBRARY_PATH) | + +**Layer constraints:** + +- Layers count toward the 250 MB unzipped limit +- Layers only work with .zip deployments, NOT container images +- Not recommended for Go/Rust — bundle deps in the deployment package +- Multiple layers with conflicting dependency versions cause subtle bugs; merge order matters + +### Container Image Dockerfile + +```dockerfile +FROM public.ecr.aws/lambda/python:3.13 + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY app.py ${LAMBDA_TASK_ROOT} + +CMD ["app.handler"] +``` + +- Use official AWS base images from `public.ecr.aws/lambda/` +- Container images do NOT support Lambda layers +- SnapStart is NOT supported with container images + +### Python Build Tips + +Use `uv` for dependency installation — **10-100x faster than pip**: + +```bash +uv pip install -r requirements.txt --target ./package +``` + +Cross-platform build flags (when building on non-Linux): + +```bash +pip install -r requirements.txt \ + --target ./package \ + --platform manylinux2014_x86_64 \ + --only-binary=:all: +``` + +Use `manylinux2014_aarch64` for arm64. Exclude `__pycache__`, `.pyc`, tests, docs. + +--- + +## Memory and Timeout Tuning + +### Memory + +| Parameter | Value | +|---|---| +| Minimum | 128 MB | +| Maximum | 10,240 MB (10 GB) | +| Increment | 1 MB | +| Default | 128 MB | +| 1 vCPU at | 1,769 MB | +| ~5.8 vCPUs at | 10,240 MB | + +CPU scales linearly with memory. Doubling memory doubles CPU. **Over-provisioning memory can improve performance** — faster execution = less total duration. + +**Tuning process:** + +1. Start at 256–512 MB (128 MB only for trivial event routers) +2. Monitor `Max Memory Used` in CloudWatch REPORT lines +3. Use **AWS Lambda Power Tuning** (open-source Step Functions tool): + +```bash +aws stepfunctions start-execution \ + --state-machine-arn arn:aws:states:REGION:ACCOUNT:stateMachine:powerTuningStateMachine \ + --input '{ + "lambdaARN": "arn:aws:lambda:REGION:ACCOUNT:function:my-function", + "powerValues": [128, 256, 512, 1024, 1769, 3008], + "num": 50, + "payload": "{\"test\": true}" + }' +``` + +### Ephemeral Storage (/tmp) + +| Parameter | Value | +|---|---| +| Minimum / Default | 512 MB | +| Maximum | 10,240 MB (10 GB) | +| Extra cost | Above 512 MB | + +- Content **persists across warm invocations** (use as transient cache) +- Content is NOT cleared after invoke failures +- SnapStart requires ≤ 512 MB ephemeral storage + +### Timeout + +| Parameter | Value | +|---|---| +| Minimum | 1 second | +| Maximum | 900 seconds (15 minutes) | +| Default | 3 seconds | + +**Critical integration limits:** + +- API Gateway REST API: **29s default** (adjustable for Regional/private APIs since June 2024; edge-optimized remains 29s max) +- API Gateway HTTP API: **30-second hard limit** +- SQS visibility timeout must be **≥ 6× function timeout** (AWS recommendation) + +### Other Limits + +| Resource | Limit | +|---|---| +| Environment variables (total) | 4 KB | +| Sync invocation payload (request/response) | 6 MB each | +| Async invocation payload | 1 MB | +| Streamed response | 200 MB (first 6 MB uncapped, then 2 MBps) | +| File descriptors | 1,024 | +| Processes/threads | 1,024 | +| Concurrent executions (default) | 1,000 per region (soft limit) | +| Scaling rate | 1,000 new environments every 10s per function | +| Function code storage (.zip) | 75 GB per region (soft limit) | + +--- + +## VPC Connectivity + +### Hyperplane ENI + +Lambda uses **Hyperplane Elastic Network Interfaces** (shared, not per-function): + +- Shared across functions using the same subnet + security group combination +- Each ENI supports **65,000 connections/ports** +- First-time ENI creation: **several minutes** (function stays in `Pending`) +- ENIs reclaimed after **14 days of inactivity** (function goes `Inactive`) +- Removing VPC config takes up to **20 minutes** for ENI cleanup +- Default quota: **500 Hyperplane ENIs per VPC** (Lambda-specific soft limit, can be increased). The broader VPC ENI service quota is **5,000 per region** by default. + +### Internet Access Patterns + +**Lambda in a VPC NEVER gets a public IP**, even in a public subnet. + +**Pattern 1: Private Subnet + NAT Gateway** (most common) + +``` +Lambda → Private Subnet → Route Table → NAT Gateway → IGW → Internet +``` + +- Deploy in each AZ for HA + +**Pattern 2: VPC Endpoints** (for AWS services) + +``` +Lambda → Private Subnet → VPC Endpoint → AWS Service +``` + +- **Gateway endpoints:** S3, DynamoDB +- **Interface endpoints:** STS, Secrets Manager, SQS, etc. +- Traffic stays on AWS network — lower latency + +#### Pattern 3: IPv6 Egress-Only Internet Gateway + +``` +Lambda → Dual-Stack Subnet → Egress-Only IGW → Internet (IPv6) +``` + +- Eliminates NAT Gateway for IPv6 traffic +- Requires dual-stack subnets and IPv6-capable endpoints +- Set `Ipv6AllowedForDualStack=true` in function config + +### Required IAM Permissions + +VPC-attached functions need `AWSLambdaVPCAccessExecutionRole` managed policy or equivalent EC2 network interface permissions. + +### Best Practices + +- Reuse subnet + security group combos across functions to share ENIs +- Use multiple subnets across AZs for HA +- Prefer VPC endpoints over NAT Gateway for AWS service access +- Don't attach to VPC unless accessing private resources (RDS, ElastiCache, etc.) + +--- + +## Execution Roles + +One execution role per function. Key Lambda-specific managed policies: + +| Policy | Grants | +|---|---| +| `AWSLambdaBasicExecutionRole` | CloudWatch Logs only | +| `AWSLambdaVPCAccessExecutionRole` | VPC ENI management | +| `AWSLambdaDynamoDBExecutionRole` | DynamoDB Streams | +| `AWSLambdaSQSQueueExecutionRole` | SQS polling | +| `AWSLambdaKinesisExecutionRole` | Kinesis Streams | + +--- + +## Runtime Lifecycle + +### Phases + +``` +┌─────────┐ ┌─────────┐ ┌──────────┐ +│ INIT │───▶│ INVOKE │───▶│ SHUTDOWN │ +│ │ │(repeat) │ │ │ +└─────────┘ └─────────┘ └──────────┘ +``` + +**Init Phase** (3 sub-phases: extension init → runtime init → function init): + +- On-demand timeout: **10 seconds** +- Provisioned/SnapStart timeout: **up to 15 minutes** +- If init exceeds 10s on-demand, Lambda retries at first invocation using the function's configured timeout + +**Invoke Phase:** + +- Limited by function timeout (max 900s) +- Each environment handles **one concurrent invocation** at a time + +**Shutdown Phase:** + +- 0 ms (no extensions), 500 ms (internal only), 2,000 ms (external extensions) +- SIGKILL if extensions don't respond in time + +**Restore Phase** (SnapStart only): + +- Resumes from cached snapshot +- 10-second timeout for restore + after-restore hooks + +### Execution Environment Reuse (Warm Starts) + +Objects initialized outside the handler persist across invocations: + +- SDK clients, DB connections, cached data all survive +- `/tmp` content persists (512 MB–10 GB) +- Background processes resume on next invocation +- **Workers have a maximum lease lifetime of ~14 hours** (observed behavior, not a documented SLA — do not depend on this value) +- Environments terminated periodically for maintenance even under continuous load + +**Common pitfall:** Global variables persist — stale DB connections, expired credentials, and leaked state across invocations cause subtle production bugs. + +### Extensions + +- **Internal:** Run in the runtime process (APM agents) +- **External:** Separate processes alongside the runtime +- Use Extensions API and Telemetry API for lifecycle events, logs, metrics, traces + +--- + +## Powertools for AWS Lambda + +Official AWS toolkit for Lambda best practices. Available for Python, TypeScript, Java, .NET. + +**Performance note:** Powertools adds cold start overhead. Use selective imports when cold start matters: + +```python +# Instead of: from aws_lambda_powertools import Logger, Tracer, Metrics +# Import only what you need if cold start is critical +from aws_lambda_powertools import Logger +``` + +### Core Utilities + +| Utility | Purpose | +|---|---| +| Logger | Structured JSON logging with correlation IDs | +| Tracer | X-Ray tracing with decorators/middleware | +| Metrics | CloudWatch metrics via Embedded Metric Format (EMF) | +| Idempotency | Make handlers idempotent using DynamoDB | +| Batch Processing | Partial failure handling for SQS, Kinesis, DynamoDB Streams | +| Event Handler | Routing for API Gateway, ALB, Function URLs, AppSync | +| Parameters | Retrieve/cache SSM, Secrets Manager, AppConfig, DynamoDB values | + +### Environment Variables + +| Variable | Purpose | +|---|---| +| `POWERTOOLS_SERVICE_NAME` | Service name for logs, metrics, traces | +| `POWERTOOLS_METRICS_NAMESPACE` | CloudWatch metrics namespace | +| `POWERTOOLS_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | +| `POWERTOOLS_TRACE_DISABLED` | Disable tracing (useful for tests) | +| `POWERTOOLS_DEV` | Dev mode (pretty-print JSON, verbose errors) | + +### Python: Logger + Tracer + Metrics + +```python +from aws_lambda_powertools import Logger, Tracer, Metrics +from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() +tracer = Tracer() +metrics = Metrics() + +@logger.inject_lambda_context(log_event=False) +@tracer.capture_lambda_handler +@metrics.log_metrics(capture_cold_start_metric=True) +def handler(event: dict, context: LambdaContext) -> dict: + logger.info("Processing order", order_id=event.get("order_id")) + metrics.add_metric(name="OrdersProcessed", unit=MetricUnit.Count, value=1) + result = process_order(event) + return {"statusCode": 200, "body": result} + +@tracer.capture_method +def process_order(event: dict) -> str: + return "processed" +``` + +### TypeScript: Logger + Tracer + Metrics + +```typescript +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; +import middy from '@middy/core'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; +import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'; + +const logger = new Logger({ serviceName: 'orderService' }); +const tracer = new Tracer({ serviceName: 'orderService' }); +const metrics = new Metrics({ namespace: 'OrderApp', serviceName: 'orderService' }); + +const lambdaHandler = async (event: any) => { + logger.info('Processing order', { orderId: event.orderId }); + metrics.addMetric('OrdersProcessed', MetricUnit.Count, 1); + const result = await processOrder(event); + return { statusCode: 200, body: JSON.stringify(result) }; +}; + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, { logEvent: false })) + .use(captureLambdaHandler(tracer)) + .use(logMetrics(metrics, { captureColdStartMetric: true })); +``` + +### Python: Idempotency + +```python +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, + idempotent, +) + +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + +@idempotent(persistence_store=persistence_layer) +def handler(event: dict, context) -> dict: + payment = process_payment(event) + return {"payment_id": payment.id, "status": "success"} +``` + +### TypeScript: Idempotency + +```typescript +import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; +import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; + +const persistenceStore = new DynamoDBPersistenceLayer({ + tableName: 'IdempotencyTable', +}); + +const processPayment = async (event: { paymentId: string; amount: number }) => { + return { paymentId: event.paymentId, status: 'success' }; +}; + +export const handler = makeIdempotent(processPayment, { + persistenceStore, +}); +``` + +### Python: Batch Processing (SQS Partial Failures) + +```python +from aws_lambda_powertools.utilities.batch import ( + BatchProcessor, + EventType, + process_partial_response, +) +from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord + +processor = BatchProcessor(event_type=EventType.SQS) + +def record_handler(record: SQSRecord): + payload = record.json_body + process_item(payload) + +def handler(event, context): + return process_partial_response( + event=event, + record_handler=record_handler, + processor=processor, + context=context, + ) +``` + +### TypeScript: Batch Processing (SQS Partial Failures) + +```typescript +import { + BatchProcessor, + EventType, + processPartialResponse, +} from '@aws-lambda-powertools/batch'; +import type { SQSRecord, SQSHandler } from 'aws-lambda'; + +const processor = new BatchProcessor(EventType.SQS); + +const recordHandler = async (record: SQSRecord): Promise => { + const payload = JSON.parse(record.body); + await processItem(payload); +}; + +export const handler: SQSHandler = async (event, context) => { + return processPartialResponse(event, recordHandler, processor, { + context, + }); +}; +``` + +### Asset Reference + +For a ready-to-use Python handler with Powertools wired, read [assets/powertools-handler.py](../assets/powertools-handler.py). diff --git a/.agents/skills/aws-serverless/references/orchestration.md b/.agents/skills/aws-serverless/references/orchestration.md new file mode 100644 index 0000000..c523f43 --- /dev/null +++ b/.agents/skills/aws-serverless/references/orchestration.md @@ -0,0 +1,449 @@ +# Orchestration Reference + +AWS Step Functions and Amazon EventBridge patterns and configuration. + +## Contents + +- [Step Functions Standard vs Express](#step-functions-standard-vs-express) +- [State machine patterns](#state-machine-patterns) +- [Error handling](#error-handling) +- [EventBridge rules and patterns](#eventbridge-rules-and-patterns) +- [EventBridge Pipes](#eventbridge-pipes) + +--- + +## Step Functions Standard vs Express + +### Decision Matrix + +| Dimension | Standard | Express | +|---|---|---| +| Max duration | 1 year | 5 minutes | +| Execution semantics | Exactly-once | At-least-once (async) / At-most-once (sync) | +| Execution history | Stored 90 days (API/console) | CloudWatch Logs only (must enable) | +| `.sync` integration | Supported | **Not supported** | +| `.waitForTaskToken` | Supported | **Not supported** | +| Distributed Map | Supported | **Not supported** | +| Activities | Supported | **Not supported** | +| Idempotency | Automatic (execution name unique for 90 days) | Not managed | + +Express sub-types: + +- **Asynchronous**: Fire-and-forget. Results via CloudWatch Logs. +- **Synchronous**: Blocks until completion. Invokable from API Gateway, Lambda, or `StartSyncExecution`. 5-min max. + +| Use Case | Type | +|---|---| +| Long-running orchestration, `.sync`/callback patterns | Standard | +| Non-idempotent operations (payments, exactly-once) | Standard | +| Distributed Map (large-scale parallel) | Standard | +| High-volume event processing (IoT, streaming) | Express | +| API-backed synchronous microservice orchestration | Synchronous Express | + +--- + +## State Machine Patterns + +### Saga Pattern (Compensating Transactions) + +Each step has a corresponding undo step invoked on failure via `Catch`. Compensations chain in reverse. + +```json +{ + "Comment": "Saga pattern — book travel", + "StartAt": "BookHotel", + "States": { + "BookHotel": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-hotel", + "TimeoutSeconds": 30, + "Catch": [{ + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.BookHotelError", + "Next": "NotifyFailure" + }], + "Next": "BookFlight" + }, + "BookFlight": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-flight", + "TimeoutSeconds": 30, + "Catch": [{ + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.BookFlightError", + "Next": "CancelHotel" + }], + "Next": "BookCar" + }, + "BookCar": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:book-car", + "TimeoutSeconds": 30, + "Catch": [{ + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.BookCarError", + "Next": "CancelFlight" + }], + "Next": "ConfirmBooking" + }, + "CancelFlight": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:cancel-flight", + "Next": "CancelHotel" + }, + "CancelHotel": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:cancel-hotel", + "Next": "NotifyFailure" + }, + "NotifyFailure": { + "Type": "Fail", + "Error": "SagaFailed", + "Cause": "One or more bookings failed; compensations executed" + }, + "ConfirmBooking": { "Type": "Succeed" } + } +} +``` + +### Parallel State + +Executes branches concurrently. **Output is an array** with one element per branch. All branches must succeed or the entire Parallel state fails. Supports `Retry` and `Catch`. + +```json +{ + "Type": "Parallel", + "Branches": [ + { + "StartAt": "ProcessImages", + "States": { + "ProcessImages": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-images", + "End": true + } + } + }, + { + "StartAt": "ProcessMetadata", + "States": { + "ProcessMetadata": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-metadata", + "End": true + } + } + } + ], + "Next": "AggregateResults" +} +``` + +### Map State + +**Inline Map**: Iterates over an array in the same execution. Max **40 concurrent** iterations. + +```json +{ + "Type": "Map", + "ItemsPath": "$.orders", + "MaxConcurrency": 10, + "ItemProcessor": { + "ProcessorConfig": { "Mode": "INLINE" }, + "StartAt": "ProcessOrder", + "States": { + "ProcessOrder": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-order", + "End": true + } + } + }, + "Next": "Done" +} +``` + +**Distributed Map**: Up to **10,000 parallel child executions**. Reads from S3 (JSON, CSV, S3 inventory). Supports `ItemBatcher`, `ItemReader`, `ResultWriter`. **Standard workflows only.** + +### Choice State + +Routes execution based on input conditions. Always include a `Default` branch. + +Comparison operators: `StringEquals`, `StringMatches`, `NumericGreaterThan`, `NumericLessThanEquals`, `BooleanEquals`, `IsPresent`, `IsNull`, `TimestampEquals`, and `Path` variants. + +```json +{ + "Type": "Choice", + "Choices": [ + { "Variable": "$.orderTotal", "NumericGreaterThan": 1000, "Next": "HighValueOrder" }, + { "Variable": "$.isPrime", "BooleanEquals": true, "Next": "PrimeProcessing" } + ], + "Default": "StandardProcessing" +} +``` + +### Agentic AI Loop Pattern (Tool Use) + +Model outputs a structured response indicating a tool call or final answer. Choice state routes accordingly. Tool results feed back in a loop. + +```json +{ + "Comment": "Agentic AI loop with tool use", + "QueryLanguage": "JSONata", + "StartAt": "InvokeModel", + "States": { + "InvokeModel": { + "Type": "Task", + "Resource": "arn:aws:states:::bedrock:invokeModel", + "Arguments": { + "ModelId": "global.anthropic.claude-sonnet-4-6", + "Body": { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 4096, + "messages": "{% $states.input.messages %}" + }, + "ContentType": "application/json", + "Accept": "application/json" + }, + "Next": "CheckAction" + }, + "CheckAction": { + "Type": "Choice", + "Choices": [ + { "Condition": "{% $states.input.Body.stop_reason = 'tool_use' %}", "Next": "ExecuteTool" } + ], + "Default": "ReturnResult" + }, + "ExecuteTool": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:execute-tool", + "TimeoutSeconds": 60, + "Next": "InvokeModel" + }, + "ReturnResult": { "Type": "Succeed" } + } +} +``` + +--- + +## Error Handling + +### Built-in Error Names + +| Error Name | Description | Retriable? | +|---|---|---| +| `States.ALL` | Wildcard — matches any error | Yes | +| `States.TaskFailed` | Wildcard for task errors (except `States.Timeout`) | Yes | +| `States.Timeout` | Task exceeded `TimeoutSeconds` or `HeartbeatSeconds` | Yes | +| `States.HeartbeatTimeout` | No heartbeat within `HeartbeatSeconds` | Yes | +| `States.Permissions` | Insufficient IAM privileges | Yes | +| `States.DataLimitExceeded` | Payload exceeds 256 KiB — **terminal** | **No** | +| `States.Runtime` | Invalid JSONPath, null payload — **terminal** | **No** | +| `States.ItemReaderFailed` | Map couldn't read from ItemReader source | Yes | +| `States.ResultWriterFailed` | Map couldn't write to ResultWriter destination | Yes | + +`States.ALL` does **not** match `States.DataLimitExceeded` or `States.Runtime`. + +### Retry Configuration + +Available on `Task`, `Parallel`, and `Map` states. Retries are attempted before catchers. + +```json +"Retry": [ + { + "ErrorEquals": ["States.Timeout"], + "IntervalSeconds": 3, + "MaxAttempts": 2, + "BackoffRate": 2.0, + "MaxDelaySeconds": 30, + "JitterStrategy": "FULL" + }, + { + "ErrorEquals": ["Lambda.ServiceException", "Lambda.SdkClientException"], + "IntervalSeconds": 1, + "MaxAttempts": 3, + "BackoffRate": 2.0 + }, + { + "ErrorEquals": ["States.ALL"], + "IntervalSeconds": 1, + "MaxAttempts": 3, + "BackoffRate": 2.0 + } +] +``` + +| Field | Default | Description | +|---|---|---| +| `ErrorEquals` | (required) | Array of error names to match | +| `IntervalSeconds` | 1 | Initial wait before first retry | +| `MaxAttempts` | 3 | Max retries; 0 = never retry | +| `BackoffRate` | 2.0 | Multiplier for exponential backoff | +| `MaxDelaySeconds` | — | Cap on computed backoff interval | +| `JitterStrategy` | `"NONE"` | `"FULL"` randomizes wait between 0 and computed interval | + +Rules: + +- `States.ALL` must be **last** in the Retry array +- Retries count as state transitions (billed in Standard workflows) +- `States.Runtime` and `States.DataLimitExceeded` **cannot be retried** +- Use `JitterStrategy: "FULL"` to prevent thundering herd + +### Catch (Fallback States) + +```json +"Catch": [ + { + "ErrorEquals": ["CustomBusinessError"], + "ResultPath": "$.error-info", + "Next": "HandleBusinessError" + }, + { + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.error-info", + "Next": "GenericErrorHandler" + } +] +``` + +- `ResultPath` preserves original input alongside the error (e.g., `"$.error-info"`) +- Without `ResultPath`, error output replaces entire input +- Retries are attempted first; catchers apply only after retries are exhausted + +### Error handling best practices + +1. **Always set `TimeoutSeconds`** on every Task state +2. **Always retry Lambda service exceptions**: `Lambda.ServiceException`, `Lambda.SdkClientException` +3. **Use `HeartbeatSeconds`** for long-running tasks +4. **Combine Retry + Catch**: Retry transient, Catch permanent +5. **Use `JitterStrategy: "FULL"`** to prevent thundering herd +6. **Listen for execution failures via EventBridge** for top-level failures + +--- + +## EventBridge Rules and Patterns + +### Event Pattern Structure + +All specified fields must match (AND). Values within an array are OR'd. + +```json +{ + "source": ["aws.ec2"], + "detail-type": ["EC2 Instance State-change Notification"], + "detail": { "state": ["terminated", "stopped"] } +} +``` + +### Advanced Pattern Operators + +| Operator | Syntax | Description | +|---|---|---| +| Exact match | `["value"]` | Field equals value | +| Prefix | `[{"prefix": "prod-"}]` | Starts with string | +| Suffix | `[{"suffix": ".json"}]` | Ends with string | +| Anything-but | `[{"anything-but": ["val"]}]` | Not in list | +| Numeric range | `[{"numeric": [">", 0, "<=", 100]}]` | Numeric comparison | +| Exists | `[{"exists": true}]` | Field must be present | +| Wildcard | `[{"wildcard": "prod-*-east"}]` | Glob-style matching | + +### EventBridge best practices + +1. **Dedicated event bus per application domain** — default bus for AWS service events only +2. **Be precise with patterns** — broad patterns increase risk of infinite loops +3. **One target per rule** — simplifies debugging and IAM permissions +4. **Use DLQs on targets** — capture failed event deliveries +5. **Use the EventBridge Sandbox** to test patterns before deploying + +### Step Functions Status Change Events + +Step Functions emits to the default bus automatically: + +```json +{ + "source": ["aws.states"], + "detail-type": ["Step Functions Execution Status Change"], + "detail": { "status": ["FAILED", "TIMED_OUT", "ABORTED"] } +} +``` + +### Integration Patterns + +**SFN → EventBridge** (publish events from a workflow): + +```json +{ + "Type": "Task", + "QueryLanguage": "JSONata", + "Resource": "arn:aws:states:::events:putEvents", + "Arguments": { + "Entries": [{ + "Detail": { "orderId": "{% $states.input.orderId %}", "status": "PROCESSED" }, + "DetailType": "OrderProcessed", + "EventBusName": "my-app-bus", + "Source": "my-app.orders" + }] + }, + "Next": "Done" +} +``` + +**EventBridge → SFN**: Rule target is the state machine ARN. Event payload becomes execution input. + +**Fan-out**: Single event triggers multiple workflows via multiple rules on the same bus. + +--- + +## EventBridge Pipes + +### Architecture + +``` +Source → [Filter] → [Enrichment] → [Transform] → Target +``` + +Eliminates intermediary Lambda functions for point-to-point integrations. + +### Supported Sources + +| Source | Notes | +|---|---| +| Amazon SQS | Standard and FIFO queues | +| Amazon Kinesis Data Streams | Shard-level polling | +| Amazon DynamoDB Streams | Change data capture | +| Amazon MSK / Self-managed Kafka | Topic-level consumption | +| Amazon MQ | ActiveMQ and RabbitMQ | + +### Enrichment Options + +Lambda, API Gateway, EventBridge API Destinations, Step Functions (Synchronous Express). + +### Key Features + +- **Filtering**: Event patterns filter at the source — pay only for matched events +- **Ordering**: Maintains event ordering within batches +- **Built-in retry + DLQ**: Source-level retry with dead-letter queue support + +### Pipes vs Rules + +| Dimension | Pipes | Rules | +|---|---|---| +| Topology | Point-to-point (1→1) | Fan-out (1→N) | +| Sources | SQS, Kinesis, DDB Streams, MSK, MQ | Any event on a bus | +| Enrichment | Built-in | Not built-in | +| Use case | Replace Lambda glue | Event routing and distribution | + +--- + +## Lambda durable functions vs Step Functions + +Lambda durable functions let you write reliable multi-step workflows as plain code (TypeScript, Python, Java) with automatic checkpointing — the SDK persists each step's result and replays from the checkpoint on interruption, enabling executions up to 1 year with zero compute during waits. Use the **aws-lambda-durable-functions** skill for full guidance. + +| Question | Lambda durable functions | Step Functions | +|---|---|---| +| Primary focus? | Application logic in Lambda | Orchestration across AWS services | +| Programming model? | Standard code (TS/Python/Java) | Amazon States Language (ASL) or visual designer | +| AWS service integrations? | Primarily Lambda | 200+ native integrations | +| Who reads the workflow? | Developers | Non-technical stakeholders | +| Best for? | Distributed transactions, stateful logic, AI agent loops | Business process automation, multi-service orchestration | diff --git a/.agents/skills/aws-serverless/references/production.md b/.agents/skills/aws-serverless/references/production.md new file mode 100644 index 0000000..7dae057 --- /dev/null +++ b/.agents/skills/aws-serverless/references/production.md @@ -0,0 +1,493 @@ +# Production-Ready Serverless on AWS + +Quick-reference for shipping Lambda workloads to production. Covers the pre-deployment checklist, architecture trade-offs, and operational patterns for production traffic. + +## Contents + +- [Production readiness checklist](#production-readiness-checklist) +- [Architecture decisions](#architecture-decisions) +- [Observability](#observability) +- [Security hardening](#security-hardening) +- [Testing strategies](#testing-strategies) +- [Idempotency patterns](#idempotency-patterns) +- [Response streaming](#response-streaming) +- [Anti-patterns](#anti-patterns) + +--- + +## Production readiness checklist + +Walk through every item before the first production deployment. + +### Compute + +- [ ] Memory right-sized (use AWS Lambda Power Tuning or load testing) +- [ ] Timeout set explicitly (P99 + buffer, never the 3 s default) +- [ ] Reserved concurrency configured to protect downstream systems +- [ ] Dead-letter queue (DLQ) or on-failure destination for every async invocation +- [ ] Environment variables for all config (bucket names, table names, endpoints) +- [ ] Code signing enabled (if compliance requires it) +- [ ] SDK clients initialized outside handler (reuse across warm invocations) +- [ ] Deployment package size minimized (exclude tests, docs, unused dependencies) + +### Observability + +- [ ] Structured JSON logging via Powertools Logger +- [ ] X-Ray active tracing enabled +- [ ] Custom metrics emitted via Embedded Metric Format (EMF) +- [ ] CloudWatch Alarms on Errors, Throttles, Duration P99, IteratorAge, ConcurrentExecutions, DLQ depth +- [ ] Log retention policy set — do not leave at unlimited +- [ ] Correlation IDs propagated to downstream services +- [ ] Lambda Insights enabled for system-level metrics (CPU, memory, network) + +### Security + +- [ ] One IAM execution role per function, scoped to exact resource ARNs +- [ ] No secrets in environment variables — use Secrets Manager / SSM with caching +- [ ] Input validation on every event payload (JSON Schema, Zod, Pydantic) +- [ ] VPC placement only when required (RDS, ElastiCache); VPC endpoints for AWS services +- [ ] GuardDuty Lambda Protection enabled +- [ ] Security Hub Lambda controls enabled +- [ ] Dependency scanning in CI (`npm audit`, `pip-audit`, Snyk) +- [ ] Amazon Inspector Lambda scanning enabled +- [ ] Function URLs use `AWS_IAM` auth (not `NONE`) in production + +### Reliability + +- [ ] Every handler is idempotent +- [ ] Partial batch failure reporting enabled (SQS, Kinesis, DynamoDB Streams) +- [ ] `BisectBatchOnFunctionError` enabled for stream sources (isolates poison records) +- [ ] Retry config tuned — `MaximumRetryAttempts`, `MaximumEventAgeInSeconds` +- [ ] Circuit breakers on downstream HTTP calls +- [ ] Reserved concurrency = 0 documented as emergency kill switch +- [ ] Graceful error handling — catch, log, and return meaningful errors (no unhandled exceptions) + +### Deployment + +- [ ] Aliases + weighted traffic shifting (or CodeDeploy canary/linear) +- [ ] Rollback alarms wired into the deployment pipeline +- [ ] All infrastructure defined in code (CDK, SAM, or CloudFormation) +- [ ] Separate AWS accounts for dev, staging, production +- [ ] Automated smoke tests run post-deployment before full traffic shift +- [ ] Pre-traffic hooks (BeforeAllowTraffic) validate function health before shifting + +--- + +## Architecture decisions + +### Monolith Lambda vs micro-Lambda + +| Aspect | Lambdalith (single function) | Micro-Lambda (function per route) | +|---|---|---| +| Cold starts | One function to warm; larger package | Many functions; smaller, faster init | +| IAM granularity | Single broad role | Per-function least-privilege | +| Deployment | Everything together; simpler CI/CD | Independent; more pipeline complexity | +| Observability | One log group; harder per-route metrics | Per-function metrics, alarms, logs | +| Scaling | Single concurrency pool | Independent scaling + reserved concurrency per function | +| DX | Familiar Express/FastAPI style | More AWS-native; requires IaC discipline | + +**Guidance**: Prefer micro-Lambda for greenfield (least privilege, independent scaling, granular observability). Use Lambdalith when migrating existing Express/FastAPI apps or when team size makes deployment simplicity more valuable than granularity. + +### Function URLs vs API Gateway + +| Feature | Function URLs | API Gateway (HTTP API) | API Gateway (REST API) | +|---|---|---|---| +| Auth | IAM only (or in-code) | IAM, JWT, Lambda authorizers | IAM, Cognito, Lambda authorizers, API keys | +| Rate limiting | None built-in | Built-in throttling | Throttling + usage plans | +| Response streaming | Yes (native) | No | Yes (proxy integration) | +| Custom domains | Via CloudFront | Built-in | Built-in | +| WAF | No (use CloudFront) | No (use CloudFront) | Yes | +| Request validation | None | None | JSON Schema | +| Caching | Via CloudFront | None | Built-in | +| WebSocket | No | No | No (separate WebSocket API required) | + +**Use Function URLs** for: internal service-to-service (IAM auth), Lambdalith + CloudFront, streaming, webhook receivers. + +**Use API Gateway** for: public APIs needing rate limiting, JWT/Cognito auth, multi-function path routing, WAF without CloudFront. + +### Reserved vs Provisioned Concurrency + +| Aspect | Reserved Concurrency | Provisioned Concurrency | +|---|---|---| +| Purpose | Guarantee capacity + protect downstream | Eliminate cold starts | +| Cold starts | Still possible | Eliminated (pre-warmed) | +| Throttling | Throttles at the limit | Spills to on-demand beyond provisioned | +| Use case | Protect a database; guarantee capacity | Latency-sensitive APIs; payment processing | + +Decision flow: + +1. **Need to limit scaling** → Reserved concurrency +2. **Need to eliminate cold starts** → Provisioned concurrency (try SnapStart first — no additional cost for Java; caching + restore charges for Python/.NET) +3. **Need both** → Set provisioned ≤ reserved; reserved acts as the ceiling + +--- + +## Observability + +### Powertools setup (Python / TypeScript / Java / .NET) + +**Logger** — structured JSON, correlation IDs injected automatically, log level via env var. + +**Tracer** — wraps X-Ray SDK; auto-captures AWS SDK calls, HTTP requests, handler. Add custom subsegments for critical paths. Annotate traces with business keys (customer ID, order ID) for filtering. + +**Metrics** — emits via Embedded Metric Format. Zero latency impact. + +### EMF vs PutMetricData + +| | EMF (Powertools Metrics) | `PutMetricData` API | +|---|---|---| +| Latency impact | Zero — writes to stdout | Synchronous API call (~5–20 ms) | +| Complexity | One-liner with Powertools | Manual batching, error handling | +| Recommendation | **Use this** | Avoid in hot paths | + +### Minimum alarm set + +Set these six alarms on every production function: + +| Alarm | Metric | Threshold | Period | Why | +|---|---|---|---|---| +| Error rate | `Errors / Invocations` | > 1 % | 5 min | Catch bugs and upstream failures | +| Throttles | `Throttles` | > 0 | 5 min | Concurrency limit hit | +| Duration P99 | `Duration` P99 | > 80 % of timeout | 5 min | Catch slow functions before timeout | +| Iterator age | `IteratorAge` | > 60 s | 5 min | Stream processing falling behind | +| Concurrent executions | `ConcurrentExecutions` | > 80 % of reserved | 5 min | Approaching throttle threshold | +| DLQ depth | SQS `ApproximateNumberOfMessagesVisible` | > 0 | 5 min | Failed messages accumulating | + +### Log retention + +Set retention when creating log groups. Defaults to "never expire" — storage accumulates continuously. Choose a retention period based on your compliance and debugging needs. + +--- + +## Security hardening + +### One role per function + +Never share IAM roles across functions. Scope every policy to specific resource ARNs: + +```yaml +# Good +Effect: Allow +Action: dynamodb:PutItem +Resource: arn:aws:dynamodb:us-east-1:123456789012:table/OrdersTable + +# Bad +Effect: Allow +Action: dynamodb:* +Resource: "*" +``` + +Use IAM Access Analyzer to identify unused permissions and generate least-privilege policies. + +### Secrets management + +- Store in **Secrets Manager** or **SSM Parameter Store** (SecureString) +- Cache in the execution environment with **Powertools Parameters** (avoids API call per invocation) +- Rotate automatically via Secrets Manager rotation Lambdas +- Environment variables are visible in the Lambda console and API — never put secrets there + +### Input validation + +Validate at the handler boundary before business logic runs: + +| Language | Library | +|---|---| +| TypeScript | Zod, io-ts, JSON Schema | +| Python | Pydantic, Powertools Validation (JSON Schema) | +| Java | Bean Validation (JSR 380), JSON Schema | + +Powertools Validation supports envelope extraction for API Gateway, SQS, EventBridge, etc. + +### VPC: endpoints over NAT Gateway + +If your function must be in a VPC, use **VPC endpoints** for AWS service access instead of NAT Gateway: + +| | VPC Endpoint | NAT Gateway | +|---|---|---| +| Latency | Lower (stays on AWS backbone) | Higher (extra hop) | + +Create endpoints for: DynamoDB (gateway), S3 (gateway), SQS, Secrets Manager, SSM, KMS. + +--- + +## Testing strategies + +### The serverless testing pyramid (inverted) + +``` + ┌─────────────┐ + │ E2E Tests │ Few — full workflow verification + ├─────────────┤ + │ Integration │ Many — THIS IS THE MOST VALUABLE LAYER + │ (in cloud) │ Test real service interactions + ├─────────────┤ + │ Unit Tests │ Fast — pure business logic only + └─────────────┘ +``` + +Serverless apps are primarily about service integrations, not complex business logic. Integration tests in the cloud detect the most impactful defects. + +### Structure code for testability + +``` +handler (thin adapter) + → extract + validate event + → call business logic (pure functions — unit test these) + → call AWS services (integration test these in the cloud) +``` + +### What to test where + +| Layer | What | How | +|---|---|---| +| Unit | Business logic (calculations, transforms, validation) | Local, fast, mocked dependencies | +| Integration | Service contracts (DynamoDB reads/writes, SQS send/receive, IAM permissions) | Deploy to AWS, test against real services | +| E2E | Full workflows (API → Lambda → DynamoDB → Stream → Lambda → SQS) | Dedicated staging environment; poll for async side effects | + +### Fast iteration + +- **`sam sync`** — hot-deploys code changes to AWS in seconds +- **`cdk watch`** — watches for file changes and auto-deploys +- Each developer gets an isolated test stack (separate account or prefixed stack name) + +### What NOT to do + +- Don't rely on LocalStack / DynamoDB Local as primary testing — they diverge from real AWS (IAM, quotas, error codes) +- Don't mock AWS SDK calls for integration tests — you'll miss permission and config issues +- Don't skip cloud testing because "it's slow" — use `sam sync` / `cdk watch` + +--- + +## Idempotency patterns + +Lambda guarantees **at-least-once** execution. Duplicates happen from: async retries, SQS visibility timeout expiry, stream shard replays, client retries on timeout, Step Functions task retries. + +### Powertools Idempotency utility + +Uses DynamoDB to track processed events. Available for Python, TypeScript, Java, .NET. + +**Python:** + +```python +from aws_lambda_powertools.utilities.idempotency import ( + DynamoDBPersistenceLayer, idempotent +) + +persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + +@idempotent(persistence_store=persistence) +def handler(event, context): + payment = process_payment(event) + return {"statusCode": 200, "body": payment} +``` + +**TypeScript:** + +```typescript +import { makeIdempotent } from "@aws-lambda-powertools/idempotency"; +import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb"; + +const persistence = new DynamoDBPersistenceLayer({ tableName: "IdempotencyTable" }); + +export const handler = makeIdempotent(async (event) => { + const payment = await processPayment(event); + return { statusCode: 200, body: JSON.stringify(payment) }; +}, { persistenceStore: persistence }); +``` + +### DynamoDB table design + +``` +Table: IdempotencyTable + PK: id (String) — hash of the idempotency key + Attributes: + status: INPROGRESS | COMPLETED | EXPIRED + data: cached response payload + expiration: TTL epoch timestamp + TTL attribute: expiration +``` + +### Choosing the idempotency key + +| Event source | Key | +|---|---| +| SQS | `messageId` | +| EventBridge | `detail.id` or composite of event fields | +| DynamoDB Streams | `eventID` | +| API Gateway / Function URL | `Idempotency-Key` header or request body hash | +| Step Functions | Execution ID + task token | + +### TTL for cleanup + +Set TTL based on how long duplicates can arrive. Typical values: + +- API retries: 1 hour +- SQS retries: match the queue's `maxReceiveCount` × visibility timeout +- Stream replays: 24 hours (Kinesis retention default) + +DynamoDB automatically deletes expired items (typically within a few days of TTL expiry). + +--- + +## Response streaming + +### When to use + +| Use case | Why streaming helps | +|---|---| +| Large payloads (> 6 MB) | Buffered limit is 6 MB; streaming supports up to 200 MB | +| TTFB-sensitive responses | Client sees partial data immediately (HTML shell, then content) | +| Server-sent events (SSE) | Real-time updates to browser clients | +| LLM / AI token streaming | Stream tokens as generated (conversational AI-style) | +| Large file generation | CSV/PDF rows streamed as produced | + +### Constraints + +- **Function URLs** are simplest for streaming. REST API also supports streaming via proxy integration with STREAM transfer mode. HTTP API does **not** support streaming. +- **200 MB** response limit +- **2 MBps** bandwidth cap after the first 6 MB +- Billed for full function duration even if client disconnects +- Node.js has native support; other runtimes use custom runtime or Lambda Web Adapter +- **Function URL streaming is NOT supported for VPC-attached functions.** Use the `InvokeWithResponseStream` API as an alternative. + +### Node.js example + +```javascript +export const handler = awslambda.streamifyResponse( + async (event, responseStream, context) => { + const metadata = { + statusCode: 200, + headers: { "Content-Type": "text/html" }, + }; + responseStream = awslambda.HttpResponseStream.from(responseStream, metadata); + + responseStream.write(""); + for (const chunk of generateContent()) { + responseStream.write(chunk); + } + responseStream.write(""); + responseStream.end(); + } +); +``` + +### When NOT to use + +- Small JSON responses (< 6 MB) — buffered is simpler +- When you need API Gateway features (rate limiting, caching, WAF) without CloudFront +- VPC-based functions needing Function URL streaming (use `InvokeWithResponseStream` API instead) + +--- + +## Sources + +- [AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html) +- [Lambda Concurrency and Scaling](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html) +- [Response Streaming](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html) +- [How to Test Serverless Functions](https://docs.aws.amazon.com/lambda/latest/dg/testing-guide.html) +- [Serverless Applications Lens — Well-Architected](https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/welcome.html) +- [Powertools for AWS Lambda](https://docs.powertools.aws.dev/lambda/) + +--- + +## Anti-patterns + +Common mistakes that cause production issues in serverless applications. Each pairs the problem with the correct alternative. + +### Avoid: Lambda calling Lambda synchronously + +Synchronous Lambda-to-Lambda invocation doubles latency, creates tight coupling, and makes error handling fragile. + +```python +# BAD: Direct synchronous invocation +lambda_client.invoke(FunctionName='downstream', InvocationType='RequestResponse', Payload=json.dumps(event)) +``` + +### Instead: Use Step Functions or SQS + +```python +# GOOD: Decouple via SQS +sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps(event)) +``` + +Or use Step Functions for orchestration when you need the result. + +--- + +### Avoid: Monolithic handler without intentional design + +Routing logic stuffed into a single handler without considering trade-offs prevents independent scaling, broadens IAM blast radius, and increases cold start times. + +```python +# BAD: One function handling all routes without considering trade-offs +def handler(event, context): + path = event['path'] + if path == '/users': return handle_users(event) + elif path == '/orders': return handle_orders(event) + elif path == '/products': return handle_products(event) +``` + +### Instead: Choose deliberately + +For greenfield projects, prefer one function per route (least privilege, independent scaling, granular observability). For migrations from Express/FastAPI or small teams prioritizing deployment simplicity, a Lambdalith is a valid choice — see [Architecture decisions](#architecture-decisions) for trade-offs. + +--- + +### Avoid: Secrets in environment variables + +Visible in console and API, 4 KB total limit for all environment variables combined. + +```python +# BAD: Secret in env var +db_password = os.environ['DB_PASSWORD'] +``` + +### Instead: Use Secrets Manager with Powertools caching + +```python +# GOOD: Cached secret retrieval +from aws_lambda_powertools.utilities import parameters +db_password = parameters.get_secret("my-db-secret", max_age=300) +``` + +--- + +### Avoid: Skipping idempotency + +Lambda delivers at-least-once; duplicates cause duplicate records. + +### Instead: Use Powertools Idempotency + +```python +from aws_lambda_powertools.utilities.idempotency import idempotent, DynamoDBPersistenceLayer + +persistence = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + +@idempotent(persistence_store=persistence) +def handler(event, context): + return process_payment(event) +``` + +--- + +### Avoid: VPC when not needed + +Adds cold start latency. Only attach Lambda to a VPC for private resources (RDS, ElastiCache, Elasticsearch). Use VPC endpoints for AWS service access instead. + +--- + +### Avoid: Default 3s timeout + +Legitimate requests fail silently. Set timeout based on load-test P99 + buffer. Set SDK/HTTP client timeouts shorter than Lambda timeout to get meaningful errors instead of generic timeouts. + +--- + +### Avoid: Missing DLQ + +Failed async invocations and event source messages are discarded without notification. Configure dead-letter queues on all async invocations and event source mappings. + +--- + +### Avoid: CloudWatch Logs retention = forever + +Storage accumulates continuously. Set a retention period — do not leave at unlimited. diff --git a/.agents/skills/aws-serverless/references/troubleshooting.md b/.agents/skills/aws-serverless/references/troubleshooting.md new file mode 100644 index 0000000..5f816bb --- /dev/null +++ b/.agents/skills/aws-serverless/references/troubleshooting.md @@ -0,0 +1,711 @@ +# Serverless Troubleshooting Reference + +Actionable error lookup tables: exact error string → cause → fix with CLI commands. + +## Contents + +- [Quick fixes](#quick-fixes) +- [Lambda Error Lookup](#lambda-error-lookup) +- [API Gateway Error Lookup](#api-gateway-error-lookup) +- [Step Functions Error Lookup](#step-functions-error-lookup) +- [SAM/CDK Error Lookup](#samcdk-error-lookup) +- [Timeout Debugging](#timeout-debugging) +- [OOM Debugging](#out-of-memory-oom-debugging) +- [Throttling Diagnosis](#throttling-diagnosis) +- [CloudWatch Logs Insights Queries](#cloudwatch-logs-insights-queries) +- [X-Ray Tracing](#x-ray-tracing) + +--- + +## Quick fixes + +### 502 Bad Gateway from API Gateway +Lambda proxy integration requires `{ statusCode: int, headers: {}, body: "string" }`. +The `body` must be a string (`JSON.stringify()`), not an object. API Gateway returns 502 when it cannot parse the Lambda response — the function ran successfully but the response shape was wrong. Note: string statusCode (e.g., "200") is silently coerced to integer, and missing statusCode defaults to 200. + +### CORS errors +With Lambda proxy integration, Lambda must return CORS headers — the API Gateway console "Enable CORS" button does not work for Lambda proxy integration. Add `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers` to every Lambda response including errors. For HTTP API, use the built-in `CorsConfiguration` instead. CORS is enforced by the browser, not the server — missing headers cause the browser to block the response even though the API call succeeded. + +### Lambda timeout + API Gateway 504 +API Gateway has a hard integration timeout: REST API default 29s (configurable 50ms–29s; Regional/private APIs can request higher), HTTP API max 30s (can be lowered, cannot be raised). This is independent of Lambda's 15-min limit. The 504 means API Gateway gave up waiting, not that Lambda failed. For long operations, return 202 immediately, process via SQS or Step Functions, poll or use WebSocket for results. + +### VPC Lambda cannot reach internet +Lambda in a VPC needs a **private** subnet + NAT Gateway in a **public** subnet. Placing Lambda in a public subnet does NOT give it a public IP — Lambda never gets a public IP regardless of subnet type because Lambda's network interface is managed by the service and doesn't support public IP assignment. For AWS services only, use VPC endpoints (free for S3 and DynamoDB gateway endpoints). + +### ImportModuleError / MODULE_NOT_FOUND +Handler path doesn't match file structure, or dependencies weren't bundled. Lambda extracts code to `/var/task` and layers to `/opt` — if the handler path doesn't match the file's location relative to `/var/task`, the runtime can't find it. Python: `pip install -r requirements.txt -t ./package --platform manylinux2014_x86_64 --only-binary=:all:`. Node: verify `exports.handler` exists and `node_modules` is included. Use `sam build` to handle cross-platform packaging automatically. + +--- + +## Lambda Error Lookup + +### Runtime.ImportModuleError + +**Error:** `Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'lambda_function'` +**Cause:** Handler references a module missing from the deployment package. + +```bash +pip install -r requirements.txt -t ./package +cd package && zip -r ../deployment.zip . && cd .. && zip deployment.zip lambda_function.py +# Or: sam build && sam deploy +``` + +### Runtime.HandlerNotFound + +**Error:** `Runtime.HandlerNotFound: Handler 'handler' missing on module 'function'` +**Cause:** File exists but function/method name doesn't match handler setting. + +```bash +aws lambda update-function-configuration --function-name my-func --handler app.lambda_handler +# Python: file.function Node: file.export Java: package.Class::method +``` + +### Task timed out + +**Error:** `Task timed out after 3.00 seconds` +**Cause:** Execution exceeded configured timeout. Slow downstream calls, low memory/CPU, or VPC delays. + +```bash +aws lambda update-function-configuration --function-name my-func --timeout 30 +aws lambda update-function-configuration --function-name my-func --memory-size 512 +# Set SDK/HTTP timeouts shorter than Lambda timeout for meaningful errors +``` + +### Runtime.OutOfMemory (OOM) + +**Error:** `Runtime.OutOfMemory: ... signal: killed` or `Runtime exited without providing a reason` +**Cause:** Function exceeded allocated memory — kernel sent SIGKILL. + +```bash +# Check REPORT lines: Max Memory Used vs Memory Size +aws lambda update-function-configuration --function-name my-func --memory-size 1024 +# Stream large files instead of loading into memory; bound global caches +``` + +### AccessDeniedException + +**Error:** `AccessDeniedException: ... not authorized to perform: lambda:InvokeFunction` +**Cause:** Calling IAM principal lacks `lambda:InvokeFunction` permission. + +```bash +aws lambda add-permission --function-name my-func \ + --statement-id AllowInvoke --action lambda:InvokeFunction \ + --principal s3.amazonaws.com --source-arn arn:aws:s3:::my-bucket +``` + +### TooManyRequestsException + +**Error:** `TooManyRequestsException: Rate Exceeded.` +**Cause:** Function exceeded account concurrency limit (default 1,000). + +```bash +aws lambda get-account-settings +aws service-quotas request-service-quota-increase \ + --service-code lambda --quota-code L-B99A9384 --desired-value 3000 +aws lambda put-function-concurrency --function-name my-func --reserved-concurrent-executions 100 +``` + +### InvalidParameterValueException (size) + +**Error:** `Unzipped size must be smaller than 262144000 bytes` +**Cause:** Package exceeds 50 MB zipped / 250 MB unzipped. + +```bash +find ./package -name "*.pyc" -delete && find ./package -name "*.dist-info" -type d -exec rm -rf {} + +aws lambda publish-layer-version --layer-name my-deps --zip-file fileb://layer.zip --compatible-runtimes python3.13 +# Or upload via S3, or switch to container image packaging (10 GB limit) +``` + +### ETIMEDOUT (VPC) + +**Error:** `Error: connect ETIMEDOUT 176.32.98.189:443` +**Cause:** VPC Lambda can't reach internet — missing NAT Gateway or VPC Endpoint. + +```bash +aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=subnet-xxx" +aws ec2 create-route --route-table-id rtb-xxx --destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-xxx +# Or use VPC Endpoints for AWS services: +aws ec2 create-vpc-endpoint --vpc-id vpc-xxx --service-name com.amazonaws.us-east-1.s3 --route-table-ids rtb-xxx +``` + +### MODULE_NOT_FOUND + +**Error:** `Error: Cannot find module 'my-module'` +**Cause:** Node.js dependency missing — not bundled or built on incompatible platform. + +```bash +npm install --production +sam build --use-container # for native modules +unzip -l deployment.zip | grep my-module # verify inclusion +``` + +### RecursiveInvocationException + +**Error:** `RecursiveInvocationException: Recursive invocation detected` +**Cause:** Function writes to a resource that triggers itself again (~16 invocations before halt). + +```bash +# Emergency stop +aws lambda put-function-concurrency --function-name my-func --reserved-concurrent-executions 0 +# Fix: use separate input/output buckets or prefix filters in trigger config +``` + +### SnapStart Errors + +**Error:** `SnapStartException` / `SnapStartNotReadyException` / `SnapStartTimeoutException` +**Cause:** SnapStart failed during snapshot — init threw exception or uses non-snapshottable resources (e.g., open network connections). + +```bash +aws lambda get-function --function-name my-func --query 'Configuration.SnapStart' +# Java: Use CRaC hooks — beforeCheckpoint() to close connections, afterRestore() to reopen +# Python: Use snapshot_restore runtime hooks to re-establish connections after restore +# .NET: Use SnapshotRestore register hooks for before-snapshot and after-restore actions +``` + +### Sandbox.Timedout + +**Error:** `Sandbox.Timedout` +**Cause:** Function exceeded its timeout. In newer runtimes, this covers both init-phase and invoke-phase timeouts. A suppressed init failure consumes the invoke timeout. + +```bash +aws lambda update-function-configuration --function-name my-func --timeout 60 --memory-size 1024 +# Move heavy initialization to lazy loading inside the handler +``` + +### ENILimitReachedException + +**Error:** `ENILimitReachedException` +**Cause:** VPC reached network interface quota. Lambda Hyperplane ENIs have a default quota of 500 per VPC (see lambda.md); the overall VPC ENI quota is 5,000 per region. Check which limit applies. + +```bash +aws service-quotas request-service-quota-increase --service-code vpc --quota-code L-DF5E4CA3 --desired-value 10000 +# Consolidate functions to use same subnet + security group combinations +``` + +### InvalidZipFileException + +**Error:** `InvalidZipFileException: Could not unzip uploaded file.` +**Cause:** Invalid ZIP or handler nested in subdirectory instead of at root. + +```bash +unzip -t deployment.zip # verify integrity +cd my-folder && zip -r ../deployment.zip . && cd .. # files at root, not nested +``` + +### CodeStorageExceededException + +**Error:** `CodeStorageExceededException: Code storage limit exceeded.` +**Cause:** Account exceeded 75 GB code storage per region (all versions + layers). + +```bash +aws lambda list-versions-by-function --function-name my-func +aws lambda delete-function --function-name my-func --qualifier 1 +aws lambda list-layers # delete unused layers too +``` + +--- + +## API Gateway Error Lookup + +### Malformed Lambda Proxy Response (502) + +**Error:** `Malformed Lambda proxy response` → 502 +**Cause:** Lambda response missing required format — `body` must be a string, response must be a JSON object (not a plain string or array). + +```python +return {"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"msg": "ok"})} +``` + +```javascript +return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ msg: "ok" }) }; +``` + +### Missing Authentication Token (403) + +**Error:** `403 Forbidden: Missing Authentication Token` +**Cause:** URL doesn't match any resource/method, or API not deployed to stage. Usually routing, not auth. + +```bash +aws apigateway create-deployment --rest-api-id abc123 --stage-name prod +# Verify: https://{api-id}.execute-api.{region}.amazonaws.com/{stage}/{resource} +``` + +### Invalid Permissions on Lambda (500) + +**Error:** `Invalid permissions on Lambda function` +**Cause:** API Gateway lacks `lambda:InvokeFunction` permission on the target function. + +```bash +aws lambda add-permission --function-name my-func --statement-id apigw-invoke \ + --action lambda:InvokeFunction --principal apigateway.amazonaws.com \ + --source-arn "arn:aws:execute-api:us-east-1:123456789012:api-id/*/GET/resource" +``` + +### Endpoint Request Timed Out (504) + +**Error:** `Endpoint request timed out` → 504 +**Cause:** Lambda didn't respond within 29s (REST) / 30s (HTTP) integration timeout. + +```bash +aws lambda update-function-configuration --function-name my-func --memory-size 1024 +# For long operations: return 202 immediately, process async, poll for results +``` + +### Authorizer Unauthorized (401) + +**Error:** `Unauthorized` (401) +**Cause:** Lambda authorizer returned deny, threw error, or timed out. + +```bash +aws logs tail /aws/lambda/my-authorizer --since 1h --filter-pattern ERROR +# Verify authorizer returns: { principalId, policyDocument: { Statement: [{ Effect: "Allow" }] } } +``` + +### WAF Access Denied (403) + +**Error:** `403 Forbidden` with `x-amzn-errortype: ForbiddenException` +**Cause:** AWS WAF rule matched — IP denylist, rate limit, or injection detection. + +```bash +# Check WAF sampled requests in console to identify blocking rule +# Test rules in Count mode before switching to Block +``` + +### CORS Errors + +**Error:** `blocked by CORS policy: No 'Access-Control-Allow-Origin' header` +**Cause:** Lambda proxy integration must return CORS headers; HTTP APIs can configure at API level. + +```yaml +# SAM Globals +Globals: + Api: + Cors: + AllowOrigin: "'*'" + AllowMethods: "'GET,POST,OPTIONS'" + AllowHeaders: "'Content-Type,Authorization'" +``` + +```bash +# HTTP API +aws apigatewayv2 update-api --api-id abc123 \ + --cors-configuration AllowOrigins="*",AllowMethods="GET,POST",AllowHeaders="Content-Type" +``` + +### Internal Server Error — Lambda Throttled (500) + +**Error:** 500 with CloudWatch log `Lambda invocation failed with status 429` +**Cause:** Lambda throttled but API Gateway surfaces as 500. + +```bash +# Increase Lambda concurrency (see TooManyRequestsException above) +aws apigateway update-stage --rest-api-id abc123 --stage-name prod \ + --patch-operations op=replace,path=/*/*/throttling/rateLimit,value=1000 +``` + +--- + +## Step Functions Error Lookup + +### States.TaskFailed + +**Error:** `States.TaskFailed` +**Cause:** Task failed — unhandled Lambda exception, service error, or missing permissions. + +```json +"Retry": [{"ErrorEquals": ["States.TaskFailed","Lambda.ServiceException","Lambda.SdkClientException"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0}], +"Catch": [{"ErrorEquals": ["States.TaskFailed"], "Next": "HandleError", "ResultPath": "$.error"}] +``` + +### States.Timeout + +**Error:** `States.Timeout` +**Cause:** Task exceeded `TimeoutSeconds` or missed `HeartbeatSeconds` deadline. + +```json +{"Type": "Task", "Resource": "arn:aws:lambda:...", "TimeoutSeconds": 300, "HeartbeatSeconds": 60, "Next": "NextState"} +``` + +### States.DataLimitExceeded + +**Error:** `States.DataLimitExceeded` +**Cause:** State input/output exceeded 256 KB. Cannot be caught by `States.ALL`. + +**Fix:** Store large data in S3, pass only S3 keys between states. Use `InputPath`/`OutputPath` to filter. + +### ExecutionAlreadyExists + +**Error:** `ExecutionAlreadyExists` +**Cause:** Execution name must be unique per state machine for 90 days. + +```bash +aws stepfunctions start-execution --state-machine-arn arn:aws:states:... \ + --name "exec-$(date +%s)" --input '{}' +# Or omit --name for auto-generated names +``` + +### States.Permissions + +**Error:** `States.Permissions: insufficient privileges` +**Cause:** Execution role lacks permission to invoke target service. + +```bash +aws iam list-attached-role-policies --role-name StepFunctionsRole +# Add lambda:InvokeFunction, dynamodb:PutItem, etc. to the execution role +``` + +--- + +## SAM/CDK Error Lookup + +### Stale Build Cache + +**Error:** `sam build` uses old dependencies after updating requirements.txt, or `--clear-cache` flag unrecognized. +**Cause:** SAM caches build artifacts. There is no `--clear-cache` flag. + +```bash +sam build --no-cached # Force clean build (correct flag) +rm -rf .aws-sam/cache # Or manually delete cache directory +``` + +### PythonPipBuilder:ResolveDependencies + +**Error:** `PythonPipBuilder:ResolveDependencies - pip install returned a non-zero exit code` +**Cause:** Dependency version conflicts or missing native libraries. + +```bash +sam build --use-container --no-cached +# Use binary wheels: psycopg2-binary instead of psycopg2 +``` + +### DockerBuildFailed + +**Error:** `DockerBuildFailed: Docker build failed.` +**Cause:** Docker not running or Dockerfile errors. + +```bash +docker info # verify running +sudo systemctl start docker # start if needed +``` + +### Cannot find module 'esbuild' + +**Error:** `Cannot find module 'esbuild'` +**Cause:** CDK `NodejsFunction` needs esbuild for bundling. + +```bash +npm install --save-dev esbuild +``` + +### CREATE_FAILED + +**Error:** `CREATE_FAILED: AWS::Lambda::Function` +**Cause:** Invalid runtime, missing S3 code, role not ready, or package too large. + +```bash +aws cloudformation describe-stack-events --stack-name my-stack \ + --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[LogicalResourceId,ResourceStatusReason]" --output table +``` + +### UPDATE_ROLLBACK_FAILED + +**Error:** `UPDATE_ROLLBACK_FAILED` +**Cause:** Update failed and rollback also failed — resource manually deleted or permissions changed. + +```bash +aws cloudformation continue-update-rollback --stack-name my-stack +aws cloudformation continue-update-rollback --stack-name my-stack --resources-to-skip MyFunction +``` + +### Security Constraints Not Satisfied + +**Error:** `Security Constraints Not Satisfied` +**Cause:** SAM template missing required properties (Handler, Runtime, CodeUri). + +```bash +sam validate --lint +``` + +### CDK Bootstrap Required + +**Error:** `This stack uses assets, so the toolkit stack must be deployed` +**Cause:** Target account/region not bootstrapped. + +```bash +cdk bootstrap aws://123456789012/us-east-1 +``` + +### Circular Dependency + +**Error:** `Circular dependency between resources: [MyFunction, MyRole, ...]` +**Cause:** Resources reference each other in a cycle. + +```yaml +# Break cycle: give the function an explicit name and hardcode the ARN +MyFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: my-function-name # explicit name + +MyRole: + Type: AWS::IAM::Role + Properties: + Policies: + - PolicyDocument: + Statement: + - Effect: Allow + Action: lambda:InvokeFunction + # No ${MyFunction} reference — no implicit dependency + Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:my-function-name" +# Or restructure to eliminate the cycle (extract IAM role/policy into a separate resource) +``` + +--- + +## Timeout Debugging + +``` +Function times out +├── INIT phase? (Sandbox.Timedout) +│ ├── YES → Increase timeout + memory, lazy-load heavy deps +│ └── NO → INVOKE phase +│ ├── Timeout ≈ avg duration? → Set to 2-3x average +│ ├── Calling external services? → Set SDK timeouts < Lambda timeout +│ ├── CPU-bound? → Increase memory (1,769 MB = 1 vCPU) +│ └── VPC? → Check NAT Gateway / security group / VPC Endpoints +``` + +```bash +aws lambda get-function-configuration --function-name my-func --query '[Timeout,MemorySize]' +aws cloudwatch get-metric-statistics --namespace AWS/Lambda --metric-name Duration \ + --dimensions Name=FunctionName,Value=my-func --period 300 --statistics Average Maximum \ + --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) --end-time $(date -u +%Y-%m-%dT%H:%M:%S) +# For percentiles, use a separate call: +aws cloudwatch get-metric-statistics --namespace AWS/Lambda --metric-name Duration \ + --dimensions Name=FunctionName,Value=my-func --period 300 --extended-statistics p99 \ + --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) --end-time $(date -u +%Y-%m-%dT%H:%M:%S) +``` + +--- + +## Out-of-Memory (OOM) Debugging + +``` +Runtime.OutOfMemory / signal: killed +├── Check REPORT: Max Memory Used ≈ Memory Size? → OOM confirmed +├── Immediate? → Payload/dependency too large → increase memory +├── Gradual? → Memory leak → check global vars accumulating across warm invocations +└── Fix: increase memory, stream large files, bound caches +``` + +| Memory (MB) | vCPUs | Use Case | +|-------------|-------|----------| +| 128 | ~0.08 | Simple transforms | +| 512 | ~0.3 | Moderate processing | +| 1,769 | 1.0 | CPU-intensive single-threaded | +| 3,538 | 2.0 | Multi-threaded | +| 10,240 | ~5.8 | Heavy compute, ML inference | + +--- + +## Throttling Diagnosis + +| Concept | Default | Notes | +|---------|---------|-------| +| Account concurrency | 1,000/region | Request increase via Service Quotas | +| Reserved concurrency | None | Guarantees AND caps function concurrency | +| Concurrency scaling rate | 1,000 envs/10s | Per function, uniform across regions | + +| Invocation Type | Throttle Behavior | +|-----------------|-------------------| +| Synchronous (API GW) | Returns 429 (API GW may show 500) | +| Async (S3, SNS) | Auto-retries up to 6 hours | +| SQS trigger | Returns to queue, backs off | +| Kinesis/DDB Streams | Retries batch, blocks shard | + +```bash +aws lambda get-account-settings +aws cloudwatch get-metric-statistics --namespace AWS/Lambda --metric-name Throttles \ + --dimensions Name=FunctionName,Value=my-func --period 60 --statistics Sum \ + --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) --end-time $(date -u +%Y-%m-%dT%H:%M:%S) +``` + +--- + +## CloudWatch Logs Insights Queries + +Run against `/aws/lambda/FUNCTION_NAME`. For API Gateway, use the access log group. + +### Cold Starts + +``` +filter @type = "REPORT" | filter ispresent(@initDuration) +| stats count() as coldStarts, avg(@initDuration) as avgInitMs, max(@initDuration) as maxInitMs, pct(@initDuration, 99) as p99InitMs by bin(1h) +``` + +### Cold Start Percentage + +``` +filter @type = "REPORT" +| stats count() as total, sum(ispresent(@initDuration)) as coldStarts, sum(ispresent(@initDuration)) * 100.0 / count() as pct by bin(1h) +``` + +### Errors by Type + +``` +filter @message like /(?i)error|exception/ +| parse @message /(?[A-Za-z]+Error|[A-Za-z]+Exception)/ +| stats count() as cnt by errorType | sort cnt desc +``` + +### Timeouts + +``` +filter @message like /Task timed out/ | stats count() as timeouts by bin(1h) | sort bin desc +``` + +### Memory Utilization + +``` +filter @type = "REPORT" +| stats max(@memorySize/1e6) as provisionedMB, avg(@maxMemoryUsed/1e6) as avgUsedMB, max(@maxMemoryUsed/1e6) as maxUsedMB, pct(@maxMemoryUsed/1e6, 99) as p99UsedMB +``` + +### Out-of-Memory Detection (>90% memory) + +``` +filter @type = "REPORT" | filter @maxMemoryUsed / @memorySize > 0.9 +| fields @timestamp, @requestId, @maxMemoryUsed/1e6 as usedMB, @memorySize/1e6 as allocatedMB | sort @timestamp desc | limit 50 +``` + +### Overprovisioned Memory (<50% used) + +``` +filter @type = "REPORT" +| stats max(@memorySize/1e6) as provMB, max(@maxMemoryUsed/1e6) as peakMB, max(@maxMemoryUsed)*100.0/max(@memorySize) as pct +| filter pct < 50 +``` + +### Memory Growth (Leak Detection) + +``` +filter @type = "REPORT" | stats avg(@maxMemoryUsed/1e6) as avgMemMB by bin(5m) | sort bin asc +``` + +### Latency Percentiles + +``` +filter @type = "REPORT" +| stats avg(@duration) as avg, pct(@duration,50) as p50, pct(@duration,90) as p90, pct(@duration,95) as p95, pct(@duration,99) as p99, max(@duration) as max by bin(1h) +``` + +### Slowest Invocations + +``` +filter @type = "REPORT" +| fields @timestamp, @requestId, @duration, @maxMemoryUsed/1000000 as memMB, ispresent(@initDuration) as coldStart +| sort @duration desc | limit 20 +``` + +### API Gateway 5xx + +``` +filter status >= 500 | stats count() as errors by status, path, httpMethod | sort errors desc +``` + +### API Gateway 5xx Over Time + +``` +filter status >= 500 | stats count() by bin(5m) | sort bin desc +``` + +### Throttle Events + +``` +filter @message like /Rate Exceeded|TooManyRequestsException|Throttl/ +| fields @timestamp, @requestId, @message | sort @timestamp desc | limit 50 +``` + +### Billed Duration + +``` +filter @type = "REPORT" +| stats count() as invocations, sum(@billedDuration)/1000 as totalBilledSec, avg(@billedDuration) as avgBilledMs by bin(1d) +``` + +### Error Messages with Request IDs + +``` +filter @message like /(?i)error|exception|fail/ +| fields @timestamp, @requestId, @message | sort @timestamp desc | limit 50 +``` + +--- + +## X-Ray Tracing + +### Enable in SAM + +```yaml +Globals: + Function: + Tracing: Active +``` + +### Enable in CDK + +```typescript +new lambda.Function(this, 'Fn', { + tracing: lambda.Tracing.ACTIVE, // adds AWSXRayDaemonWriteAccess automatically +}); +``` + +### Required IAM +`AWSXRayDaemonWriteAccess` managed policy on the execution role. SAM/CDK add this automatically. + +### Default Sampling +1 request/second (reservoir) + 5% of additional requests. + +### Instrument SDK Calls + +```python +from aws_xray_sdk.core import patch_all +patch_all() +``` + +```javascript +// SDK v3 (Node.js 18+) +const { captureAWSv3Client } = require('aws-xray-sdk-core'); +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const ddb = captureAWSv3Client(new DynamoDBClient({})); +``` + +### Query Traces + +```bash +aws xray get-trace-summaries --start-time $(date -u -d '1 hour ago' +%s) --end-time $(date -u +%s) \ + --filter-expression 'service("my-func") AND fault' +aws xray batch-get-traces --trace-ids "1-xxx-yyy" +``` + +### Enable for API Gateway + +```yaml +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + TracingEnabled: true +``` + +### Enable for Step Functions + +```yaml +Resources: + MyStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Tracing: + Enabled: true +``` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..b6175e7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint*)", + "Bash(npm run type-check*)", + "Bash(npm run ci*)" + ] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9b3266..4b343c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI/CD Pipeline on: push: - branches: [main, develop] + branches: [main] pull_request: - branches: [main, develop] + branches: [main] jobs: # Frontend CI Pipeline - Independent diff --git a/.gitignore b/.gitignore index c6fb194..a7ec501 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ docker-compose.override.yml # IDE .vscode/ + +# Claude Code personal settings +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7c83a3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,88 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A web application for orchestrating GitHub Actions workflows and managing AWS Lambda functions. Uses GitHub App authentication (Octokit) to list repos, view/trigger workflows, and monitor runs. + +## Monorepo Structure + +npm workspaces monorepo with two packages: `frontend/` and `backend/`. Docker Compose orchestrates both for local development. + +``` +sf-deploy-app/ +├── frontend/ # React 18 + TypeScript + Vite + MUI 5 +├── backend/ # Express + TypeScript + Octokit + AWS SDK +└── docker-compose.yml +``` + +## Commands + +All commands run from the project root: + +| Task | Command | +|------|---------| +| Start dev (Docker) | `npm run dev` | +| Start dev + rebuild | `npm run dev:build` | +| Stop containers | `npm run down` | +| Install all deps | `npm run install:all` | +| Lint both workspaces | `npm run lint` | +| Auto-fix lint | `npm run lint:fix` | +| Format (Prettier) | `npm run format` | +| Check formatting | `npm run format:check` | +| TypeScript check | `npm run type-check` | +| Build both | `npm run build` | +| CI checks (type-check + lint + format) | `npm run ci` | + +### Running a single workspace + +```bash +# Frontend only +npm run lint:frontend +npm run build:frontend +cd frontend && npm run dev # Vite on port 3000 + +# Backend only +npm run lint:backend +npm run build:backend +cd backend && npm run dev # ts-node-dev on port 4000 +``` + +## Architecture + +**Frontend** (port 3000): React 18 SPA with Material-UI. Vite dev server proxies `/api` requests to the backend (`http://backend:4000` in Docker). Two main views via tabs: `GitHubActions.tsx` and `LambdaFunctions.tsx`. + +**Backend** (port 4000): Express server with TypeScript. `GitHubService` class wraps Octokit for GitHub App auth. Environment variables loaded from `backend/.env` (see `backend/.env.example`). + +### Backend API Routes + +- `GET /api/health` -- health check with service status +- `GET /api/github/repositories` -- list accessible repos +- `GET /api/github/workflows/:owner/:repo` -- list workflows +- `GET /api/github/runs/:owner/:repo?workflow_id=` -- list workflow runs +- `POST /api/github/trigger/:owner/:repo/:workflowId` -- trigger workflow dispatch +- `GET /api/lambda/functions` -- list Lambda functions (mocked) +- `POST /api/lambda/invoke/:functionName` -- invoke Lambda (mocked) + +### Key Environment Variables (backend/.env) + +- `GITHUB_APP_ID` -- GitHub App ID +- `GITHUB_PRIVATE_KEY` or `GITHUB_PRIVATE_KEY_PATH` -- GitHub App private key +- `GITHUB_INSTALLATION_ID` -- GitHub App installation ID + +## Code Quality + +- **TypeScript**: `strict: true` in both `frontend/tsconfig.json` and `backend/tsconfig.json` +- **Prettier**: Single quotes, 100 print width, trailing commas es5, arrow parens avoid, LF line endings +- **Husky + lint-staged**: Pre-commit runs lint fix + format on staged `.ts`/`.tsx` files per workspace + + +## Skills & AI tooling + +**External skills** (lockfile-managed — update with `npx skills check` / `npx skills update`): +- `aws-sdk-js-v3-usage` — from aws/agent-toolkit-for-aws +- `aws-serverless` — from aws/agent-toolkit-for-aws + +**Global tooling available in every session:** lean-ctx (prefer `ctx_*` MCP tools for reads/search/shell — token-compressed), superpowers process skills, and graphify (no graph built for this repo). + diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json index c3b36fa..82c762a 100644 --- a/backend/.eslintrc.json +++ b/backend/.eslintrc.json @@ -20,7 +20,7 @@ "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "warn", "no-console": "off" }, "ignorePatterns": [ diff --git a/backend/src/github.service.ts b/backend/src/github.service.ts index 08df5fb..bfc3fc5 100644 --- a/backend/src/github.service.ts +++ b/backend/src/github.service.ts @@ -1,8 +1,15 @@ import { App } from '@octokit/app'; -import { Octokit } from '@octokit/rest'; import * as fs from 'fs'; import * as path from 'path'; +type InstallationOctokit = Awaited>; + +export interface GitHubRepository { + full_name: string; + name: string; + owner: string; +} + export interface GitHubWorkflow { id: number; name: string; @@ -26,7 +33,7 @@ export interface WorkflowRun { export class GitHubService { private app: App; - private installationOctokit: any = null; // Use 'any' to avoid type conflicts + private installationOctokit: InstallationOctokit | null = null; constructor() { if (!process.env.GITHUB_APP_ID) { @@ -83,7 +90,7 @@ export class GitHubService { } } - private getOctokit(): any { + private getOctokit(): InstallationOctokit { if (!this.installationOctokit) { throw new Error('GitHub App not initialized. Call initialize() first.'); } @@ -99,7 +106,7 @@ export class GitHubService { repo, }); - return response.data.workflows.map((workflow: any) => ({ + return response.data.workflows.map(workflow => ({ id: workflow.id, name: workflow.name, state: workflow.state, @@ -130,7 +137,7 @@ export class GitHubService { per_page: 20, }); - return response.data.workflow_runs.map((run: any) => ({ + return response.data.workflow_runs.map(run => ({ id: run.id, name: run.name || 'Unnamed Workflow', status: run.status || 'unknown', @@ -152,7 +159,7 @@ export class GitHubService { repo: string, workflowId: string, ref: string = 'main', - inputs: any = {} + inputs: Record = {} ): Promise { const octokit = this.getOctokit(); @@ -173,7 +180,7 @@ export class GitHubService { } } - async getRepositories(): Promise> { + async getRepositories(): Promise { const octokit = this.getOctokit(); try { @@ -182,14 +189,14 @@ export class GitHubService { per_page: 100, }); - const repos = response.data.repositories.map((repo: any) => ({ + const repos = response.data.repositories.map(repo => ({ full_name: repo.full_name, name: repo.name, owner: repo.owner?.login || '', })); console.log(`✅ Found ${repos.length} accessible repositories`); - repos.forEach((repo: any) => console.log(` 📁 ${repo.full_name}`)); + repos.forEach(repo => console.log(` 📁 ${repo.full_name}`)); return repos; } catch (error) { diff --git a/backend/src/index.ts b/backend/src/index.ts index 02c2f2a..2850267 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import { GitHubService } from './github.service'; @@ -9,9 +9,9 @@ const app = express(); const port = 4000; app.use(cors()); -app.use(express.json()); +app.use(express.json({ limit: '1mb' })); -let githubService: GitHubService; +let githubService: GitHubService | null = null; // Initialize GitHub service if credentials are available if ( @@ -57,14 +57,18 @@ const lambdaFunctions = [ }, ]; +// Middleware that ensures GitHub service is available before handling the request +function requireGitHub(_req: Request, res: Response, next: NextFunction) { + if (!githubService) { + return res.status(503).json({ error: 'GitHub service not initialized' }); + } + next(); +} + // GitHub Actions endpoints -app.get('/api/github/repositories', async (req, res) => { +app.get('/api/github/repositories', requireGitHub, async (_req, res) => { try { - if (!githubService) { - return res.status(500).json({ error: 'GitHub service not initialized' }); - } - - const repos = await githubService.getRepositories(); + const repos = await githubService!.getRepositories(); res.json(repos); } catch (error) { console.error('Error fetching repositories:', error); @@ -72,14 +76,10 @@ app.get('/api/github/repositories', async (req, res) => { } }); -app.get('/api/github/workflows/:owner/:repo', async (req, res) => { +app.get('/api/github/workflows/:owner/:repo', requireGitHub, async (req, res) => { try { - if (!githubService) { - return res.status(500).json({ error: 'GitHub service not initialized' }); - } - const { owner, repo } = req.params; - const workflows = await githubService.getWorkflows(owner, repo); + const workflows = await githubService!.getWorkflows(owner, repo); res.json(workflows); } catch (error) { console.error('Error fetching workflows:', error); @@ -87,16 +87,12 @@ app.get('/api/github/workflows/:owner/:repo', async (req, res) => { } }); -app.get('/api/github/runs/:owner/:repo', async (req, res) => { +app.get('/api/github/runs/:owner/:repo', requireGitHub, async (req, res) => { try { - if (!githubService) { - return res.status(500).json({ error: 'GitHub service not initialized' }); - } - const { owner, repo } = req.params; const { workflow_id } = req.query; - const runs = await githubService.getWorkflowRuns( + const runs = await githubService!.getWorkflowRuns( owner, repo, workflow_id ? parseInt(workflow_id as string) : undefined @@ -108,16 +104,12 @@ app.get('/api/github/runs/:owner/:repo', async (req, res) => { } }); -app.post('/api/github/trigger/:owner/:repo/:workflowId', async (req, res) => { +app.post('/api/github/trigger/:owner/:repo/:workflowId', requireGitHub, async (req, res) => { try { - if (!githubService) { - return res.status(500).json({ error: 'GitHub service not initialized' }); - } - const { owner, repo, workflowId } = req.params; const { ref = 'main', inputs = {} } = req.body; - await githubService.triggerWorkflow(owner, repo, workflowId, ref, inputs); + await githubService!.triggerWorkflow(owner, repo, workflowId, ref, inputs); res.json({ success: true, message: 'Workflow triggered successfully' }); } catch (error) { console.error('Error triggering workflow:', error); diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 3821156..ee5af8e 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -28,7 +28,7 @@ "rules": { "react-refresh/only-export-components": "warn", "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "warn", "react/prop-types": "off", "react-hooks/exhaustive-deps": "warn", "no-console": "off" diff --git a/frontend/package.json b/frontend/package.json index 6b491eb..22b770d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,8 +21,7 @@ "axios": "^1.6.0", "date-fns": "^2.30.0", "react": "^18.0.0", - "react-dom": "^18.0.0", - "react-router-dom": "^6.18.0" + "react-dom": "^18.0.0" }, "devDependencies": { "@types/react": "^18.2.37", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f7f3711..ed9d647 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import { CssBaseline, AppBar, Toolbar, Typography, Container, Tabs, Tab, Box } from '@mui/material'; +import ErrorBoundary from './components/ErrorBoundary'; import GitHubActions from './components/GitHubActions'; import LambdaFunctions from './components/LambdaFunctions'; @@ -48,29 +49,39 @@ const App: React.FC = () => { return ( - - - - GitHub Actions & AWS Lambda Orchestrator - - - + + + + + GitHub Actions & AWS Lambda Orchestrator + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + + ); }; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..5406640 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Alert, Box, Button, Typography } from '@mui/material'; + +interface Props { + children: React.ReactNode; +} + +interface State { + hasError: boolean; +} + +class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return ( + + window.location.reload()}> + Reload + + } + > + Something went wrong. + + + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/GitHubActions.tsx b/frontend/src/components/GitHubActions.tsx index 14cca17..9667421 100644 --- a/frontend/src/components/GitHubActions.tsx +++ b/frontend/src/components/GitHubActions.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import { Card, CardContent, @@ -34,6 +34,7 @@ import { } from '@mui/icons-material'; import { formatDistanceToNow } from 'date-fns'; import axios from 'axios'; +import { getErrorMessage } from '../utils/getErrorMessage'; interface Repository { full_name: string; @@ -70,12 +71,7 @@ const GitHubActions: React.FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - loadRepositories(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const loadRepositories = useCallback(async () => { + const loadRepositories = async () => { try { setLoading(true); const response = await axios.get('/api/github/repositories'); @@ -86,12 +82,15 @@ const GitHubActions: React.FC = () => { loadWorkflows(firstRepo); } } catch (err: unknown) { - const error = err as { response?: { data?: { error?: string } } }; - setError(error.response?.data?.error || 'Failed to load repositories'); + setError(getErrorMessage(err, 'Failed to load repositories')); } finally { setLoading(false); } - }, []); + }; + + useEffect(() => { + loadRepositories(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps -- intentional: load once on mount const loadWorkflows = async (repo: Repository) => { try { @@ -103,8 +102,7 @@ const GitHubActions: React.FC = () => { setWorkflows(workflowsResponse.data); setWorkflowRuns(runsResponse.data); } catch (err: unknown) { - const error = err as { response?: { data?: { error?: string } } }; - setError(error.response?.data?.error || 'Failed to load workflows'); + setError(getErrorMessage(err, 'Failed to load workflows')); } finally { setLoading(false); } @@ -120,8 +118,7 @@ const GitHubActions: React.FC = () => { // Refresh workflow runs after triggering setTimeout(() => loadWorkflows(selectedRepo), 2000); } catch (err: unknown) { - const error = err as { response?: { data?: { error?: string } } }; - setError(error.response?.data?.error || 'Failed to trigger workflow'); + setError(getErrorMessage(err, 'Failed to trigger workflow')); } }; diff --git a/frontend/src/components/LambdaFunctions.tsx b/frontend/src/components/LambdaFunctions.tsx index 8a55e5d..79fba73 100644 --- a/frontend/src/components/LambdaFunctions.tsx +++ b/frontend/src/components/LambdaFunctions.tsx @@ -32,6 +32,7 @@ import { } from '@mui/icons-material'; import { formatDistanceToNow } from 'date-fns'; import axios from 'axios'; +import { getErrorMessage } from '../utils/getErrorMessage'; interface LambdaFunction { name: string; @@ -67,8 +68,8 @@ const LambdaFunctions: React.FC = () => { setError(null); const response = await axios.get('/api/lambda/functions'); setFunctions(response.data); - } catch (err: any) { - setError(err.response?.data?.error || 'Failed to load Lambda functions'); + } catch (err: unknown) { + setError(getErrorMessage(err, 'Failed to load Lambda functions')); } finally { setLoading(false); } @@ -97,8 +98,8 @@ const LambdaFunctions: React.FC = () => { const response = await axios.post(`/api/lambda/invoke/${invokeDialog.functionName}`, payload); setInvocationResult(response.data); - } catch (err: any) { - setError(err.response?.data?.error || 'Failed to invoke Lambda function'); + } catch (err: unknown) { + setError(getErrorMessage(err, 'Failed to invoke Lambda function')); } }; diff --git a/frontend/src/utils/getErrorMessage.ts b/frontend/src/utils/getErrorMessage.ts new file mode 100644 index 0000000..2d53a21 --- /dev/null +++ b/frontend/src/utils/getErrorMessage.ts @@ -0,0 +1,11 @@ +import axios from 'axios'; + +export function getErrorMessage(err: unknown, fallback: string): string { + if (axios.isAxiosError(err)) { + return err.response?.data?.error || fallback; + } + if (err instanceof Error) { + return err.message; + } + return fallback; +} diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..38e6c19 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "skills": { + "aws-sdk-js-v3-usage": { + "source": "aws/agent-toolkit-for-aws", + "sourceType": "github", + "skillPath": "skills/core-skills/aws-sdk-js-v3-usage/SKILL.md", + "computedHash": "b2c5175e7e8951cb6b2d65ddda09187906feb222cdd97a3757a82f7fa142a873" + }, + "aws-serverless": { + "source": "aws/agent-toolkit-for-aws", + "sourceType": "github", + "skillPath": "skills/core-skills/aws-serverless/SKILL.md", + "computedHash": "dfae98db0b5f558c08427e20c608996a3378be9bb3097a5062440a1a0960b77d" + } + } +}