Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: e2e

# End-to-end AWS Lambda instrumentation suite. Path-filtered so it only runs when
# the construct or the suite changes, and restricted to the canonical repo so forks
# (which lack OIDC) no-op. When it runs, the AWS OIDC / dd-sts steps must succeed --
# an auth/federation failure fails the job loudly. See e2e/README.md for the contract.
on:
push:
branches: [main]
paths:
- "src/**"
- "e2e/**"
- "package.json"
- "yarn.lock"
- ".github/workflows/e2e.yml"
pull_request:
paths:
- "src/**"
- "e2e/**"
- "package.json"
- "yarn.lock"
- ".github/workflows/e2e.yml"

# One run per ref; cancel superseded runs so we never have two suites racing for
# the same shared account budget.
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
lambda-e2e:
# Skip on forks: OIDC federation only exists on the canonical repo.
if: github.repository == 'DataDog/datadog-cdk-constructs'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
env:
AWS_REGION: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }}
DD_SITE: ${{ vars.DD_SITE_E2E || 'datadoghq.com' }}
SKIP_LAMBDA_TESTS: ${{ vars.SKIP_LAMBDA_TESTS || 'false' }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

# Enable Corepack before setup-node so its package-manager cache step detects
# the yarn berry version from package.json instead of falling back to yarn 1.
- name: Enable Corepack
run: corepack enable

- name: Set up Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 24

- name: Cache Node modules
id: cache-node-modules
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}

- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn install --immutable

# Dedicated GitHub-OIDC role in the serverless sandbox account, scoped to deploy
# one-e2e-cdk-lambda-* via the CDK bootstrap roles. See serverless-ci/e2e/iam-infra.md.
- name: Configure AWS credentials via OIDC
if: env.SKIP_LAMBDA_TESTS != 'true'
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN_E2E }}
aws-region: ${{ env.AWS_REGION }}

# Short-lived Datadog API + App keys via OIDC federation (dd-sts), governed by the
# policy in dd-source. No static Datadog keys are stored in this repo.
- name: Get Datadog credentials (dd-sts)
id: dd-sts
if: env.SKIP_LAMBDA_TESTS != 'true'
uses: DataDog/dd-sts-action@2e8187910199bd93129520183c093e19aa585c75 # v1.0.0
with:
policy: datadog-cdk-constructs-e2e

# The account is already CDK-bootstrapped; deploy reads the bootstrap version
# from SSM, so no bootstrap step is needed.
- name: Resolve account id
if: env.SKIP_LAMBDA_TESTS != 'true'
run: echo "CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)" >> "$GITHUB_ENV"

