From 49ca47569b6ce90749fe36a2d68075809f1b06a9 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 10 Jun 2026 18:07:09 -0500 Subject: [PATCH 01/10] Sample for AWS lambda worker --- .gitignore | 1 + README.md | 3 + lambda-worker/README.md | 250 ++++++++++++++++++ lambda-worker/build.gradle | 63 +++++ lambda-worker/deploy-lambda.sh | 34 +++ lambda-worker/extra-setup-steps | 44 +++ ...-role-for-temporal-lambda-invoke-test.yaml | 94 +++++++ lambda-worker/mk-iam-role.sh | 14 + .../otel-collector-config.yaml.sample | 33 +++ .../lambdaworker/GreetingActivities.java | 12 + .../lambdaworker/GreetingActivitiesImpl.java | 20 ++ .../samples/lambdaworker/LambdaFunction.java | 21 ++ .../lambdaworker/LambdaWorkerSample.java | 46 ++++ .../samples/lambdaworker/SampleWorkflow.java | 12 + .../lambdaworker/SampleWorkflowImpl.java | 28 ++ .../samples/lambdaworker/Starter.java | 50 ++++ lambda-worker/src/main/resources/logback.xml | 15 ++ .../lambdaworker/LambdaWorkerSampleTest.java | 62 +++++ .../lambdaworker/SampleWorkflowTest.java | 32 +++ lambda-worker/temporal.toml.sample | 9 + settings.gradle | 8 +- 21 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 lambda-worker/README.md create mode 100644 lambda-worker/build.gradle create mode 100755 lambda-worker/deploy-lambda.sh create mode 100755 lambda-worker/extra-setup-steps create mode 100644 lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml create mode 100755 lambda-worker/mk-iam-role.sh create mode 100644 lambda-worker/otel-collector-config.yaml.sample create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java create mode 100644 lambda-worker/src/main/resources/logback.xml create mode 100644 lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java create mode 100644 lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java create mode 100644 lambda-worker/temporal.toml.sample diff --git a/.gitignore b/.gitignore index a1abef91..f587ebda 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target .DS_Store .idea .gradle +settings.local.gradle **/build/ **/out/ .classpath diff --git a/README.md b/README.md index 1465a9cc..898cc5b1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ It contains the following modules: * [SpringBoot](/springboot): showcases SpringBoot autoconfig integration. * [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies. * [Spring AI](/springai): demonstrates the Temporal Spring AI integration — durable AI agents with chat models, tools, MCP servers, vector stores, and embeddings. +* [Lambda Worker](/lambda-worker): demonstrates running a Temporal Java Worker inside AWS Lambda. ## Learn more about Temporal and Java SDK @@ -115,6 +116,8 @@ See the README.md file in each main sample directory for cut/paste Gradle comman - [**Environment Configuration**](/core/src/main/java/io/temporal/samples/envconfig): Load client configuration from TOML files with programmatic overrides. +- [**Lambda Worker**](/lambda-worker): Demonstrates running a Temporal Java Worker inside AWS Lambda with Worker Deployment Versioning. + #### API demonstrations - [**Workflow Streams**](/core/src/main/java/io/temporal/samples/workflowstreams): Demonstrates a durable publish/subscribe log hosted inside a workflow, using the experimental `temporal-workflowstreams` module. diff --git a/lambda-worker/README.md b/lambda-worker/README.md new file mode 100644 index 00000000..44cc4668 --- /dev/null +++ b/lambda-worker/README.md @@ -0,0 +1,250 @@ +# Lambda Worker + +This sample demonstrates a Temporal Java Worker running inside an AWS Lambda function. +It registers a simple greeting Workflow and Activity, configures Worker Deployment +Versioning, and includes helper scripts for packaging the Lambda and configuring Temporal +Cloud invocation. + +For local SDK co-development, create `settings.local.gradle` in the repository root and +add `includeBuild '../sdk-java'`. That file is ignored by Git and makes Gradle use the +in-development `io.temporal:temporal-aws-lambda` SDK add-on before it is published. + +## Prerequisites + +- Java 17+ +- AWS CLI configured with permissions to create Lambda functions, IAM roles, and + CloudFormation stacks +- A Temporal Cloud namespace with Serverless Workers enabled, or a self-hosted Temporal + Service configured for AWS Lambda Serverless Workers +- A Temporal Cloud API key. This walkthrough deploys it as a Lambda environment variable + because these are development-only secrets. + +## Files + +| File | Description | +|------|-------------| +| `src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java` | AWS Lambda handler | +| `src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java` | Shared task queue, deployment version, and worker registrations | +| `src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow*.java` | Sample Workflow interface and implementation | +| `src/main/java/io/temporal/samples/lambdaworker/GreetingActivities*.java` | Sample Activity interface and implementation | +| `src/main/java/io/temporal/samples/lambdaworker/Starter.java` | Local helper that starts a Workflow for the Lambda worker | +| `temporal.toml.sample` | Temporal client connection configuration | +| `otel-collector-config.yaml.sample` | Optional ADOT collector configuration | +| `deploy-lambda.sh` | Builds and uploads the Lambda deployment package | +| `mk-iam-role.sh` | Creates the role Temporal Cloud assumes to invoke Lambda | +| `iam-role-for-temporal-lambda-invoke-test.yaml` | CloudFormation template for the invocation role | +| `extra-setup-steps` | Optional IAM and Lambda settings for OpenTelemetry | + +## Build + +```bash +./gradlew :lambda-worker:test +./gradlew :lambda-worker:shadowJar +``` + +The Lambda handler string is: + +```text +io.temporal.samples.lambdaworker.LambdaFunction::handleRequest +``` + +## Configure Environment + +Set AWS, Temporal, and sample names first. Use unique values if you share the account or +namespace with other developers. + +```bash +export AWS_PROFILE= +export AWS_REGION=us-west-2 +export AWS_DEFAULT_REGION="$AWS_REGION" + +export TEMPORAL_ADDRESS=..tmprl.cloud:7233 +export TEMPORAL_NAMESPACE=. +export TEMPORAL_API_KEY= +export TEMPORAL_TLS=true + +export FUNCTION_NAME=my-temporal-java-worker +export EXECUTION_ROLE_NAME="${FUNCTION_NAME}-exec" +export STACK_NAME="${FUNCTION_NAME}-invoke" +export EXTERNAL_ID="${FUNCTION_NAME}-external-id" + +export DEPLOYMENT_NAME=my-app +export BUILD_ID=build-1 +export TASK_QUEUE=serverless-task-queue-java +export WORKFLOW_PREFIX=serverless-workflow-id-java +``` + +The Lambda worker reads these environment variables: + +```bash +TEMPORAL_ADDRESS +TEMPORAL_NAMESPACE +TEMPORAL_API_KEY +TEMPORAL_TASK_QUEUE +TEMPORAL_WORKER_DEPLOYMENT_NAME +TEMPORAL_WORKER_BUILD_ID +``` + +The local starter also reads `TEMPORAL_TASK_QUEUE` and `TEMPORAL_WORKFLOW_ID_PREFIX`. + +You can also use `temporal.toml.sample` as a starting point for `temporal.toml` and set +`TEMPORAL_CONFIG_FILE=temporal.toml`. + +`TEMPORAL_TASK_QUEUE`, `TEMPORAL_WORKER_DEPLOYMENT_NAME`, +`TEMPORAL_WORKER_BUILD_ID`, and `TEMPORAL_WORKFLOW_ID_PREFIX` are optional. The values +above are the sample defaults. + +## Deploy Lambda + +Create the Lambda execution role: + +```bash +cat > /tmp/temporal-lambda-trust-policy.json <<'JSON' +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +JSON + +aws iam create-role \ + --role-name "$EXECUTION_ROLE_NAME" \ + --assume-role-policy-document file:///tmp/temporal-lambda-trust-policy.json \ + --query 'Role.Arn' \ + --output text + +aws iam attach-role-policy \ + --role-name "$EXECUTION_ROLE_NAME" \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +export EXECUTION_ROLE_ARN="$( + aws iam get-role \ + --role-name "$EXECUTION_ROLE_NAME" \ + --query 'Role.Arn' \ + --output text +)" +``` + +Build the deployment jar and create the Java 17 Lambda function. The jar is small enough +for direct upload in this sample; use S3 if your local artifact grows beyond Lambda's +direct upload limit. + +```bash +./gradlew :lambda-worker:shadowJar + +aws lambda create-function \ + --function-name "$FUNCTION_NAME" \ + --runtime java17 \ + --handler io.temporal.samples.lambdaworker.LambdaFunction::handleRequest \ + --role "$EXECUTION_ROLE_ARN" \ + --zip-file fileb://lambda-worker/build/libs/lambda-worker-1.0.0-all.jar \ + --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_WORKER_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_WORKER_BUILD_ID=$BUILD_ID}" \ + --timeout 90 \ + --memory-size 1024 \ + --query 'FunctionArn' \ + --output text + +aws lambda wait function-active --function-name "$FUNCTION_NAME" + +export FUNCTION_ARN="$( + aws lambda get-function \ + --function-name "$FUNCTION_NAME" \ + --query 'Configuration.FunctionArn' \ + --output text +)" +``` + +To update code after the function exists: + +```bash +./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +``` + +If direct upload is too large, set `LAMBDA_CODE_S3_BUCKET` and rerun: + +```bash +LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +``` + +## Configure Invocation + +Create the IAM role that Temporal Cloud assumes to invoke the Lambda: + +```bash +cd lambda-worker +./mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$FUNCTION_ARN" +cd .. + +aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" + +export INVOCATION_ROLE_ARN="$( + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query "Stacks[0].Outputs[?OutputKey=='RoleARN'].OutputValue | [0]" \ + --output text +)" +``` + +Create and route the Worker Deployment Version: + +```bash +temporal worker deployment create --name "$DEPLOYMENT_NAME" + +temporal worker deployment create-version \ + --deployment-name "$DEPLOYMENT_NAME" \ + --build-id "$BUILD_ID" \ + --aws-lambda-function-arn "$FUNCTION_ARN" \ + --aws-lambda-assume-role-arn "$INVOCATION_ROLE_ARN" \ + --aws-lambda-assume-role-external-id "$EXTERNAL_ID" + +temporal worker deployment set-current-version \ + --deployment-name "$DEPLOYMENT_NAME" \ + --build-id "$BUILD_ID" \ + --allow-no-pollers \ + --yes +``` + +An async Lambda smoke test returns immediately and should produce worker startup logs: + +```bash +aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{}' \ + /tmp/lambda-worker-response.json \ + --query 'StatusCode' \ + --output text +``` + +A synchronous invoke can run until the Lambda worker exits near the function timeout. If +you want to wait for that path, set the AWS CLI read timeout higher than the function +timeout. + +## Start Workflow + +After the Worker Deployment Version is current, start the sample Workflow: + +```bash +export TEMPORAL_TASK_QUEUE="$TASK_QUEUE" +export TEMPORAL_WORKFLOW_ID_PREFIX="$WORKFLOW_PREFIX" + +./gradlew -q :lambda-worker:execute \ + -PmainClass=io.temporal.samples.lambdaworker.Starter +``` + +The starter only creates a Workflow Execution. It does not start a local Worker. The +important value is `TEMPORAL_TASK_QUEUE`; it must match the task queue configured on the +Lambda function. + +## Local SDK Development + +For local development of the Workflow and Activity logic, run the unit tests. They use +`TestWorkflowRule` and do not require AWS or a running Temporal Service. diff --git a/lambda-worker/build.gradle b/lambda-worker/build.gradle new file mode 100644 index 00000000..a1658c53 --- /dev/null +++ b/lambda-worker/build.gradle @@ -0,0 +1,63 @@ +dependencies { + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + implementation "io.temporal:temporal-aws-lambda:$javaSDKVersion" + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' + + testImplementation "io.temporal:temporal-testing:$javaSDKVersion" + testImplementation "junit:junit:4.13.2" + testImplementation(platform("org.junit:junit-bom:5.10.3")) + testRuntimeOnly "org.junit.vintage:junit-vintage-engine" + + dependencies { + errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') + errorprone('com.google.errorprone:error_prone_core:2.28.0') + } +} + +tasks.register('execute', JavaExec) { + mainClass = findProperty("mainClass") ?: "" + classpath = sourceSets.main.runtimeClasspath + if (findProperty("args")) { + args findProperty("args").tokenize() + } +} + +tasks.register('shadowJar', Jar) { + archiveBaseName = 'lambda-worker' + archiveClassifier = 'all' + archiveVersion = jarVersion + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dependsOn configurations.runtimeClasspath + def mergedServicesDir = layout.buildDirectory.dir('generated/mergedServices') + + from sourceSets.main.output + from({ + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + }) { + exclude 'META-INF/services/**' + } + from(mergedServicesDir) + + doFirst { + File servicesOutput = mergedServicesDir.get().asFile + delete servicesOutput + File servicesDir = new File(servicesOutput, 'META-INF/services') + servicesDir.mkdirs() + + Map> services = [:].withDefault { new LinkedHashSet<>() } + configurations.runtimeClasspath.files.findAll { it.isFile() }.each { artifact -> + zipTree(artifact).matching { include 'META-INF/services/*' }.files.each { serviceFile -> + serviceFile.eachLine('UTF-8') { line -> + String service = line.trim() + if (!service.isEmpty() && !service.startsWith('#')) { + services[serviceFile.name].add(service) + } + } + } + } + services.each { name, providers -> + new File(servicesDir, name).text = providers.join(System.lineSeparator()) + System.lineSeparator() + } + } +} diff --git a/lambda-worker/deploy-lambda.sh b/lambda-worker/deploy-lambda.sh new file mode 100755 index 00000000..bcf94a2d --- /dev/null +++ b/lambda-worker/deploy-lambda.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -euo pipefail + +FUNCTION_NAME="${1:?Usage: deploy-lambda.sh }" +MAX_DIRECT_UPLOAD_BYTES=50000000 + +cd "$(dirname "$0")/.." +./gradlew :lambda-worker:shadowJar + +JAR_FILE="$(find lambda-worker/build/libs -name 'lambda-worker-*-all.jar' | head -n 1)" + +if stat -f%z "$JAR_FILE" >/dev/null 2>&1; then + JAR_SIZE="$(stat -f%z "$JAR_FILE")" +else + JAR_SIZE="$(stat -c%s "$JAR_FILE")" +fi + +if [[ -n "${LAMBDA_CODE_S3_BUCKET:-}" ]]; then + S3_KEY="${LAMBDA_CODE_S3_KEY:-lambda-worker/$(basename "$JAR_FILE")}" + aws s3 cp "$JAR_FILE" "s3://$LAMBDA_CODE_S3_BUCKET/$S3_KEY" + aws lambda update-function-code \ + --function-name "$FUNCTION_NAME" \ + --s3-bucket "$LAMBDA_CODE_S3_BUCKET" \ + --s3-key "$S3_KEY" + exit 0 +fi + +if (( JAR_SIZE > MAX_DIRECT_UPLOAD_BYTES )); then + echo "Artifact is ${JAR_SIZE} bytes, which is too large for direct Lambda upload." >&2 + echo "Set LAMBDA_CODE_S3_BUCKET and rerun to upload through S3." >&2 + exit 1 +fi + +aws lambda update-function-code --function-name "$FUNCTION_NAME" --zip-file "fileb://$JAR_FILE" diff --git a/lambda-worker/extra-setup-steps b/lambda-worker/extra-setup-steps new file mode 100755 index 00000000..8c6dd0f8 --- /dev/null +++ b/lambda-worker/extra-setup-steps @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +ROLE_NAME="${1:?Usage: extra-setup-steps }" +FUNCTION_NAME="${2:?Usage: extra-setup-steps }" +REGION="${3:?Usage: extra-setup-steps }" +ACCOUNT_ID="${4:?Usage: extra-setup-steps }" + +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\": \"*\" + } + ] + }" + +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --tracing-config Mode=Active diff --git a/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml b/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml new file mode 100644 index 00000000..2e9054e2 --- /dev/null +++ b/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml @@ -0,0 +1,94 @@ +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: A string you choose. Use the same value when creating the Worker Deployment Version. + AllowedPattern: "[a-zA-Z0-9_+=,.@-]*" + MinLength: 5 + MaxLength: 45 + + LambdaFunctionARNs: + Type: CommaDelimitedList + Description: Comma-separated list of Lambda function ARNs to invoke. + + RoleName: + Type: String + Default: "Temporal-Cloud-Serverless-Worker" + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Temporal Cloud Configuration" + Parameters: + - AssumeRoleExternalId + - Label: + default: "Lambda Configuration" + Parameters: + - LambdaFunctionARNs + - RoleName + ParameterLabels: + AssumeRoleExternalId: + default: "External ID" + LambdaFunctionARNs: + default: "Lambda Function ARNs" + RoleName: + default: "IAM Role Name" + +Resources: + TemporalCloudServerlessWorker: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${RoleName}-${AWS::StackName}" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: + [ + arn:aws:iam::902542641901:role/wci-lambda-invoke, + arn:aws:iam::160190466495:role/wci-lambda-invoke, + arn:aws:iam::819232936619:role/wci-lambda-invoke, + arn:aws:iam::829909441867:role/wci-lambda-invoke, + arn:aws:iam::354116250941: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 + + 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 LambdaFunctionARNs + Roles: + - !Sub "${RoleName}-${AWS::StackName}" + +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 RoleName + + LambdaFunctionARNs: + Description: The Lambda function ARNs that can be invoked. + Value: !Join [", ", !Ref LambdaFunctionARNs] diff --git a/lambda-worker/mk-iam-role.sh b/lambda-worker/mk-iam-role.sh new file mode 100755 index 00000000..20aeae5d --- /dev/null +++ b/lambda-worker/mk-iam-role.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +STACK_NAME="${1:?Usage: mk-iam-role.sh }" +EXTERNAL_ID="${2:?Usage: mk-iam-role.sh }" +LAMBDA_ARN="${3:?Usage: mk-iam-role.sh }" + +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=LambdaFunctionARNs,ParameterValue="\"$LAMBDA_ARN\"" \ + --capabilities CAPABILITY_NAMED_IAM diff --git a/lambda-worker/otel-collector-config.yaml.sample b/lambda-worker/otel-collector-config.yaml.sample new file mode 100644 index 00000000..790be3d0 --- /dev/null +++ b/lambda-worker/otel-collector-config.yaml.sample @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "localhost:4317" + http: + endpoint: "localhost:4318" + +exporters: + debug: + awsxray: + region: us-west-2 + awsemf: + namespace: TemporalWorkerMetrics + log_group_name: /aws/lambda/ + 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 diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java new file mode 100644 index 00000000..620b7212 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java @@ -0,0 +1,12 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +/** Activity interface used by the sample workflow. */ +@ActivityInterface +public interface GreetingActivities { + + @ActivityMethod + String createGreeting(String name); +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java new file mode 100644 index 00000000..07349515 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java @@ -0,0 +1,20 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.Activity; +import io.temporal.activity.ActivityInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Activity implementation that returns a simple greeting. */ +public class GreetingActivitiesImpl implements GreetingActivities { + + private static final Logger logger = LoggerFactory.getLogger(GreetingActivitiesImpl.class); + + @Override + public String createGreeting(String name) { + ActivityInfo info = Activity.getExecutionContext().getInfo(); + logger.info( + "Running activity {} for workflow {}", info.getActivityType(), info.getWorkflowId()); + return "Hello, " + name + "!"; + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java new file mode 100644 index 00000000..547a6d71 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -0,0 +1,21 @@ +package io.temporal.samples.lambdaworker; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import io.temporal.aws.lambda.LambdaWorker; +import io.temporal.common.WorkerDeploymentVersion; + +/** AWS Lambda entry point for the Temporal worker. */ +public class LambdaFunction implements RequestHandler { + + private static final RequestHandler WORKER = + LambdaWorker.run( + new WorkerDeploymentVersion( + LambdaWorkerSample.deploymentName(), LambdaWorkerSample.buildId()), + LambdaWorkerSample::configure); + + @Override + public Void handleRequest(Object input, Context context) { + return WORKER.handleRequest(input, context); + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java new file mode 100644 index 00000000..84868d0b --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -0,0 +1,46 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.aws.lambda.LambdaWorkerOptions; + +/** Shared constants and worker registration for the Lambda Worker sample. */ +public final class LambdaWorkerSample { + + public static final String TASK_QUEUE_ENV = "TEMPORAL_TASK_QUEUE"; + public static final String DEPLOYMENT_NAME_ENV = "TEMPORAL_WORKER_DEPLOYMENT_NAME"; + public static final String BUILD_ID_ENV = "TEMPORAL_WORKER_BUILD_ID"; + public static final String WORKFLOW_ID_PREFIX_ENV = "TEMPORAL_WORKFLOW_ID_PREFIX"; + + public static final String DEFAULT_TASK_QUEUE = "serverless-task-queue-java"; + public static final String DEFAULT_WORKFLOW_ID_PREFIX = "serverless-workflow-id-java"; + public static final String DEFAULT_DEPLOYMENT_NAME = "my-app"; + public static final String DEFAULT_BUILD_ID = "build-1"; + + public static void configure(LambdaWorkerOptions options) { + options.setTaskQueue(taskQueue()); + options.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); + options.registerActivitiesImplementations(new GreetingActivitiesImpl()); + } + + public static String taskQueue() { + return envOrDefault(TASK_QUEUE_ENV, DEFAULT_TASK_QUEUE); + } + + public static String workflowIdPrefix() { + return envOrDefault(WORKFLOW_ID_PREFIX_ENV, DEFAULT_WORKFLOW_ID_PREFIX); + } + + public static String deploymentName() { + return envOrDefault(DEPLOYMENT_NAME_ENV, DEFAULT_DEPLOYMENT_NAME); + } + + public static String buildId() { + return envOrDefault(BUILD_ID_ENV, DEFAULT_BUILD_ID); + } + + private static String envOrDefault(String name, String defaultValue) { + String value = System.getenv(name); + return value == null || value.isBlank() ? defaultValue : value; + } + + private LambdaWorkerSample() {} +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java new file mode 100644 index 00000000..25aa58f8 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java @@ -0,0 +1,12 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +/** Sample workflow run by the Lambda worker. */ +@WorkflowInterface +public interface SampleWorkflow { + + @WorkflowMethod + String getGreeting(String name); +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java new file mode 100644 index 00000000..0f6e7cbb --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java @@ -0,0 +1,28 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.ActivityOptions; +import io.temporal.common.VersioningBehavior; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowVersioningBehavior; +import java.time.Duration; +import org.slf4j.Logger; + +/** Workflow implementation that executes a greeting Activity. */ +public class SampleWorkflowImpl implements SampleWorkflow { + + private static final Logger logger = Workflow.getLogger(SampleWorkflowImpl.class); + + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build()); + + @Override + @WorkflowVersioningBehavior(VersioningBehavior.PINNED) + public String getGreeting(String name) { + logger.info("SampleWorkflow started for {}", name); + String result = activities.createGreeting(name); + logger.info("SampleWorkflow completed with {}", result); + return result; + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java new file mode 100644 index 00000000..c8e46a4b --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java @@ -0,0 +1,50 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** Local helper that starts a Workflow execution for the Lambda worker to process. */ +public class Starter { + + public static void main(String[] args) { + String name = args.length > 0 ? String.join(" ", args) : "Serverless Lambda Worker!"; + + WorkflowServiceStubs service = null; + try { + ClientConfigProfile profile = ClientConfigProfile.load(); + service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + WorkflowClient client = + WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + + SampleWorkflow workflow = + client.newWorkflowStub( + SampleWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId(LambdaWorkerSample.workflowIdPrefix() + "-" + UUID.randomUUID()) + .setTaskQueue(LambdaWorkerSample.taskQueue()) + .build()); + + WorkflowExecution execution = WorkflowClient.start(workflow::getGreeting, name); + System.out.printf( + "Started workflow WorkflowID=%s RunID=%s%n", + execution.getWorkflowId(), execution.getRunId()); + + String result = WorkflowStub.fromTyped(workflow).getResult(String.class); + System.out.println("Workflow result: " + result); + } catch (IOException e) { + throw new RuntimeException("Failed to load Temporal client configuration", e); + } finally { + if (service != null) { + service.shutdown(); + service.awaitTermination(10, TimeUnit.SECONDS); + } + } + } +} diff --git a/lambda-worker/src/main/resources/logback.xml b/lambda-worker/src/main/resources/logback.xml new file mode 100644 index 00000000..7c5faa44 --- /dev/null +++ b/lambda-worker/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + + diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java new file mode 100644 index 00000000..a4ca9927 --- /dev/null +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java @@ -0,0 +1,62 @@ +package io.temporal.samples.lambdaworker; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.amazonaws.services.lambda.runtime.RequestHandler; +import io.temporal.aws.lambda.LambdaWorker; +import io.temporal.aws.lambda.LambdaWorkerOptions; +import io.temporal.common.WorkerDeploymentVersion; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +/** Tests Lambda worker registration without AWS. */ +public class LambdaWorkerSampleTest { + + @Test + public void configureSetsTaskQueueAndRegistrations() throws Exception { + LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(baseEnv()); + + LambdaWorkerSample.configure(options); + + assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); + assertEquals(2, registrations(options).size()); + + RequestHandler handler = + LambdaWorker.newHandler( + new WorkerDeploymentVersion( + LambdaWorkerSample.DEFAULT_DEPLOYMENT_NAME, LambdaWorkerSample.DEFAULT_BUILD_ID), + options); + assertNotNull(handler); + } + + @Test + public void configureUsesDefaultTaskQueueWhenNoProcessEnvironmentOverride() throws Exception { + Map env = baseEnv(); + env.put(LambdaWorkerOptions.TEMPORAL_TASK_QUEUE, "from-env"); + LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(env); + + LambdaWorkerSample.configure(options); + + assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); + assertEquals(2, registrations(options).size()); + } + + private static Map baseEnv() { + Map env = new HashMap<>(); + env.put(LambdaWorkerOptions.TEMPORAL_CONFIG_FILE, "/nonexistent/temporal.toml"); + return env; + } + + private static List registrations(LambdaWorkerOptions options) throws Exception { + Field registrations = LambdaWorkerOptions.class.getDeclaredField("registrations"); + registrations.setAccessible(true); + Object value = registrations.get(options); + assertTrue(value instanceof List); + return (List) value; + } +} diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java new file mode 100644 index 00000000..d4d2367b --- /dev/null +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java @@ -0,0 +1,32 @@ +package io.temporal.samples.lambdaworker; + +import static org.junit.Assert.assertEquals; + +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.TestWorkflowRule; +import org.junit.Rule; +import org.junit.Test; + +/** Unit test for the sample Workflow and Activity. */ +public class SampleWorkflowTest { + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(SampleWorkflowImpl.class) + .setActivityImplementations(new GreetingActivitiesImpl()) + .build(); + + @Test + public void workflowReturnsGreeting() { + SampleWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + SampleWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals( + "Hello, Serverless Lambda Worker!!", workflow.getGreeting("Serverless Lambda Worker!")); + } +} diff --git a/lambda-worker/temporal.toml.sample b/lambda-worker/temporal.toml.sample new file mode 100644 index 00000000..0117d5cf --- /dev/null +++ b/lambda-worker/temporal.toml.sample @@ -0,0 +1,9 @@ +[profile.default] +address = "..tmprl.cloud:7233" +namespace = "." +api_key = "" + +# For mTLS instead of API key auth, remove api_key and uncomment: +# [profile.default.tls] +# client_cert_path = "client.pem" +# client_key_path = "client.key" diff --git a/settings.gradle b/settings.gradle index b99ad281..c6756a18 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,10 @@ rootProject.name = 'temporal-java-samples' + +def localSettings = file('settings.local.gradle') +if (localSettings.exists()) { + apply from: localSettings +} + include 'core' include 'springai:basic' include 'springai:mcp' @@ -6,4 +12,4 @@ include 'springai:multimodel' include 'springai:rag' include 'springboot' include 'springboot-basic' - +include 'lambda-worker' From c2fd02ebc7e7496adca5bfe21e2ae207b2ab2cd3 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 15 Jun 2026 17:25:13 -0500 Subject: [PATCH 02/10] Use builder for immutable LambdaWorkerOptions --- .../samples/lambdaworker/LambdaWorkerSample.java | 8 ++++---- .../samples/lambdaworker/LambdaWorkerSampleTest.java | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java index 84868d0b..77c3282a 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -15,10 +15,10 @@ public final class LambdaWorkerSample { public static final String DEFAULT_DEPLOYMENT_NAME = "my-app"; public static final String DEFAULT_BUILD_ID = "build-1"; - public static void configure(LambdaWorkerOptions options) { - options.setTaskQueue(taskQueue()); - options.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); - options.registerActivitiesImplementations(new GreetingActivitiesImpl()); + public static void configure(LambdaWorkerOptions.Builder builder) { + builder.setTaskQueue(taskQueue()); + builder.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); + builder.registerActivitiesImplementations(new GreetingActivitiesImpl()); } public static String taskQueue() { diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java index a4ca9927..b1c9a21b 100644 --- a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java @@ -19,9 +19,10 @@ public class LambdaWorkerSampleTest { @Test public void configureSetsTaskQueueAndRegistrations() throws Exception { - LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(baseEnv()); + LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(baseEnv()); - LambdaWorkerSample.configure(options); + LambdaWorkerSample.configure(builder); + LambdaWorkerOptions options = builder.build(); assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); assertEquals(2, registrations(options).size()); @@ -38,9 +39,10 @@ public void configureSetsTaskQueueAndRegistrations() throws Exception { public void configureUsesDefaultTaskQueueWhenNoProcessEnvironmentOverride() throws Exception { Map env = baseEnv(); env.put(LambdaWorkerOptions.TEMPORAL_TASK_QUEUE, "from-env"); - LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(env); + LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(env); - LambdaWorkerSample.configure(options); + LambdaWorkerSample.configure(builder); + LambdaWorkerOptions options = builder.build(); assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); assertEquals(2, registrations(options).size()); From a4a78eb21a716e72bcd1195e9f490b792ab0b1bc Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 13 Jul 2026 15:26:39 -0500 Subject: [PATCH 03/10] Update Lambda worker sample for merged SDK --- lambda-worker/README.md | 7 ++++--- lambda-worker/build.gradle | 19 +++++++++++++++---- .../samples/lambdaworker/LambdaFunction.java | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lambda-worker/README.md b/lambda-worker/README.md index 44cc4668..fb6b1a05 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -5,9 +5,10 @@ It registers a simple greeting Workflow and Activity, configures Worker Deployme Versioning, and includes helper scripts for packaging the Lambda and configuring Temporal Cloud invocation. -For local SDK co-development, create `settings.local.gradle` in the repository root and -add `includeBuild '../sdk-java'`. That file is ignored by Git and makes Gradle use the -in-development `io.temporal:temporal-aws-lambda` SDK add-on before it is published. +This sample uses the `1.37.0-SNAPSHOT` SDK artifacts for AWS Lambda worker support. For +local SDK co-development, create `settings.local.gradle` in the repository root and add +`includeBuild '../sdk-java'`. That file is ignored by Git and makes Gradle use the local +SDK checkout instead of resolving snapshot artifacts. ## Prerequisites diff --git a/lambda-worker/build.gradle b/lambda-worker/build.gradle index a1658c53..266a08d2 100644 --- a/lambda-worker/build.gradle +++ b/lambda-worker/build.gradle @@ -1,10 +1,21 @@ +def lambdaWorkerJavaSDKVersion = '1.37.0-SNAPSHOT' + +repositories { + maven { + url "https://central.sonatype.com/repository/maven-snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + dependencies { - implementation "io.temporal:temporal-sdk:$javaSDKVersion" - implementation "io.temporal:temporal-envconfig:$javaSDKVersion" - implementation "io.temporal:temporal-aws-lambda:$javaSDKVersion" + implementation "io.temporal:temporal-sdk:$lambdaWorkerJavaSDKVersion" + implementation "io.temporal:temporal-envconfig:$lambdaWorkerJavaSDKVersion" + implementation "io.temporal:temporal-aws-lambda:$lambdaWorkerJavaSDKVersion" implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' - testImplementation "io.temporal:temporal-testing:$javaSDKVersion" + testImplementation "io.temporal:temporal-testing:$lambdaWorkerJavaSDKVersion" testImplementation "junit:junit:4.13.2" testImplementation(platform("org.junit:junit-bom:5.10.3")) testRuntimeOnly "org.junit.vintage:junit-vintage-engine" diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java index 547a6d71..9dd78c22 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -9,7 +9,7 @@ public class LambdaFunction implements RequestHandler { private static final RequestHandler WORKER = - LambdaWorker.run( + LambdaWorker.define( new WorkerDeploymentVersion( LambdaWorkerSample.deploymentName(), LambdaWorkerSample.buildId()), LambdaWorkerSample::configure); From fdb039a074b6b667858ec8a5479f87882966fc60 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 15 Jul 2026 12:32:40 -0500 Subject: [PATCH 04/10] Use released SDK dependencies for Lambda sample --- .gitignore | 1 - lambda-worker/README.md | 5 +---- lambda-worker/build.gradle | 19 ++++--------------- settings.gradle | 5 ----- 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index f587ebda..a1abef91 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target .DS_Store .idea .gradle -settings.local.gradle **/build/ **/out/ .classpath diff --git a/lambda-worker/README.md b/lambda-worker/README.md index fb6b1a05..62b03439 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -5,10 +5,7 @@ It registers a simple greeting Workflow and Activity, configures Worker Deployme Versioning, and includes helper scripts for packaging the Lambda and configuring Temporal Cloud invocation. -This sample uses the `1.37.0-SNAPSHOT` SDK artifacts for AWS Lambda worker support. For -local SDK co-development, create `settings.local.gradle` in the repository root and add -`includeBuild '../sdk-java'`. That file is ignored by Git and makes Gradle use the local -SDK checkout instead of resolving snapshot artifacts. +It uses the same published Temporal Java SDK version as the other samples in this repository. ## Prerequisites diff --git a/lambda-worker/build.gradle b/lambda-worker/build.gradle index 266a08d2..a1658c53 100644 --- a/lambda-worker/build.gradle +++ b/lambda-worker/build.gradle @@ -1,21 +1,10 @@ -def lambdaWorkerJavaSDKVersion = '1.37.0-SNAPSHOT' - -repositories { - maven { - url "https://central.sonatype.com/repository/maven-snapshots/" - mavenContent { - snapshotsOnly() - } - } -} - dependencies { - implementation "io.temporal:temporal-sdk:$lambdaWorkerJavaSDKVersion" - implementation "io.temporal:temporal-envconfig:$lambdaWorkerJavaSDKVersion" - implementation "io.temporal:temporal-aws-lambda:$lambdaWorkerJavaSDKVersion" + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + implementation "io.temporal:temporal-aws-lambda:$javaSDKVersion" implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' - testImplementation "io.temporal:temporal-testing:$lambdaWorkerJavaSDKVersion" + testImplementation "io.temporal:temporal-testing:$javaSDKVersion" testImplementation "junit:junit:4.13.2" testImplementation(platform("org.junit:junit-bom:5.10.3")) testRuntimeOnly "org.junit.vintage:junit-vintage-engine" diff --git a/settings.gradle b/settings.gradle index c6756a18..b97d02a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,5 @@ rootProject.name = 'temporal-java-samples' -def localSettings = file('settings.local.gradle') -if (localSettings.exists()) { - apply from: localSettings -} - include 'core' include 'springai:basic' include 'springai:mcp' From a76c0882dfdf3635bd8e5b54f85cf6739e571439 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Tue, 21 Jul 2026 12:24:26 -0500 Subject: [PATCH 05/10] Refactor Lambda worker sample layout --- lambda-worker/.gitignore | 2 + lambda-worker/README.md | 70 +++++++++---------- lambda-worker/{ => deploy}/deploy-lambda.sh | 7 +- .../enable-telemetry.sh} | 8 +-- ...-role-for-temporal-lambda-invoke-test.yaml | 0 lambda-worker/{ => deploy}/mk-iam-role.sh | 3 +- ...le => otel-collector-config.template.yaml} | 0 .../lambdaworker/LambdaWorkerSampleTest.java | 64 ----------------- lambda-worker/starter/build.gradle | 19 +++++ .../samples/lambdaworker/Starter.java | 10 ++- ...ral.toml.sample => temporal.template.toml} | 5 +- lambda-worker/{ => worker}/build.gradle | 8 --- .../lambdaworker/GreetingActivities.java | 0 .../lambdaworker/GreetingActivitiesImpl.java | 0 .../samples/lambdaworker/LambdaFunction.java | 6 +- .../lambdaworker/LambdaWorkerSample.java | 20 +----- .../samples/lambdaworker/SampleWorkflow.java | 0 .../lambdaworker/SampleWorkflowImpl.java | 0 .../src/main/resources/logback.xml | 0 .../lambdaworker/SampleWorkflowTest.java | 0 settings.gradle | 3 +- 21 files changed, 87 insertions(+), 138 deletions(-) create mode 100644 lambda-worker/.gitignore rename lambda-worker/{ => deploy}/deploy-lambda.sh (81%) rename lambda-worker/{extra-setup-steps => deploy/enable-telemetry.sh} (71%) rename lambda-worker/{ => deploy}/iam-role-for-temporal-lambda-invoke-test.yaml (100%) rename lambda-worker/{ => deploy}/mk-iam-role.sh (80%) rename lambda-worker/{otel-collector-config.yaml.sample => otel-collector-config.template.yaml} (100%) delete mode 100644 lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java create mode 100644 lambda-worker/starter/build.gradle rename lambda-worker/{ => starter}/src/main/java/io/temporal/samples/lambdaworker/Starter.java (80%) rename lambda-worker/{temporal.toml.sample => temporal.template.toml} (56%) rename lambda-worker/{ => worker}/build.gradle (90%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java (100%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java (100%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java (72%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java (51%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java (100%) rename lambda-worker/{ => worker}/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java (100%) rename lambda-worker/{ => worker}/src/main/resources/logback.xml (100%) rename lambda-worker/{ => worker}/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java (100%) diff --git a/lambda-worker/.gitignore b/lambda-worker/.gitignore new file mode 100644 index 00000000..69b7c323 --- /dev/null +++ b/lambda-worker/.gitignore @@ -0,0 +1,2 @@ +temporal.toml +otel-collector-config.yaml diff --git a/lambda-worker/README.md b/lambda-worker/README.md index 62b03439..00eb109d 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -5,6 +5,9 @@ It registers a simple greeting Workflow and Activity, configures Worker Deployme Versioning, and includes helper scripts for packaging the Lambda and configuring Temporal Cloud invocation. +The deployable Worker and local Workflow starter are separate Gradle projects, so +starter-only code and dependencies are not included in the Lambda artifact. + It uses the same published Temporal Java SDK version as the other samples in this repository. ## Prerequisites @@ -17,27 +20,19 @@ It uses the same published Temporal Java SDK version as the other samples in thi - A Temporal Cloud API key. This walkthrough deploys it as a Lambda environment variable because these are development-only secrets. -## Files - -| File | Description | -|------|-------------| -| `src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java` | AWS Lambda handler | -| `src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java` | Shared task queue, deployment version, and worker registrations | -| `src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow*.java` | Sample Workflow interface and implementation | -| `src/main/java/io/temporal/samples/lambdaworker/GreetingActivities*.java` | Sample Activity interface and implementation | -| `src/main/java/io/temporal/samples/lambdaworker/Starter.java` | Local helper that starts a Workflow for the Lambda worker | -| `temporal.toml.sample` | Temporal client connection configuration | -| `otel-collector-config.yaml.sample` | Optional ADOT collector configuration | -| `deploy-lambda.sh` | Builds and uploads the Lambda deployment package | -| `mk-iam-role.sh` | Creates the role Temporal Cloud assumes to invoke Lambda | -| `iam-role-for-temporal-lambda-invoke-test.yaml` | CloudFormation template for the invocation role | -| `extra-setup-steps` | Optional IAM and Lambda settings for OpenTelemetry | +## Layout + +- `worker/` contains the Lambda handler, Workflow, Activity, and deployable Worker project. +- `starter/` contains the local Workflow starter project. +- `deploy/` contains the AWS deployment scripts and CloudFormation template. +- `temporal.template.toml` and `otel-collector-config.template.yaml` are configuration + templates for local and Lambda setup. ## Build ```bash -./gradlew :lambda-worker:test -./gradlew :lambda-worker:shadowJar +./gradlew :lambda-worker:worker:test +./gradlew :lambda-worker:worker:shadowJar ``` The Lambda handler string is: @@ -79,18 +74,20 @@ TEMPORAL_ADDRESS TEMPORAL_NAMESPACE TEMPORAL_API_KEY TEMPORAL_TASK_QUEUE -TEMPORAL_WORKER_DEPLOYMENT_NAME -TEMPORAL_WORKER_BUILD_ID +TEMPORAL_LAMBDA_DEPLOYMENT_NAME +TEMPORAL_LAMBDA_BUILD_ID ``` -The local starter also reads `TEMPORAL_TASK_QUEUE` and `TEMPORAL_WORKFLOW_ID_PREFIX`. +The local starter also reads `TEMPORAL_TASK_QUEUE` and +`TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX`. -You can also use `temporal.toml.sample` as a starting point for `temporal.toml` and set -`TEMPORAL_CONFIG_FILE=temporal.toml`. +You can also copy `lambda-worker/temporal.template.toml` to +`lambda-worker/temporal.toml`, fill in the connection details, and set +`TEMPORAL_CONFIG_FILE=lambda-worker/temporal.toml`. The generated file is ignored by Git. -`TEMPORAL_TASK_QUEUE`, `TEMPORAL_WORKER_DEPLOYMENT_NAME`, -`TEMPORAL_WORKER_BUILD_ID`, and `TEMPORAL_WORKFLOW_ID_PREFIX` are optional. The values -above are the sample defaults. +`TEMPORAL_TASK_QUEUE`, `TEMPORAL_LAMBDA_DEPLOYMENT_NAME`, +`TEMPORAL_LAMBDA_BUILD_ID`, and `TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX` are optional. The +values above are the sample defaults. ## Deploy Lambda @@ -135,15 +132,15 @@ for direct upload in this sample; use S3 if your local artifact grows beyond Lam direct upload limit. ```bash -./gradlew :lambda-worker:shadowJar +./gradlew :lambda-worker:worker:shadowJar aws lambda create-function \ --function-name "$FUNCTION_NAME" \ --runtime java17 \ --handler io.temporal.samples.lambdaworker.LambdaFunction::handleRequest \ --role "$EXECUTION_ROLE_ARN" \ - --zip-file fileb://lambda-worker/build/libs/lambda-worker-1.0.0-all.jar \ - --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_WORKER_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_WORKER_BUILD_ID=$BUILD_ID}" \ + --zip-file fileb://lambda-worker/worker/build/libs/lambda-worker-1.0.0-all.jar \ + --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_LAMBDA_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_LAMBDA_BUILD_ID=$BUILD_ID}" \ --timeout 90 \ --memory-size 1024 \ --query 'FunctionArn' \ @@ -162,13 +159,13 @@ export FUNCTION_ARN="$( To update code after the function exists: ```bash -./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" ``` If direct upload is too large, set `LAMBDA_CODE_S3_BUCKET` and rerun: ```bash -LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" ``` ## Configure Invocation @@ -176,9 +173,7 @@ LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy-lambda.sh "$FUNCTION_ Create the IAM role that Temporal Cloud assumes to invoke the Lambda: ```bash -cd lambda-worker -./mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$FUNCTION_ARN" -cd .. +./lambda-worker/deploy/mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$FUNCTION_ARN" aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" @@ -232,10 +227,9 @@ After the Worker Deployment Version is current, start the sample Workflow: ```bash export TEMPORAL_TASK_QUEUE="$TASK_QUEUE" -export TEMPORAL_WORKFLOW_ID_PREFIX="$WORKFLOW_PREFIX" +export TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX="$WORKFLOW_PREFIX" -./gradlew -q :lambda-worker:execute \ - -PmainClass=io.temporal.samples.lambdaworker.Starter +./gradlew -q :lambda-worker:starter:execute ``` The starter only creates a Workflow Execution. It does not start a local Worker. The @@ -246,3 +240,7 @@ Lambda function. For local development of the Workflow and Activity logic, run the unit tests. They use `TestWorkflowRule` and do not require AWS or a running Temporal Service. + +```bash +./gradlew :lambda-worker:worker:test +``` diff --git a/lambda-worker/deploy-lambda.sh b/lambda-worker/deploy/deploy-lambda.sh similarity index 81% rename from lambda-worker/deploy-lambda.sh rename to lambda-worker/deploy/deploy-lambda.sh index bcf94a2d..f18fd989 100755 --- a/lambda-worker/deploy-lambda.sh +++ b/lambda-worker/deploy/deploy-lambda.sh @@ -4,10 +4,11 @@ set -euo pipefail FUNCTION_NAME="${1:?Usage: deploy-lambda.sh }" MAX_DIRECT_UPLOAD_BYTES=50000000 -cd "$(dirname "$0")/.." -./gradlew :lambda-worker:shadowJar +REPOSITORY_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$REPOSITORY_DIR" +./gradlew :lambda-worker:worker:shadowJar -JAR_FILE="$(find lambda-worker/build/libs -name 'lambda-worker-*-all.jar' | head -n 1)" +JAR_FILE="$(find lambda-worker/worker/build/libs -name 'lambda-worker-*-all.jar' | head -n 1)" if stat -f%z "$JAR_FILE" >/dev/null 2>&1; then JAR_SIZE="$(stat -f%z "$JAR_FILE")" diff --git a/lambda-worker/extra-setup-steps b/lambda-worker/deploy/enable-telemetry.sh similarity index 71% rename from lambda-worker/extra-setup-steps rename to lambda-worker/deploy/enable-telemetry.sh index 8c6dd0f8..e0313194 100755 --- a/lambda-worker/extra-setup-steps +++ b/lambda-worker/deploy/enable-telemetry.sh @@ -1,10 +1,10 @@ #!/bin/bash set -euo pipefail -ROLE_NAME="${1:?Usage: extra-setup-steps }" -FUNCTION_NAME="${2:?Usage: extra-setup-steps }" -REGION="${3:?Usage: extra-setup-steps }" -ACCOUNT_ID="${4:?Usage: extra-setup-steps }" +ROLE_NAME="${1:?Usage: enable-telemetry.sh }" +FUNCTION_NAME="${2:?Usage: enable-telemetry.sh }" +REGION="${3:?Usage: enable-telemetry.sh }" +ACCOUNT_ID="${4:?Usage: enable-telemetry.sh }" aws iam put-role-policy \ --role-name "$ROLE_NAME" \ diff --git a/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml b/lambda-worker/deploy/iam-role-for-temporal-lambda-invoke-test.yaml similarity index 100% rename from lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml rename to lambda-worker/deploy/iam-role-for-temporal-lambda-invoke-test.yaml diff --git a/lambda-worker/mk-iam-role.sh b/lambda-worker/deploy/mk-iam-role.sh similarity index 80% rename from lambda-worker/mk-iam-role.sh rename to lambda-worker/deploy/mk-iam-role.sh index 20aeae5d..da596849 100755 --- a/lambda-worker/mk-iam-role.sh +++ b/lambda-worker/deploy/mk-iam-role.sh @@ -4,10 +4,11 @@ set -euo pipefail STACK_NAME="${1:?Usage: mk-iam-role.sh }" EXTERNAL_ID="${2:?Usage: mk-iam-role.sh }" LAMBDA_ARN="${3:?Usage: mk-iam-role.sh }" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" aws cloudformation create-stack \ --stack-name "$STACK_NAME" \ - --template-body file://iam-role-for-temporal-lambda-invoke-test.yaml \ + --template-body "file://${SCRIPT_DIR}/iam-role-for-temporal-lambda-invoke-test.yaml" \ --parameters \ ParameterKey=AssumeRoleExternalId,ParameterValue="$EXTERNAL_ID" \ ParameterKey=LambdaFunctionARNs,ParameterValue="\"$LAMBDA_ARN\"" \ diff --git a/lambda-worker/otel-collector-config.yaml.sample b/lambda-worker/otel-collector-config.template.yaml similarity index 100% rename from lambda-worker/otel-collector-config.yaml.sample rename to lambda-worker/otel-collector-config.template.yaml diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java deleted file mode 100644 index b1c9a21b..00000000 --- a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.temporal.samples.lambdaworker; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.amazonaws.services.lambda.runtime.RequestHandler; -import io.temporal.aws.lambda.LambdaWorker; -import io.temporal.aws.lambda.LambdaWorkerOptions; -import io.temporal.common.WorkerDeploymentVersion; -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; - -/** Tests Lambda worker registration without AWS. */ -public class LambdaWorkerSampleTest { - - @Test - public void configureSetsTaskQueueAndRegistrations() throws Exception { - LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(baseEnv()); - - LambdaWorkerSample.configure(builder); - LambdaWorkerOptions options = builder.build(); - - assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); - assertEquals(2, registrations(options).size()); - - RequestHandler handler = - LambdaWorker.newHandler( - new WorkerDeploymentVersion( - LambdaWorkerSample.DEFAULT_DEPLOYMENT_NAME, LambdaWorkerSample.DEFAULT_BUILD_ID), - options); - assertNotNull(handler); - } - - @Test - public void configureUsesDefaultTaskQueueWhenNoProcessEnvironmentOverride() throws Exception { - Map env = baseEnv(); - env.put(LambdaWorkerOptions.TEMPORAL_TASK_QUEUE, "from-env"); - LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(env); - - LambdaWorkerSample.configure(builder); - LambdaWorkerOptions options = builder.build(); - - assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); - assertEquals(2, registrations(options).size()); - } - - private static Map baseEnv() { - Map env = new HashMap<>(); - env.put(LambdaWorkerOptions.TEMPORAL_CONFIG_FILE, "/nonexistent/temporal.toml"); - return env; - } - - private static List registrations(LambdaWorkerOptions options) throws Exception { - Field registrations = LambdaWorkerOptions.class.getDeclaredField("registrations"); - registrations.setAccessible(true); - Object value = registrations.get(options); - assertTrue(value instanceof List); - return (List) value; - } -} diff --git a/lambda-worker/starter/build.gradle b/lambda-worker/starter/build.gradle new file mode 100644 index 00000000..df615ecc --- /dev/null +++ b/lambda-worker/starter/build.gradle @@ -0,0 +1,19 @@ +dependencies { + implementation project(':lambda-worker:worker') + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' + + dependencies { + errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') + errorprone('com.google.errorprone:error_prone_core:2.28.0') + } +} + +tasks.register('execute', JavaExec) { + mainClass = 'io.temporal.samples.lambdaworker.Starter' + classpath = sourceSets.main.runtimeClasspath + if (findProperty("args")) { + args findProperty("args").tokenize() + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java b/lambda-worker/starter/src/main/java/io/temporal/samples/lambdaworker/Starter.java similarity index 80% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java rename to lambda-worker/starter/src/main/java/io/temporal/samples/lambdaworker/Starter.java index c8e46a4b..6ad4afa4 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java +++ b/lambda-worker/starter/src/main/java/io/temporal/samples/lambdaworker/Starter.java @@ -13,6 +13,9 @@ /** Local helper that starts a Workflow execution for the Lambda worker to process. */ public class Starter { + private static final String WORKFLOW_ID_PREFIX_ENV = "TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX"; + private static final String DEFAULT_WORKFLOW_ID_PREFIX = "serverless-workflow-id-java"; + public static void main(String[] args) { String name = args.length > 0 ? String.join(" ", args) : "Serverless Lambda Worker!"; @@ -27,7 +30,7 @@ public static void main(String[] args) { client.newWorkflowStub( SampleWorkflow.class, WorkflowOptions.newBuilder() - .setWorkflowId(LambdaWorkerSample.workflowIdPrefix() + "-" + UUID.randomUUID()) + .setWorkflowId(workflowIdPrefix() + "-" + UUID.randomUUID()) .setTaskQueue(LambdaWorkerSample.taskQueue()) .build()); @@ -47,4 +50,9 @@ public static void main(String[] args) { } } } + + private static String workflowIdPrefix() { + String value = System.getenv(WORKFLOW_ID_PREFIX_ENV); + return value == null || value.isBlank() ? DEFAULT_WORKFLOW_ID_PREFIX : value; + } } diff --git a/lambda-worker/temporal.toml.sample b/lambda-worker/temporal.template.toml similarity index 56% rename from lambda-worker/temporal.toml.sample rename to lambda-worker/temporal.template.toml index 0117d5cf..d478e3eb 100644 --- a/lambda-worker/temporal.toml.sample +++ b/lambda-worker/temporal.template.toml @@ -1,9 +1,10 @@ [profile.default] address = "..tmprl.cloud:7233" namespace = "." -api_key = "" -# For mTLS instead of API key auth, remove api_key and uncomment: +# Set TEMPORAL_API_KEY in the environment instead of storing it in this file. + +# For mTLS instead of API key auth, leave TEMPORAL_API_KEY unset and uncomment: # [profile.default.tls] # client_cert_path = "client.pem" # client_key_path = "client.key" diff --git a/lambda-worker/build.gradle b/lambda-worker/worker/build.gradle similarity index 90% rename from lambda-worker/build.gradle rename to lambda-worker/worker/build.gradle index a1658c53..a1937981 100644 --- a/lambda-worker/build.gradle +++ b/lambda-worker/worker/build.gradle @@ -15,14 +15,6 @@ dependencies { } } -tasks.register('execute', JavaExec) { - mainClass = findProperty("mainClass") ?: "" - classpath = sourceSets.main.runtimeClasspath - if (findProperty("args")) { - args findProperty("args").tokenize() - } -} - tasks.register('shadowJar', Jar) { archiveBaseName = 'lambda-worker' archiveClassifier = 'all' diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java similarity index 100% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java similarity index 100% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java similarity index 72% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java index 9dd78c22..5fc5eb39 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java +++ b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -12,7 +12,11 @@ public class LambdaFunction implements RequestHandler { LambdaWorker.define( new WorkerDeploymentVersion( LambdaWorkerSample.deploymentName(), LambdaWorkerSample.buildId()), - LambdaWorkerSample::configure); + builder -> { + builder.setTaskQueue(LambdaWorkerSample.taskQueue()); + builder.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); + builder.registerActivitiesImplementations(new GreetingActivitiesImpl()); + }); @Override public Void handleRequest(Object input, Context context) { diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java similarity index 51% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java index 77c3282a..b24c87ed 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java +++ b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -1,34 +1,20 @@ package io.temporal.samples.lambdaworker; -import io.temporal.aws.lambda.LambdaWorkerOptions; - -/** Shared constants and worker registration for the Lambda Worker sample. */ +/** Shared configuration for the Lambda Worker sample. */ public final class LambdaWorkerSample { public static final String TASK_QUEUE_ENV = "TEMPORAL_TASK_QUEUE"; - public static final String DEPLOYMENT_NAME_ENV = "TEMPORAL_WORKER_DEPLOYMENT_NAME"; - public static final String BUILD_ID_ENV = "TEMPORAL_WORKER_BUILD_ID"; - public static final String WORKFLOW_ID_PREFIX_ENV = "TEMPORAL_WORKFLOW_ID_PREFIX"; + public static final String DEPLOYMENT_NAME_ENV = "TEMPORAL_LAMBDA_DEPLOYMENT_NAME"; + public static final String BUILD_ID_ENV = "TEMPORAL_LAMBDA_BUILD_ID"; public static final String DEFAULT_TASK_QUEUE = "serverless-task-queue-java"; - public static final String DEFAULT_WORKFLOW_ID_PREFIX = "serverless-workflow-id-java"; public static final String DEFAULT_DEPLOYMENT_NAME = "my-app"; public static final String DEFAULT_BUILD_ID = "build-1"; - public static void configure(LambdaWorkerOptions.Builder builder) { - builder.setTaskQueue(taskQueue()); - builder.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); - builder.registerActivitiesImplementations(new GreetingActivitiesImpl()); - } - public static String taskQueue() { return envOrDefault(TASK_QUEUE_ENV, DEFAULT_TASK_QUEUE); } - public static String workflowIdPrefix() { - return envOrDefault(WORKFLOW_ID_PREFIX_ENV, DEFAULT_WORKFLOW_ID_PREFIX); - } - public static String deploymentName() { return envOrDefault(DEPLOYMENT_NAME_ENV, DEFAULT_DEPLOYMENT_NAME); } diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java similarity index 100% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java similarity index 100% rename from lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java rename to lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java diff --git a/lambda-worker/src/main/resources/logback.xml b/lambda-worker/worker/src/main/resources/logback.xml similarity index 100% rename from lambda-worker/src/main/resources/logback.xml rename to lambda-worker/worker/src/main/resources/logback.xml diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java b/lambda-worker/worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java similarity index 100% rename from lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java rename to lambda-worker/worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java diff --git a/settings.gradle b/settings.gradle index b97d02a4..b19f2fd7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,5 @@ include 'springai:multimodel' include 'springai:rag' include 'springboot' include 'springboot-basic' -include 'lambda-worker' +include 'lambda-worker:starter' +include 'lambda-worker:worker' From de942142ac3ccef1f9c089e4f865ce96d4aa0562 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Tue, 21 Jul 2026 12:55:53 -0500 Subject: [PATCH 06/10] Avoid exposing Lambda environment during deploy --- lambda-worker/deploy/deploy-lambda.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lambda-worker/deploy/deploy-lambda.sh b/lambda-worker/deploy/deploy-lambda.sh index f18fd989..7341193e 100755 --- a/lambda-worker/deploy/deploy-lambda.sh +++ b/lambda-worker/deploy/deploy-lambda.sh @@ -22,7 +22,9 @@ if [[ -n "${LAMBDA_CODE_S3_BUCKET:-}" ]]; then aws lambda update-function-code \ --function-name "$FUNCTION_NAME" \ --s3-bucket "$LAMBDA_CODE_S3_BUCKET" \ - --s3-key "$S3_KEY" + --s3-key "$S3_KEY" \ + --query 'FunctionArn' \ + --output text exit 0 fi @@ -32,4 +34,8 @@ if (( JAR_SIZE > MAX_DIRECT_UPLOAD_BYTES )); then exit 1 fi -aws lambda update-function-code --function-name "$FUNCTION_NAME" --zip-file "fileb://$JAR_FILE" +aws lambda update-function-code \ + --function-name "$FUNCTION_NAME" \ + --zip-file "fileb://$JAR_FILE" \ + --query 'FunctionArn' \ + --output text From 48c9f44acc1c626d0f5f89542f5c0463f67e59b7 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 22 Jul 2026 13:16:59 -0500 Subject: [PATCH 07/10] Fix README description of `temporal.toml` Co-authored-by: Dan Plyukhin --- lambda-worker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda-worker/README.md b/lambda-worker/README.md index 00eb109d..ba9094c6 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -83,7 +83,7 @@ The local starter also reads `TEMPORAL_TASK_QUEUE` and You can also copy `lambda-worker/temporal.template.toml` to `lambda-worker/temporal.toml`, fill in the connection details, and set -`TEMPORAL_CONFIG_FILE=lambda-worker/temporal.toml`. The generated file is ignored by Git. +`TEMPORAL_CONFIG_FILE=lambda-worker/temporal.toml`. The `temporal.toml` file is ignored by Git. `TEMPORAL_TASK_QUEUE`, `TEMPORAL_LAMBDA_DEPLOYMENT_NAME`, `TEMPORAL_LAMBDA_BUILD_ID`, and `TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX` are optional. The From 52ab0353a47a2deb24c272285ff6f5c8d0f9dbd1 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 22 Jul 2026 13:45:22 -0500 Subject: [PATCH 08/10] Add docs link for self-hosted serverless workers Co-authored-by: Dan Plyukhin --- lambda-worker/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda-worker/README.md b/lambda-worker/README.md index ba9094c6..adfd2d3b 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -15,8 +15,8 @@ It uses the same published Temporal Java SDK version as the other samples in thi - Java 17+ - AWS CLI configured with permissions to create Lambda functions, IAM roles, and CloudFormation stacks -- A Temporal Cloud namespace with Serverless Workers enabled, or a self-hosted Temporal - Service configured for AWS Lambda Serverless Workers +- A Temporal Cloud namespace with Serverless Workers enabled, or a [self-hosted Temporal Service](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/self-hosted-setup) + configured for AWS Lambda Serverless Workers - A Temporal Cloud API key. This walkthrough deploys it as a Lambda environment variable because these are development-only secrets. From 317ab5e3663154f3192d3833aaf050c8a0749c0a Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 22 Jul 2026 13:47:51 -0500 Subject: [PATCH 09/10] Clarify API key necessity Co-authored-by: Dan Plyukhin --- lambda-worker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda-worker/README.md b/lambda-worker/README.md index adfd2d3b..aafdbd3a 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -17,7 +17,7 @@ It uses the same published Temporal Java SDK version as the other samples in thi CloudFormation stacks - A Temporal Cloud namespace with Serverless Workers enabled, or a [self-hosted Temporal Service](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/self-hosted-setup) configured for AWS Lambda Serverless Workers -- A Temporal Cloud API key. This walkthrough deploys it as a Lambda environment variable +- A Temporal Cloud API key (if using Temporal Cloud). This walkthrough deploys it as a Lambda environment variable because these are development-only secrets. ## Layout From 7198733e370a1a1259e2acd4e5d20953ff40951c Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 22 Jul 2026 15:17:00 -0500 Subject: [PATCH 10/10] Address AWS Lambda sample review feedback --- lambda-worker/README.md | 144 +++++++++++++++--- lambda-worker/deploy/enable-telemetry.sh | 13 +- lambda-worker/deploy/mk-iam-role.sh | 12 +- ...=> temporal-cloud-lambda-invoke-role.yaml} | 15 +- .../otel-collector-config.template.yaml | 8 +- lambda-worker/worker/build.gradle | 3 + .../samples/lambdaworker/LambdaFunction.java | 2 + 7 files changed, 144 insertions(+), 53 deletions(-) rename lambda-worker/deploy/{iam-role-for-temporal-lambda-invoke-test.yaml => temporal-cloud-lambda-invoke-role.yaml} (90%) diff --git a/lambda-worker/README.md b/lambda-worker/README.md index aafdbd3a..dd9e599b 100644 --- a/lambda-worker/README.md +++ b/lambda-worker/README.md @@ -15,18 +15,20 @@ It uses the same published Temporal Java SDK version as the other samples in thi - Java 17+ - AWS CLI configured with permissions to create Lambda functions, IAM roles, and CloudFormation stacks -- A Temporal Cloud namespace with Serverless Workers enabled, or a [self-hosted Temporal Service](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/self-hosted-setup) - configured for AWS Lambda Serverless Workers -- A Temporal Cloud API key (if using Temporal Cloud). This walkthrough deploys it as a Lambda environment variable - because these are development-only secrets. +- An AWS-hosted Temporal Cloud namespace with Serverless Workers enabled, or a + [self-hosted Temporal Service](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/self-hosted-setup) + version 1.31.0 or later with the AWS Lambda Worker Controller setup completed +- A Temporal Cloud API key (if using Temporal Cloud). This walkthrough deploys it as a + Lambda environment variable because these are development-only secrets. ## Layout - `worker/` contains the Lambda handler, Workflow, Activity, and deployable Worker project. - `starter/` contains the local Workflow starter project. - `deploy/` contains the AWS deployment scripts and CloudFormation template. -- `temporal.template.toml` and `otel-collector-config.template.yaml` are configuration - templates for local and Lambda setup. +- `temporal.template.toml` is a Temporal connection configuration template. +- `otel-collector-config.template.yaml` configures the ADOT collector packaged with the + Lambda Worker. ## Build @@ -35,6 +37,9 @@ It uses the same published Temporal Java SDK version as the other samples in thi ./gradlew :lambda-worker:worker:shadowJar ``` +The `shadowJar` task packages `otel-collector-config.template.yaml` at the root of the Lambda +artifact as `otel-collector-config.yaml`. + The Lambda handler string is: ```text @@ -44,12 +49,15 @@ io.temporal.samples.lambdaworker.LambdaFunction::handleRequest ## Configure Environment Set AWS, Temporal, and sample names first. Use unique values if you share the account or -namespace with other developers. +namespace with other developers. The connection values below are for Temporal Cloud. For a +self-hosted Service, use its frontend address, Namespace, and TLS or authentication settings; +leave `TEMPORAL_API_KEY` unset if the Service does not require one. ```bash export AWS_PROFILE= export AWS_REGION=us-west-2 export AWS_DEFAULT_REGION="$AWS_REGION" +export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)" export TEMPORAL_ADDRESS=..tmprl.cloud:7233 export TEMPORAL_NAMESPACE=. @@ -78,6 +86,12 @@ TEMPORAL_LAMBDA_DEPLOYMENT_NAME TEMPORAL_LAMBDA_BUILD_ID ``` +The ADOT collector extension reads +`OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml`. The Java Lambda Worker +uses `OtelLambdaWorkerConfigurationHelper` to send Temporal traces and metrics to the collector +over OTLP. The collector exports traces to AWS X-Ray and metrics to the +`TemporalWorkerMetrics` CloudWatch namespace. + The local starter also reads `TEMPORAL_TASK_QUEUE` and `TEMPORAL_LAMBDA_WORKFLOW_ID_PREFIX`. @@ -140,7 +154,7 @@ aws lambda create-function \ --handler io.temporal.samples.lambdaworker.LambdaFunction::handleRequest \ --role "$EXECUTION_ROLE_ARN" \ --zip-file fileb://lambda-worker/worker/build/libs/lambda-worker-1.0.0-all.jar \ - --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_LAMBDA_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_LAMBDA_BUILD_ID=$BUILD_ID}" \ + --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_LAMBDA_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_LAMBDA_BUILD_ID=$BUILD_ID,OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml}" \ --timeout 90 \ --memory-size 1024 \ --query 'FunctionArn' \ @@ -148,32 +162,45 @@ aws lambda create-function \ aws lambda wait function-active --function-name "$FUNCTION_NAME" -export FUNCTION_ARN="$( +./lambda-worker/deploy/enable-telemetry.sh \ + "$EXECUTION_ROLE_NAME" \ + "$FUNCTION_NAME" \ + "$AWS_REGION" \ + "$AWS_ACCOUNT_ID" + +aws lambda wait function-updated --function-name "$FUNCTION_NAME" + +export FUNCTION_BASE_ARN="$( aws lambda get-function \ --function-name "$FUNCTION_NAME" \ --query 'Configuration.FunctionArn' \ --output text )" -``` - -To update code after the function exists: -```bash -./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" +export FUNCTION_VERSION_ARN="$( + aws lambda publish-version \ + --function-name "$FUNCTION_NAME" \ + --description "Build ID $BUILD_ID" \ + --query 'FunctionArn' \ + --output text +)" ``` -If direct upload is too large, set `LAMBDA_CODE_S3_BUCKET` and rerun: - -```bash -LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" -``` +`enable-telemetry.sh` grants the execution role permission to send traces and EMF logs, enables +active tracing, and attaches AWS's collector-only ADOT Lambda layer. Its default layer ARN is for +the sample's `x86_64` architecture in standard AWS regions. Set `ADOT_COLLECTOR_LAYER_ARN` before +running the script to use another compatible regional layer. ## Configure Invocation -Create the IAM role that Temporal Cloud assumes to invoke the Lambda: +For Temporal Cloud, create the IAM role that Temporal Cloud assumes to invoke the Lambda. The +wildcard suffix authorizes invocation of every immutable version published for this function: ```bash -./lambda-worker/deploy/mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$FUNCTION_ARN" +./lambda-worker/deploy/mk-iam-role.sh \ + "$STACK_NAME" \ + "$EXTERNAL_ID" \ + "${FUNCTION_BASE_ARN}:*" aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" @@ -185,7 +212,15 @@ export INVOCATION_ROLE_ARN="$( )" ``` -Create and route the Worker Deployment Version: +The included CloudFormation template trusts Temporal Cloud's AWS identities and must not be +used for a self-hosted Temporal Service. For self-hosted deployments, complete the +[self-hosted Serverless Workers setup](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/self-hosted-setup), +then set `INVOCATION_ROLE_ARN` to the role created by that process. + +## Create Worker Deployment Version + +Create and route the Worker Deployment Version. The Temporal CLI can connect to either Temporal +Cloud or a self-hosted Service using the connection configuration above. ```bash temporal worker deployment create --name "$DEPLOYMENT_NAME" @@ -193,7 +228,7 @@ temporal worker deployment create --name "$DEPLOYMENT_NAME" temporal worker deployment create-version \ --deployment-name "$DEPLOYMENT_NAME" \ --build-id "$BUILD_ID" \ - --aws-lambda-function-arn "$FUNCTION_ARN" \ + --aws-lambda-function-arn "$FUNCTION_VERSION_ARN" \ --aws-lambda-assume-role-arn "$INVOCATION_ROLE_ARN" \ --aws-lambda-assume-role-external-id "$EXTERNAL_ID" @@ -208,7 +243,7 @@ An async Lambda smoke test returns immediately and should produce worker startup ```bash aws lambda invoke \ - --function-name "$FUNCTION_NAME" \ + --function-name "$FUNCTION_VERSION_ARN" \ --invocation-type Event \ --cli-binary-format raw-in-base64-out \ --payload '{}' \ @@ -221,6 +256,47 @@ A synchronous invoke can run until the Lambda worker exits near the function tim you want to wait for that path, set the AWS CLI read timeout higher than the function timeout. +## Deploy an Updated Version + +Each Temporal Build ID should point to an immutable Lambda function version. To deploy an +update, choose a new Build ID, update the Lambda environment, upload the new code, and publish a +new Lambda version: + +```bash +export BUILD_ID=build-2 + +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_LAMBDA_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_LAMBDA_BUILD_ID=$BUILD_ID,OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml}" \ + --query 'FunctionArn' \ + --output text + +aws lambda wait function-updated --function-name "$FUNCTION_NAME" + +./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" + +aws lambda wait function-updated --function-name "$FUNCTION_NAME" + +export FUNCTION_VERSION_ARN="$( + aws lambda publish-version \ + --function-name "$FUNCTION_NAME" \ + --description "Build ID $BUILD_ID" \ + --query 'FunctionArn' \ + --output text +)" +``` + +If direct upload is too large, set `LAMBDA_CODE_S3_BUCKET` when running `deploy-lambda.sh`: + +```bash +LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy/deploy-lambda.sh "$FUNCTION_NAME" +``` + +Create the new Worker Deployment Version and make it current using the commands in +[Create Worker Deployment Version](#create-worker-deployment-version), omitting the +`temporal worker deployment create` command because the deployment already exists. Existing +Worker Deployment Versions continue to reference their original Lambda versions. + ## Start Workflow After the Worker Deployment Version is current, start the sample Workflow: @@ -236,6 +312,26 @@ The starter only creates a Workflow Execution. It does not start a local Worker. important value is `TEMPORAL_TASK_QUEUE`; it must match the task queue configured on the Lambda function. +## Verify OpenTelemetry + +After invoking the Lambda or completing a Workflow, inspect the function logs for ADOT collector +startup and export messages: + +```bash +aws logs tail "/aws/lambda/$FUNCTION_NAME" --since 10m +``` + +Temporal SDK metrics should also appear in the `TemporalWorkerMetrics` CloudWatch namespace: + +```bash +aws cloudwatch list-metrics \ + --namespace TemporalWorkerMetrics \ + --query 'Metrics[].MetricName' \ + --output text +``` + +Temporal tracing spans are exported to AWS X-Ray and can be inspected in the X-Ray trace view. + ## Local SDK Development For local development of the Workflow and Activity logic, run the unit tests. They use diff --git a/lambda-worker/deploy/enable-telemetry.sh b/lambda-worker/deploy/enable-telemetry.sh index e0313194..209ebf78 100755 --- a/lambda-worker/deploy/enable-telemetry.sh +++ b/lambda-worker/deploy/enable-telemetry.sh @@ -5,6 +5,7 @@ ROLE_NAME="${1:?Usage: enable-telemetry.sh FUNCTION_NAME="${2:?Usage: enable-telemetry.sh }" REGION="${3:?Usage: enable-telemetry.sh }" ACCOUNT_ID="${4:?Usage: enable-telemetry.sh }" +ADOT_COLLECTOR_LAYER_ARN="${ADOT_COLLECTOR_LAYER_ARN:-arn:aws:lambda:${REGION}:901920570463:layer:aws-otel-collector-amd64-ver-0-117-0:1}" aws iam put-role-policy \ --role-name "$ROLE_NAME" \ @@ -28,17 +29,13 @@ aws iam put-role-policy \ \"xray:PutTelemetryRecords\" ], \"Resource\": \"*\" - }, - { - \"Effect\": \"Allow\", - \"Action\": [ - \"cloudwatch:PutMetricData\" - ], - \"Resource\": \"*\" } ] }" aws lambda update-function-configuration \ --function-name "$FUNCTION_NAME" \ - --tracing-config Mode=Active + --layers "$ADOT_COLLECTOR_LAYER_ARN" \ + --tracing-config Mode=Active \ + --query 'FunctionArn' \ + --output text diff --git a/lambda-worker/deploy/mk-iam-role.sh b/lambda-worker/deploy/mk-iam-role.sh index da596849..d0b3d535 100755 --- a/lambda-worker/deploy/mk-iam-role.sh +++ b/lambda-worker/deploy/mk-iam-role.sh @@ -1,15 +1,15 @@ #!/bin/bash set -euo pipefail -STACK_NAME="${1:?Usage: mk-iam-role.sh }" -EXTERNAL_ID="${2:?Usage: mk-iam-role.sh }" -LAMBDA_ARN="${3:?Usage: mk-iam-role.sh }" +STACK_NAME="${1:?Usage: mk-iam-role.sh }" +EXTERNAL_ID="${2:?Usage: mk-iam-role.sh }" +LAMBDA_ARN_PATTERN="${3:?Usage: mk-iam-role.sh }" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" aws cloudformation create-stack \ --stack-name "$STACK_NAME" \ - --template-body "file://${SCRIPT_DIR}/iam-role-for-temporal-lambda-invoke-test.yaml" \ + --template-body "file://${SCRIPT_DIR}/temporal-cloud-lambda-invoke-role.yaml" \ --parameters \ ParameterKey=AssumeRoleExternalId,ParameterValue="$EXTERNAL_ID" \ - ParameterKey=LambdaFunctionARNs,ParameterValue="\"$LAMBDA_ARN\"" \ - --capabilities CAPABILITY_NAMED_IAM + ParameterKey=LambdaFunctionARNs,ParameterValue="\"$LAMBDA_ARN_PATTERN\"" \ + --capabilities CAPABILITY_IAM diff --git a/lambda-worker/deploy/iam-role-for-temporal-lambda-invoke-test.yaml b/lambda-worker/deploy/temporal-cloud-lambda-invoke-role.yaml similarity index 90% rename from lambda-worker/deploy/iam-role-for-temporal-lambda-invoke-test.yaml rename to lambda-worker/deploy/temporal-cloud-lambda-invoke-role.yaml index 2e9054e2..e048f446 100644 --- a/lambda-worker/deploy/iam-role-for-temporal-lambda-invoke-test.yaml +++ b/lambda-worker/deploy/temporal-cloud-lambda-invoke-role.yaml @@ -1,3 +1,5 @@ +# This trust policy is for Temporal Cloud. Self-hosted Temporal Services must trust their +# own AWS identity; see the self-hosted Serverless Workers setup documentation. AWSTemplateFormatVersion: "2010-09-09" Description: Creates an IAM role that Temporal Cloud can assume to invoke Lambda functions for Serverless Workers. @@ -13,10 +15,6 @@ Parameters: Type: CommaDelimitedList Description: Comma-separated list of Lambda function ARNs to invoke. - RoleName: - Type: String - Default: "Temporal-Cloud-Serverless-Worker" - Metadata: AWS::CloudFormation::Interface: ParameterGroups: @@ -28,20 +26,15 @@ Metadata: default: "Lambda Configuration" Parameters: - LambdaFunctionARNs - - RoleName ParameterLabels: AssumeRoleExternalId: default: "External ID" LambdaFunctionARNs: default: "Lambda Function ARNs" - RoleName: - default: "IAM Role Name" - Resources: TemporalCloudServerlessWorker: Type: AWS::IAM::Role Properties: - RoleName: !Sub "${RoleName}-${AWS::StackName}" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: @@ -76,7 +69,7 @@ Resources: - lambda:GetFunction Resource: !Ref LambdaFunctionARNs Roles: - - !Sub "${RoleName}-${AWS::StackName}" + - !Ref TemporalCloudServerlessWorker Outputs: RoleARN: @@ -87,7 +80,7 @@ Outputs: RoleName: Description: The name of the IAM role. - Value: !Ref RoleName + Value: !Ref TemporalCloudServerlessWorker LambdaFunctionARNs: Description: The Lambda function ARNs that can be invoked. diff --git a/lambda-worker/otel-collector-config.template.yaml b/lambda-worker/otel-collector-config.template.yaml index 790be3d0..c0ccbda7 100644 --- a/lambda-worker/otel-collector-config.template.yaml +++ b/lambda-worker/otel-collector-config.template.yaml @@ -9,11 +9,11 @@ receivers: exporters: debug: awsxray: - region: us-west-2 + region: ${env:AWS_REGION} awsemf: namespace: TemporalWorkerMetrics - log_group_name: /aws/lambda/ - region: us-west-2 + log_group_name: /aws/lambda/${env:AWS_LAMBDA_FUNCTION_NAME} + region: ${env:AWS_REGION} dimension_rollup_option: NoDimensionRollup resource_to_telemetry_conversion: enabled: true @@ -28,6 +28,6 @@ service: exporters: [awsemf] telemetry: logs: - level: debug + level: info metrics: address: localhost:8888 diff --git a/lambda-worker/worker/build.gradle b/lambda-worker/worker/build.gradle index a1937981..c6dcc092 100644 --- a/lambda-worker/worker/build.gradle +++ b/lambda-worker/worker/build.gradle @@ -24,6 +24,9 @@ tasks.register('shadowJar', Jar) { def mergedServicesDir = layout.buildDirectory.dir('generated/mergedServices') from sourceSets.main.output + from(file('../otel-collector-config.template.yaml')) { + rename 'otel-collector-config.template.yaml', 'otel-collector-config.yaml' + } from({ configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }) { diff --git a/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java index 5fc5eb39..6ff29cda 100644 --- a/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java +++ b/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -3,6 +3,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import io.temporal.aws.lambda.LambdaWorker; +import io.temporal.aws.lambda.OtelLambdaWorkerConfigurationHelper; import io.temporal.common.WorkerDeploymentVersion; /** AWS Lambda entry point for the Temporal worker. */ @@ -13,6 +14,7 @@ public class LambdaFunction implements RequestHandler { new WorkerDeploymentVersion( LambdaWorkerSample.deploymentName(), LambdaWorkerSample.buildId()), builder -> { + OtelLambdaWorkerConfigurationHelper.configure(builder); builder.setTaskQueue(LambdaWorkerSample.taskQueue()); builder.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); builder.registerActivitiesImplementations(new GreetingActivitiesImpl());