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 @@ -4,3 +4,6 @@ __pycache__
.vscode
.DS_Store
.claude
.mypy_cache/
**/client.key
**/client.pem
2 changes: 2 additions & 0 deletions lambda_worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package/
temporal-sdk.toml
86 changes: 86 additions & 0 deletions lambda_worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Lambda Worker

This sample demonstrates how to run a Temporal Worker inside an AWS Lambda function using
the [`lambda_worker`](https://python.temporal.io/temporalio.contrib.aws.lambda_worker.html)
contrib 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.

## 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)
- Python 3.10+

## Files

| File | Description |
|------|-------------|
| `lambda_function.py` | Lambda entry point -- configures the worker, registers Workflows/Activities, and exports the handler |
| `workflows.py` | Sample Workflow that executes a greeting Activity |
| `activities.py` | Sample Activity that returns a greeting string |
| `starter.py` | Helper program to start a Workflow execution from a local machine |
| `temporal.toml` | Temporal client connection configuration (update with your namespace) |
| `otel-collector-config.yaml` | OpenTelemetry Collector sidecar configuration for ADOT |
| `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

If you want traces, metrics, and logs, you'll have to attach the ADOT layet to your Lambda function.
You will need to add the appropriate layer for your runtime and region. See [this page
](https://aws-otel.github.io/docs/getting-started/lambda#getting-started-with-aws-lambda-layers)
for more info.

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

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

Update `otel-collector-config.yaml` with your function name and region as needed.

### 4. Deploy the Lambda function

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

This installs Python dependencies, bundles them with your code and configuration files,
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 uv run python lambda_worker/starter.py
```
Empty file added lambda_worker/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions lambda_worker/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from temporalio import activity


@activity.defn
async def hello_activity(name: str) -> str:
activity.logger.info("HelloActivity started with name: %s", name)
return f"Hello, {name}!"
34 changes: 34 additions & 0 deletions lambda_worker/deploy-lambda.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/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-python"

# Install the published temporalio package (Linux wheels) and OTel dependencies
# TODO: Remove explicit OTel deps once lambda-worker-otel extra is published
uv pip install --target "$SCRIPT_DIR/package" --python-platform x86_64-unknown-linux-gnu --no-build \
temporalio \
"opentelemetry-api>=1.11.1,<2" \
"opentelemetry-sdk>=1.11.1,<2" \
"opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2" \
"opentelemetry-semantic-conventions>=0.40b0,<1" \
"opentelemetry-sdk-extension-aws>=2.0.0,<3"

# Overlay the local SDK's pure-Python source (for unpublished contrib code)
# TODO: Remove this step once the contrib package is published
cp -r "$SDK_DIR/temporalio/contrib" "$SCRIPT_DIR/package/temporalio"

# Copy application code into the package directory (all at zip root)
cp "$SCRIPT_DIR/lambda_function.py" "$SCRIPT_DIR/workflows.py" \
"$SCRIPT_DIR/activities.py" "$SCRIPT_DIR/package/"

# Bundle with configuration files
cd "$SCRIPT_DIR/package"
zip -r "$SCRIPT_DIR/function.zip" .
cd "$SCRIPT_DIR"
zip function.zip client.pem client.key temporal.toml otel-collector-config.yaml

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

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
85 changes: 85 additions & 0 deletions lambda_worker/iam-role-for-temporal-lambda-invoke-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CloudFormation template for creating an IAM role that Temporal Cloud can assume to invoke Lambda functions.
AWSTemplateFormatVersion: "2010-09-09"
Description: Creates an IAM role that Temporal Cloud can assume to invoke Lambda functions for Serverless Workers.

Parameters:
AssumeRoleExternalId:
Type: String
Description: The External ID provided by Temporal Cloud
AllowedPattern: "[a-zA-Z0-9_+=,.@-]*"
MinLength: 5
MaxLength: 45

LambdaFunctionARN:
Type: String
Description: >-
The ARN of the Lambda function to invoke
(e.g., arn:aws:lambda:us-west-2:123456789012:function:worker-1)

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Temporal Cloud Configuration"
Parameters:
- AssumeRoleExternalId
- Label:
default: "Lambda Configuration"
Parameters:
- LambdaFunctionARN
ParameterLabels:
AssumeRoleExternalId:
default: "External ID (provided by Temporal Cloud)"
LambdaFunctionARN:
default: "Lambda Function ARN"

Resources:
TemporalCloudServerlessWorker:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub
- "Temporal-Cloud-Serverless-Worker-${LambdaName}"
- LambdaName: !Select [6, !Split [":", !Ref LambdaFunctionARN]]
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: [arn:aws:iam::031568301006:role/wci-lambda-invoke]
Action: sts:AssumeRole
Condition:
StringEquals:
"sts:ExternalId": [!Ref AssumeRoleExternalId]
Description: "The role Temporal Cloud uses to invoke Lambda functions for Serverless Workers"
MaxSessionDuration: 3600 # 1 hour

TemporalCloudLambdaInvokePermissions:
Type: AWS::IAM::Policy
DependsOn: TemporalCloudServerlessWorker
Properties:
PolicyName: "Temporal-Cloud-Lambda-Invoke-Permissions"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
- lambda:GetFunction
Resource: [!Ref LambdaFunctionARN]
Roles:
- !Ref TemporalCloudServerlessWorker

Outputs:
RoleARN:
Description: The ARN of the IAM role created for Temporal Cloud
Value: !GetAtt TemporalCloudServerlessWorker.Arn
Export:
Name: !Sub "${AWS::StackName}-RoleARN"

RoleName:
Description: The name of the IAM role
Value: !Ref TemporalCloudServerlessWorker

LambdaFunctionARN:
Description: The Lambda function ARN that can be invoked
Value: !Ref LambdaFunctionARN
18 changes: 18 additions & 0 deletions lambda_worker/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from activities import hello_activity
from temporalio.common import WorkerDeploymentVersion
from temporalio.contrib.aws.lambda_worker import LambdaWorkerConfig, run_worker
from temporalio.contrib.aws.lambda_worker.otel import apply_defaults
from workflows import TASK_QUEUE, SampleWorkflow


def configure(config: LambdaWorkerConfig) -> None:
config.worker_config["task_queue"] = TASK_QUEUE
config.worker_config["workflows"] = [SampleWorkflow]
config.worker_config["activities"] = [hello_activity]
apply_defaults(config)


lambda_handler = run_worker(
WorkerDeploymentVersion(deployment_name="my-app", build_id="build-1"),
configure,
)
17 changes: 17 additions & 0 deletions lambda_worker/mk-iam-role.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -euo pipefail

# Creates the IAM role that allows Temporal Cloud to invoke your Lambda function.
# You can find the External ID in your Temporal Cloud namespace settings.

STACK_NAME="${1:?Usage: mk-iam-role.sh <stack-name> <external-id> <lambda-arn>}"
EXTERNAL_ID="${2:?Usage: mk-iam-role.sh <stack-name> <external-id> <lambda-arn>}"
LAMBDA_ARN="${3:?Usage: mk-iam-role.sh <stack-name> <external-id> <lambda-arn>}"

aws cloudformation create-stack \
--stack-name "$STACK_NAME" \
--template-body file://iam-role-for-temporal-lambda-invoke-test.yaml \
--parameters \
ParameterKey=AssumeRoleExternalId,ParameterValue="$EXTERNAL_ID" \
ParameterKey=LambdaFunctionARN,ParameterValue="$LAMBDA_ARN" \
--capabilities CAPABILITY_NAMED_IAM
35 changes: 35 additions & 0 deletions lambda_worker/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
receivers:
otlp:
protocols:
grpc:
endpoint: "localhost:4317"
http:
endpoint: "localhost:4318"

exporters:
debug:
awsxray:
region: us-west-2
awsemf:
# AWS EMF exporter for metrics
# These are example configurations
namespace: TemporalWorkerMetrics
log_group_name: /aws/lambda/<your-function-name>
region: us-west-2
dimension_rollup_option: NoDimensionRollup
resource_to_telemetry_conversion:
enabled: true

service:
pipelines:
traces:
receivers: [otlp]
exporters: [awsxray, debug]
metrics:
receivers: [otlp]
exporters: [awsemf]
telemetry:
logs:
level: debug
metrics:
address: localhost:8888
24 changes: 24 additions & 0 deletions lambda_worker/starter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import asyncio

from temporalio.client import Client
from temporalio.envconfig import ClientConfig

from lambda_worker.workflows import TASK_QUEUE, SampleWorkflow


async def main() -> None:
config = ClientConfig.load_client_connect_config()
client = await Client.connect(**config)
print("Connected to Temporal Service")

result = await client.execute_workflow(
SampleWorkflow.run,
"Serverless Lambda Worker!",
id="serverless-workflow-id-1",
task_queue=TASK_QUEUE,
)
print(f"Workflow result: {result}")


if __name__ == "__main__":
asyncio.run(main())
6 changes: 6 additions & 0 deletions lambda_worker/temporal.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
address = "<your-namespace>.<account>.tmprl.cloud:7233"
namespace = "<your-namespace>.<account>"
[profile.default.tls]
client_cert_path = "client.pem"
client_key_path = "client.key"
Loading
Loading