Skip to content

Commit 6c6b703

Browse files
committed
chore: add e2e lambda lifecycle test and workflow
1 parent f0c6f88 commit 6c6b703

14 files changed

Lines changed: 1583 additions & 9 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: e2e
2+
3+
# End-to-end AWS Lambda instrumentation suite. Path-filtered so it only runs when
4+
# the construct or the suite changes, and restricted to the canonical repo so forks
5+
# (which lack OIDC) no-op. When it runs, the AWS OIDC / dd-sts steps must succeed --
6+
# an auth/federation failure fails the job loudly. See e2e/README.md for the contract.
7+
on:
8+
push:
9+
branches: [main]
10+
paths:
11+
- "src/**"
12+
- "e2e/**"
13+
- "package.json"
14+
- "yarn.lock"
15+
- ".github/workflows/e2e.yml"
16+
pull_request:
17+
paths:
18+
- "src/**"
19+
- "e2e/**"
20+
- "package.json"
21+
- "yarn.lock"
22+
- ".github/workflows/e2e.yml"
23+
24+
# One run per ref; cancel superseded runs so we never have two suites racing for
25+
# the same shared account budget.
26+
concurrency:
27+
group: e2e-${{ github.ref }}
28+
cancel-in-progress: true
29+
30+
jobs:
31+
lambda-e2e:
32+
# Skip on forks: OIDC federation only exists on the canonical repo.
33+
if: github.repository == 'DataDog/datadog-cdk-constructs'
34+
runs-on: ubuntu-latest
35+
permissions:
36+
id-token: write
37+
contents: read
38+
env:
39+
AWS_REGION: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }}
40+
DD_SITE: ${{ vars.DD_SITE_E2E || 'datadoghq.com' }}
41+
SKIP_LAMBDA_TESTS: ${{ vars.SKIP_LAMBDA_TESTS || 'false' }}
42+
steps:
43+
- name: Checkout
44+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
45+
46+
# Enable Corepack before setup-node so its package-manager cache step detects
47+
# the yarn berry version from package.json instead of falling back to yarn 1.
48+
- name: Enable Corepack
49+
run: corepack enable
50+
51+
- name: Set up Node 24
52+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
53+
with:
54+
node-version: 24
55+
56+
- name: Cache Node modules
57+
id: cache-node-modules
58+
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
59+
with:
60+
path: "**/node_modules"
61+
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
62+
63+
- name: Install dependencies
64+
if: steps.cache-node-modules.outputs.cache-hit != 'true'
65+
run: yarn install --immutable
66+
67+
# Dedicated GitHub-OIDC role in the serverless sandbox account, scoped to deploy
68+
# one-e2e-cdk-lambda-* via the CDK bootstrap roles. See serverless-ci/e2e/iam-infra.md.
69+
- name: Configure AWS credentials via OIDC
70+
if: env.SKIP_LAMBDA_TESTS != 'true'
71+
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
72+
with:
73+
role-to-assume: ${{ vars.AWS_ROLE_ARN_E2E }}
74+
aws-region: ${{ env.AWS_REGION }}
75+
76+
# Short-lived Datadog API + App keys via OIDC federation (dd-sts), governed by the
77+
# policy in dd-source. No static Datadog keys are stored in this repo.
78+
- name: Get Datadog credentials (dd-sts)
79+
id: dd-sts
80+
if: env.SKIP_LAMBDA_TESTS != 'true'
81+
uses: DataDog/dd-sts-action@2e8187910199bd93129520183c093e19aa585c75 # v1.0.0
82+
with:
83+
policy: datadog-cdk-constructs-e2e
84+
85+
# The account is already CDK-bootstrapped; deploy reads the bootstrap version
86+
# from SSM, so no bootstrap step is needed.
87+
- name: Resolve account id
88+
if: env.SKIP_LAMBDA_TESTS != 'true'
89+
run: echo "CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)" >> "$GITHUB_ENV"
90+
91+
- name: Run Lambda e2e suite
92+
run: yarn test:e2e
93+
env:
94+
# Baked into the function at synth + used by the construct/extension.
95+
DD_API_KEY: ${{ steps.dd-sts.outputs.api_key }}
96+
# Used by the telemetry checker to query spans/logs.
97+
DATADOG_API_KEY: ${{ steps.dd-sts.outputs.api_key }}
98+
DATADOG_APP_KEY: ${{ steps.dd-sts.outputs.app_key }}

