Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,6 @@ package-lock.json
*/**/pnpm-lock.yaml

.idea/

**/client.key
**/client.pem
3 changes: 3 additions & 0 deletions .scripts/copy-shared-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const TSCONFIG_EXCLUDE = [
'production',
'hello-world-js',
'food-delivery',
'lambda-worker',
'nestjs-exchange-rates',
'empty',
'hello-world',
Expand All @@ -29,6 +30,7 @@ const GITIGNORE_EXCLUDE = [
'hello-world-js',
'protobufs',
'food-delivery',
'lambda-worker',
'nestjs-exchange-rates',
];
const ESLINTRC_EXCLUDE = [
Expand Down Expand Up @@ -61,6 +63,7 @@ const POST_CREATE_EXCLUDE = [
'patching-api',
'signals-queries',
'activities-cancellation-heartbeating',
'lambda-worker',
'nestjs-exchange-rates',
'food-delivery',
'search-attributes',
Expand Down
3 changes: 2 additions & 1 deletion .scripts/list-of-samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"hello-world-js",
"hello-world-mtls",
"interceptors-opentelemetry",
"lambda-worker",
"message-passing",
"monorepo-folders",
"mutex",
Expand Down Expand Up @@ -51,4 +52,4 @@
"worker-specific-task-queues",
"worker-versioning"
]
}
}
3 changes: 1 addition & 2 deletions activities-examples/.npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
link-workspace-packages = true
prefer-workspace-packages = true
package-lock=false
3 changes: 3 additions & 0 deletions lambda-worker/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
lib
.eslintrc.js
48 changes: 48 additions & 0 deletions lambda-worker/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { builtinModules } = require('module');

const ALLOWED_NODE_BUILTINS = new Set(['assert']);

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'deprecation'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
// recommended for safety
'@typescript-eslint/no-floating-promises': 'error', // forgetting to await Activities and Workflow APIs is bad
'deprecation/deprecation': 'warn',

// code style preference
'object-shorthand': ['error', 'always'],

// relaxed rules, for convenience
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['src/workflows.ts', 'src/workflows-*.ts', 'src/workflows/*.ts'],
rules: {
'no-restricted-imports': [
'error',
...builtinModules.filter((m) => !ALLOWED_NODE_BUILTINS.has(m)).flatMap((m) => [m, `node:${m}`]),
],
},
},
],
};
5 changes: 5 additions & 0 deletions lambda-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib
node_modules
workflow-bundle.js
function.zip
package/
1 change: 1 addition & 0 deletions lambda-worker/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions lambda-worker/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
1 change: 1 addition & 0 deletions lambda-worker/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions lambda-worker/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
131 changes: 131 additions & 0 deletions lambda-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Lambda Worker

