Skip to content

Commit bdfa583

Browse files
authored
feat(commons): add Lambda Metadata Service support (#5106)
1 parent 03034db commit bdfa583

7 files changed

Lines changed: 323 additions & 26 deletions

File tree

docs/features/metadata.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: Metadata
3+
description: Utility to fetch data from the AWS Lambda Metadata endpoint
4+
status: new
5+
---
6+
7+
<!-- markdownlint-disable MD043 -->
8+
The Metadata utility allows you to fetch data from the AWS Lambda Metadata Endpoint (LMDS). This can be useful for retrieving information about the Lambda function, such as the Availability Zone ID.
9+
10+
## Getting started
11+
12+
### Installation
13+
14+
Add the library to your project:
15+
16+
```shell
17+
npm install @aws-lambda-powertools/commons
18+
```
19+
20+
### Usage
21+
22+
You can fetch data from the LMDS using the `getMetadata` function. For example, to retrieve the Availability Zone ID:
23+
24+
???+ tip
25+
Metadata is cached for the duration of the Lambda execution, so subsequent calls to `getMetadata` will return the cached data.
26+
27+
=== "index.ts"
28+
29+
```ts hl_lines="1 5 8"
30+
--8<-- "examples/snippets/commons/metadata.ts"
31+
```
32+
33+
### Available metadata
34+
35+
| Property | Type | Description |
36+
| -------------------- | -------- | ------------------------------------------------------- |
37+
| `AvailabilityZoneId` | `string` | The AZ where the function is running (e.g., `use1-az1`) |
38+
39+
## Testing your code
40+
41+
The metadata endpoint is not available during local development or testing. To ease testing, the `getMetadata` function automatically detects when it's running in a non-Lambda environment and returns an empty object. This allows you to write tests without needing to mock the LMDS responses.
42+
43+
If instead you want to mock specific metadata values for testing purposes, you can do so by setting environment variables that correspond to the metadata endpoint and authentication token, as well as mocking the `fetch` function to return the desired metadata. Here's an example of how to do this:
44+
45+
=== "index.test.ts"
46+
47+
```ts hl_lines="11-14 24-26 35"
48+
--8<-- "examples/snippets/commons/testingMetadata.ts"
49+
```
50+
51+
We also expose a `clearMetadataCache` function that can be used to clear the cached metadata, allowing you to test different metadata values within the same execution context.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getMetadata } from '@aws-lambda-powertools/commons/utils/metadata';
2+
import { Logger } from '@aws-lambda-powertools/logger';
3+
4+
const logger = new Logger({ serviceName: 'serverlessAirline' });
5+
const metadata = await getMetadata();
6+
7+
export const handler = async () => {
8+
const { AvailabilityZoneId: azId } = metadata;
9+
logger.appendKeys({ azId });
10+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
clearMetadataCache,
3+
getMetadata,
4+
} from '@aws-lambda-powertools/commons/utils/metadata';
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6+
7+
describe('function: getMetadata', async () => {
8+
let fetchMock: ReturnType<typeof vi.fn>;
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks();
12+
clearMetadataCache();
13+
fetchMock = vi.fn();
14+
vi.stubGlobal('fetch', fetchMock);
15+
});
16+
17+
afterEach(() => {
18+
vi.unstubAllEnvs();
19+
vi.unstubAllGlobals();
20+
});
21+
22+
it('fetches metadata and caches the response', async () => {
23+
// Prepare
24+
vi.stubEnv('AWS_LAMBDA_INITIALIZATION_TYPE', 'on-demand');
25+
vi.stubEnv('AWS_LAMBDA_METADATA_API', '127.0.0.1:1234');
26+
vi.stubEnv('AWS_LAMBDA_METADATA_TOKEN', 'test-token');
27+
28+
const payload = { runtime: 'nodejs20.x' };
29+
fetchMock.mockResolvedValue({
30+
ok: true,
31+
json: vi.fn().mockResolvedValue(payload),
32+
});
33+
34+
// Act
35+
const resultA = await getMetadata();
36+
const resultB = await getMetadata();
37+
38+
// Assess
39+
expect(resultA).toEqual(payload);
40+
expect(resultB).toBe(resultA);
41+
expect(fetchMock).toHaveBeenCalledTimes(1);
42+
expect(fetchMock).toHaveBeenCalledWith(
43+
'http://127.0.0.1:1234/2026-01-15/metadata/execution-environment',
44+
expect.objectContaining({
45+
headers: {
46+
Authorization: 'Bearer test-token',
47+
},
48+
signal: expect.any(AbortSignal),
49+
})
50+
);
51+
});
52+
});

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ nav:
5858
- features/parser.md
5959
- features/validation.md
6060
- features/kafka.md
61+
- features/metadata.md
6162
- Environment variables: environment-variables.md
6263
- Upgrade guide: upgrade.md
6364
- Community Content: we_made_this.md
@@ -193,6 +194,7 @@ plugins:
193194
- features/parser.md
194195
- features/validation.md
195196
- features/kafka.md
197+
- features/metadata.md
196198
Environment variables:
197199
- environment-variables.md
198200
Upgrade guide:

