diff --git a/sample-apps/apigateway-lambda/README.md b/sample-apps/apigateway-lambda/README.md new file mode 100644 index 0000000000..6ecc80986b --- /dev/null +++ b/sample-apps/apigateway-lambda/README.md @@ -0,0 +1,58 @@ +## API Gateway + Lambda Sample Application + +The directory contains the source code and the Infrastructure as Code (IaC) to create the sample app in your AWS account. + +### Prerequisite +Before you begin, ensure you have the following installed: +- Java 17 +- Gradle +- Terraform +- AWS CLI configured with appropriate credentials + +### Getting Started + +#### 1. Build the application +```bash +# Change to the project directory +cd sample-apps/apigateway-lambda + +# Build the application using Gradle +gradle clean build + +# Prepare the Lambda deployment package +gradle createLambdaZip +``` + +#### 2. Deploy the application +```bash +# Change to the terraform directory +cd terraform + +# Initialize Terraform +terraform init + +# (Optional) Review the deployment plan for better understanding of the components +terraform plan + +# Deploy +terraform apply +``` + +#### 3. Testing the applicating +After successful deployment, Terraform will output the API Gateway endpoint URL. You can test the application using: +```bash +curl +``` + +#### 4. Clean Up +To avoid incurring unnecessary charges, remember to destroy the resources when you are done: +```bash +terraform destroy +``` + +#### (Optional) Instrumenting with Application Signals Lambda Layer +You can choose to instrument the Lambda function with Application Signals Lambda Layer upon deployment by passing in the layer ARN to the `adot_layer_arn` variable. +You must have the layer already published to your account before executing the following command. +```bash +terraform apply -var "adot_layer_arn=" +``` \ No newline at end of file diff --git a/sample-apps/apigateway-lambda/build.gradle.kts b/sample-apps/apigateway-lambda/build.gradle.kts new file mode 100644 index 0000000000..66992540ab --- /dev/null +++ b/sample-apps/apigateway-lambda/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + java + application +} + +application { + mainClass.set("com.amazon.sampleapp.LambdaHandler") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation("com.amazonaws:aws-lambda-java-core:1.2.2") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("software.amazon.awssdk:s3:2.29.23") + implementation("org.json:json:20240303") + implementation("org.slf4j:jcl-over-slf4j:2.0.16") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "com.amazon.sampleapp.LambdaHandler" + } +} + +tasks.register("createLambdaZip") { + dependsOn("build") + from(tasks.compileJava.get()) + from(tasks.processResources.get()) + into("lib") { + from(configurations.runtimeClasspath.get()) + } + archiveFileName.set("lambda-function.zip") + destinationDirectory.set(layout.buildDirectory.dir("distributions")) +} diff --git a/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java new file mode 100644 index 0000000000..f3e11bc38d --- /dev/null +++ b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java @@ -0,0 +1,82 @@ +package com.amazon.sampleapp; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.io.IOException; +import java.util.Map; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONObject; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class LambdaHandler implements RequestHandler> { + + private final OkHttpClient client = new OkHttpClient(); + private final S3Client s3Client = S3Client.create(); + + @Override + public Map handleRequest(Object input, Context context) { + System.out.println("Executing LambdaHandler"); + + // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime + // try and get the trace id from environment variable _X_AMZN_TRACE_ID. If it's not present + // there + // then try the system property. + String traceId = + System.getenv("_X_AMZN_TRACE_ID") != null + ? System.getenv("_X_AMZN_TRACE_ID") + : System.getProperty("com.amazonaws.xray.traceHeader"); + + System.out.println("Trace ID: " + traceId); + + JSONObject responseBody = new JSONObject(); + responseBody.put("traceId", traceId); + + // Make a remote call using OkHttp + System.out.println("Making a remote call using OkHttp"); + String url = "https://www.amazon.com"; + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + responseBody.put("httpRequest", "Request successful"); + } catch (IOException e) { + context.getLogger().log("Error: " + e.getMessage()); + responseBody.put("httpRequest", "Request failed"); + } + System.out.println("Remote call done"); + + // Make a S3 HeadBucket call to check whether the bucket exists + System.out.println("Making a S3 HeadBucket call"); + String bucketName = "SomeDummyBucket"; + try { + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName).build(); + s3Client.headBucket(headBucketRequest); + responseBody.put("s3Request", "Bucket exists and is accessible: " + bucketName); + } catch (S3Exception e) { + if (e.statusCode() == 403) { + responseBody.put("s3Request", "Access denied to bucket: " + bucketName); + } else if (e.statusCode() == 404) { + responseBody.put("s3Request", "Bucket does not exist: " + bucketName); + } else { + System.err.println("Error checking bucket: " + e.awsErrorDetails().errorMessage()); + responseBody.put( + "s3Request", "Error checking bucket: " + e.awsErrorDetails().errorMessage()); + } + } + System.out.println("S3 HeadBucket call done"); + + // return a response in the ApiGateway proxy format + return Map.of( + "isBase64Encoded", + false, + "statusCode", + 200, + "body", + responseBody.toString(), + "headers", + Map.of("Content-Type", "application/json")); + } +} diff --git a/sample-apps/apigateway-lambda/terraform/main.tf b/sample-apps/apigateway-lambda/terraform/main.tf new file mode 100644 index 0000000000..6881f0e1ce --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/main.tf @@ -0,0 +1,119 @@ +### Lambda function +locals { + architecture = var.architecture == "x86_64" ? "amd64" : "arm64" +} + +resource "aws_iam_role" "lambda_role" { + name = "lambda_execution_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { Service = "lambda.amazonaws.com" } + }] + }) +} + +resource "aws_iam_policy" "s3_access" { + name = "S3ListBucketPolicy" + description = "Allow Lambda to check a given S3 bucket exists" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["s3:ListBucket"], + Resource = "*" + }] + }) +} + +resource "aws_iam_role_policy_attachment" "attach_execution_role_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy_attachment" "attach_s3_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.s3_access.arn +} + +resource "aws_iam_role_policy_attachment" "attach_xray_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" +} + +resource "aws_lambda_function" "sampleLambdaFunction" { + function_name = var.function_name + runtime = var.runtime + timeout = 300 + handler = "com.amazon.sampleapp.LambdaHandler::handleRequest" + role = aws_iam_role.lambda_role.arn + filename = "${path.module}/../build/distributions/lambda-function.zip" + source_code_hash = filebase64sha256("${path.module}/../build/distributions/lambda-function.zip") + architectures = [var.architecture] + memory_size = 512 + tracing_config { + mode = var.lambda_tracing_mode + } + layers = var.adot_layer_arn != null && var.adot_layer_arn != "" ? [var.adot_layer_arn] : [] + environment { + variables = var.adot_layer_arn != null && var.adot_layer_arn != "" ? { + AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument" + } : {} + } +} + +### API Gateway proxy to Lambda function +resource "aws_api_gateway_rest_api" "apigw_lambda_api" { + name = var.api_gateway_name +} + +resource "aws_api_gateway_resource" "apigw_lambda_resource" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + parent_id = aws_api_gateway_rest_api.apigw_lambda_api.root_resource_id + path_part = "lambda" +} + +resource "aws_api_gateway_method" "apigw_lambda_method" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = "ANY" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "apigw_lambda_integration" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = aws_api_gateway_method.apigw_lambda_method.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.sampleLambdaFunction.invoke_arn +} + +resource "aws_api_gateway_deployment" "apigw_lambda_deployment" { + depends_on = [ + aws_api_gateway_integration.apigw_lambda_integration + ] + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id +} + +resource "aws_api_gateway_stage" "test" { + stage_name = "default" + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + deployment_id = aws_api_gateway_deployment.apigw_lambda_deployment.id + xray_tracing_enabled = var.apigw_tracing_enabled +} + +resource "aws_lambda_permission" "apigw_lambda_invoke" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.sampleLambdaFunction.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.apigw_lambda_api.execution_arn}/*/*" +} + +# Output the API Gateway URL +output "invoke_url" { + value = "${aws_api_gateway_stage.test.invoke_url}/lambda" +} diff --git a/sample-apps/apigateway-lambda/terraform/variables.tf b/sample-apps/apigateway-lambda/terraform/variables.tf new file mode 100644 index 0000000000..2dc99f0685 --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/variables.tf @@ -0,0 +1,43 @@ +## Lambda function related configurations +variable "function_name" { + type = string + description = "Name of sample app function" + default = "aws-opentelemetry-distro-java" +} + +variable "architecture" { + type = string + description = "Lambda function architecture, either arm64 or x86_64" + default = "x86_64" +} + +variable "runtime" { + type = string + description = "Java runtime version used for Lambda Function" + default = "java17" +} + +variable "lambda_tracing_mode" { + type = string + description = "Lambda function tracing mode" + default = "Active" +} + +variable "adot_layer_arn" { + type = string + description = "ARN of the ADOT JAVA layer" + default = null +} + +## API Gateway related configurations +variable "api_gateway_name" { + type = string + description = "Name of API gateway to create" + default = "aws-opentelemetry-distro-java" +} + +variable "apigw_tracing_enabled" { + type = string + description = "API Gateway REST API tracing enabled or not" + default = "true" +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f4ec99f115..decf81d145 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include(":smoke-tests:spring-boot") include(":sample-apps:springboot") include(":sample-apps:spark") include(":sample-apps:spark-awssdkv1") +include(":sample-apps:apigateway-lambda") // Used for contract tests include("appsignals-tests:images:mock-collector")