Skip to content

Commit

Permalink
Add comments explaining the code
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Kaiser committed Jan 9, 2025
1 parent 3cb2f15 commit c08a29c
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import software.amazon.awscdk.App;
import software.amazon.awscdk.StackProps;

/**
* Main CDK application class that serves as the entry point for deploying the S3 Object Lambda infrastructure.
*/
public class S3ObjectLambdaApp extends App {

public static void main(final String... args) {
Expand All @@ -12,6 +15,9 @@ public static void main(final String... args) {
app.synth();
}

/**
* Creates a new instance of the S3ObjectLambdaStack with the specified properties.
*/
public S3ObjectLambdaStack createStack(StackProps stackProps) {
return new S3ObjectLambdaStack(this, "S3ObjectLambdaStack", stackProps);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,28 @@
import static software.amazon.awscdk.services.s3objectlambda.CfnAccessPoint.*;

public class S3ObjectLambdaStack extends Stack {

private static final String S3_ACCESS_POINT_NAME = "s3-access-point";
private static final String OBJECT_LAMBDA_ACCESS_POINT_NAME = "object-lambda-access-point";

/**
* Constructs a new S3ObjectLambdaStack.
*/
public S3ObjectLambdaStack(final Construct scope, final String id, final StackProps props) {
super(scope, id, props);
super(scope, id, props);

// Construct the access point ARN using the region, account ID and access point name
var accessPoint = "arn:aws:s3:" + Aws.REGION + ":" + Aws.ACCOUNT_ID + ":accesspoint/" + S3_ACCESS_POINT_NAME;

// Create a new S3 bucket with secure configuration including:
var s3ObjectLambdaBucket = Bucket.Builder.create(this, "S3ObjectLambdaBucket")
.removalPolicy(RemovalPolicy.RETAIN)
.autoDeleteObjects(false)
.accessControl(BucketAccessControl.BUCKET_OWNER_FULL_CONTROL)
.encryption(BucketEncryption.S3_MANAGED)
.blockPublicAccess(BlockPublicAccess.BLOCK_ALL)
.build();

// Create bucket policy statement allowing access through access points
var s3ObjectLambdaBucketPolicyStatement = PolicyStatement.Builder.create()
.actions(List.of("*"))
.principals(List.of(new AnyPrincipal()))
Expand All @@ -52,20 +60,30 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr
)
)
.build();

// Attach the policy to the bucket
s3ObjectLambdaBucket.addToResourcePolicy(s3ObjectLambdaBucketPolicyStatement);

// Create the Lambda function that will transform objects
var s3ObjectLambdaFunction = createS3ObjectLambdaFunction();

// Add permission for Lambda to write GetObject responses for s3 object
var s3ObjectLambdaFunctionPolicyStatement = PolicyStatement.Builder.create()
.effect(Effect.ALLOW)
.resources(List.of("*"))
.actions(List.of("s3-object-lambda:WriteGetObjectResponse"))
.build();
s3ObjectLambdaFunction.addToRolePolicy(s3ObjectLambdaFunctionPolicyStatement);

// Add permission for the account root to invoke the Lambda function
var s3ObjectLambdaFunctionPermission = Permission.builder()
.action("lambda:InvokeFunction")
.principal(new AccountRootPrincipal())
.sourceAccount(Aws.ACCOUNT_ID)
.build();
s3ObjectLambdaFunction.addPermission("S3ObjectLambdaPermission", s3ObjectLambdaFunctionPermission);

// Create policy allowing Lambda function to get objects through the access point
var s3ObjectLambdaAccessPointPolicyStatement = PolicyStatement.Builder.create()
.sid("S3ObjectLambdaAccessPointPolicyStatement")
.effect(Effect.ALLOW)
Expand All @@ -76,16 +94,22 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr
)
.resources(List.of(accessPoint + "/object/*"))
.build();

// Create policy document containing the access point policy
var s3ObjectLambdaAccessPointPolicyDocument = PolicyDocument.Builder.create()
.statements(List.of(
s3ObjectLambdaAccessPointPolicyStatement
))
.build();

// Create the S3 access point for direct bucket access
software.amazon.awscdk.services.s3.CfnAccessPoint.Builder.create(this, "S3ObjectLambdaS3AccessPoint")
.bucket(s3ObjectLambdaBucket.getBucketName())
.name(S3_ACCESS_POINT_NAME)
.policy(s3ObjectLambdaAccessPointPolicyDocument)
.build();

// Create the Object Lambda access point that will transform objects
var s3ObjectLambdaAccessPoint = CfnAccessPoint.Builder.create(this, "S3ObjectLambdaAccessPoint")
.name(OBJECT_LAMBDA_ACCESS_POINT_NAME)
.objectLambdaConfiguration(ObjectLambdaConfigurationProperty.builder()
Expand All @@ -107,28 +131,39 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr
)
.build();
CfnOutput.Builder.create(this, "s3ObjectLambdaBucketArn")
.value(s3ObjectLambdaBucket.getBucketArn())
.value(s3ObjectLambdaBucket.getBucketArn()) // Export bucket ARN
.build();
CfnOutput.Builder.create(this, "s3ObjectLambdaFunctionArn")
.value(s3ObjectLambdaFunction.getFunctionArn())
.value(s3ObjectLambdaFunction.getFunctionArn()) // Export Lambda function ARN
.build();
CfnOutput.Builder.create(this, "s3ObjectLambdaAccessPointArn")
.value(s3ObjectLambdaAccessPoint.getAttrArn())
.value(s3ObjectLambdaAccessPoint.getAttrArn()) // Export access point ARN
.build();

// Create output with Console URL for easy access to the Lambda access point
CfnOutput.Builder.create(this, "s3ObjectLambdaAccessPointUrl")
.value("https://console.aws.amazon.com/s3/olap/" + Aws.ACCOUNT_ID + "/" + OBJECT_LAMBDA_ACCESS_POINT_NAME + "?region=" + Aws.REGION)
.build();
}

/**
* Creates the Lambda function that will process S3 Object Lambda requests.
* This method configures the function's runtime, code, and build process.
*
* @return A Lambda Function construct configured for S3 Object Lambda processing
*/
private Function createS3ObjectLambdaFunction() {
// Define Maven packaging commands to build the Lambda function
List<String> packagingInstructions = List.of(
"/bin/sh",
"-c",
// Build the project and copy the JAR to the asset output directory
"mvn -e -q clean package && cp /asset-input/target/lambda-1.0-SNAPSHOT.jar /asset-output/"
);
// Configure the bundling options for packaging the Lambda function
var builderOptions = BundlingOptions.builder()
.command(packagingInstructions)
.image(Runtime.JAVA_17.getBundlingImage())
.command(packagingInstructions) // Set the Maven build commands
.image(Runtime.JAVA_17.getBundlingImage()) // Use Java 17 runtime image
.volumes(
singletonList(
DockerVolume.builder()
Expand All @@ -139,10 +174,12 @@ private Function createS3ObjectLambdaFunction() {
.user("root")
.outputType(ARCHIVED)
.build();

// Create the Lambda function with specified configuration
return Function.Builder.create(this, "S3ObjectLambdaFunction")
.runtime(Runtime.JAVA_17)
.functionName("S3ObjectLambdaFunction")
.memorySize(2048)
.runtime(Runtime.JAVA_17) // Set Java 17 runtime
.functionName("S3ObjectLambdaFunction") // Set function name
.memorySize(2048) // Allocate 2GB memory
.code(
Code.fromAsset(
"../lambda/",
Expand All @@ -152,4 +189,4 @@ private Function createS3ObjectLambdaFunction() {
.handler("com.myorg.S3ObjectLambdaTransformer::handleRequest")
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,33 @@

import static java.net.http.HttpResponse.BodyHandlers.ofInputStream;

/**
* AWS Lambda function handler that processes S3 Object Lambda requests.
* This class implements the transformation logic for S3 objects accessed
* through
* the Object Lambda Access Point.
*/
public class S3ObjectLambdaTransformer {

/**
* Main handler method that processes S3 Object Lambda events.
* This method retrieves the original object from S3, applies transformations,
* and returns the modified object data.
*
* @param event The S3 Object Lambda event containing request details
* @param context The Lambda execution context
*/
public void handleRequest(S3ObjectLambdaEvent event, Context context) {
try (var s3Client = S3Client.create()) {
// Create JSON mapper and log the incoming event
var objectMapper = createObjectMapper();
log(context, "event: " + writeValue(objectMapper, event));

// Extract the pre-signed URL from the event context
var objectContext = event.getGetObjectContext();
var s3Url = objectContext.getInputS3Url();

// Create HTTP client and fetch the original object
var uri = URI.create(s3Url);
var httpClient = HttpClient.newBuilder().build();
var httpRequest = HttpRequest.newBuilder(uri).GET().build();
Expand All @@ -39,48 +58,83 @@ public void handleRequest(S3ObjectLambdaEvent event, Context context) {
.requestRoute(event.outputRoute())
.requestToken(event.outputToken())
.statusCode(response.statusCode());
// Process successful responses (HTTP 200)
if (response.statusCode() == 200) {
// Build metadata object with content length and hash values
var metadata = TransformedObject.Metadata.builder()
.withLength((long) responseBodyBytes.length)
.withMD5(DigestUtils.md5Hex(responseBodyBytes))
.withSHA1(DigestUtils.sha1Hex(responseBodyBytes))
.withSHA256(DigestUtils.sha256Hex(responseBodyBytes))
.withLength((long) responseBodyBytes.length) // Set content length
.withMD5(DigestUtils.md5Hex(responseBodyBytes)) // Calculate MD5 hash
.withSHA1(DigestUtils.sha1Hex(responseBodyBytes)) // Calculate SHA1 hash
.withSHA256(DigestUtils.sha256Hex(responseBodyBytes)) // Calculate SHA256 hash
.build();
// Create transformed object containing the metadata
var transformedObject = TransformedObject.builder()
.withMetadata(metadata)
.build();
// Log the transformed object for debugging
log(context, "transformedObject: " + writeValue(objectMapper, transformedObject));
requestBody = RequestBody.fromString(writeValue(objectMapper, transformedObject));
} else {
} else {
// Handle non-200 HTTP responses by setting the error message
writeGetObjectResponseRequestBuilder
.errorMessage(new String(responseBodyBytes));
.errorMessage(new String(responseBodyBytes)); // Convert error response body to string and set as error message
}
// Write the final response back to S3 Object Lambda with either transformed object or error details
s3Client.writeGetObjectResponse(writeGetObjectResponseRequestBuilder.build(), requestBody);
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Error while handling request: " + e.getMessage(), e);
}
// Wrap and rethrow any IO or threading exceptions that occur during processing
throw new RuntimeException("Error while handling request: " + e.getMessage(), e) }
}

/**
* Reads all bytes from the input stream of an HTTP response.
* Converts the response stream into a byte array for processing.
*
* @param response HTTP response containing the input stream to read
* @return byte array containing all the data from the input stream
* @throws IOException if an I/O error occurs while reading the stream
*/
private static byte[] readBytes(HttpResponse<InputStream> response) throws IOException {
try (var inputStream = response.body()) {
return inputStream.readAllBytes();
}
}

/**
* Logs a message to CloudWatch using the Lambda context logger.
* Prefixes the message with the request ID for tracing purposes.
*
* @param context Lambda execution context containing the logger
* @param message Message to be logged
*/
private static void log(Context context, String message) {
if (message != null) {
Optional.ofNullable(context)
.map(Context::getLogger)
.ifPresent(lambdaLogger -> lambdaLogger.log(message));
.map(Context::getLogger)
.ifPresent(lambdaLogger -> lambdaLogger.log(message));
}
}

/**
* Creates and configures a Jackson ObjectMapper for JSON serialization.
* Enables pretty printing and disables failing on empty beans.
*
* @return Configured ObjectMapper instance
*/
private static ObjectMapper createObjectMapper() {
var objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return objectMapper;
}

/**
* Serializes an object to a JSON string using the provided ObjectMapper.
* Handles JsonProcessingException by returning an error message.
*
* @param objectMapper The ObjectMapper to use for serialization
* @param object The object to serialize
* @return JSON string representation of the object or error message
*/
private static String writeValue(ObjectMapper objectMapper, Object object) {
try {
return objectMapper.writeValueAsString(object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

import lombok.*;

/**
* Represents a transformed S3 object with its metadata.
* This class uses Lombok annotations for builder pattern, toString, and data methods generation.
*/
@Builder(setterPrefix = "with", builderClassName = "TransformedObjectBuilder")
@ToString
@Data
public class TransformedObject {

/**
* Record class representing the metadata of a transformed object.
* Contains various hash values and the object length for verification purposes.
*/
@Builder(setterPrefix = "with", builderClassName = "MetadataBuilder")
public record Metadata(Long length, String MD5, String SHA1, String SHA256) {

Expand Down

0 comments on commit c08a29c

Please sign in to comment.