- name: Run Lambda e2e suite
run: yarn test:e2e
env:
# Baked into the function at synth + used by the construct/extension.
DD_API_KEY: ${{ steps.dd-sts.outputs.api_key }}
# Used by the telemetry checker to query spans/logs.
DATADOG_API_KEY: ${{ steps.dd-sts.outputs.api_key }}
DATADOG_APP_KEY: ${{ steps.dd-sts.outputs.app_key }}
3 changes: 3 additions & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .npmignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Synced from serverless-ci/e2e/shared by the e2e-shared sync. Generated code is not
# hand-formatted here -- the source-of-truth style can't satisfy every consumer's
# prettier config, so the formatter skips these files.
e2e/helpers/exec.ts
e2e/helpers/naming.ts
e2e/helpers/lambda-telemetry-checker.ts
e2e/helpers/lambda-verifier.ts
10 changes: 10 additions & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
"esbuild",
"standard-version",
"@aws-cdk/aws-lambda-python-alpha@^2.134.0-alpha.0",
// e2e suite (runs out of e2e/, separate from the jsii-packaged library)
"vitest@^3.2.4",
"@datadog/datadog-api-client@^1.34.1",
],
gitignore: [
"*.js",
Expand All @@ -64,6 +67,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
".DS_Store",
"integration_tests/cdk.out",
"integration_tests/testlib",
"e2e/cdk.out",
"e2e/.build",
"e2e/.env.local",
"bin",
"obj",
"__pycache__",
Expand All @@ -76,6 +82,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
"!NOTICE",
"/scripts",
"/integration_tests",
"/e2e",
".prettierrc",
"/.ncurc.cjs",
"cdk.out/*",
Expand All @@ -85,7 +92,9 @@ const project = new awscdk.AwsCdkConstructLibrary({
"/examples",
],
scripts: {
"check-formatting": "prettier --check src/**/*.ts integration_tests/**/*.ts examples/**/*.ts",
"check-formatting": "prettier --check src/**/*.ts integration_tests/**/*.ts examples/**/*.ts e2e/**/*.ts",
// Runs the AWS Lambda e2e lifecycle suite. Requires cloud auth + DD keys; see e2e/README.md.
"test:e2e": "vitest run --config e2e/vitest.config.ts e2e",
},
pullRequestTemplate: false,
dependabot: false,
Expand Down
81 changes: 81 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Lambda e2e suite

End-to-end test for the `DatadogLambda` construct against real AWS, conforming to the
serverless instrumentation e2e contract (`serverless-ci/e2e/spec.md`). It deploys an
uninstrumented Node.js Lambda, applies the construct, proves telemetry flows to
Datadog, asserts re-apply is idempotent, removes instrumentation, and verifies a clean
end-state -- tearing the function down regardless of outcome.

## What it does

The construct is the instrumentation mechanism, so APPLY is `cdk deploy` of the
stack (`app/app.ts`) with `E2E_INSTRUMENT=true`; REMOVE is `cdk destroy` of that
same stack, leaving a clean (no-function) end-state:

1. **Provision** -- deploy the workload uninstrumented (`E2E_INSTRUMENT=false`),
uniquely named `one-e2e-cdk-lambda-<runid>` and freshness-tagged at creation.
2. **APPLY** -- deploy with `E2E_INSTRUMENT=true`; verify config: pinned Datadog Node
+ Extension layers, handler redirected to the Datadog wrapper, and the required
`DD_*` env vars asserted by identity (`DD_SERVICE`, `DD_ENV`, `DD_VERSION`,
`DD_SITE`, `DD_API_KEY`, `DD_TRACE_ENABLED`, `DD_LOGS_INJECTION`, run id in
`DD_TAGS`).
3. **Trigger** -- `aws lambda invoke`, then poll Datadog spans + logs filtered by the
run id and assert each carries the expected `service`/`env`/`version`/run id.
4. **Re-APPLY** -- `cdk diff --fail`; assert no diff (idempotent).
5. **REMOVE** -- `cdk destroy` the stack; assert the function is gone
(`get-function-configuration` returns `ResourceNotFoundException`), proving a clean
end-state.

Pinned artifact versions live in `helpers/versions.ts`; bump them deliberately so a
failure blames this construct's wiring, not an upstream layer/tracer change.

## Prerequisites

- **AWS auth** with permission to deploy Lambda + CloudFormation in a
CDK-bootstrapped account/region. Locally:
`aws-vault exec sso-serverless-sandbox-account-admin -- yarn test:e2e`.
Bootstrap once per account/region if needed: `npx cdk bootstrap`.
- **Datadog keys** for the org telemetry lands in:
- `DD_API_KEY` -- baked into the function (used by the construct + extension).
- `DATADOG_API_KEY` / `DATADOG_APP_KEY` -- used by the telemetry checker to query
spans and logs. (`DD_API_KEY` / `DD_APP_KEY` are accepted as fallbacks.)
- **`DD_SITE`** -- defaults to `datadoghq.com`; set to match the key's org.
- Node 22+ and `yarn install`.

## Run

```bash
# full lifecycle (real deploys + telemetry; ~10-15 min)
# Datadog auth: dd-auth mints short-lived keys for the org -- no pasted keys.
# It injects $DD_API_KEY and $DD_APP_KEY into the wrapped command only; DD_API_KEY
# is baked into the function, and DATADOG_API_KEY/DATADOG_APP_KEY feed the checker.
aws-vault exec sso-serverless-sandbox-account-admin -- \
dd-auth --domain app.datadoghq.com -- bash -c '
export DATADOG_API_KEY="$DD_API_KEY" DATADOG_APP_KEY="$DD_APP_KEY"
yarn test:e2e
'

# skip (no-op) -- what forks/CI without secrets do
SKIP_LAMBDA_TESTS=true yarn test:e2e
```

## CI

`.github/workflows/e2e.yml` runs this behind path filters (construct or suite
changes) with `SKIP_LAMBDA_TESTS` as a kill switch. AWS access is via GitHub OIDC
into the dedicated `gha-datadog-cdk-e2e` role in the serverless sandbox account
(`arn:aws:iam::425362996713:role/gha-datadog-cdk-e2e`), scoped to deploy
`one-e2e-cdk-lambda-*` through the CDK bootstrap roles. Config comes from repo
variables (`AWS_ROLE_ARN_E2E`, `AWS_REGION_E2E`, `DD_SITE_E2E`); telemetry keys are minted
at runtime via [`DataDog/dd-sts-action`](https://github.com/DataDog/dd-sts-action) under the
`datadog-cdk-constructs-e2e` policy (GitHub OIDC → short-lived Datadog API + App keys), so no
static Datadog keys live in this repo. When the construct or suite changes the suite runs for
real and the AWS OIDC / dd-sts steps must succeed -- an auth failure fails the job loudly. The
IAM resources are cataloged in `serverless-ci/e2e/iam-infra.md`.

## Hygiene

Every resource is named `one-e2e-cdk-lambda-<runid>` and tagged
`one_e2e_created:<unix-ts>` at creation. The in-test teardown is best-effort; the
cross-repo sweeper is the real guarantee, reaping any `one-e2e-` resource older than
the grace window.
76 changes: 76 additions & 0 deletions e2e/helpers/e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed
* under the Apache License Version 2.0.
*
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2026 Datadog, Inc.
*/

import { type E2ENaming } from "./naming";
import { type ExpectedLayers, type LambdaVerifierConfig } from "./lambda-verifier";
import { E2E_EXTENSION_LAYER_VERSION, E2E_NODE_LAYER_VERSION } from "./versions";

// Repo-local config feeding the shared e2e helpers. This file is NOT synced -- it holds
// everything specific to datadog-cdk-constructs that the shared, parameterized helpers
// read through their config arguments.

export const NAMING: E2ENaming = { tool: "cdk", platform: "lambda" };

export const ENV_NAME = process.env.E2E_ENV ?? "e2e";
export const ENV_VERSION = process.env.E2E_VERSION ?? "1.0.0";

// Datadog's public layer account (commercial, non-GovCloud). The e2e defaults to ap-northeast-3.
const DD_ACCOUNT_ID = "464622532012";

// Transient cloud-provider errors safe to retry, passed as ExecOptions.retryPatterns.
export const RETRY_PATTERNS = [
"RequestTimeout",
"Throttling",
"TooManyRequests",
"Rate exceeded",
"ServiceUnavailable",
"InternalFailure",
"ResourceConflictException",
"OperationAbortedException",
"ETIMEDOUT",
"ECONNRESET",
"EAI_AGAIN",
"Connection reset",
"timed out",
"UPDATE_IN_PROGRESS",
];

// The CDK app names the function after the run-unique service name, so the deployed
// function name is the service name itself.
export const functionName = (serviceName: string): string => serviceName;

// Pinned artifact versions come from this repo's e2e/helpers/versions.ts, so a version
// mismatch blames the construct's wiring, not upstream layer drift.
const expectedLayerArns = (region: string): ExpectedLayers => ({
node: `arn:aws:lambda:${region}:${DD_ACCOUNT_ID}:layer:Datadog-Node22-x:${E2E_NODE_LAYER_VERSION}`,
extension: `arn:aws:lambda:${region}:${DD_ACCOUNT_ID}:layer:Datadog-Extension:${E2E_EXTENSION_LAYER_VERSION}`,
});

export const VERIFIER: LambdaVerifierConfig = {
functionName,
expectedLayerArns,
redirectHandler: "/opt/nodejs/node_modules/datadog-lambda-js/handler.handler",
originalHandler: "index.handler",
// The construct tags every instrumented function with its own marker tag.
toolTag: { key: "dd_cdk_construct", pattern: /.+/ },
env: {
apiKeyVars: ["DD_API_KEY", "DD_API_KEY_SECRET_ARN", "DD_KMS_API_KEY", "DD_API_KEY_SSM_ARN"],
present: ["DD_SITE"],
values: (serviceName) => ({
DD_SERVICE: serviceName,
DD_ENV: ENV_NAME,
DD_VERSION: ENV_VERSION,
DD_TRACE_ENABLED: "true",
// With the extension, log collection is enabled via DD_SERVERLESS_LOGS_ENABLED;
// the construct intentionally forces DD_LOGS_INJECTION=false in this path.
DD_SERVERLESS_LOGS_ENABLED: "true",
DD_LOGS_INJECTION: "false",
DD_LAMBDA_HANDLER: "index.handler",
}),
},
};
19 changes: 19 additions & 0 deletions e2e/helpers/versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed
* under the Apache License Version 2.0.
*
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2021 Datadog, Inc.
*/

import * as lambda from "aws-cdk-lib/aws-lambda";

// Pinned artifact versions. Pinning the layer/extension versions (and the single
// canonical runtime) means an e2e failure blames this construct's wiring, not an
// upstream layer/tracer change. Bump deliberately. See spec.md ("Rules").
export const E2E_RUNTIME = lambda.Runtime.NODEJS_22_X;

// `Datadog-Node22-x` layer version.
export const E2E_NODE_LAYER_VERSION = 130;
// `Datadog-Extension` layer version.
export const E2E_EXTENSION_LAYER_VERSION = 83;
Loading
Loading