Skip to content

Commit eae57c5

Browse files
authored
APIGW + Lambda sample app (#961)
1 parent c00d3af commit eae57c5

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
## API Gateway + Lambda Sample Application
2+
3+
The directory contains the source code and the Infrastructure as Code (IaC) to create the sample app in your AWS account.
4+
5+
### Prerequisite
6+
Before you begin, ensure you have the following installed:
7+
- Java 17
8+
- Gradle
9+
- Terraform
10+
- AWS CLI configured with appropriate credentials
11+
12+
### Getting Started
13+
14+
#### 1. Build the application
15+
```bash
16+
# Change to the project directory
17+
cd sample-apps/apigateway-lambda
18+
19+
# Build the application using Gradle
20+
gradle clean build
21+
22+
# Prepare the Lambda deployment package
23+
gradle createLambdaZip
24+
```
25+
26+
#### 2. Deploy the application
27+
```bash
28+
# Change to the terraform directory
29+
cd terraform
30+
31+
# Initialize Terraform
32+
terraform init
33+
34+
# (Optional) Review the deployment plan for better understanding of the components
35+
terraform plan
36+
37+
# Deploy
38+
terraform apply
39+
```
40+
41+
#### 3. Testing the applicating
42+
After successful deployment, Terraform will output the API Gateway endpoint URL. You can test the application using:
43+
```bash
44+
curl <API_Gateway_URL>
45+
```
46+
47+
#### 4. Clean Up
48+
To avoid incurring unnecessary charges, remember to destroy the resources when you are done:
49+
```bash
50+
terraform destroy
51+
```
52+
53+
#### (Optional) Instrumenting with Application Signals Lambda Layer
54+
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.
55+
You must have the layer already published to your account before executing the following command.
56+
```bash
57+
terraform apply -var "adot_layer_arn=<APPLICATION_SIGNALS_LAYER_FULL_ARN_WITH_VERSION>"
58+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
plugins {
2+
java
3+
application
4+
}
5+
6+
application {
7+
mainClass.set("com.amazon.sampleapp.LambdaHandler")
8+
}
9+
10+
java {
11+
toolchain {
12+
languageVersion.set(JavaLanguageVersion.of(17))
13+
}
14+
}
15+
16+
dependencies {
17+
implementation("com.amazonaws:aws-lambda-java-core:1.2.2")
18+
implementation("com.squareup.okhttp3:okhttp:4.11.0")
19+
implementation("software.amazon.awssdk:s3:2.29.23")
20+
implementation("org.json:json:20240303")
21+
implementation("org.slf4j:jcl-over-slf4j:2.0.16")
22+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
23+
}
24+
25+
tasks.jar {
26+
manifest {
27+
attributes["Main-Class"] = "com.amazon.sampleapp.LambdaHandler"
28+
}
29+
}
30+
31+
tasks.register<Zip>("createLambdaZip") {
32+
dependsOn("build")
33+
from(tasks.compileJava.get())
34+
from(tasks.processResources.get())
35+
into("lib") {
36+
from(configurations.runtimeClasspath.get())
37+
}
38+
archiveFileName.set("lambda-function.zip")
39+
destinationDirectory.set(layout.buildDirectory.dir("distributions"))
40+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.amazon.sampleapp;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.RequestHandler;
5+
import java.io.IOException;
6+
import java.util.Map;
7+
import okhttp3.OkHttpClient;
8+
import okhttp3.Request;
9+
import okhttp3.Response;
10+
import org.json.JSONObject;
11+
import software.amazon.awssdk.services.s3.S3Client;
12+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
13+
import software.amazon.awssdk.services.s3.model.S3Exception;
14+
15+
public class LambdaHandler implements RequestHandler<Object, Map<String, Object>> {
16+
17+
private final OkHttpClient client = new OkHttpClient();
18+
private final S3Client s3Client = S3Client.create();
19+
20+
@Override
21+
public Map<String, Object> handleRequest(Object input, Context context) {
22+
System.out.println("Executing LambdaHandler");
23+
24+
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
25+
// try and get the trace id from environment variable _X_AMZN_TRACE_ID. If it's not present
26+
// there
27+
// then try the system property.
28+
String traceId =
29+
System.getenv("_X_AMZN_TRACE_ID") != null
30+
? System.getenv("_X_AMZN_TRACE_ID")
31+
: System.getProperty("com.amazonaws.xray.traceHeader");
32+
33+
System.out.println("Trace ID: " + traceId);
34+
35+
JSONObject responseBody = new JSONObject();
36+
responseBody.put("traceId", traceId);
37+
38+
// Make a remote call using OkHttp
39+
System.out.println("Making a remote call using OkHttp");
40+
String url = "https://www.amazon.com";
41+
Request request = new Request.Builder().url(url).build();
42+
43+
try (Response response = client.newCall(request).execute()) {
44+
responseBody.put("httpRequest", "Request successful");
45+
} catch (IOException e) {
46+
context.getLogger().log("Error: " + e.getMessage());
47+
responseBody.put("httpRequest", "Request failed");
48+
}
49+
System.out.println("Remote call done");
50+
51+
// Make a S3 HeadBucket call to check whether the bucket exists
52+
System.out.println("Making a S3 HeadBucket call");
53+
String bucketName = "SomeDummyBucket";
54+
try {
55+
HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName).build();
56+
s3Client.headBucket(headBucketRequest);
57+
responseBody.put("s3Request", "Bucket exists and is accessible: " + bucketName);
58+
} catch (S3Exception e) {
59+
if (e.statusCode() == 403) {
60+
responseBody.put("s3Request", "Access denied to bucket: " + bucketName);
61+
} else if (e.statusCode() == 404) {
62+
responseBody.put("s3Request", "Bucket does not exist: " + bucketName);
63+
} else {
64+
System.err.println("Error checking bucket: " + e.awsErrorDetails().errorMessage());
65+
responseBody.put(
66+
"s3Request", "Error checking bucket: " + e.awsErrorDetails().errorMessage());
67+
}
68+
}
69+
System.out.println("S3 HeadBucket call done");
70+
71+
// return a response in the ApiGateway proxy format
72+
return Map.of(
73+
"isBase64Encoded",
74+
false,
75+
"statusCode",
76+
200,
77+
"body",
78+
responseBody.toString(),
79+
"headers",
80+
Map.of("Content-Type", "application/json"));
81+
}
82+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
### Lambda function
2+
locals {
3+
architecture = var.architecture == "x86_64" ? "amd64" : "arm64"
4+
}
5+
6+
resource "aws_iam_role" "lambda_role" {
7+
name = "lambda_execution_role"
8+
assume_role_policy = jsonencode({
9+
Version = "2012-10-17",
10+
Statement = [{
11+
Action = "sts:AssumeRole",
12+
Effect = "Allow",
13+
Principal = { Service = "lambda.amazonaws.com" }
14+
}]
15+
})
16+
}
17+
18+
resource "aws_iam_policy" "s3_access" {
19+
name = "S3ListBucketPolicy"
20+
description = "Allow Lambda to check a given S3 bucket exists"
21+
policy = jsonencode({
22+
Version = "2012-10-17",
23+
Statement = [{
24+
Effect = "Allow",
25+
Action = ["s3:ListBucket"],
26+
Resource = "*"
27+
}]
28+
})
29+
}
30+
31+
resource "aws_iam_role_policy_attachment" "attach_execution_role_policy" {
32+
role = aws_iam_role.lambda_role.name
33+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
34+
}
35+
36+
resource "aws_iam_role_policy_attachment" "attach_s3_policy" {
37+
role = aws_iam_role.lambda_role.name
38+
policy_arn = aws_iam_policy.s3_access.arn
39+
}
40+
41+
resource "aws_iam_role_policy_attachment" "attach_xray_policy" {
42+
role = aws_iam_role.lambda_role.name
43+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
44+
}
45+
46+
resource "aws_lambda_function" "sampleLambdaFunction" {
47+
function_name = var.function_name
48+
runtime = var.runtime
49+
timeout = 300
50+
handler = "com.amazon.sampleapp.LambdaHandler::handleRequest"
51+
role = aws_iam_role.lambda_role.arn
52+
filename = "${path.module}/../build/distributions/lambda-function.zip"
53+
source_code_hash = filebase64sha256("${path.module}/../build/distributions/lambda-function.zip")
54+
architectures = [var.architecture]
55+
memory_size = 512
56+
tracing_config {
57+
mode = var.lambda_tracing_mode
58+
}
59+
layers = var.adot_layer_arn != null && var.adot_layer_arn != "" ? [var.adot_layer_arn] : []
60+
environment {
61+
variables = var.adot_layer_arn != null && var.adot_layer_arn != "" ? {
62+
AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument"
63+
} : {}
64+
}
65+
}
66+
67+
### API Gateway proxy to Lambda function
68+
resource "aws_api_gateway_rest_api" "apigw_lambda_api" {
69+
name = var.api_gateway_name
70+
}
71+
72+
resource "aws_api_gateway_resource" "apigw_lambda_resource" {
73+
rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id
74+
parent_id = aws_api_gateway_rest_api.apigw_lambda_api.root_resource_id
75+
path_part = "lambda"
76+
}
77+
78+
resource "aws_api_gateway_method" "apigw_lambda_method" {
79+
rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id
80+
resource_id = aws_api_gateway_resource.apigw_lambda_resource.id
81+
http_method = "ANY"
82+
authorization = "NONE"
83+
}
84+
85+
resource "aws_api_gateway_integration" "apigw_lambda_integration" {
86+
rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id
87+
resource_id = aws_api_gateway_resource.apigw_lambda_resource.id
88+
http_method = aws_api_gateway_method.apigw_lambda_method.http_method
89+
integration_http_method = "POST"
90+
type = "AWS_PROXY"
91+
uri = aws_lambda_function.sampleLambdaFunction.invoke_arn
92+
}
93+
94+
resource "aws_api_gateway_deployment" "apigw_lambda_deployment" {
95+
depends_on = [
96+
aws_api_gateway_integration.apigw_lambda_integration
97+
]
98+
rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id
99+
}
100+
101+
resource "aws_api_gateway_stage" "test" {
102+
stage_name = "default"
103+
rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id
104+
deployment_id = aws_api_gateway_deployment.apigw_lambda_deployment.id
105+
xray_tracing_enabled = var.apigw_tracing_enabled
106+
}
107+
108+
resource "aws_lambda_permission" "apigw_lambda_invoke" {
109+
statement_id = "AllowAPIGatewayInvoke"
110+
action = "lambda:InvokeFunction"
111+
function_name = aws_lambda_function.sampleLambdaFunction.function_name
112+
principal = "apigateway.amazonaws.com"
113+
source_arn = "${aws_api_gateway_rest_api.apigw_lambda_api.execution_arn}/*/*"
114+
}
115+
116+
# Output the API Gateway URL
117+
output "invoke_url" {
118+
value = "${aws_api_gateway_stage.test.invoke_url}/lambda"
119+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
## Lambda function related configurations
2+
variable "function_name" {
3+
type = string
4+
description = "Name of sample app function"
5+
default = "aws-opentelemetry-distro-java"
6+
}
7+
8+
variable "architecture" {
9+
type = string
10+
description = "Lambda function architecture, either arm64 or x86_64"
11+
default = "x86_64"
12+
}
13+
14+
variable "runtime" {
15+
type = string
16+
description = "Java runtime version used for Lambda Function"
17+
default = "java17"
18+
}
19+
20+
variable "lambda_tracing_mode" {
21+
type = string
22+
description = "Lambda function tracing mode"
23+
default = "Active"
24+
}
25+
26+
variable "adot_layer_arn" {
27+
type = string
28+
description = "ARN of the ADOT JAVA layer"
29+
default = null
30+
}
31+
32+
## API Gateway related configurations
33+
variable "api_gateway_name" {
34+
type = string
35+
description = "Name of API gateway to create"
36+
default = "aws-opentelemetry-distro-java"
37+
}
38+
39+
variable "apigw_tracing_enabled" {
40+
type = string
41+
description = "API Gateway REST API tracing enabled or not"
42+
default = "true"
43+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ include(":smoke-tests:spring-boot")
5151
include(":sample-apps:springboot")
5252
include(":sample-apps:spark")
5353
include(":sample-apps:spark-awssdkv1")
54+
include(":sample-apps:apigateway-lambda")
5455

5556
// Used for contract tests
5657
include("appsignals-tests:images:mock-collector")

0 commit comments

Comments
 (0)