Skip to content

Commit 480bb4b

Browse files
authored
feat(lmds): Add support for Lambda Metadata Service (#2424)
* feat(lmds): Add powertools-lambda-metadata package. * Remove unncessary docstring. * Enable HTTP protocol for native tests. * Add docs. * Add powertools-lambda-metadata utility in GH actions workflows paths. * Remove refresh() method from public API surface area. * Move validation logic to getRequiredEnvironmentVar. * Docs for testing your code. * Add E2E tests. * Adjust lmds utility pom.xml to modernized tracing agent config. * Fix E2E tests to return proper json for parsing in test suite. * Print function error on failure in E2E tests. * Add reflect-config.json for powertools-lambda-metadata.
1 parent d8a3e08 commit 480bb4b

File tree

33 files changed

+9814
-6
lines changed

33 files changed

+9814
-6
lines changed

.github/workflows/check-build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ on:
3030
- 'powertools-tracing/**'
3131
- 'powertools-tracing/**'
3232
- 'powertools-validation/**'
33+
- 'powertools-lambda-metadata/**'
3334
- 'examples/**'
3435
- 'pom.xml'
3536
- 'examples/pom.xml'
@@ -54,6 +55,7 @@ on:
5455
- 'powertools-tracing/**'
5556
- 'powertools-tracing/**'
5657
- 'powertools-validation/**'
58+
- 'powertools-lambda-metadata/**'
5759
- 'pom.xml'
5860
- 'examples/**'
5961
- 'examples/pom.xml'

.github/workflows/check-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ on:
3131
- 'powertools-tracing/**'
3232
- 'powertools-tracing/**'
3333
- 'powertools-validation/**'
34+
- 'powertools-lambda-metadata/**'
3435
- 'pom.xml'
3536

3637
name: E2E Tests

.github/workflows/check-spotbugs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ on:
2828
- 'powertools-tracing/**'
2929
- 'powertools-validation/**'
3030
- 'powertools-test-suite/**'
31+
- 'powertools-lambda-metadata/**'
3132
- 'pom.xml'
3233
- '.github/workflows/**'
3334

docs/utilities/lambda_metadata.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
---
2+
title: Lambda Metadata
3+
description: Utility
4+
---
5+
6+
Lambda Metadata provides idiomatic access to the Lambda Metadata Endpoint (LMDS), eliminating boilerplate code for retrieving execution environment metadata like Availability Zone ID.
7+
8+
## Key features
9+
10+
* Retrieve Lambda execution environment metadata with a single method call
11+
* Automatic caching for the sandbox lifetime, avoiding repeated HTTP calls
12+
* Thread-safe access for concurrent executions (compatible with [Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank"})
13+
* Automatic [SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html){target="_blank"} cache invalidation via [CRaC](https://openjdk.org/projects/crac/){target="_blank"} integration
14+
* Lightweight with minimal external dependencies, using built-in `HttpURLConnection`
15+
* GraalVM support
16+
17+
## Getting started
18+
19+
### Installation
20+
21+
=== "Maven"
22+
23+
```xml hl_lines="3-7"
24+
<dependencies>
25+
...
26+
<dependency>
27+
<groupId>software.amazon.lambda</groupId>
28+
<artifactId>powertools-lambda-metadata</artifactId>
29+
<version>{{ powertools.version }}</version>
30+
</dependency>
31+
...
32+
</dependencies>
33+
```
34+
35+
=== "Gradle"
36+
37+
```groovy hl_lines="6"
38+
repositories {
39+
mavenCentral()
40+
}
41+
42+
dependencies {
43+
implementation 'software.amazon.lambda:powertools-lambda-metadata:{{ powertools.version }}'
44+
}
45+
46+
sourceCompatibility = 11
47+
targetCompatibility = 11
48+
```
49+
50+
### IAM Permissions
51+
52+
No additional IAM permissions are required. The Lambda Metadata Endpoint is available within the Lambda execution environment and uses a Bearer token provided automatically via environment variables.
53+
54+
### Basic usage
55+
56+
Retrieve metadata using `LambdaMetadataClient.get()`:
57+
58+
=== "App.java"
59+
60+
```java hl_lines="1 2 9 10"
61+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
62+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
63+
64+
public class App implements RequestHandler<Object, String> {
65+
66+
@Override
67+
public String handleRequest(Object input, Context context) {
68+
// Fetch metadata (automatically cached after first call)
69+
LambdaMetadata metadata = LambdaMetadataClient.get();
70+
String azId = metadata.getAvailabilityZoneId(); // e.g., "use1-az1"
71+
72+
return "{\"az\": \"" + azId + "\"}";
73+
}
74+
}
75+
```
76+
77+
!!! info "At launch, only `availabilityZoneId` is available. The API is designed to support additional metadata fields as LMDS evolves."
78+
79+
### Caching behavior
80+
81+
Metadata is **cached automatically** after the first call. Subsequent calls return the cached value without making HTTP requests.
82+
83+
=== "CachingExample.java"
84+
85+
```java hl_lines="9 12"
86+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
87+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
88+
89+
public class CachingExample implements RequestHandler<Object, String> {
90+
91+
@Override
92+
public String handleRequest(Object input, Context context) {
93+
// First call: fetches from endpoint and caches
94+
LambdaMetadata metadata = LambdaMetadataClient.get();
95+
96+
// Subsequent calls: returns cached value (no HTTP call)
97+
LambdaMetadata metadataAgain = LambdaMetadataClient.get();
98+
99+
// Both return the same cached instance
100+
assert metadata == metadataAgain;
101+
102+
return "{\"az\": \"" + metadata.getAvailabilityZoneId() + "\"}";
103+
}
104+
}
105+
```
106+
107+
This is safe because metadata (like Availability Zone) never changes during a sandbox's lifetime.
108+
109+
## Advanced
110+
111+
### Eager loading at module level
112+
113+
For predictable latency, fetch metadata at class initialization:
114+
115+
=== "EagerLoadingExample.java"
116+
117+
```java hl_lines="7"
118+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
119+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
120+
121+
public class EagerLoadingExample implements RequestHandler<Object, String> {
122+
123+
// Fetch during cold start (class loading)
124+
private static final LambdaMetadata METADATA = LambdaMetadataClient.get();
125+
126+
@Override
127+
public String handleRequest(Object input, Context context) {
128+
// No latency hit here - already cached
129+
return "{\"az\": \"" + METADATA.getAvailabilityZoneId() + "\"}";
130+
}
131+
}
132+
```
133+
134+
#### SnapStart considerations
135+
136+
When using [SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html){target="_blank"}, the function may restore in a different Availability Zone. The utility automatically handles this by registering with CRaC to invalidate the cache after restore.
137+
138+
Using the same eager loading pattern above, the cache is automatically invalidated on SnapStart restore, ensuring subsequent calls to `LambdaMetadataClient.get()` return refreshed metadata.
139+
140+
!!! note "For module-level usage with SnapStart, ensure `LambdaMetadataClient` is referenced during initialization so the CRaC hook registers before the snapshot is taken."
141+
142+
### Lambda Managed Instances
143+
144+
For [Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank"} (multi-threaded concurrency), no changes are needed. The utility uses thread-safe caching with `AtomicReference` to ensure correct behavior across concurrent executions on the same instance.
145+
146+
=== "ManagedInstanceHandler.java"
147+
148+
```java hl_lines="9"
149+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
150+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
151+
152+
public class ManagedInstanceHandler implements RequestHandler<Object, String> {
153+
154+
@Override
155+
public String handleRequest(Object input, Context context) {
156+
// Thread-safe: multiple concurrent invocations safely share cached metadata
157+
LambdaMetadata metadata = LambdaMetadataClient.get();
158+
return "{\"az\": \"" + metadata.getAvailabilityZoneId() + "\"}";
159+
}
160+
}
161+
```
162+
163+
### Error handling
164+
165+
The utility throws `LambdaMetadataException` when the metadata endpoint is unavailable or returns an error:
166+
167+
=== "ErrorHandlingExample.java"
168+
169+
```java hl_lines="2 7 14 18 21"
170+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
171+
import software.amazon.lambda.powertools.metadata.exception.LambdaMetadataException;
172+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
173+
import software.amazon.lambda.powertools.logging.Logging;
174+
import org.slf4j.Logger;
175+
import org.slf4j.LoggerFactory;
176+
import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry;
177+
178+
public class ErrorHandlingExample implements RequestHandler<Object, String> {
179+
180+
private static final Logger LOG = LoggerFactory.getLogger(ErrorHandlingExample.class);
181+
182+
@Override
183+
@Logging
184+
public String handleRequest(Object input, Context context) {
185+
String az;
186+
try {
187+
LambdaMetadata metadata = LambdaMetadataClient.get();
188+
az = metadata.getAvailabilityZoneId();
189+
} catch (LambdaMetadataException e) {
190+
LOG.warn("Could not fetch metadata", entry("statusCode", e.getStatusCode()), entry("error", e.getMessage()));
191+
az = "unknown";
192+
}
193+
194+
return "{\"az\": \"" + az + "\"}";
195+
}
196+
}
197+
```
198+
199+
## Testing your code
200+
201+
When running outside a Lambda execution environment (e.g., in unit tests), the `AWS_LAMBDA_METADATA_API` and `AWS_LAMBDA_METADATA_TOKEN` environment variables are not available. Calling `LambdaMetadataClient.get()` in this context throws a `LambdaMetadataException`.
202+
203+
### Mocking LambdaMetadataClient
204+
205+
For tests where you need to control the metadata values, use Mockito's `mockStatic` to mock `LambdaMetadataClient.get()`:
206+
207+
=== "MockedMetadataTest.java"
208+
209+
```java hl_lines="15-17"
210+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
211+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
212+
import org.mockito.MockedStatic;
213+
import org.junit.jupiter.api.Test;
214+
import static org.assertj.core.api.Assertions.assertThat;
215+
import static org.mockito.Mockito.*;
216+
217+
class MockedMetadataTest {
218+
219+
@Test
220+
void shouldUseMetadataInHandler() {
221+
LambdaMetadata mockMetadata = mock(LambdaMetadata.class);
222+
when(mockMetadata.getAvailabilityZoneId()).thenReturn("use1-az1");
223+
224+
try (MockedStatic<LambdaMetadataClient> mockedClient =
225+
mockStatic(LambdaMetadataClient.class)) {
226+
mockedClient.when(LambdaMetadataClient::get).thenReturn(mockMetadata);
227+
228+
App handler = new App();
229+
String result = handler.handleRequest(null, null);
230+
231+
assertThat(result).contains("use1-az1");
232+
}
233+
}
234+
}
235+
```
236+
237+
### Using WireMock
238+
239+
For integration tests, you can use [WireMock](https://wiremock.org/){target="_blank"} to mock the metadata HTTP endpoint. Set `AWS_LAMBDA_METADATA_API` and `AWS_LAMBDA_METADATA_TOKEN` environment variables using [junit-pioneer](https://junit-pioneer.org/docs/environment-variables/){target="_blank"}, and stub the endpoint response:
240+
241+
=== "WireMockMetadataTest.java"
242+
243+
```java hl_lines="10-12"
244+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
245+
import static org.assertj.core.api.Assertions.assertThat;
246+
247+
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
248+
import org.junitpioneer.jupiter.SetEnvironmentVariable;
249+
import org.junit.jupiter.api.Test;
250+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
251+
import software.amazon.lambda.powertools.metadata.internal.LambdaMetadataHttpClient;
252+
253+
@WireMockTest(httpPort = 8089)
254+
@SetEnvironmentVariable(key = "AWS_LAMBDA_METADATA_API", value = "localhost:8089")
255+
@SetEnvironmentVariable(key = "AWS_LAMBDA_METADATA_TOKEN", value = "test-token")
256+
class WireMockMetadataTest {
257+
258+
@Test
259+
void shouldFetchMetadataFromEndpoint() {
260+
stubFor(get(urlEqualTo("/2026-01-15/metadata/execution-environment"))
261+
.withHeader("Authorization", equalTo("Bearer test-token"))
262+
.willReturn(aResponse()
263+
.withStatus(200)
264+
.withHeader("Content-Type", "application/json")
265+
.withBody("{\"AvailabilityZoneID\": \"use1-az1\"}")));
266+
267+
LambdaMetadataHttpClient client = new LambdaMetadataHttpClient();
268+
LambdaMetadata metadata = client.fetchMetadata();
269+
270+
assertThat(metadata.getAvailabilityZoneId()).isEqualTo("use1-az1");
271+
}
272+
}
273+
```
274+
275+
## Using with other Powertools utilities
276+
277+
Lambda Metadata integrates seamlessly with other Powertools utilities to enrich your observability data with Availability Zone information.
278+
279+
=== "IntegratedExample.java"
280+
281+
```java
282+
import software.amazon.lambda.powertools.logging.Logging;
283+
import software.amazon.lambda.powertools.tracing.Tracing;
284+
import software.amazon.lambda.powertools.tracing.TracingUtils;
285+
import software.amazon.lambda.powertools.metrics.FlushMetrics;
286+
import software.amazon.lambda.powertools.metrics.Metrics;
287+
import software.amazon.lambda.powertools.metrics.MetricsFactory;
288+
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
289+
import software.amazon.lambda.powertools.metadata.LambdaMetadata;
290+
import software.amazon.lambda.powertools.metadata.LambdaMetadataClient;
291+
import org.slf4j.Logger;
292+
import org.slf4j.LoggerFactory;
293+
import org.slf4j.MDC;
294+
295+
public class IntegratedExample implements RequestHandler<Object, String> {
296+
297+
private static final Logger LOG = LoggerFactory.getLogger(IntegratedExample.class);
298+
private static final Metrics metrics = MetricsFactory.getMetricsInstance();
299+
300+
@Logging
301+
@Tracing
302+
@FlushMetrics(captureColdStart = true)
303+
@Override
304+
public String handleRequest(Object input, Context context) {
305+
LambdaMetadata metadata = LambdaMetadataClient.get();
306+
String azId = metadata.getAvailabilityZoneId();
307+
308+
// Add AZ as dimension for all metrics
309+
metrics.addDimension("availability_zone_id", azId);
310+
311+
// Add AZ to structured logs
312+
MDC.put("availability_zone_id", azId);
313+
LOG.info("Processing request");
314+
315+
// Add AZ to traces
316+
TracingUtils.putAnnotation("availability_zone_id", azId);
317+
318+
// Add metrics
319+
metrics.addMetric("RequestProcessed", 1, MetricUnit.COUNT);
320+
321+
return "{\"status\": \"ok\"}";
322+
}
323+
}
324+
```

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ nav:
2222
- utilities/validation.md
2323
- utilities/custom_resources.md
2424
- utilities/serialization.md
25+
- utilities/lambda_metadata.md
2526
- Processes:
2627
- processes/maintainers.md
2728
- "Versioning policy": processes/versioning.md
@@ -114,6 +115,7 @@ plugins:
114115
- utilities/batch.md
115116
- utilities/kafka.md
116117
- utilities/large_messages.md
118+
- utilities/lambda_metadata.md
117119
- utilities/validation.md
118120
- utilities/custom_resources.md
119121
- utilities/serialization.md

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<module>powertools-e2e-tests</module>
7171
<module>powertools-e2e-tests/handlers</module>
7272
<module>powertools-batch</module>
73+
<module>powertools-lambda-metadata</module>
7374
<module>powertools-parameters/powertools-parameters-ssm</module>
7475
<module>powertools-parameters/powertools-parameters-secrets</module>
7576
<module>powertools-parameters/powertools-parameters-dynamodb</module>

0 commit comments

Comments
 (0)