From 7eea6e43aed11f2ec3757fc3a4611116ca8ca3aa Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 15 Mar 2023 14:49:14 +0100 Subject: [PATCH 1/7] build(examples): Add example for the powertools-cloudformation module --- examples/pom.xml | 1 + .../README.md | 40 +++++ .../infra/cdk/.gitignore | 14 ++ .../infra/cdk/pom.xml | 67 +++++++ ...owertoolsExamplesCloudformationCdkApp.java | 16 ++ ...ertoolsExamplesCloudformationCdkStack.java | 87 +++++++++ ...wertoolsExamplesCloudformationCdkTest.java | 24 +++ .../infra/sam/events/create_event.json | 12 ++ .../infra/sam/events/delete_event.json | 14 ++ .../infra/sam/events/update_event.json | 14 ++ .../infra/sam/template.yaml | 54 ++++++ .../pom.xml | 123 +++++++++++++ .../src/main/java/helloworld/App.java | 166 ++++++++++++++++++ .../src/main/resources/log4j2.xml | 18 ++ 14 files changed, 650 insertions(+) create mode 100644 examples/powertools-examples-cloudformation/README.md create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/.gitignore create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/pom.xml create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/create_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/update_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/template.yaml create mode 100644 examples/powertools-examples-cloudformation/pom.xml create mode 100644 examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml diff --git a/examples/pom.xml b/examples/pom.xml index cca621163..c9b8ea8ae 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -21,6 +21,7 @@ powertools-examples-serialization powertools-examples-sqs powertools-examples-validation + powertools-examples-cloudformation diff --git a/examples/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md new file mode 100644 index 000000000..ed13ce399 --- /dev/null +++ b/examples/powertools-examples-cloudformation/README.md @@ -0,0 +1,40 @@ +# Cloudformation Custom Resource Example + +This project contains an example of Lambda function using the CloudFormation module of Lambda Powertools for Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/custom_resources/). + +## Deploy the sample application + +This sample can be used either with the Serverless Application Model (SAM) or with CDK. + +### Deploy with SAM CLI +To use the SAM CLI, you need the following tools. + +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) +* Maven - [Install Maven](https://maven.apache.org/install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +To build and deploy this application for the first time, run the following in your shell: + +```bash +cd infra/sam +sam build +sam deploy --guided +``` + +### Deploy with CDK +To use CDK you need the following tools. + +* CDK - [Install CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) +* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) +* Maven - [Install Maven](https://maven.apache.org/install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +To build and deploy this application for the first time, run the following in your shell: + +```bash +cd infra/cdk +mvn package +cdk synth +cdk deploy +``` \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/infra/cdk/.gitignore b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore new file mode 100644 index 000000000..2d91c7c6e --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore @@ -0,0 +1,14 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + +/cdk.json diff --git a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml new file mode 100644 index 000000000..4ede7e838 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + com.myorg + powertools-examples-cloudformation-cdk + 0.1 + + + UTF-8 + 2.59.0 + [10.0.0,11.0.0) + 5.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 9 + 9 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + com.myorg.PowertoolsExamplesCloudformationCdkApp + + + + + + + + aspectj + aspectjrt + 1.5.4 + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java new file mode 100644 index 000000000..84060171b --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java @@ -0,0 +1,16 @@ +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.StackProps; + +public class PowertoolsExamplesCloudformationCdkApp { + public static void main(final String[] args) { + App app = new App(); + + new PowertoolsExamplesCloudformationCdkStack(app, "PowertoolsExamplesCloudformationCdkStack", StackProps.builder() + .build()); + + app.synth(); + } +} + diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java new file mode 100644 index 000000000..06a06593b --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java @@ -0,0 +1,87 @@ +package com.myorg; + +import software.amazon.awscdk.*; +import software.amazon.awscdk.services.iam.Effect; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.PolicyStatementProps; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.FunctionProps; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.s3.assets.AssetOptions; +import software.constructs.Construct; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static java.util.Map.entry; +import static software.amazon.awscdk.BundlingOutput.ARCHIVED; + +public class PowertoolsExamplesCloudformationCdkStack extends Stack { + + public static final String SAMPLE_BUCKET_NAME = "sample-bucket-name-20230315-abc123"; + + public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + + List functionPackagingInstructions = Arrays.asList( + "/bin/sh", + "-c", + "mvn clean install" + + "&& cp target/powertools-examples-cloudformation-*.jar /asset-output/" + ); + BundlingOptions bundlingOptions = BundlingOptions.builder() + .command(functionPackagingInstructions) + .image(Runtime.JAVA_11.getBundlingImage()) + .volumes(singletonList( + // Mount local .m2 repo to avoid download all the dependencies again inside the container + DockerVolume.builder() + .hostPath(System.getProperty("user.home") + "/.m2/") + .containerPath("/root/.m2/") + .build() + )) + .user("root") + .outputType(ARCHIVED) + .build(); + + + Function helloWorldFunction = new Function(this, "HelloWorldFunction", FunctionProps.builder() + .runtime(Runtime.JAVA_11) + .code(Code.fromAsset("../../", AssetOptions.builder().bundling(bundlingOptions) + .build())) + .handler("helloworld.App::handleRequest") + .memorySize(512) + .timeout(Duration.seconds(20)) + .environment(Map.ofEntries(entry("JAVA_TOOL_OPTIONS", "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"))) + .build()); + helloWorldFunction.addToRolePolicy(new PolicyStatement(PolicyStatementProps.builder() + .effect(Effect.ALLOW) + .actions(List.of("s3:*")) + .resources(List.of("*")) + .build())); + + CfnParameter bucketName = CfnParameter.Builder.create(this, "BucketNameParam") + .type("String") + .defaultValue(SAMPLE_BUCKET_NAME) + .build(); + CfnParameter retentionDays = CfnParameter.Builder.create(this, "RetentionDaysParam") + .type("Number") + .defaultValue(10) + .build(); + + + CustomResource.Builder.create(this, "HelloWorldCustomResource") + .serviceToken(helloWorldFunction.getFunctionArn()) + .properties(Map + .of("BucketName", bucketName.getValueAsString(),"RetentionDays", retentionDays.getValueAsNumber())) + .build(); + + } +} diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java b/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java new file mode 100644 index 000000000..60ed8f516 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java @@ -0,0 +1,24 @@ + package com.myorg; + + import software.amazon.awscdk.App; + import software.amazon.awscdk.assertions.Template; + import java.io.IOException; + + import java.util.HashMap; + + import org.junit.jupiter.api.Test; + + public class PowertoolsExamplesCloudformationCdkTest { + + @Test + public void testStack() throws IOException { + App app = new App(); + PowertoolsExamplesCloudformationCdkStack stack = new PowertoolsExamplesCloudformationCdkStack(app, "test"); + + Template template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", new HashMap() {{ + put("MemorySize", 512); + }}); + } + } diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json new file mode 100644 index 000000000..26ef0a03b --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json @@ -0,0 +1,12 @@ +{ + "RequestType": "Create", + "ResponseURL": "http://pre-signed-S3-url-for-response", + "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid", + "RequestId": "unique id for this create request", + "ResourceType": "Custom::TestResource", + "ResourceProperties": { + "BucketName": "test-bucket-20230307-1", + "RetentionDays" : 10, + "StackName": "MyStack" + } +} diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json new file mode 100644 index 000000000..d18fdd3e4 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json @@ -0,0 +1,14 @@ +{ + "RequestType": "Delete", + "ResponseURL": "http://pre-signed-S3-url-for-response", + "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid", + "RequestId": "unique id for this create request", + "ResourceType": "Custom::TestResource", + "LogicalResourceId": "MyTestResource", + "PhysicalResourceId": "test-bucket-20230307-1", + "ResourceProperties": { + "BucketName": "test-bucket-20230307-1", + "RetentionDays" : 10, + "StackName": "MyStack" + } +} diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json new file mode 100644 index 000000000..5a5ae2e3f --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json @@ -0,0 +1,14 @@ +{ + "RequestType": "Update", + "ResponseURL": "http://pre-signed-S3-url-for-response", + "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid", + "RequestId": "unique id for this create request", + "ResourceType": "Custom::TestResource", + "LogicalResourceId": "MyTestResource", + "PhysicalResourceId": "test-bucket-20230307-1", + "ResourceProperties": { + "BucketName": "test-bucket-20230307-1", + "RetentionDays" : 100, + "StackName": "MyStack" + } +} diff --git a/examples/powertools-examples-cloudformation/infra/sam/template.yaml b/examples/powertools-examples-cloudformation/infra/sam/template.yaml new file mode 100644 index 000000000..9e5cc59b4 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam/template.yaml @@ -0,0 +1,54 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + powertools-examples-cloudformation + + Sample SAM Template for powertools-examples-cloudformation + +Globals: + Function: + Timeout: 20 + MemorySize: 128 + +Parameters: + BucketNameParam: + Type: String + Default: sample-bucket-name-20230307-abc123 + RetentionDaysParam: + Type: Number + Default: 10 + + +Resources: + HelloWorldCustomResource: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt HelloWorldFunction.Arn + BucketName: !Ref BucketNameParam + RetentionDays: !Ref RetentionDaysParam + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../../ + Handler: helloworld.App::handleRequest + Runtime: java11 + Architectures: + - x86_64 + MemorySize: 512 + Policies: + - Statement: + - Sid: bucketaccess1 + Effect: Allow + Action: + - s3:* + Resource: '*' + Environment: + Variables: + PARAM1: VALUE + JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + +Outputs: + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml new file mode 100644 index 000000000..5a5542e3b --- /dev/null +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -0,0 +1,123 @@ + + 4.0.0 + powertools-examples-cloudformation + jar + AWS Lambda Powertools for Java library Examples - CloudFormation + + + powertools-examples + software.amazon.lambda + 1.14.0 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + junit + junit + 4.13.2 + test + + + software.amazon.lambda + powertools-cloudformation + ${project.version} + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + software.amazon.awssdk + bom + ${aws.sdk.version} + pom + import + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + software.amazon.awssdk + apache-client + + + commons-logging + commons-logging + + + + + org.apache.logging.log4j + log4j-jcl + ${log4j.version} + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + + + package + + shade + + + + + + + + + + + io.github.edwgiz + log4j-maven-shade-plugin-extensions + 2.20.0 + + + + + + diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java new file mode 100644 index 000000000..89ad14456 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -0,0 +1,166 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.waiters.WaiterResponse; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.services.s3.waiters.S3Waiter; +import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; +import software.amazon.lambda.powertools.cloudformation.Response; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Handler for requests to Lambda function. + */ + +public class App extends AbstractCustomResourceHandler { + public static final String EXPIRE_OBJECTS_ID = "expire-all-objects-after-x-days"; + private final static Logger log = LogManager.getLogger(App.class); + private final S3Client s3Client; + + public App() { + super(); + s3Client = S3Client.builder().httpClientBuilder(ApacheHttpClient.builder()).build(); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"), "RetentionDays cannot be null."); + + log.info(cloudFormationCustomResourceEvent); + String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); + log.info("Bucket Name {}", bucketName); + int retentionDays = Integer.parseInt(String.valueOf(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"))); + log.info("Retention Period (Days) {}", retentionDays); + + try { + createBucketWithRetention(bucketName, retentionDays); + return Response.success(bucketName); + } catch (AwsServiceException | SdkClientException e) { + log.error(e); + return Response.failed(bucketName); + } + } + + @Override + protected Response update(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"), "RetentionDays cannot be null."); + log.info(cloudFormationCustomResourceEvent); + String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId(); + log.info("Physical Resource ID {}", physicalResourceId); + int retentionDays = Integer.parseInt(String.valueOf(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"))); + log.info("Retention Period (Days) {}", retentionDays); + String newBucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); + + if (!physicalResourceId.equals(newBucketName)) { + // Custom Resource is configured with a different bucket name + try { + createBucketWithRetention(newBucketName, retentionDays); + return Response.success(newBucketName); + } catch (AwsServiceException | SdkClientException e) { + log.error(e); + return Response.failed(newBucketName); + } + } else { + // Bucket name has not changed - check for changes in Lifecycle configuration + try { + updateLifecycleRules(physicalResourceId, retentionDays); + return Response.success(physicalResourceId); + } catch (AwsServiceException | SdkClientException e) { + log.error(e); + return Response.failed(physicalResourceId); + } + } + } + + @Override + protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getPhysicalResourceId(), "PhysicalResourceId cannot be null."); + + log.info(cloudFormationCustomResourceEvent); + String bucketName = cloudFormationCustomResourceEvent.getPhysicalResourceId(); + log.info("Bucket Name {}", bucketName); + + if (bucketExists(bucketName)) { + try { + s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()); + log.info("Bucket Deleted {}", bucketName); + return Response.success(bucketName); + } catch (AwsServiceException | SdkClientException e) { + log.error(e); + return Response.failed(bucketName); + } + } else { + log.info("Bucket already deleted - no action"); + return Response.success(bucketName); + } + + } + + private boolean bucketExists(String bucketName) { + try { + HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); + if (headBucketResponse.sdkHttpResponse().isSuccessful()) { + return true; + } + } catch (NoSuchBucketException e) { + log.info("Bucket does not exist"); + return false; + } + return false; + } + + private void updateLifecycleRules(String physicalResourceId, int retentionDays) { + GetBucketLifecycleConfigurationResponse bucketLifecycleConfiguration = s3Client.getBucketLifecycleConfiguration(GetBucketLifecycleConfigurationRequest.builder().bucket(physicalResourceId).build()); + log.info(bucketLifecycleConfiguration); + + List oldRules = new ArrayList<>(bucketLifecycleConfiguration.rules()); + Optional oldExpireLifecycleRule = oldRules.stream().filter(lifecycleRule -> lifecycleRule.id().equals(EXPIRE_OBJECTS_ID)).findFirst(); + // Check if the existing configuration is equals to the new configuration + if (oldExpireLifecycleRule.isPresent()) { + if (oldExpireLifecycleRule.get().expiration().days() == retentionDays) { + log.info("Retention period is already set to {}. No further actions", retentionDays); + return; + } else { + oldRules.remove(oldExpireLifecycleRule.get()); + } + } + + LifecycleRule updatedLifecycleRule = LifecycleRule.builder().id(EXPIRE_OBJECTS_ID).filter(LifecycleRuleFilter.builder().prefix("").build()).status("Enabled").expiration(LifecycleExpiration.builder().days(retentionDays).build()).build(); + + List newRules = new ArrayList<>(); + newRules.add(updatedLifecycleRule); + newRules.addAll(oldRules); + + log.info("Updating Lifecycle configuration"); + s3Client.putBucketLifecycleConfiguration(PutBucketLifecycleConfigurationRequest.builder().bucket(physicalResourceId).lifecycleConfiguration(BucketLifecycleConfiguration.builder().rules(newRules).build()).build()); + } + + + private void createBucketWithRetention(String bucketName, int retentionDays) { + S3Waiter waiter = s3Client.waiter(); + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder().bucket(bucketName).build(); + s3Client.createBucket(createBucketRequest); + WaiterResponse waiterResponse = waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build()); + waiterResponse.matched().response().ifPresent(log::info); + log.info("Bucket Created {}", bucketName); + + LifecycleRule lifecycleRule = LifecycleRule.builder().id(EXPIRE_OBJECTS_ID).filter(LifecycleRuleFilter.builder().prefix("").build()).status("Enabled").expiration(LifecycleExpiration.builder().days(retentionDays).build()).build(); + s3Client.putBucketLifecycleConfiguration(PutBucketLifecycleConfigurationRequest.builder().bucket(bucketName).lifecycleConfiguration(BucketLifecycleConfiguration.builder().rules(lifecycleRule).build()).build()); + } +} \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml new file mode 100644 index 000000000..ca07238cf --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} [%p] <%X{AWSRequestId}> (%t) %c:%L: %m%n + + + + + + + + + + + + \ No newline at end of file From 67f5c5c793f0d05c4a9e5936c8b81ba9e686ac12 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Mon, 20 Mar 2023 14:18:58 +0100 Subject: [PATCH 2/7] PR Feedback --- .../infra/cdk/.gitignore | 1 - .../infra/cdk/cdk.json | 37 +++++++++++++++++++ .../infra/cdk/pom.xml | 10 +---- ...ertoolsExamplesCloudformationCdkStack.java | 6 ++- .../infra/sam/template.yaml | 9 +++-- .../pom.xml | 11 +----- .../src/main/resources/log4j2.xml | 1 - 7 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/cdk.json diff --git a/examples/powertools-examples-cloudformation/infra/cdk/.gitignore b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore index 2d91c7c6e..1db21f162 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/.gitignore +++ b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore @@ -11,4 +11,3 @@ target .cdk.staging cdk.out -/cdk.json diff --git a/examples/powertools-examples-cloudformation/infra/cdk/cdk.json b/examples/powertools-examples-cloudformation/infra/cdk/cdk.json new file mode 100644 index 000000000..fe011b328 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/cdk.json @@ -0,0 +1,37 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true + } +} diff --git a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml index 4ede7e838..3753f18a7 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml +++ b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml @@ -21,8 +21,8 @@ maven-compiler-plugin 3.8.1 - 9 - 9 + 8 + 8 @@ -38,12 +38,6 @@ - - aspectj - aspectjrt - 1.5.4 - - software.amazon.awscdk diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java index 06a06593b..ea5d502eb 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java @@ -63,7 +63,11 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str .build()); helloWorldFunction.addToRolePolicy(new PolicyStatement(PolicyStatementProps.builder() .effect(Effect.ALLOW) - .actions(List.of("s3:*")) + .actions(List.of("s3:GetLifecycleConfiguration", + "s3:PutLifecycleConfiguration", + "s3:CreateBucket", + "s3:ListBucket", + "s3:DeleteBucket")) .resources(List.of("*")) .build())); diff --git a/examples/powertools-examples-cloudformation/infra/sam/template.yaml b/examples/powertools-examples-cloudformation/infra/sam/template.yaml index 9e5cc59b4..a88039955 100644 --- a/examples/powertools-examples-cloudformation/infra/sam/template.yaml +++ b/examples/powertools-examples-cloudformation/infra/sam/template.yaml @@ -8,7 +8,6 @@ Description: > Globals: Function: Timeout: 20 - MemorySize: 128 Parameters: BucketNameParam: @@ -18,7 +17,6 @@ Parameters: Type: Number Default: 10 - Resources: HelloWorldCustomResource: Type: AWS::CloudFormation::CustomResource @@ -41,11 +39,14 @@ Resources: - Sid: bucketaccess1 Effect: Allow Action: - - s3:* + - s3:GetLifecycleConfiguration + - s3:PutLifecycleConfiguration + - s3:CreateBucket + - s3:ListBucket + - s3:DeleteBucket Resource: '*' Environment: Variables: - PARAM1: VALUE JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Outputs: diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 5a5542e3b..af98f7ba6 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -15,18 +15,12 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + ${lambda.core.version} com.amazonaws aws-lambda-java-events - 3.11.0 - - - junit - junit - 4.13.2 - test + ${lambda.events.version} software.amazon.lambda @@ -46,7 +40,6 @@ org.apache.logging.log4j log4j-api - ${log4j.version} software.amazon.awssdk diff --git a/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml index ca07238cf..8e8162128 100644 --- a/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml @@ -12,7 +12,6 @@ - \ No newline at end of file From ed8c3a290c70dfb5d476bfffe032736c25a1fd36 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 30 Mar 2023 17:11:53 +0200 Subject: [PATCH 3/7] Update version and fix CDK project --- .../com/myorg/PowertoolsExamplesCloudformationCdkStack.java | 6 ++++-- examples/powertools-examples-cloudformation/pom.xml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java index ea5d502eb..bdd68acbe 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java @@ -18,6 +18,7 @@ import static java.util.Collections.singletonList; import static java.util.Map.entry; import static software.amazon.awscdk.BundlingOutput.ARCHIVED; +import static software.amazon.awscdk.BundlingOutput.NOT_ARCHIVED; public class PowertoolsExamplesCloudformationCdkStack extends Stack { @@ -35,7 +36,8 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str "/bin/sh", "-c", "mvn clean install" + - "&& cp target/powertools-examples-cloudformation-*.jar /asset-output/" + "&& mkdir /asset-output/lib" + + "&& cp target/powertools-examples-cloudformation-*.jar /asset-output/lib" ); BundlingOptions bundlingOptions = BundlingOptions.builder() .command(functionPackagingInstructions) @@ -48,7 +50,7 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str .build() )) .user("root") - .outputType(ARCHIVED) + .outputType(NOT_ARCHIVED) .build(); diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index af98f7ba6..d7794b695 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -8,7 +8,7 @@ powertools-examples software.amazon.lambda - 1.14.0 + 1.15.0 From f4b3fd21d990b7dfca39c813ca4e2561a70643cd Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 30 Mar 2023 17:43:30 +0200 Subject: [PATCH 4/7] Simplify example by removing lifecycle configuration --- .../src/main/java/helloworld/App.java | 61 +++---------------- 1 file changed, 8 insertions(+), 53 deletions(-) diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index 89ad14456..1433b4d4a 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -14,17 +14,13 @@ import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; import software.amazon.lambda.powertools.cloudformation.Response; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; -import java.util.Optional; /** * Handler for requests to Lambda function. */ public class App extends AbstractCustomResourceHandler { - public static final String EXPIRE_OBJECTS_ID = "expire-all-objects-after-x-days"; private final static Logger log = LogManager.getLogger(App.class); private final S3Client s3Client; @@ -37,16 +33,12 @@ public App() { protected Response create(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"), "RetentionDays cannot be null."); log.info(cloudFormationCustomResourceEvent); String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); log.info("Bucket Name {}", bucketName); - int retentionDays = Integer.parseInt(String.valueOf(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"))); - log.info("Retention Period (Days) {}", retentionDays); - try { - createBucketWithRetention(bucketName, retentionDays); + createBucket(bucketName); return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { log.error(e); @@ -58,32 +50,25 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom protected Response update(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"), "RetentionDays cannot be null."); + log.info(cloudFormationCustomResourceEvent); String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Physical Resource ID {}", physicalResourceId); - int retentionDays = Integer.parseInt(String.valueOf(cloudFormationCustomResourceEvent.getResourceProperties().get("RetentionDays"))); - log.info("Retention Period (Days) {}", retentionDays); + String newBucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); if (!physicalResourceId.equals(newBucketName)) { - // Custom Resource is configured with a different bucket name + // The bucket name has changed - create a new bucket try { - createBucketWithRetention(newBucketName, retentionDays); + createBucket(newBucketName); return Response.success(newBucketName); } catch (AwsServiceException | SdkClientException e) { log.error(e); return Response.failed(newBucketName); } } else { - // Bucket name has not changed - check for changes in Lifecycle configuration - try { - updateLifecycleRules(physicalResourceId, retentionDays); - return Response.success(physicalResourceId); - } catch (AwsServiceException | SdkClientException e) { - log.error(e); - return Response.failed(physicalResourceId); - } + // Bucket name has not changed - No changes + return Response.success(physicalResourceId); } } @@ -125,42 +110,12 @@ private boolean bucketExists(String bucketName) { return false; } - private void updateLifecycleRules(String physicalResourceId, int retentionDays) { - GetBucketLifecycleConfigurationResponse bucketLifecycleConfiguration = s3Client.getBucketLifecycleConfiguration(GetBucketLifecycleConfigurationRequest.builder().bucket(physicalResourceId).build()); - log.info(bucketLifecycleConfiguration); - - List oldRules = new ArrayList<>(bucketLifecycleConfiguration.rules()); - Optional oldExpireLifecycleRule = oldRules.stream().filter(lifecycleRule -> lifecycleRule.id().equals(EXPIRE_OBJECTS_ID)).findFirst(); - // Check if the existing configuration is equals to the new configuration - if (oldExpireLifecycleRule.isPresent()) { - if (oldExpireLifecycleRule.get().expiration().days() == retentionDays) { - log.info("Retention period is already set to {}. No further actions", retentionDays); - return; - } else { - oldRules.remove(oldExpireLifecycleRule.get()); - } - } - - LifecycleRule updatedLifecycleRule = LifecycleRule.builder().id(EXPIRE_OBJECTS_ID).filter(LifecycleRuleFilter.builder().prefix("").build()).status("Enabled").expiration(LifecycleExpiration.builder().days(retentionDays).build()).build(); - - List newRules = new ArrayList<>(); - newRules.add(updatedLifecycleRule); - newRules.addAll(oldRules); - - log.info("Updating Lifecycle configuration"); - s3Client.putBucketLifecycleConfiguration(PutBucketLifecycleConfigurationRequest.builder().bucket(physicalResourceId).lifecycleConfiguration(BucketLifecycleConfiguration.builder().rules(newRules).build()).build()); - } - - - private void createBucketWithRetention(String bucketName, int retentionDays) { + private void createBucket(String bucketName) { S3Waiter waiter = s3Client.waiter(); CreateBucketRequest createBucketRequest = CreateBucketRequest.builder().bucket(bucketName).build(); s3Client.createBucket(createBucketRequest); WaiterResponse waiterResponse = waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build()); waiterResponse.matched().response().ifPresent(log::info); log.info("Bucket Created {}", bucketName); - - LifecycleRule lifecycleRule = LifecycleRule.builder().id(EXPIRE_OBJECTS_ID).filter(LifecycleRuleFilter.builder().prefix("").build()).status("Enabled").expiration(LifecycleExpiration.builder().days(retentionDays).build()).build(); - s3Client.putBucketLifecycleConfiguration(PutBucketLifecycleConfigurationRequest.builder().bucket(bucketName).lifecycleConfiguration(BucketLifecycleConfiguration.builder().rules(lifecycleRule).build()).build()); } } \ No newline at end of file From 8828870ed40128a4ba66ec967b6ad08e17458f4b Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Tue, 11 Jul 2023 16:25:04 +0200 Subject: [PATCH 5/7] chore: java8 compatibility --- .../README.md | 4 +- ...ertoolsExamplesCloudformationCdkStack.java | 28 +-- .../pom.xml | 169 ++++++++++++++---- 3 files changed, 155 insertions(+), 46 deletions(-) diff --git a/examples/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md index ed13ce399..8b31e5658 100644 --- a/examples/powertools-examples-cloudformation/README.md +++ b/examples/powertools-examples-cloudformation/README.md @@ -10,7 +10,7 @@ This sample can be used either with the Serverless Application Model (SAM) or wi To use the SAM CLI, you need the following tools. * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) -* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) +* Java 8 - [Install Java 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) * Maven - [Install Maven](https://maven.apache.org/install.html) * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) @@ -26,7 +26,7 @@ sam deploy --guided To use CDK you need the following tools. * CDK - [Install CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) -* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) +* Java 8 - [Install Java 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) * Maven - [Install Maven](https://maven.apache.org/install.html) * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java index bdd68acbe..9226d4310 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java @@ -1,5 +1,6 @@ package com.myorg; +import software.amazon.awscdk.Stack; import software.amazon.awscdk.*; import software.amazon.awscdk.services.iam.Effect; import software.amazon.awscdk.services.iam.PolicyStatement; @@ -11,13 +12,15 @@ import software.amazon.awscdk.services.s3.assets.AssetOptions; import software.constructs.Construct; +import java.io.Serializable; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; + import static java.util.Collections.singletonList; -import static java.util.Map.entry; -import static software.amazon.awscdk.BundlingOutput.ARCHIVED; import static software.amazon.awscdk.BundlingOutput.NOT_ARCHIVED; public class PowertoolsExamplesCloudformationCdkStack extends Stack { @@ -53,7 +56,6 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str .outputType(NOT_ARCHIVED) .build(); - Function helloWorldFunction = new Function(this, "HelloWorldFunction", FunctionProps.builder() .runtime(Runtime.JAVA_11) .code(Code.fromAsset("../../", AssetOptions.builder().bundling(bundlingOptions) @@ -61,19 +63,20 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str .handler("helloworld.App::handleRequest") .memorySize(512) .timeout(Duration.seconds(20)) - .environment(Map.ofEntries(entry("JAVA_TOOL_OPTIONS", "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"))) + .environment(Collections + .singletonMap("JAVA_TOOL_OPTIONS", "-XX:+TieredCompilation -XX:TieredStopAtLevel=1")) .build()); helloWorldFunction.addToRolePolicy(new PolicyStatement(PolicyStatementProps.builder() .effect(Effect.ALLOW) - .actions(List.of("s3:GetLifecycleConfiguration", + .actions(Arrays.asList("s3:GetLifecycleConfiguration", "s3:PutLifecycleConfiguration", "s3:CreateBucket", "s3:ListBucket", "s3:DeleteBucket")) - .resources(List.of("*")) - .build())); + .resources(singletonList("*")).build())); - CfnParameter bucketName = CfnParameter.Builder.create(this, "BucketNameParam") + CfnParameter bucketName = CfnParameter.Builder + .create(this, "BucketNameParam") .type("String") .defaultValue(SAMPLE_BUCKET_NAME) .build(); @@ -83,10 +86,13 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str .build(); - CustomResource.Builder.create(this, "HelloWorldCustomResource") + Map crProperties = new HashMap<>(); + crProperties.put("BucketName", bucketName.getValueAsString()); + crProperties.put("RetentionDays", retentionDays.getValueAsNumber()); + CustomResource.Builder + .create(this, "HelloWorldCustomResource") .serviceToken(helloWorldFunction.getFunctionArn()) - .properties(Map - .of("BucketName", bucketName.getValueAsString(),"RetentionDays", retentionDays.getValueAsNumber())) + .properties(crProperties) .build(); } diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index d7794b695..198a85894 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -1,15 +1,34 @@ 4.0.0 + + software.amazon.lambda.examples + 1.17.0-SNAPSHOT powertools-examples-cloudformation jar + AWS Lambda Powertools for Java library Examples - CloudFormation - - powertools-examples - software.amazon.lambda - 1.15.0 - + + 2.20.0 + 1.8 + 1.8 + true + 1.2.2 + 3.11.2 + 2.20.102 + + + + + software.amazon.awssdk + bom + ${aws.sdk.version} + pom + import + + + @@ -40,13 +59,7 @@ org.apache.logging.log4j log4j-api - - - software.amazon.awssdk - bom - ${aws.sdk.version} - pom - import + ${log4j.version} software.amazon.awssdk @@ -85,32 +98,122 @@ - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - - - - package - - shade - - - - - - - - + dev.aspectj + aspectj-maven-plugin + 1.13.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + + + + + - io.github.edwgiz - log4j-maven-shade-plugin-extensions - 2.20.0 + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.15 + + + + jdk8 + + (,11) + + + 1.9.7 + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj.plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + test-compile + + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + From 4780e40599fd1441b046f0c7846859361469a96f Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Tue, 11 Jul 2023 17:04:56 +0200 Subject: [PATCH 6/7] chore: add some comments --- .../src/main/java/helloworld/App.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index 1433b4d4a..fefded282 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -29,8 +29,17 @@ public App() { s3Client = S3Client.builder().httpClientBuilder(ApacheHttpClient.builder()).build(); } + /** + * This method is invoked when CloudFormation Creates the Custom Resource. + * The method creates an Amazon S3 Bucket with the provided `BucketName` + * + * @param cloudFormationCustomResourceEvent Create Event from CloudFormation + * @param context Lambda Context + * @return Response to send to CloudFormation + */ @Override protected Response create(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + // Validate the CloudFormation Custom Resource event Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); @@ -38,59 +47,93 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); log.info("Bucket Name {}", bucketName); try { + // Create the S3 bucket with the given bucketName createBucket(bucketName); + // Return a successful response with the bucketName as the physicalResourceId return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { + // In case of error, return a failed response, with the bucketName as the physicalResourceId log.error(e); return Response.failed(bucketName); } } + /** + * This method is invoked when CloudFormation Updates the Custom Resource. + * The method creates an Amazon S3 Bucket with the provided `BucketName`, if the `BucketName` differs from the previous `BucketName` + * + * @param cloudFormationCustomResourceEvent Update Event from CloudFormation + * @param context Lambda Context + * @return Response to send to CloudFormation + */ @Override protected Response update(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + // Validate the CloudFormation Custom Resource event Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); log.info(cloudFormationCustomResourceEvent); + // Get the physicalResourceId. physicalResourceId is the value returned to CloudFormation in the Create request, and passed in on subsequent requests (e.g. UPDATE or DELETE) String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Physical Resource ID {}", physicalResourceId); + // Get the BucketName from the CloudFormation Event String newBucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); + // Check if the physicalResourceId equals the new BucketName if (!physicalResourceId.equals(newBucketName)) { // The bucket name has changed - create a new bucket try { + // Create a new bucket with the newBucketName createBucket(newBucketName); + // Return a successful response with the newBucketName return Response.success(newBucketName); } catch (AwsServiceException | SdkClientException e) { log.error(e); return Response.failed(newBucketName); } } else { - // Bucket name has not changed - No changes + // Bucket name has not changed, and no changes are needed. + // Return a successful response with the previous physicalResourceId return Response.success(physicalResourceId); } } + /** + * This method is invoked when CloudFormation Deletes the Custom Resource. + * NOTE: CloudFormation will DELETE a resource, if during the UPDATE a new physicalResourceId is returned. + * Refer to the Powertools Java Documentation for more details. + * + * @param cloudFormationCustomResourceEvent Delete Event from CloudFormation + * @param context Lambda Context + * @return Response to send to CloudFormation + */ @Override protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) { + // Validate the CloudFormation Custom Resource event Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null."); Objects.requireNonNull(cloudFormationCustomResourceEvent.getPhysicalResourceId(), "PhysicalResourceId cannot be null."); log.info(cloudFormationCustomResourceEvent); + // Get the physicalResourceId. physicalResourceId is the value provided to CloudFormation in the Create request. String bucketName = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Bucket Name {}", bucketName); + // Check if a bucket with bucketName exists if (bucketExists(bucketName)) { try { + // If it exists, delete the bucket s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()); log.info("Bucket Deleted {}", bucketName); + // Return a successful response with bucketName as the physicalResourceId return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { + // Return a failed response in case of errors during the bucket deletion log.error(e); return Response.failed(bucketName); } } else { + // If the bucket does not exist, return a successful response with the bucketName as the physicalResourceId log.info("Bucket already deleted - no action"); return Response.success(bucketName); } From 04f172785f238247f9c2fd98356d94271a9ef5e6 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Mon, 17 Jul 2023 15:08:23 +0200 Subject: [PATCH 7/7] PR feedback --- .../README.md | 6 ++--- .../infra/cdk/pom.xml | 8 ------- ...ertoolsExamplesCloudformationCdkStack.java | 14 ++--------- ...wertoolsExamplesCloudformationCdkTest.java | 24 ------------------- .../infra/sam/template.yaml | 5 ---- .../src/main/java/helloworld/App.java | 4 ++-- 6 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java diff --git a/examples/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md index 8b31e5658..6dbffcf37 100644 --- a/examples/powertools-examples-cloudformation/README.md +++ b/examples/powertools-examples-cloudformation/README.md @@ -1,6 +1,6 @@ # Cloudformation Custom Resource Example -This project contains an example of Lambda function using the CloudFormation module of Lambda Powertools for Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/custom_resources/). +This project contains an example of Lambda function using the CloudFormation module of Powertools for AWS Lambda in Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/custom_resources/). ## Deploy the sample application @@ -19,7 +19,7 @@ To build and deploy this application for the first time, run the following in yo ```bash cd infra/sam sam build -sam deploy --guided +sam deploy --guided --parameter-overrides BucketNameParam=my-unique-bucket-20230717 ``` ### Deploy with CDK @@ -36,5 +36,5 @@ To build and deploy this application for the first time, run the following in yo cd infra/cdk mvn package cdk synth -cdk deploy +cdk deploy -c BucketNameParam=my-unique-bucket-20230718 ``` \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml index 3753f18a7..30172fa3f 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml +++ b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml @@ -11,7 +11,6 @@ UTF-8 2.59.0 [10.0.0,11.0.0) - 5.7.1 @@ -50,12 +49,5 @@ constructs ${constructs.version} - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java index 9226d4310..e880a3534 100644 --- a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java @@ -75,20 +75,10 @@ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final Str "s3:DeleteBucket")) .resources(singletonList("*")).build())); - CfnParameter bucketName = CfnParameter.Builder - .create(this, "BucketNameParam") - .type("String") - .defaultValue(SAMPLE_BUCKET_NAME) - .build(); - CfnParameter retentionDays = CfnParameter.Builder.create(this, "RetentionDaysParam") - .type("Number") - .defaultValue(10) - .build(); - + String bucketName = (String) this.getNode().tryGetContext("BucketNameParam"); Map crProperties = new HashMap<>(); - crProperties.put("BucketName", bucketName.getValueAsString()); - crProperties.put("RetentionDays", retentionDays.getValueAsNumber()); + crProperties.put("BucketName", bucketName); CustomResource.Builder .create(this, "HelloWorldCustomResource") .serviceToken(helloWorldFunction.getFunctionArn()) diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java b/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java deleted file mode 100644 index 60ed8f516..000000000 --- a/examples/powertools-examples-cloudformation/infra/cdk/src/test/java/com/myorg/PowertoolsExamplesCloudformationCdkTest.java +++ /dev/null @@ -1,24 +0,0 @@ - package com.myorg; - - import software.amazon.awscdk.App; - import software.amazon.awscdk.assertions.Template; - import java.io.IOException; - - import java.util.HashMap; - - import org.junit.jupiter.api.Test; - - public class PowertoolsExamplesCloudformationCdkTest { - - @Test - public void testStack() throws IOException { - App app = new App(); - PowertoolsExamplesCloudformationCdkStack stack = new PowertoolsExamplesCloudformationCdkStack(app, "test"); - - Template template = Template.fromStack(stack); - - template.hasResourceProperties("AWS::Lambda::Function", new HashMap() {{ - put("MemorySize", 512); - }}); - } - } diff --git a/examples/powertools-examples-cloudformation/infra/sam/template.yaml b/examples/powertools-examples-cloudformation/infra/sam/template.yaml index a88039955..a7ce4adf1 100644 --- a/examples/powertools-examples-cloudformation/infra/sam/template.yaml +++ b/examples/powertools-examples-cloudformation/infra/sam/template.yaml @@ -12,10 +12,6 @@ Globals: Parameters: BucketNameParam: Type: String - Default: sample-bucket-name-20230307-abc123 - RetentionDaysParam: - Type: Number - Default: 10 Resources: HelloWorldCustomResource: @@ -23,7 +19,6 @@ Resources: Properties: ServiceToken: !GetAtt HelloWorldFunction.Arn BucketName: !Ref BucketNameParam - RetentionDays: !Ref RetentionDaysParam HelloWorldFunction: Type: AWS::Serverless::Function diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index fefded282..c7744cd5a 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -31,7 +31,7 @@ public App() { /** * This method is invoked when CloudFormation Creates the Custom Resource. - * The method creates an Amazon S3 Bucket with the provided `BucketName` + * In this example, the method creates an Amazon S3 Bucket with the provided `BucketName` * * @param cloudFormationCustomResourceEvent Create Event from CloudFormation * @param context Lambda Context @@ -60,7 +60,7 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom /** * This method is invoked when CloudFormation Updates the Custom Resource. - * The method creates an Amazon S3 Bucket with the provided `BucketName`, if the `BucketName` differs from the previous `BucketName` + * In this example, the method creates an Amazon S3 Bucket with the provided `BucketName`, if the `BucketName` differs from the previous `BucketName` (for initial creation) * * @param cloudFormationCustomResourceEvent Update Event from CloudFormation * @param context Lambda Context