Skip to content

Commit

Permalink
APIGW + Lambda sample app (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
srprash authored Dec 3, 2024
1 parent c00d3af commit eae57c5
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 0 deletions.
58 changes: 58 additions & 0 deletions sample-apps/apigateway-lambda/README.md
Original file line number Diff line number Diff line change
@@ -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 <API_Gateway_URL>
```

#### 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=<APPLICATION_SIGNALS_LAYER_FULL_ARN_WITH_VERSION>"
```
40 changes: 40 additions & 0 deletions sample-apps/apigateway-lambda/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Zip>("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"))
}
Original file line number Diff line number Diff line change
@@ -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<Object, Map<String, Object>> {

private final OkHttpClient client = new OkHttpClient();
private final S3Client s3Client = S3Client.create();

@Override
public Map<String, Object> 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"));
}
}
119 changes: 119 additions & 0 deletions sample-apps/apigateway-lambda/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
43 changes: 43 additions & 0 deletions sample-apps/apigateway-lambda/terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit eae57c5

Please sign in to comment.