This sample demonstrates how to run a Temporal Worker inside an AWS Lambda function using
the [`@temporalio/lambda-worker`](https://typescript.temporal.io) package. It includes
optional OpenTelemetry instrumentation that exports traces and metrics through AWS Distro
for OpenTelemetry (ADOT).

The sample registers a simple greeting Workflow and Activity, but the pattern applies to
any Workflow/Activity definitions.

The sample includes [`@aws-lambda-powertools/logger`](https://docs.aws.amazon.com/powertools/typescript/latest/features/logger/),
which `@temporalio/lambda-worker` automatically detects and uses to produce structured JSON
logs that CloudWatch Logs can parse natively. If you don't need structured logging, you can
remove the dependency and the SDK will fall back to its default human-readable logger.

> **Note:** `@temporalio/lambda-worker` is not yet published. The `package.json` currently
> references it via a local `file:` path to `../../sdk-node/packages/lambda-worker`.
> TODO: Replace with a versioned dependency (e.g. `^1.15.0`) once the package is published.

## Prerequisites

- A [Temporal Cloud](https://temporal.io/cloud) namespace (or a self-hosted Temporal
cluster accessible from your Lambda)
- AWS CLI configured with permissions to create Lambda functions, IAM roles, and
CloudFormation stacks
- mTLS client certificate and key for your Temporal namespace (place as `client.pem` and
`client.key` in this directory)
- Node.js 22+

## Files

| File | Description |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `src/index.ts` | Lambda entry point — configures the worker, registers Workflows/Activities, enables OTel, and exports the handler |
| `src/workflows.ts` | Sample Workflow that executes a greeting Activity |
| `src/activities.ts` | Sample Activity that returns a greeting string |
| `src/client.ts` | Helper program to start a Workflow execution from a local machine |
| `src/scripts/build-workflow-bundle.ts` | Pre-bundles Workflow code with OTel interceptor modules for Lambda cold start performance |
| `temporal.toml` | Temporal client connection configuration (update with your namespace) |
| `otel-collector-config.yaml` | OpenTelemetry Collector configuration for ADOT (routes metrics to CloudWatch, traces to X-Ray) |
| `deploy-lambda.sh` | Packages and deploys the Lambda function |
| `mk-iam-role.sh` | Creates the IAM role that allows Temporal Cloud to invoke the Lambda |
| `iam-role-for-temporal-lambda-invoke-test.yaml` | CloudFormation template for the IAM role |
| `extra-setup-steps` | Additional IAM and Lambda configuration for OpenTelemetry support |

## Setup

### 1. Configure Temporal connection

Edit `temporal.toml` with your Temporal Cloud namespace address and credentials. In production,
we'd recommend reading your credentials from a secret store, but to keep this example simple
the toml file defaults to reading them from keys bundled along with the Lambda code.

### 2. Create the IAM role

This creates the IAM role that Temporal Cloud assumes to invoke your Lambda function:

```bash
./mk-iam-role.sh <stack-name> <external-id> <lambda-arn>
```

The External ID is provided by Temporal Cloud in your namespace's serverless worker
configuration.

### 3. (Optional) Enable OpenTelemetry

The sample calls `applyDefaults(config)` in the handler, which registers Temporal SDK
interceptors for tracing Workflow, Activity, and Nexus calls, and configures the Core SDK
to export metrics via OTLP. To complete the setup, attach two ADOT Lambda layers:

1. **ADOT JavaScript layer** — auto-instruments the handler and exports Node.js-side
traces to X-Ray. See [this page](https://aws-otel.github.io/docs/getting-started/lambda/lambda-js)
for the layer ARN for your region.
2. **ADOT Collector layer** (`aws-otel-collector-amd64`) — runs the OTel Collector as a
Lambda extension, receiving Temporal Core SDK metrics via OTLP and forwarding them to
CloudWatch/X-Ray. See [this page](https://aws-otel.github.io/docs/getting-started/lambda)
for the layer ARN.

Update `otel-collector-config.yaml` with your function name and region, then set the
following environment variables on your Lambda:

```
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-handler

I think this should be otel-handler. otel-instrument seems to be for Python, not Node.js

OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml
```

`AWS_LAMBDA_EXEC_WRAPPER` enables the JS layer's auto-instrumentation.
`OPENTELEMETRY_COLLECTOR_CONFIG_URI` points the collector at the custom config that
routes metrics to CloudWatch EMF and traces to X-Ray.

Enable X-Ray active tracing on the Lambda function (required for traces to appear):

```bash
aws lambda update-function-configuration --function-name <function-name> \
--tracing-config Mode=Active
```

Then run the extra setup to grant the Lambda role the necessary permissions:

```bash
./extra-setup-steps <role-name> <function-name> <region> <account-id>
```

### 4. Deploy the Lambda function

Create a Lambda function in AWS with:

- **Runtime**: Node.js >=20
- **Handler**: `index.handler` (the default)
- **Architecture**: x86_64

It's likely you will need to increase the default memory limit in AWS for your lambda. A minimum of
256MB is recommended.

Then deploy:

```bash
./deploy-lambda.sh <function-name>
```

This compiles TypeScript, pre-bundles Workflow code, packages everything with dependencies,
and uploads to AWS Lambda.

### 5. Start a Workflow

Use the starter program to execute a Workflow on the Lambda worker, using
the same config file the Lambda uses for connecting to the server:

```bash
TEMPORAL_CONFIG_FILE=temporal.toml pnpm workflow
```
54 changes: 54 additions & 0 deletions lambda-worker/deploy-lambda.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash
set -euo pipefail

FUNCTION_NAME="${1:?Usage: deploy-lambda.sh <function-name>}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SDK_DIR="$SCRIPT_DIR/../../sdk-node"

# Build TypeScript
cd "$SCRIPT_DIR"
pnpm build

# Bundle workflows
pnpm build:workflow-bundle

# Create packaging directory
rm -rf "$SCRIPT_DIR/package"
mkdir -p "$SCRIPT_DIR/package"

# Copy compiled JS to package root (so index.js is at zip root for the default handler)
cp "$SCRIPT_DIR/lib/"*.js "$SCRIPT_DIR/package/"

# Copy workflow bundle alongside the handler
cp "$SCRIPT_DIR/workflow-bundle.js" "$SCRIPT_DIR/package/"

# Install production dependencies.
# TODO: Once @temporalio/lambda-worker is published, remove the sed and the
# manual copy below — npm install will handle everything.
cd "$SCRIPT_DIR/package"
sed '/@temporalio\/lambda-worker/d' "$SCRIPT_DIR/package.json" > package.json
npm install --omit=dev --ignore-scripts

# Strip native binaries for platforms other than Lambda's (linux x86_64)
find node_modules/@temporalio/core-bridge/releases -mindepth 1 -maxdepth 1 \
! -name 'x86_64-unknown-linux-gnu' -exec rm -rf {} +

# Manually place the local lambda-worker package (not yet published)
mkdir -p node_modules/@temporalio/lambda-worker
cp "$SDK_DIR/packages/lambda-worker/package.json" node_modules/@temporalio/lambda-worker/
cp -r "$SDK_DIR/packages/lambda-worker/lib" node_modules/@temporalio/lambda-worker/

# Copy config files and certs
cp "$SCRIPT_DIR/temporal.toml" "$SCRIPT_DIR/otel-collector-config.yaml" \
"$SCRIPT_DIR/client.pem" "$SCRIPT_DIR/client.key" "$SCRIPT_DIR/package/"

# Create zip
cd "$SCRIPT_DIR/package"
zip -r "$SCRIPT_DIR/function.zip" .

# Deploy
aws lambda update-function-code --function-name "$FUNCTION_NAME" \
--zip-file fileb://"$SCRIPT_DIR/function.zip"

# Cleanup
rm -rf "$SCRIPT_DIR/package" "$SCRIPT_DIR/function.zip"
48 changes: 48 additions & 0 deletions lambda-worker/extra-setup-steps
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -euo pipefail

# Additional setup steps for OpenTelemetry support.
# These are needed if you want metrics, logs, and traces from your Lambda worker.

ROLE_NAME="${1:?Usage: extra-setup-steps <role-name> <function-name> <region> <account-id>}"
FUNCTION_NAME="${2:?Usage: extra-setup-steps <role-name> <function-name> <region> <account-id>}"
REGION="${3:?Usage: extra-setup-steps <role-name> <function-name> <region> <account-id>}"
ACCOUNT_ID="${4:?Usage: extra-setup-steps <role-name> <function-name> <region> <account-id>}"

# Needed to allow metrics/logs/traces to be published
aws iam put-role-policy \
--role-name "$ROLE_NAME" \
--policy-name ADOT-Telemetry-Permissions \
--policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Effect\": \"Allow\",
\"Action\": [
\"logs:CreateLogGroup\",
\"logs:CreateLogStream\",
\"logs:PutLogEvents\"
],
\"Resource\": \"arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/lambda/${FUNCTION_NAME}:*\"
},
{
\"Effect\": \"Allow\",
\"Action\": [
\"xray:PutTraceSegments\",
\"xray:PutTelemetryRecords\"
],
\"Resource\": \"*\"
},
{
\"Effect\": \"Allow\",
\"Action\": [
\"cloudwatch:PutMetricData\"
],
\"Resource\": \"*\"
}
]
}"

# Needed to make traces show up with type: `"AWS::Lambda::Function"` filter
aws lambda update-function-configuration \
--function-name "$FUNCTION_NAME" --tracing-config Mode=Active
Loading