├── .github ├── actions │ └── sam-build-and-deploy │ │ └── action.yml └── workflows │ └── java-deploy-pipeline.yaml ├── .gitignore ├── LICENSE ├── README.md ├── infrastructure ├── cdk │ ├── .gitignore │ ├── README.md │ ├── cdk.json │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── product │ │ │ └── infrastructure │ │ │ ├── DatabaseStack.java │ │ │ ├── DatabaseStackProps.java │ │ │ ├── InfrastructureApp.java │ │ │ ├── InfrastructureStack.java │ │ │ └── NetworkingStack.java │ │ └── test │ │ └── java │ │ └── com │ │ └── product │ │ └── infrastructure │ │ └── InfrastructureTest.java └── db-setup │ ├── dependency-reduced-pom.xml │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── com │ │ └── amazon │ │ └── aws │ │ ├── AwsSecret.java │ │ └── DBSetupHandler.java │ └── resources │ └── setup.sql ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── product │ │ │ └── api │ │ │ ├── ApplicationConfiguration.java │ │ │ ├── ApplicationProperties.java │ │ │ ├── AwsSecret.java │ │ │ ├── JpaConfiguration.java │ │ │ ├── ProductApiApplication.java │ │ │ ├── ProductController.java │ │ │ ├── SnapshotConfiguration.java │ │ │ ├── StreamLambdaHandler.java │ │ │ └── core │ │ │ ├── IProductRepository.java │ │ │ ├── Product.java │ │ │ ├── ProductDTO.java │ │ │ └── ProductService.java │ └── resources │ │ ├── application.properties │ │ ├── log4j2.xml │ │ └── schema-postgres.sql └── test │ └── java │ └── ProductApiIT.java └── template.yaml /.github/actions/sam-build-and-deploy/action.yml: -------------------------------------------------------------------------------- 1 | name: "SAM Build & Deploy" 2 | description: "Build & Deploy SAM application" 3 | inputs: 4 | aws-access-key: 5 | required: true 6 | description: "AWS Access Key" 7 | aws-secret-key: 8 | required: true 9 | description: "AWS Secret Key" 10 | aws-region: 11 | required: true 12 | description: "AWS Region" 13 | template-file-path: 14 | required: true 15 | description: "Path to the SAM template file" 16 | stack-name: 17 | required: true 18 | description: "The name of the CloudFormation stack to deploy" 19 | s3-bucket-name: 20 | required: true 21 | description: "The name of the S3 bucket to store SAM artefacts" 22 | security-group-id: 23 | required: true 24 | description: "The ID of the security group to use" 25 | subnet-1-id: 26 | required: true 27 | description: "The ID of the first subnet to use" 28 | subnet-2-id: 29 | required: true 30 | description: "The ID of the second subnet to use" 31 | secret-name: 32 | required: true 33 | description: "The name of the secret holding the database credentials" 34 | 35 | runs: 36 | using: "composite" 37 | steps: 38 | - uses: actions/setup-python@v2 39 | - uses: actions/setup-java@v2 40 | with: 41 | java-version: 11 42 | - uses: aws-actions/setup-sam@v1 43 | - uses: aws-actions/configure-aws-credentials@v1 44 | with: 45 | aws-access-key-id: ${{ inputs.aws-access-key }} 46 | aws-secret-access-key: ${{ inputs.aws-secret-key }} 47 | aws-region: ${{ inputs.aws-region }} 48 | 49 | # Build and deploy ARM 50 | - run: sam build -t ${{ inputs.template-file-path }} 51 | shell: bash 52 | - run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${{inputs.stack-name}} --s3-prefix ${{inputs.stack-name}} --s3-bucket ${{ inputs.s3-bucket-name }} --capabilities CAPABILITY_IAM --parameter-overrides ParameterKey=SecurityGroupId,ParameterValue=${{ inputs.security-group-id }} ParameterKey=Subnet1Id,ParameterValue=${{ inputs.subnet-1-id }} ParameterKey=Subnet2Id,ParameterValue=${{ inputs.subnet-2-id }} ParameterKey=DatabaseSecretName,ParameterValue=${{ inputs.secret-name }} 53 | shell: bash -------------------------------------------------------------------------------- /.github/workflows/java-deploy-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: JavaSamDeploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - src 10 | jobs: 11 | 12 | build-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build and Deploy 17 | uses: ./.github/actions/sam-build-and-deploy 18 | with: 19 | aws-access-key: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | aws-secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | aws-region: ${{ secrets.AWS_REGION }} 22 | template-file-path: ./template.yaml 23 | stack-name: java-spring 24 | s3-bucket-name: aws-sam-cli-managed-default-samclisourcebucket-1kgurv0ac87fw 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | samconfig.toml 26 | **/.aws-sam/* 27 | **/.idea/* 28 | 29 | .classpath 30 | .project 31 | .settings/ 32 | target/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 James Eastham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringBoot on Lambda 2 | 3 | Example repository for running a SpringBoot API on AWS Lambda. This GitHub repo coincides with a [series on Youtube](https://www.youtube.com/watch?v=A1rYiHTy9Lg&list=PLCOG9xkUD90IDm9tcY-5nMK6X6g8SD-Sz) walking through how to build and deploy SpringBoot API's on Lambda. 4 | 5 | ## Deployment 6 | 7 | To deploy this sample into your own AWS account two seperate steps are required. The first is to deploy the infrastructure, the second to deploy the application code. 8 | 9 | ### Infrastructure 10 | 11 | The infrastructure is built using the AWS CDK. The deployed resources contain: 12 | 13 | - A VPC 14 | - 2 private, 2 public subnets 15 | - 2 NAT Gateways 16 | - Postgres RDS Instance 17 | - Secrets Manager secret for credentials 18 | - Lambda function for applying database changes 19 | 20 | To deploy the infrastructure, run the following commands in order: 21 | 22 | ``` 23 | cd infrastructure/db-setup 24 | mvn clean install 25 | cd ../cdk 26 | cdk deploy 27 | ``` 28 | 29 | This will compile the db-update Lambda function and then deploy the CDK infrastructure to your account. 3 values will output to the console that are required to deploy the application. 30 | 31 | ### Application 32 | 33 | The application is deployed using AWS SAM. You can [install AWS SAM here](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html). To deploy the application run the below commands: 34 | 35 | ``` 36 | sam build 37 | sam deploy --guided 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /infrastructure/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | .classpath.txt 2 | target 3 | .classpath 4 | .project 5 | .idea 6 | .settings 7 | .vscode 8 | *.iml 9 | 10 | # CDK asset staging directory 11 | .cdk.staging 12 | cdk.out 13 | 14 | -------------------------------------------------------------------------------- /infrastructure/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK Java project! 2 | 3 | This is a blank project for CDK development with Java. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests. 8 | 9 | ## Useful commands 10 | 11 | * `mvn package` compile and run tests 12 | * `cdk ls` list all stacks in the app 13 | * `cdk synth` emits the synthesized CloudFormation template 14 | * `cdk deploy` deploy this stack to your default AWS account/region 15 | * `cdk diff` compare deployed stack with current state 16 | * `cdk docs` open CDK documentation 17 | 18 | Enjoy! 19 | -------------------------------------------------------------------------------- /infrastructure/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "mvn -e -q compile exec:java", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "target", 11 | "pom.xml", 12 | "src/test" 13 | ] 14 | }, 15 | "context": { 16 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 17 | "@aws-cdk/core:stackRelativeExports": true, 18 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 19 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 22 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 23 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 24 | "@aws-cdk/core:checkSecretUsage": true, 25 | "@aws-cdk/aws-iam:minimizePolicies": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 28 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 29 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 30 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 31 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 32 | "@aws-cdk/core:enablePartitionLiterals": true, 33 | "@aws-cdk/core:target-partitions": [ 34 | "aws", 35 | "aws-cn" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /infrastructure/cdk/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.product 7 | infrastructure 8 | 0.1 9 | 10 | 11 | UTF-8 12 | 2.50.0 13 | [10.0.0,11.0.0) 14 | 5.7.1 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 3.8.1 23 | 24 | 1.8 25 | 1.8 26 | 27 | 28 | 29 | 30 | org.codehaus.mojo 31 | exec-maven-plugin 32 | 3.0.0 33 | 34 | com.product.infrastructure.InfrastructureApp 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | software.amazon.awscdk 44 | aws-cdk-lib 45 | ${cdk.version} 46 | 47 | 48 | 49 | software.constructs 50 | constructs 51 | ${constructs.version} 52 | 53 | 54 | 55 | org.junit.jupiter 56 | junit-jupiter 57 | ${junit.version} 58 | test 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/main/java/com/product/infrastructure/DatabaseStack.java: -------------------------------------------------------------------------------- 1 | package com.product.infrastructure; 2 | 3 | import software.amazon.awscdk.Duration; 4 | import software.amazon.awscdk.services.ec2.*; 5 | import software.amazon.awscdk.services.lambda.Code; 6 | import software.amazon.awscdk.services.lambda.Function; 7 | import software.amazon.awscdk.services.lambda.Runtime; 8 | import software.amazon.awscdk.services.rds.*; 9 | import software.constructs.Construct; 10 | 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | 14 | public class DatabaseStack extends Construct{ 15 | public DatabaseStack(final Construct construct, final String id, DatabaseStackProps stackProps){ 16 | super(construct, id); 17 | 18 | DatabaseSecret dbSecret = createDatabaseSecret(); 19 | 20 | ArrayList dbSecurityGroups = new ArrayList(); 21 | dbSecurityGroups.add(createDatabaseSecurityGroup(stackProps.getVpc())); 22 | 23 | DatabaseInstance dbInstance = new DatabaseInstance(this, "product-api", DatabaseInstanceProps.builder() 24 | .vpc(stackProps.getVpc()) 25 | .databaseName("productapi") 26 | .allowMajorVersionUpgrade(false) 27 | .backupRetention(Duration.days(0)) 28 | .instanceIdentifier("productapi") 29 | .vpcSubnets(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_WITH_EGRESS).build()) 30 | .securityGroups(dbSecurityGroups) 31 | .engine(DatabaseInstanceEngine.postgres(PostgresInstanceEngineProps.builder().version(PostgresEngineVersion.VER_13_7).build())) 32 | .instanceType(InstanceType.of(InstanceClass.T4G, InstanceSize.MICRO)) 33 | .credentials(Credentials.fromSecret(dbSecret)) 34 | .build()); 35 | 36 | dbInstance.getConnections().allowFromAnyIpv4(Port.tcp(5432)); 37 | dbInstance.getConnections().allowFrom(dbInstance.getConnections().getSecurityGroups().get(0), Port.tcp(5432)); 38 | dbInstance.getConnections().allowFrom(stackProps.getApplicationSecurityGroup(), Port.tcp(5432)); 39 | 40 | createDbSetupLambdaFunction(stackProps, dbInstance, dbSecret); 41 | 42 | } 43 | 44 | private SecurityGroup createDatabaseSecurityGroup(IVpc vpc) { 45 | SecurityGroup databaseSecurityGroup = SecurityGroup.Builder.create(this, "DatabaseSG") 46 | .securityGroupName("DatabaseSG") 47 | .allowAllOutbound(false) 48 | .vpc(vpc) 49 | .build(); 50 | 51 | databaseSecurityGroup.addIngressRule( 52 | Peer.ipv4(vpc.getVpcCidrBlock()), 53 | Port.tcp(5432), 54 | "Allow Database Traffic from local network"); 55 | 56 | return databaseSecurityGroup; 57 | } 58 | 59 | private DatabaseSecret createDatabaseSecret() { 60 | return DatabaseSecret.Builder 61 | .create(this, "postgres") 62 | .secretName("product-api-db-secret") 63 | .username("postgres").build(); 64 | } 65 | 66 | private Function createDbSetupLambdaFunction(DatabaseStackProps props, DatabaseInstance database, DatabaseSecret secret) { 67 | ArrayList securityGroups = new ArrayList(); 68 | securityGroups.add(props.getApplicationSecurityGroup()); 69 | 70 | Function function = Function.Builder.create(this, "DBSetupLambdaFunction") 71 | .runtime(Runtime.JAVA_11) 72 | .memorySize(512) 73 | .timeout(Duration.seconds(29)) 74 | .code(Code.fromAsset("../db-setup/target/db-setup.jar")) 75 | .handler("com.amazon.aws.DBSetupHandler::handleRequest") 76 | .vpc(props.getVpc()) 77 | .securityGroups(securityGroups) 78 | .environment(new HashMap() {{ 79 | put("SECRET_NAME", secret.getSecretName()); 80 | put("DB_CONNECTION_URL", "jdbc:postgresql://" + database.getDbInstanceEndpointAddress() + ":5432/productapi"); 81 | put("DB_USER", "postgres"); 82 | }}) 83 | .build(); 84 | 85 | secret.grantRead(function); 86 | 87 | return function; 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/main/java/com/product/infrastructure/DatabaseStackProps.java: -------------------------------------------------------------------------------- 1 | package com.product.infrastructure; 2 | 3 | import software.amazon.awscdk.services.ec2.ISecurityGroup; 4 | import software.amazon.awscdk.services.ec2.IVpc; 5 | 6 | public class DatabaseStackProps { 7 | static DatabaseStackProps props; 8 | 9 | private IVpc vpc; 10 | private ISecurityGroup applicationSecurityGroup; 11 | 12 | public static DatabaseStackProps builder() { 13 | props = new DatabaseStackProps(); 14 | 15 | return props; 16 | } 17 | 18 | public DatabaseStackProps withVpc(IVpc vpc) { 19 | props.setVpc(vpc); 20 | 21 | return props; 22 | } 23 | 24 | public DatabaseStackProps withApplicationSecurityGroup(ISecurityGroup sg) { 25 | props.setApplicationSecurityGroup(sg); 26 | 27 | return props; 28 | } 29 | 30 | public DatabaseStackProps build() { 31 | return props; 32 | } 33 | 34 | public IVpc getVpc() { 35 | return vpc; 36 | } 37 | 38 | private void setVpc(IVpc vpc) { 39 | this.vpc = vpc; 40 | } 41 | 42 | public ISecurityGroup getApplicationSecurityGroup() 43 | { 44 | return applicationSecurityGroup; 45 | } 46 | 47 | private void setApplicationSecurityGroup(ISecurityGroup sg) { 48 | this.applicationSecurityGroup = sg; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/main/java/com/product/infrastructure/InfrastructureApp.java: -------------------------------------------------------------------------------- 1 | package com.product.infrastructure; 2 | 3 | import software.amazon.awscdk.App; 4 | import software.amazon.awscdk.StackProps; 5 | 6 | public class InfrastructureApp { 7 | public static void main(final String[] args) { 8 | App app = new App(); 9 | 10 | new InfrastructureStack(app, "InfrastructureStack", StackProps.builder() 11 | // If you don't specify 'env', this stack will be environment-agnostic. 12 | // Account/Region-dependent features and context lookups will not work, 13 | // but a single synthesized template can be deployed anywhere. 14 | 15 | // Uncomment the next block to specialize this stack for the AWS Account 16 | // and Region that are implied by the current CLI configuration. 17 | /* 18 | .env(Environment.builder() 19 | .account(System.getenv("CDK_DEFAULT_ACCOUNT")) 20 | .region(System.getenv("CDK_DEFAULT_REGION")) 21 | .build()) 22 | */ 23 | 24 | // Uncomment the next block if you know exactly what Account and Region you 25 | // want to deploy the stack to. 26 | /* 27 | .env(Environment.builder() 28 | .account("123456789012") 29 | .region("us-east-1") 30 | .build()) 31 | */ 32 | 33 | // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 34 | .build()); 35 | 36 | app.synth(); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/main/java/com/product/infrastructure/InfrastructureStack.java: -------------------------------------------------------------------------------- 1 | package com.product.infrastructure; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import software.amazon.awscdk.services.ec2.IpAddresses; 5 | import software.amazon.awscdk.services.ec2.Vpc; 6 | import software.amazon.awscdk.services.ec2.VpcAttributes; 7 | import software.amazon.awscdk.services.ec2.VpcProps; 8 | import software.constructs.Construct; 9 | import software.amazon.awscdk.Stack; 10 | import software.amazon.awscdk.StackProps; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class InfrastructureStack extends Stack { 16 | public InfrastructureStack(final Construct scope, final String id) { 17 | this(scope, id, null); 18 | } 19 | 20 | public InfrastructureStack(final Construct scope, final String id, final StackProps props) { 21 | super(scope, id, props); 22 | 23 | NetworkingStack networking = new NetworkingStack(this, "NetworkingStack"); 24 | 25 | DatabaseStack databaseStack = new DatabaseStack(this, "DatabaseStack", DatabaseStackProps.builder() 26 | .withVpc(networking.getVpc()) 27 | .withApplicationSecurityGroup(networking.getApplicationSecurityGroup()) 28 | .build()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/main/java/com/product/infrastructure/NetworkingStack.java: -------------------------------------------------------------------------------- 1 | package com.product.infrastructure; 2 | 3 | import software.amazon.awscdk.CfnOutput; 4 | import software.amazon.awscdk.CfnOutputProps; 5 | import software.amazon.awscdk.services.ec2.*; 6 | import software.constructs.Construct; 7 | 8 | public class NetworkingStack extends Construct { 9 | private final Vpc vpc; 10 | private final ISecurityGroup applicationSecurityGroup; 11 | 12 | public NetworkingStack(final Construct scope, final String id){ 13 | super(scope, id); 14 | 15 | vpc = new Vpc(this, "product-api-vpc", VpcProps.builder() 16 | .ipAddresses(IpAddresses.cidr("10.0.0.0/16")) 17 | .enableDnsHostnames(true) 18 | .enableDnsSupport(true) 19 | .maxAzs(3) 20 | .build()); 21 | 22 | applicationSecurityGroup = new SecurityGroup(this, "ApplicationSecurityGroup", 23 | SecurityGroupProps 24 | .builder() 25 | .securityGroupName("applicationSG") 26 | .vpc(vpc) 27 | .allowAllOutbound(true) 28 | .build()); 29 | 30 | CfnOutput appSgOutput = new CfnOutput(this, "app-sg-output", CfnOutputProps.builder() 31 | .exportName("ApplicationSecurityGroupId") 32 | .value(applicationSecurityGroup.getSecurityGroupId()) 33 | .build()); 34 | 35 | CfnOutput subnet1 = new CfnOutput(this, "app-subnet-1", CfnOutputProps.builder() 36 | .exportName("Subnet1Id") 37 | .value(vpc.getPrivateSubnets().get(0).getSubnetId()) 38 | .build()); 39 | 40 | CfnOutput subnet2 = new CfnOutput(this, "app-subnet-2", CfnOutputProps.builder() 41 | .exportName("Subnet2Id") 42 | .value(vpc.getPrivateSubnets().get(1).getSubnetId()) 43 | .build()); 44 | } 45 | 46 | public Vpc getVpc() { 47 | return vpc; 48 | } 49 | 50 | public ISecurityGroup getApplicationSecurityGroup() { 51 | return applicationSecurityGroup; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /infrastructure/cdk/src/test/java/com/product/infrastructure/InfrastructureTest.java: -------------------------------------------------------------------------------- 1 | // package com.myorg; 2 | 3 | // import software.amazon.awscdk.App; 4 | // import software.amazon.awscdk.assertions.Template; 5 | // import java.io.IOException; 6 | 7 | // import java.util.HashMap; 8 | 9 | // import org.junit.jupiter.api.Test; 10 | 11 | // example test. To run these tests, uncomment this file, along with the 12 | // example resource in java/src/main/java/com/myorg/InfrastructureStack.java 13 | // public class InfrastructureTest { 14 | 15 | // @Test 16 | // public void testStack() throws IOException { 17 | // App app = new App(); 18 | // InfrastructureStack stack = new InfrastructureStack(app, "test"); 19 | 20 | // Template template = Template.fromStack(stack); 21 | 22 | // template.hasResourceProperties("AWS::SQS::Queue", new HashMap() {{ 23 | // put("VisibilityTimeout", 300); 24 | // }}); 25 | // } 26 | // } 27 | -------------------------------------------------------------------------------- /infrastructure/db-setup/dependency-reduced-pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.amazon.aws.example 5 | db-setup 6 | AWS Lambda function to setup the Amazon RDS Instance 7 | 1.0.0 8 | 9 | 10 | 11 | maven-shade-plugin 12 | 3.4.1 13 | 14 | 15 | package 16 | 17 | shade 18 | 19 | 20 | 21 | 22 | db-setup 23 | 24 | 25 | 26 | maven-compiler-plugin 27 | 3.10.1 28 | 29 | 11 30 | 11 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | software.amazon.awssdk 39 | bom 40 | ${aws.java.sdk.version} 41 | pom 42 | import 43 | 44 | 45 | 46 | 47 | 2.18.1 48 | 49 | 50 | -------------------------------------------------------------------------------- /infrastructure/db-setup/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.amazon.aws.example 5 | db-setup 6 | 1.0.0 7 | jar 8 | AWS Lambda function to setup the Amazon RDS Instance 9 | 10 | 11 | 2.18.1 12 | 13 | 14 | 15 | 16 | 17 | software.amazon.awssdk 18 | bom 19 | ${aws.java.sdk.version} 20 | pom 21 | import 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.postgresql 29 | postgresql 30 | 42.5.0 31 | 32 | 33 | commons-io 34 | commons-io 35 | 2.11.0 36 | 37 | 38 | com.amazonaws 39 | aws-lambda-java-core 40 | 1.2.1 41 | 42 | 43 | com.amazonaws 44 | aws-lambda-java-events 45 | 3.11.0 46 | 47 | 48 | software.amazon.lambda 49 | powertools-parameters 50 | 1.12.3 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-shade-plugin 60 | 3.4.1 61 | 62 | db-setup 63 | 64 | 65 | 66 | package 67 | 68 | shade 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-compiler-plugin 77 | 3.10.1 78 | 79 | 11 80 | 11 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /infrastructure/db-setup/src/main/java/com/amazon/aws/AwsSecret.java: -------------------------------------------------------------------------------- 1 | package com.amazon.aws; 2 | 3 | public class AwsSecret { 4 | private String username; 5 | private String password; 6 | private String host; 7 | private String engine; 8 | private String port; 9 | private String dbInstanceIdentifier; 10 | private String dbname; 11 | 12 | public String getUsername() { 13 | return username; 14 | } 15 | 16 | public String getPassword() { 17 | return password; 18 | } 19 | 20 | public String getDbname() { 21 | return dbname; 22 | } 23 | 24 | public String getHost() { 25 | return host; 26 | } 27 | 28 | public String getEngine() { 29 | return engine; 30 | } 31 | 32 | public String getPort() { 33 | return port; 34 | } 35 | 36 | public String getDbInstanceIdentifier() { 37 | return dbInstanceIdentifier; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /infrastructure/db-setup/src/main/java/com/amazon/aws/DBSetupHandler.java: -------------------------------------------------------------------------------- 1 | package com.amazon.aws; 2 | 3 | import com.amazonaws.services.lambda.runtime.Context; 4 | import com.amazonaws.services.lambda.runtime.RequestHandler; 5 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; 6 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; 7 | import org.apache.commons.io.IOUtils; 8 | import software.amazon.lambda.powertools.parameters.ParamManager; 9 | import software.amazon.lambda.powertools.parameters.SecretsProvider; 10 | import software.amazon.lambda.powertools.parameters.transform.Transformer; 11 | 12 | import java.io.IOException; 13 | import java.sql.DriverManager; 14 | import java.sql.SQLException; 15 | 16 | public class DBSetupHandler implements RequestHandler { 17 | private static final String DB_CONNECTION = System.getenv("DB_CONNECTION_URL"); 18 | private static final String DB_USER = System.getenv("DB_USER"); 19 | private static final String SECRET_NAME = System.getenv("SECRET_NAME"); 20 | 21 | private static SecretsProvider secretsProvider; 22 | 23 | public DBSetupHandler(){ 24 | secretsProvider = ParamManager.getSecretsProvider(); 25 | } 26 | 27 | public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { 28 | AwsSecret secret = secretsProvider 29 | .withTransformation(Transformer.json) 30 | .get(SECRET_NAME, AwsSecret.class); 31 | 32 | try(var connection = DriverManager.getConnection(DB_CONNECTION, DB_USER, secret.getPassword())) { 33 | try(var statement = connection.createStatement()) { 34 | try(var sqlFile = getClass().getClassLoader().getResourceAsStream("setup.sql")) { 35 | statement.executeUpdate(IOUtils.toString(sqlFile)); 36 | return new APIGatewayProxyResponseEvent() 37 | .withStatusCode(200) 38 | .withBody("DB Setup successful"); 39 | } 40 | } 41 | } catch (SQLException | IOException sqlException) { 42 | context.getLogger().log("Error connection to the database:" + sqlException.getMessage()); 43 | return new APIGatewayProxyResponseEvent() 44 | .withStatusCode(500) 45 | .withBody("Error initializing the database"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /infrastructure/db-setup/src/main/resources/setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS products(id SERIAL PRIMARY KEY, name TEXT, price Numeric); -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.5 9 | 10 | 11 | com.product 12 | product-api 13 | 1.0.0 14 | product-api 15 | Product API 16 | 17 | 11 18 | UTF-8 19 | UTF-8 20 | 5.9.0 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-jpa 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-log4j2 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-validation 38 | 39 | 40 | javax.validation 41 | validation-api 42 | 2.0.1.Final 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-data-jpa 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-configuration-processor 51 | true 52 | 53 | 54 | org.postgresql 55 | postgresql 56 | 42.5.0 57 | 58 | 59 | software.amazon.awssdk 60 | eventbridge 61 | 2.17.282 62 | 63 | 64 | software.amazon.awssdk 65 | netty-nio-client 66 | 67 | 68 | software.amazon.awssdk 69 | apache-client 70 | 71 | 72 | 73 | 74 | software.amazon.awssdk 75 | aws-crt-client 76 | 2.17.282-PREVIEW 77 | 78 | 79 | software.amazon.awssdk 80 | secretsmanager 81 | 2.18.16 82 | 83 | 84 | software.amazon.awssdk 85 | ssm 86 | 2.18.16 87 | 88 | 89 | com.google.code.gson 90 | gson 91 | 92 | 93 | io.github.crac.com.amazonaws 94 | aws-lambda-java-runtime-interface-client 95 | 1.0.0 96 | 97 | 98 | com.amazonaws.serverless 99 | aws-serverless-java-container-springboot2 100 | 1.9 101 | 102 | 103 | software.amazon.lambda 104 | powertools-tracing 105 | 1.12.3 106 | 107 | 108 | software.amazon.lambda 109 | powertools-logging 110 | 1.12.3 111 | 112 | 113 | io.rest-assured 114 | rest-assured 115 | 5.2.0 116 | test 117 | 118 | 119 | 120 | org.apache.groovy 121 | groovy-xml 122 | 123 | 124 | 125 | 126 | 127 | io.rest-assured 128 | xml-path 129 | 5.2.0 130 | test 131 | 132 | 133 | 134 | org.mockito 135 | mockito-core 136 | 4.8.0 137 | test 138 | 139 | 140 | org.mockito 141 | mockito-junit-jupiter 142 | 4.8.0 143 | test 144 | 145 | 146 | org.mockito 147 | mockito-inline 148 | 4.8.0 149 | test 150 | 151 | 152 | 153 | org.junit.jupiter 154 | junit-jupiter-engine 155 | ${junit.jupiter.version} 156 | test 157 | 158 | 159 | org.junit.jupiter 160 | junit-jupiter-api 161 | ${junit.jupiter.version} 162 | test 163 | 164 | 165 | org.junit.jupiter 166 | junit-jupiter-params 167 | ${junit.jupiter.version} 168 | test 169 | 170 | 171 | uk.org.webcompere 172 | system-stubs-jupiter 173 | 1.1.0 174 | test 175 | 176 | 177 | software.amazon.awssdk 178 | cloudformation 179 | test 180 | 2.18.16 181 | 182 | 183 | 184 | 185 | 186 | org.apache.maven.plugins 187 | maven-shade-plugin 188 | 3.4.0 189 | 190 | false 191 | 192 | 193 | 194 | package 195 | 196 | shade 197 | 198 | 199 | 200 | 201 | org.apache.tomcat.embed:* 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | org.apache.maven.plugins 211 | maven-surefire-plugin 212 | 2.22.2 213 | 214 | 218 | 219 | **/*IT.java 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/ApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | import com.google.gson.Gson; 3 | import org.springframework.context.annotation.Configuration; 4 | import software.amazon.awssdk.services.ssm.SsmClient; 5 | import software.amazon.awssdk.services.ssm.model.GetParameterRequest; 6 | import software.amazon.awssdk.services.ssm.model.GetParameterResponse; 7 | 8 | @Configuration 9 | public class ApplicationConfiguration { 10 | private final Gson gson = new Gson(); 11 | private ApplicationProperties properties; 12 | 13 | public ApplicationProperties getApplicationProperties() 14 | { 15 | if (properties != null) 16 | { 17 | return properties; 18 | } 19 | 20 | this.properties = getProps(); 21 | 22 | return properties; 23 | } 24 | 25 | private ApplicationProperties getProps() { 26 | var parameterName = System.getenv("CONFIG_PARAMETER_NAME"); 27 | if (parameterName == null || parameterName == "") { 28 | return new ApplicationProperties(); 29 | } 30 | 31 | var ssmClient = SsmClient.create(); 32 | 33 | String parameter; 34 | 35 | var getParameterRequest = GetParameterRequest.builder() 36 | .name(parameterName) 37 | .build(); 38 | 39 | GetParameterResponse result = null; 40 | 41 | try { 42 | result = ssmClient.getParameter(getParameterRequest); 43 | } 44 | catch (Exception e) { 45 | throw e; 46 | } 47 | if (result.parameter().value() != null) { 48 | parameter = result.parameter().value(); 49 | return gson.fromJson(parameter, ApplicationProperties.class); 50 | } 51 | 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | public class ApplicationProperties { 4 | private String databaseEndpoint; 5 | 6 | public String getDatabaseEndpoint() 7 | { 8 | return databaseEndpoint; 9 | } 10 | 11 | public void setMyApplicationProperty(String propertyValue) 12 | { 13 | this.databaseEndpoint = propertyValue; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/AwsSecret.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | public class AwsSecret { 4 | private String username; 5 | private String password; 6 | private String host; 7 | private String engine; 8 | private String port; 9 | private String dbInstanceIdentifier; 10 | 11 | public String getUsername() { 12 | return username; 13 | } 14 | 15 | public String getPassword() { 16 | return password; 17 | } 18 | 19 | public String getHost() { 20 | return host; 21 | } 22 | 23 | public String getEngine() { 24 | return engine; 25 | } 26 | 27 | public String getPort() { 28 | return port; 29 | } 30 | 31 | public String getDbInstanceIdentifier() { 32 | return dbInstanceIdentifier; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/JpaConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | import com.google.gson.Gson; 3 | import org.crac.Core; 4 | import org.springframework.boot.jdbc.DataSourceBuilder; 5 | 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.jdbc.datasource.SimpleDriverDataSource; 9 | import org.springframework.orm.jpa.JpaTransactionManager; 10 | import org.springframework.orm.jpa.JpaVendorAdapter; 11 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 12 | import org.springframework.orm.jpa.vendor.Database; 13 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 14 | import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; 15 | import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; 16 | import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; 17 | 18 | import javax.persistence.EntityManagerFactory; 19 | import javax.sql.DataSource; 20 | 21 | @Configuration 22 | public class JpaConfiguration { 23 | private final Gson gson = new Gson(); 24 | public JpaConfiguration() 25 | { 26 | } 27 | 28 | public DataSource dataSource() { 29 | final AwsSecret dbCredentials = getSecret(); 30 | 31 | var dataSource = new SimpleDriverDataSource(); 32 | dataSource.setDriverClass(org.postgresql.Driver.class); 33 | dataSource.setUrl("jdbc:postgresql://" + System.getenv("DATABASE_ENDPOINT") + ":" + dbCredentials.getPort() + "/productapi"); 34 | dataSource.setUsername(dbCredentials.getUsername()); 35 | dataSource.setPassword(dbCredentials.getPassword()); 36 | 37 | return dataSource; 38 | } 39 | 40 | private AwsSecret getSecret() { 41 | var secretsManagerClient = SecretsManagerClient.create(); 42 | 43 | String secret; 44 | 45 | var getSecretValueRequest = GetSecretValueRequest.builder() 46 | .secretId(System.getenv("SECRET_NAME")) 47 | .build(); 48 | 49 | GetSecretValueResponse result = null; 50 | 51 | try { 52 | result = secretsManagerClient.getSecretValue(getSecretValueRequest); 53 | } 54 | catch (Exception e) { 55 | throw e; 56 | } 57 | if (result.secretString() != null) { 58 | secret = result.secretString(); 59 | return gson.fromJson(secret, AwsSecret.class); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | @Bean 66 | public JpaTransactionManager transactionManager(EntityManagerFactory emf) { 67 | return new JpaTransactionManager(emf); 68 | } 69 | 70 | @Bean 71 | public JpaVendorAdapter jpaVendorAdapter() { 72 | HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); 73 | jpaVendorAdapter.setDatabase(Database.POSTGRESQL); 74 | jpaVendorAdapter.setGenerateDdl(true); 75 | 76 | return jpaVendorAdapter; 77 | } 78 | 79 | @Bean 80 | public LocalContainerEntityManagerFactoryBean entityManagerFactory() { 81 | LocalContainerEntityManagerFactoryBean lemfb = new LocalContainerEntityManagerFactoryBean(); 82 | lemfb.setDataSource(dataSource()); 83 | lemfb.setJpaVendorAdapter(jpaVendorAdapter()); 84 | lemfb.setPackagesToScan("com.product.api"); 85 | return lemfb; 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/com/product/api/ProductApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.springframework.boot.SpringApplication; 6 | import com.fasterxml.jackson.databind.DeserializationFeature; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.context.annotation.Bean; 10 | 11 | @SpringBootApplication() 12 | public class ProductApiApplication { 13 | private static final Logger LOG = LogManager.getLogger(); 14 | 15 | public static void main(String[] args) { 16 | LOG.info("Application startup"); 17 | 18 | SpringApplication.run(ProductApiApplication.class, args); 19 | } 20 | 21 | @Bean 22 | public ObjectMapper getObjectMapper() { 23 | ObjectMapper objectMapper = new ObjectMapper(); 24 | objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 25 | return objectMapper; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | import com.product.api.core.ProductDTO; 4 | import com.product.api.core.ProductService; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | 12 | @RestController 13 | public class ProductController { 14 | private final ProductService productService; 15 | private final ApplicationProperties applicationProperties; 16 | 17 | private static final Logger LOG = LogManager.getLogger(); 18 | 19 | public ProductController(ProductService productService, ApplicationConfiguration configuration) { 20 | this.productService = productService; 21 | this.applicationProperties = configuration.getApplicationProperties(); 22 | } 23 | 24 | @PostMapping("/product") 25 | ResponseEntity addProduct(@Valid @RequestBody ProductDTO product) { 26 | var createdProduct = productService.CreateProduct(product); 27 | 28 | return ResponseEntity.ok(createdProduct); 29 | } 30 | 31 | @GetMapping("/product") 32 | ResponseEntity> listProducts() { 33 | LOG.info("Received request to retrieve products"); 34 | 35 | var productList = productService.ListProducts(); 36 | 37 | LOG.info("Product listing successful"); 38 | 39 | return ResponseEntity.ok(productList); 40 | } 41 | 42 | @GetMapping("/product/{id}") 43 | ResponseEntity getProduct(@PathVariable long id) { 44 | var product = productService.GetProduct(id); 45 | 46 | return ResponseEntity.ok(product); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/SnapshotConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.crac.Context; 6 | import org.crac.Core; 7 | import org.crac.Resource; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.sql.DataSource; 15 | import java.sql.Connection; 16 | import java.sql.DriverManager; 17 | 18 | @Configuration 19 | public class SnapshotConfiguration implements Resource { 20 | private static final Logger LOG = LogManager.getLogger(); 21 | LocalContainerEntityManagerFactoryBean _dataSourceBean; 22 | 23 | public SnapshotConfiguration(LocalContainerEntityManagerFactoryBean dataSourceBean) 24 | { 25 | LOG.info("Snapshot config constructor"); 26 | 27 | Core.getGlobalContext().register(SnapshotConfiguration.this); 28 | 29 | _dataSourceBean = dataSourceBean; 30 | } 31 | 32 | @Override 33 | public void beforeCheckpoint(Context context) throws Exception { 34 | LOG.info("Before checkpoint"); 35 | DataSource dataSource = _dataSourceBean.getDataSource(); 36 | Connection databaseConnection = dataSource.getConnection(); 37 | 38 | if (!databaseConnection.isClosed()) 39 | { 40 | LOG.info("Closing connection"); 41 | databaseConnection.close(); 42 | } 43 | } 44 | 45 | @Override 46 | public void afterRestore(Context context) throws Exception { 47 | LOG.info("Restoring"); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/product/api/StreamLambdaHandler.java: -------------------------------------------------------------------------------- 1 | package com.product.api; 2 | 3 | import com.amazonaws.serverless.exceptions.ContainerInitializationException; 4 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest; 5 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse; 6 | import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; 7 | import com.amazonaws.services.lambda.runtime.Context; 8 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | 14 | public class StreamLambdaHandler implements RequestStreamHandler { 15 | private static SpringBootLambdaContainerHandler handler; 16 | static { 17 | try { 18 | handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(ProductApiApplication.class); 19 | // If you are using HTTP APIs with the version 2.0 of the proxy model, use the getHttpApiV2ProxyHandler 20 | // method: handler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(Application.class); 21 | } catch (ContainerInitializationException e) { 22 | // if we fail here. We re-throw the exception to force another cold start 23 | e.printStackTrace(); 24 | throw new RuntimeException("Could not initialize Spring Boot application", e); 25 | } 26 | } 27 | 28 | @Override 29 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) 30 | throws IOException { 31 | handler.proxyStream(inputStream, outputStream, context); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/product/api/core/IProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.product.api.core; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface IProductRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/core/Product.java: -------------------------------------------------------------------------------- 1 | package com.product.api.core; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "products") 7 | public class Product { 8 | public Product() 9 | { 10 | } 11 | 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private long id; 15 | private String name; 16 | private Double price; 17 | 18 | public long getId() { 19 | return id; 20 | } 21 | 22 | public void setId(long id) { 23 | this.id = id; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public void setName(String name) { 31 | this.name = name; 32 | } 33 | 34 | public Double getPrice() { 35 | return price; 36 | } 37 | 38 | public void setPrice(Double price) { 39 | this.price = price; 40 | } 41 | 42 | public ProductDTO asDto() 43 | { 44 | ProductDTO dto = new ProductDTO(); 45 | dto.setId(this.id); 46 | dto.setName(this.name); 47 | dto.setPrice(this.price); 48 | 49 | return dto; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/core/ProductDTO.java: -------------------------------------------------------------------------------- 1 | package com.product.api.core; 2 | 3 | import javax.validation.constraints.Min; 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.NotNull; 6 | 7 | public class ProductDTO { 8 | private long id; 9 | 10 | @NotBlank(message="Name cannot be blank") 11 | private String name; 12 | 13 | @Min(value = 0, message = "Price must be greater than 0") 14 | @NotNull 15 | private Double price; 16 | 17 | public long getId() { 18 | return id; 19 | } 20 | 21 | public void setId(long id) { 22 | this.id = id; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public void setName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public Double getPrice() { 34 | return price; 35 | } 36 | 37 | public void setPrice(Double price) { 38 | this.price = price; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/product/api/core/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.product.api.core; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.ArrayList; 6 | 7 | @Service 8 | public class ProductService { 9 | private final IProductRepository productRepository; 10 | 11 | public ProductService(IProductRepository productRepository){ 12 | this.productRepository = productRepository; 13 | } 14 | 15 | public ProductDTO CreateProduct(ProductDTO productDTO) { 16 | var product = new Product(); 17 | product.setName(productDTO.getName()); 18 | product.setPrice(productDTO.getPrice()); 19 | 20 | var savedProduct = this.productRepository.save(product); 21 | 22 | return savedProduct.asDto(); 23 | } 24 | 25 | public ProductDTO GetProduct(long productId) { 26 | var retrievedProduct = this.productRepository.findById(productId); 27 | 28 | return retrievedProduct.map(Product::asDto).orElse(null); 29 | 30 | } 31 | 32 | public Iterable ListProducts() { 33 | var products = this.productRepository.findAll(); 34 | 35 | var productDtoList = new ArrayList(); 36 | 37 | var productIterator = products.iterator(); 38 | 39 | while (productIterator.hasNext()) 40 | { 41 | productDtoList.add(productIterator.next().asDto()); 42 | } 43 | 44 | return productDtoList; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | 3 | spring.jpa.hibernate.ddl-auto=none 4 | 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/products 6 | spring.datasource.username=postgres 7 | spring.datasource.password=mysecretpassword 8 | 9 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/schema-postgres.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS products; 2 | CREATE TABLE products(id serial PRIMARY KEY, name VARCHAR(255), price decimal); -------------------------------------------------------------------------------- /src/test/java/ProductApiIT.java: -------------------------------------------------------------------------------- 1 | import io.restassured.RestAssured; 2 | import io.restassured.response.Response; 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class ProductApiIT { 7 | @Test 8 | public void testGetProducts_ShouldReturnProductsArray() 9 | { 10 | Response response = RestAssured.given() 11 | .header("Content-Type", "application/json") 12 | .header("Accept", "application/json") 13 | .when() 14 | .get("https://icmayost93.execute-api.us-east-1.amazonaws.com/Prod/product"); 15 | 16 | Assertions.assertEquals(response.statusCode(), 200); 17 | String responseBody = response.asString(); 18 | Assertions.assertNotNull(responseBody); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Globals: 5 | Function: 6 | Timeout: 30 7 | Environment: 8 | Variables: 9 | POWERTOOLS_SERVICE_NAME: ProductApi 10 | SECRET_NAME: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${DatabaseSecretName} 11 | DATABASE_ENDPOINT: !Ref DatabaseEndpoint 12 | 13 | Parameters: 14 | SecurityGroupId: 15 | Description: Security Group for the application 16 | Type: String 17 | Subnet1Id: 18 | Description: Subnet Id for the first subnet 19 | Type: String 20 | Subnet2Id: 21 | Description: Subnet Id for the second subnet 22 | Type: String 23 | DatabaseSecretName: 24 | Description: The name of the secret holding database credentials 25 | Type: String 26 | DatabaseEndpoint: 27 | Description: The endpoint of the database 28 | Type: String 29 | 30 | Resources: 31 | ProductApiFunction: 32 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 33 | Properties: 34 | CodeUri: . 35 | Handler: com.product.api.StreamLambdaHandler::handleRequest 36 | Runtime: java11 37 | AutoPublishAlias: production 38 | SnapStart: 39 | ApplyOn: PublishedVersions 40 | VpcConfig: 41 | SecurityGroupIds: 42 | - !Ref SecurityGroupId 43 | SubnetIds: 44 | - !Ref Subnet1Id 45 | - !Ref Subnet2Id 46 | Architectures: 47 | - x86_64 48 | MemorySize: 2048 49 | Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object 50 | Variables: 51 | JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 # More info about tiered compilation https://aws.amazon.com/blogs/compute/optimizing-aws-lambda-function-performance-for-java/ 52 | Policies: 53 | - AWSSecretsManagerGetSecretValuePolicy: 54 | SecretArn: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${DatabaseSecretName} 55 | Events: 56 | HelloWorld: 57 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 58 | Properties: 59 | Path: /{proxy+} 60 | Method: ANY 61 | --------------------------------------------------------------------------------