This sample demonstrates how to run a Temporal Worker inside an AWS Lambda
function using the Temporalio.Extensions.Aws.Lambda package. It exports
traces and metrics through AWS Distro for OpenTelemetry (ADOT).
The sample registers a greeting Workflow and Activity. A separate starter project starts the Workflow from a local machine; starter-only code is not included in the Lambda deployment package.
- A Temporal Cloud namespace, or a self-hosted Temporal cluster that the Lambda function can reach
- A Temporal API key for that namespace, stored in a local file outside this repository
- The Temporal CLI
- AWS CLI credentials with permission to manage Lambda, IAM, CloudFormation, CloudWatch Logs, CloudWatch metrics, and X-Ray in the target account
- .NET 10 SDK,
zip, andopenssl - An ADOT Collector Lambda layer ARN for the target region and architecture; see the ADOT Lambda documentation
Run the commands below from src/LambdaWorker.
Worker/contains the Lambda handler, Workflow, Activity, and worker project.Starter/contains the local Workflow starter project.Deploy/contains the AWS deployment scripts and CloudFormation template.temporal.template.tomlandotel-collector-config.template.yamlare the configuration templates copied in the setup below.
Use unique names for every dry run so existing deployments or concurrent tests cannot affect the result:
export AWS_PROFILE="<your-aws-profile>"
export AWS_REGION="us-west-2"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export SUFFIX="$(date -u +%Y%m%d%H%M%S)-$(openssl rand -hex 3)"
export FUNCTION_NAME="temporal-dotnet-lambda-${SUFFIX}"
export EXECUTION_ROLE_NAME="temporal-dotnet-exec-${SUFFIX}"
export STACK_NAME="tdnl-${SUFFIX}"
export DEPLOYMENT_NAME="dotnet-lambda-${SUFFIX}"
export BUILD_ID="build-${SUFFIX}"
export TASK_QUEUE="dotnet-lambda-tq-${SUFFIX}"
export WORKFLOW_ID_PREFIX="dotnet-lambda-wf-${SUFFIX}"
export EXTERNAL_ID="$(openssl rand -hex 16)"
export ADOT_LAYER_ARN="<adot-collector-layer-arn-for-${AWS_REGION}>"
export TEMPORAL_API_KEY_FILE="<path-to-temporal-api-key-file>"
export TEMPORAL_API_KEY="$(tr -d '\r\n' < "$TEMPORAL_API_KEY_FILE")"The AWS CLI commands and scripts inherit AWS_PROFILE and AWS_REGION.
The same EXTERNAL_ID must be supplied to both AWS IAM and Temporal Cloud.
The unsuffixed files are intentionally ignored because temporal.toml
contains namespace-specific configuration:
cp temporal.template.toml temporal.toml
cp otel-collector-config.template.yaml otel-collector-config.yamlEdit temporal.toml and replace the address and namespace placeholders:
[profile.default]
address = "<your-namespace>.<account>.tmprl.cloud:7233"
namespace = "<your-namespace>.<account>"The API key remains in TEMPORAL_API_KEY; do not add it to either file.
When an API key is present, the .NET SDK and Temporal CLI enable TLS
automatically.
The Lambda worker loads Temporal configuration in this order:
TEMPORAL_CONFIG_FILE, if set.temporal.tomlfrom the Lambda task root when running in Lambda.temporal.tomlfrom the current working directory.
Create the IAM role assumed by the Lambda service:
./Deploy/mk-lambda-execution-role.sh "$EXECUTION_ROLE_NAME"
export EXECUTION_ROLE_ARN="$(aws iam get-role \
--role-name "$EXECUTION_ROLE_NAME" \
--query Role.Arn \
--output text)"Publish the worker for Linux and create the function:
./Deploy/deploy-lambda.sh "$FUNCTION_NAME" "$EXECUTION_ROLE_ARN"The script defaults to linux-x64 and creates an x86_64 function. For
Arm64, set TEMPORAL_DOTNET_LAMBDA_RUNTIME=linux-arm64; the script then
creates an arm64 function. On later runs, the same command updates the
existing function code and waits until the update is complete.
This command supplies all environment variables together because the Lambda
--environment option replaces the complete variable map. Its output is
restricted so the API key is never echoed:
aws lambda update-function-configuration \
--function-name "$FUNCTION_NAME" \
--timeout 60 \
--layers "$ADOT_LAYER_ARN" \
--environment "Variables={TEMPORAL_API_KEY=${TEMPORAL_API_KEY},SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt,OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml,TEMPORAL_TASK_QUEUE=${TASK_QUEUE},TEMPORAL_LAMBDA_DEPLOYMENT_NAME=${DEPLOYMENT_NAME},TEMPORAL_LAMBDA_BUILD_ID=${BUILD_ID}}" \
--query '{FunctionName:FunctionName,Timeout:Timeout,Layers:Layers[*].Arn,LastUpdateStatus:LastUpdateStatus,RevisionId:RevisionId}' \
--output json
aws lambda wait function-updated --function-name "$FUNCTION_NAME"A 60-second timeout keeps a dry run short. For a production worker, choose a timeout appropriate for the workload; the worker continues polling until shortly before the Lambda deadline.
Grant the execution role permission to publish X-Ray traces and CloudWatch EMF metrics, then enable active X-Ray tracing:
./Deploy/enable-telemetry.sh \
"$EXECUTION_ROLE_NAME" \
"$FUNCTION_NAME" \
"$AWS_REGION" \
"$AWS_ACCOUNT_ID"The collector sends traces to awsxray and metrics to awsemf. It has no
diagnostic or logs exporter.
Get the Lambda ARN and create a CloudFormation stack containing the invocation role:
export LAMBDA_ARN="$(aws lambda get-function-configuration \
--function-name "$FUNCTION_NAME" \
--query FunctionArn \
--output text)"
./Deploy/mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$LAMBDA_ARN"The script waits for stack creation and prints the physical role name and ARN. Capture the ARN for the Temporal deployment version:
export INVOKE_ROLE_ARN="$(aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
--query "Stacks[0].Outputs[?OutputKey=='RoleARN'].OutputValue | [0]" \
--output text)"The deployment name and build ID supplied to Temporal must match the Lambda environment variables configured above:
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment create \
--name "$DEPLOYMENT_NAME"
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment create-version \
--deployment-name "$DEPLOYMENT_NAME" \
--build-id "$BUILD_ID" \
--aws-lambda-function-arn "$LAMBDA_ARN" \
--aws-lambda-assume-role-arn "$INVOKE_ROLE_ARN" \
--aws-lambda-assume-role-external-id "$EXTERNAL_ID"
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment set-current-version \
--deployment-name "$DEPLOYMENT_NAME" \
--build-id "$BUILD_ID" \
--allow-no-pollers \
--yesThe deployment is created explicitly because the Lambda worker has no
long-running poller that could create it lazily. For the same reason,
--allow-no-pollers is expected when setting the current version. Verify the
routing state:
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment describe \
--name "$DEPLOYMENT_NAME"You can also use Workers > Deployments > Actions > Validate Connection in the Temporal Cloud UI.
Run the separate starter project:
TEMPORAL_CONFIG_FILE="$PWD/temporal.toml" \
TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX="$WORKFLOW_ID_PREFIX" \
dotnet run \
--project Starter \
-- workflowA successful run prints the Workflow ID, Run ID, and:
Workflow result: Hello, Serverless Lambda Worker!!
Check that the Lambda and collector initialized without configuration errors:
aws logs tail "/aws/lambda/${FUNCTION_NAME}" --since 10mThe log should include the ADOT collector readiness message. The same invocation
should produce Temporal traces in X-Ray and TemporalWorkerMetrics metrics in
CloudWatch.
First reset Temporal routing, then remove the Lambda-backed version and deployment:
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment set-current-version \
--deployment-name "$DEPLOYMENT_NAME" \
--unversioned \
--allow-no-pollers \
--yes
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment delete-version \
--deployment-name "$DEPLOYMENT_NAME" \
--build-id "$BUILD_ID" \
--skip-drainage
temporal --config-file "$PWD/temporal.toml" --profile default \
worker deployment delete \
--name "$DEPLOYMENT_NAME"
aws lambda delete-function --function-name "$FUNCTION_NAME"Delete the AWS invocation stack, execution role, and retained log group:
aws cloudformation delete-stack --stack-name "$STACK_NAME"
aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
aws iam delete-role-policy \
--role-name "$EXECUTION_ROLE_NAME" \
--policy-name ADOT-Telemetry-Permissions
aws iam detach-role-policy \
--role-name "$EXECUTION_ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name "$EXECUTION_ROLE_NAME"
aws logs delete-log-group --log-group-name "/aws/lambda/${FUNCTION_NAME}"Completed Workflow histories remain in Temporal Cloud unless explicitly deleted. Use the Workflow and Run IDs printed by the starter if the dry-run history must also be removed:
temporal --config-file "$PWD/temporal.toml" --profile default \
workflow delete \
--workflow-id "<workflow-id>" \
--run-id "<run-id>"