.gitignore

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.npmignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.prettierignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Synced from serverless-ci/e2e/shared by the e2e-shared sync. Generated code is not
2+
# hand-formatted here -- the source-of-truth style can't satisfy every consumer's
3+
# prettier config, so the formatter skips these files.
4+
e2e/helpers/exec.ts
5+
e2e/helpers/naming.ts
6+
e2e/helpers/lambda-telemetry-checker.ts
7+
e2e/helpers/lambda-verifier.ts

.projen/deps.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/tasks.json

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projenrc.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
4848
"esbuild",
4949
"standard-version",
5050
"@aws-cdk/aws-lambda-python-alpha@^2.134.0-alpha.0",
51+
// e2e suite (runs out of e2e/, separate from the jsii-packaged library)
52+
"vitest@^3.2.4",
53+
"@datadog/datadog-api-client@^1.34.1",
5154
],
5255
gitignore: [
5356
"*.js",
@@ -64,6 +67,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
6467
".DS_Store",
6568
"integration_tests/cdk.out",
6669
"integration_tests/testlib",
70+
"e2e/cdk.out",
71+
"e2e/.build",
72+
"e2e/.env.local",
6773
"bin",
6874
"obj",
6975
"__pycache__",
@@ -76,6 +82,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
7682
"!NOTICE",
7783
"/scripts",
7884
"/integration_tests",
85+
"/e2e",
7986
".prettierrc",
8087
"/.ncurc.cjs",
8188
"cdk.out/*",
@@ -85,7 +92,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
8592
"/examples",
8693
],
8794
scripts: {
88-
"check-formatting": "prettier --check src/**/*.ts integration_tests/**/*.ts examples/**/*.ts",
95+
"check-formatting": "prettier --check src/**/*.ts integration_tests/**/*.ts examples/**/*.ts e2e/**/*.ts",
96+
// Runs the AWS Lambda e2e lifecycle suite. Requires cloud auth + DD keys; see e2e/README.md.
97+
"test:e2e": "vitest run --config e2e/vitest.config.ts e2e",
8998
},
9099
pullRequestTemplate: false,
91100
dependabot: false,