packages/commons/package.json

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"name": "Amazon Web Services",
77
"url": "https://aws.amazon.com"
88
},
9+
"license": "MIT-0",
910
"publishConfig": {
1011
"access": "public"
1112
},
@@ -25,7 +26,31 @@
2526
"prepack": "node ../../.github/scripts/release_patch_package_json.js ."
2627
},
2728
"homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/commons#readme",
28-
"license": "MIT-0",
29+
"repository": {
30+
"type": "git",
31+
"url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git"
32+
},
33+
"bugs": {
34+
"url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues"
35+
},
36+
"keywords": [
37+
"aws",
38+
"lambda",
39+
"powertools",
40+
"serverless",
41+
"nodejs"
42+
],
43+
"dependencies": {
44+
"@aws/lambda-invoke-store": "0.2.4"
45+
},
46+
"devDependencies": {
47+
"@aws-lambda-powertools/testing-utils": "file:../testing"
48+
},
49+
"main": "./lib/cjs/index.js",
50+
"types": "./lib/cjs/index.d.ts",
51+
"files": [
52+
"lib"
53+
],
2954
"type": "module",
3055
"exports": {
3156
".": {
@@ -58,6 +83,10 @@
5883
"import": "./lib/esm/envUtils.js",
5984
"require": "./lib/cjs/envUtils.js"
6085
},
86+
"./utils/metadata": {
87+
"import": "./lib/esm/metadata.js",
88+
"require": "./lib/cjs/metadata.js"
89+
},
6190
"./types": {
6291
"import": "./lib/esm/types/index.js",
6392
"require": "./lib/cjs/types/index.js"
@@ -89,6 +118,10 @@
89118
"lib/cjs/envUtils.d.ts",
90119
"lib/esm/envUtils.d.ts"
91120
],
121+
"utils/metadata": [
122+
"lib/cjs/metadata.d.ts",
123+
"lib/esm/metadata.d.ts"
124+
],
92125
"types": [
93126
"lib/cjs/types/index.d.ts",
94127
"lib/esm/types/index.d.ts"
@@ -98,30 +131,5 @@
98131
"lib/esm/deepMerge.d.ts"
99132
]
100133
}
101-
},
102-
"types": "./lib/cjs/index.d.ts",
103-
"main": "./lib/cjs/index.js",
104-
"files": [
105-
"lib"
106-
],
107-
"repository": {
108-
"type": "git",
109-
"url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git"
110-
},
111-
"bugs": {
112-
"url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues"
113-
},
114-
"keywords": [
115-
"aws",
116-
"lambda",
117-
"powertools",
118-
"serverless",
119-
"nodejs"
120-
],
121-
"dependencies": {
122-
"@aws/lambda-invoke-store": "0.2.4"
123-
},
124-
"devDependencies": {
125-
"@aws-lambda-powertools/testing-utils": "file:../testing"
126134
}
127135
}

packages/commons/src/metadata.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { getStringFromEnv, isDevMode } from './envUtils.js';
2+
3+
const metadataCache: Record<string, unknown> = {};
4+
5+
const clearMetadataCache = () => {
6+
for (const key of Object.keys(metadataCache)) {
7+
delete metadataCache[key];
8+
}
9+
};
10+
11+
/**
12+
* Fetches metadata from the AWS Lambda Metadata endpoint.
13+
*
14+
* When not running in a Lambda environment (e.g., during local development), it returns an empty object.
15+
*/
16+
type GetMetadataOptions = {
17+
timeout?: number;
18+
};
19+
20+
const getMetadata = async (options?: GetMetadataOptions) => {
21+
const initType = getStringFromEnv({
22+
key: 'AWS_LAMBDA_INITIALIZATION_TYPE',
23+
defaultValue: 'unknown',
24+
});
25+
26+
if (isDevMode() || initType === 'unknown') {
27+
return {};
28+
}
29+
30+
if (Object.keys(metadataCache).length > 0) {
31+
return metadataCache;
32+
}
33+
34+
const metadataBaseUrl = getStringFromEnv({ key: 'AWS_LAMBDA_METADATA_API' });
35+
const metadataToken = getStringFromEnv({ key: 'AWS_LAMBDA_METADATA_TOKEN' });
36+
37+
const res = await fetch(
38+
`http://${metadataBaseUrl}/2026-01-15/metadata/execution-environment`,
39+
{
40+
headers: {
41+
Authorization: `Bearer ${metadataToken}`,
42+
},
43+
signal: AbortSignal.timeout(options?.timeout ?? 1000),
44+
}
45+
);
46+
if (!res.ok) {
47+
throw new Error(
48+
`Failed to fetch execution environment metadata: ${res.status} ${res.statusText}`
49+
);
50+
}
51+
const data = await res.json();
52+
Object.assign(metadataCache, data);
53+
return metadataCache;
54+
};
55+
56+
export { clearMetadataCache, getMetadata, type GetMetadataOptions };

0 commit comments

Comments
 (0)