e2e/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Lambda e2e suite
2+
3+
End-to-end test for the `DatadogLambda` construct against real AWS, conforming to the
4+
serverless instrumentation e2e contract (`serverless-ci/e2e/spec.md`). It deploys an
5+
uninstrumented Node.js Lambda, applies the construct, proves telemetry flows to
6+
Datadog, asserts re-apply is idempotent, removes instrumentation, and verifies a clean
7+
end-state -- tearing the function down regardless of outcome.
8+
9+
## What it does
10+
11+
The construct is the instrumentation mechanism, so APPLY is `cdk deploy` of the
12+
stack (`app/app.ts`) with `E2E_INSTRUMENT=true`; REMOVE is `cdk destroy` of that
13+
same stack, leaving a clean (no-function) end-state:
14+
15+
1. **Provision** -- deploy the workload uninstrumented (`E2E_INSTRUMENT=false`),
16+
uniquely named `one-e2e-cdk-lambda-<runid>` and freshness-tagged at creation.
17+
2. **APPLY** -- deploy with `E2E_INSTRUMENT=true`; verify config: pinned Datadog Node
18+
+ Extension layers, handler redirected to the Datadog wrapper, and the required
19+
`DD_*` env vars asserted by identity (`DD_SERVICE`, `DD_ENV`, `DD_VERSION`,
20+
`DD_SITE`, `DD_API_KEY`, `DD_TRACE_ENABLED`, `DD_LOGS_INJECTION`, run id in
21+
`DD_TAGS`).
22+
3. **Trigger** -- `aws lambda invoke`, then poll Datadog spans + logs filtered by the
23+
run id and assert each carries the expected `service`/`env`/`version`/run id.
24+
4. **Re-APPLY** -- `cdk diff --fail`; assert no diff (idempotent).
25+
5. **REMOVE** -- `cdk destroy` the stack; assert the function is gone
26+
(`get-function-configuration` returns `ResourceNotFoundException`), proving a clean
27+
end-state.
28+
29+
Pinned artifact versions live in `helpers/versions.ts`; bump them deliberately so a
30+
failure blames this construct's wiring, not an upstream layer/tracer change.
31+
32+
## Prerequisites
33+
34+
- **AWS auth** with permission to deploy Lambda + CloudFormation in a
35+
CDK-bootstrapped account/region. Locally:
36+
`aws-vault exec sso-serverless-sandbox-account-admin -- yarn test:e2e`.
37+
Bootstrap once per account/region if needed: `npx cdk bootstrap`.
38+
- **Datadog keys** for the org telemetry lands in:
39+
- `DD_API_KEY` -- baked into the function (used by the construct + extension).
40+
- `DATADOG_API_KEY` / `DATADOG_APP_KEY` -- used by the telemetry checker to query
41+
spans and logs. (`DD_API_KEY` / `DD_APP_KEY` are accepted as fallbacks.)
42+
- **`DD_SITE`** -- defaults to `datadoghq.com`; set to match the key's org.
43+
- Node 22+ and `yarn install`.
44+
45+
## Run
46+
47+
```bash
48+
# full lifecycle (real deploys + telemetry; ~10-15 min)
49+
# Datadog auth: dd-auth mints short-lived keys for the org -- no pasted keys.
50+
# It injects $DD_API_KEY and $DD_APP_KEY into the wrapped command only; DD_API_KEY
51+
# is baked into the function, and DATADOG_API_KEY/DATADOG_APP_KEY feed the checker.
52+
aws-vault exec sso-serverless-sandbox-account-admin -- \
53+
dd-auth --domain app.datadoghq.com -- bash -c '
54+
export DATADOG_API_KEY="$DD_API_KEY" DATADOG_APP_KEY="$DD_APP_KEY"
55+
yarn test:e2e
56+
'
57+
58+
# skip (no-op) -- what forks/CI without secrets do
59+
SKIP_LAMBDA_TESTS=true yarn test:e2e
60+
```
61+
62+
## CI
63+
64+
`.github/workflows/e2e.yml` runs this behind path filters (construct or suite
65+
changes) with `SKIP_LAMBDA_TESTS` as a kill switch. AWS access is via GitHub OIDC
66+
into the dedicated `gha-datadog-cdk-e2e` role in the serverless sandbox account
67+
(`arn:aws:iam::425362996713:role/gha-datadog-cdk-e2e`), scoped to deploy
68+
`one-e2e-cdk-lambda-*` through the CDK bootstrap roles. Config comes from repo
69+
variables (`AWS_ROLE_ARN_E2E`, `AWS_REGION_E2E`, `DD_SITE_E2E`); telemetry keys are minted
70+
at runtime via [`DataDog/dd-sts-action`](https://github.com/DataDog/dd-sts-action) under the
71+
`datadog-cdk-constructs-e2e` policy (GitHub OIDC → short-lived Datadog API + App keys), so no
72+
static Datadog keys live in this repo. When the construct or suite changes the suite runs for
73+
real and the AWS OIDC / dd-sts steps must succeed -- an auth failure fails the job loudly. The
74+
IAM resources are cataloged in `serverless-ci/e2e/iam-infra.md`.
75+
76+
## Hygiene
77+
78+
Every resource is named `one-e2e-cdk-lambda-<runid>` and tagged
79+
`one_e2e_created:<unix-ts>` at creation. The in-test teardown is best-effort; the
80+
cross-repo sweeper is the real guarantee, reaping any `one-e2e-` resource older than
81+
the grace window.

e2e/helpers/e2e.config.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed
3+
* under the Apache License Version 2.0.
4+
*
5+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
6+
* Copyright 2026 Datadog, Inc.
7+
*/
8+
9+
import { type E2ENaming } from "./naming";
10+
import { type ExpectedLayers, type LambdaVerifierConfig } from "./lambda-verifier";
11+
import { E2E_EXTENSION_LAYER_VERSION, E2E_NODE_LAYER_VERSION } from "./versions";
12+
13+
// Repo-local config feeding the shared e2e helpers. This file is NOT synced -- it holds
14+
// everything specific to datadog-cdk-constructs that the shared, parameterized helpers
15+
// read through their config arguments.
16+
17+
export const NAMING: E2ENaming = { tool: "cdk", platform: "lambda" };
18+
19+
export const ENV_NAME = process.env.E2E_ENV ?? "e2e";
20+
export const ENV_VERSION = process.env.E2E_VERSION ?? "1.0.0";
21+
22+
// Datadog's public layer account (commercial, non-GovCloud). The e2e defaults to ap-northeast-3.
23+
const DD_ACCOUNT_ID = "464622532012";
24+
25+
// Transient cloud-provider errors safe to retry, passed as ExecOptions.retryPatterns.
26+
export const RETRY_PATTERNS = [
27+
"RequestTimeout",
28+
"Throttling",
29+
"TooManyRequests",
30+
"Rate exceeded",
31+
"ServiceUnavailable",
32+
"InternalFailure",
33+
"ResourceConflictException",
34+
"OperationAbortedException",
35+
"ETIMEDOUT",
36+
"ECONNRESET",
37+
"EAI_AGAIN",
38+
"Connection reset",
39+
"timed out",
40+
"UPDATE_IN_PROGRESS",
41+
];
42+
43+
// The CDK app names the function after the run-unique service name, so the deployed
44+
// function name is the service name itself.
45+
export const functionName = (serviceName: string): string => serviceName;
46+
47+
// Pinned artifact versions come from this repo's e2e/helpers/versions.ts, so a version
48+
// mismatch blames the construct's wiring, not upstream layer drift.
49+
const expectedLayerArns = (region: string): ExpectedLayers => ({
50+
node: `arn:aws:lambda:${region}:${DD_ACCOUNT_ID}:layer:Datadog-Node22-x:${E2E_NODE_LAYER_VERSION}`,
51+
extension: `arn:aws:lambda:${region}:${DD_ACCOUNT_ID}:layer:Datadog-Extension:${E2E_EXTENSION_LAYER_VERSION}`,
52+
});
53+
54+
export const VERIFIER: LambdaVerifierConfig = {
55+
functionName,
56+
expectedLayerArns,
57+
redirectHandler: "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler",
58+
originalHandler: "index.handler",
59+
// The construct tags every instrumented function with its own marker tag.
60+
toolTag: { key: "dd_cdk_construct", pattern: /.+/ },
61+
env: {
62+
apiKeyVars: ["DD_API_KEY", "DD_API_KEY_SECRET_ARN", "DD_KMS_API_KEY", "DD_API_KEY_SSM_ARN"],
63+
present: ["DD_SITE"],
64+
values: (serviceName) => ({
65+
DD_SERVICE: serviceName,
66+
DD_ENV: ENV_NAME,
67+
DD_VERSION: ENV_VERSION,
68+
DD_TRACE_ENABLED: "true",
69+
// With the extension, log collection is enabled via DD_SERVERLESS_LOGS_ENABLED;
70+
// the construct intentionally forces DD_LOGS_INJECTION=false in this path.
71+
DD_SERVERLESS_LOGS_ENABLED: "true",
72+
DD_LOGS_INJECTION: "false",
73+
DD_LAMBDA_HANDLER: "index.handler",
74+
}),
75+
},
76+
};

e2e/helpers/versions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed
3+
* under the Apache License Version 2.0.
4+
*
5+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
6+
* Copyright 2021 Datadog, Inc.
7+
*/
8+
9+
import * as lambda from "aws-cdk-lib/aws-lambda";
10+
11+
// Pinned artifact versions. Pinning the layer/extension versions (and the single
12+
// canonical runtime) means an e2e failure blames this construct's wiring, not an
13+
// upstream layer/tracer change. Bump deliberately. See spec.md ("Rules").
14+
export const E2E_RUNTIME = lambda.Runtime.NODEJS_22_X;
15+
16+
// `Datadog-Node22-x` layer version.
17+
export const E2E_NODE_LAYER_VERSION = 130;
18+
// `Datadog-Extension` layer version.
19+
export const E2E_EXTENSION_LAYER_VERSION = 83;

0 commit comments

Comments
 (0)