Even or Odd – Lambda Edition

Very recently, I showed you one of the probably easiest ways, to host your very own Java-based web-service. Remember, we added providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1' to the build.gradle file, implemented a WebServlet, and finally dropped a war file into Tomcat’s webapps directory.

I know, you’ll have a hard time finding paying customers for the simple even-or-odd service, but that wasn’t the point. On the other hand, if you really wanted to deploy this service, making it available to the world, there would be a couple considerations:

  • Who is the best hosting provider (reach/cost/repsonse-time/..) to host your service?
  • What operating system should you choose?
  • What Java JRE would be the best and from which provider?
  • What Tomcat version (or alternative container) should you use?
  • How often would you need to deploy updates to your stack (OS/Java/Tomcat/App) to not run into security, privacy, etc. issues?
  • There are probably more question sand concerns …

Now let’s take a look at a very different approach. We are still using Java, in fact we will be reusing most of the code, but this time, we are going to deploy directly into the cloud. Welcome to the AWS Lambda, which lets you run code without provisioning or managing servers. Moreover, you pay only for the compute time you consume. But before looking at Java code and Gradle build script, let’s get some administrative stuff out of the way 1st … (This will take a while, take your time.)

1 Creating an AWS Account

Create an AWS Account (aka Root User Account) here:
https://portal.aws.amazon.com/gp/aws/developer/registration/index.html
A credit card number and working phone is needed during the setup process. Select “Basic” for the support plan.

2 Creating an IAM Account

Create an IAM Account (aka AWS Identity and Access Management (IAM) user) here:
https://console.aws.amazon.com/iam

2.1 Group

Select ‘Group’ on the left side and then click the “Create new Group” button.
Enter a name (e.g. MyEduGroup) and in the next step add these two policies. As I found out just recently, policies for a specific Lambda function can be referenced in its template.yml file, which I will tell you about at the end of this post. It’s still good to know right now that policies can be attached to a function instead of attaching it to a group or a user.

  • IAMFullAccess
  • PowerUserAccess

Review and click the “Create Group” button.

2.2 User

Select ‘User’ on the left side and then click the “Add user” button, to create a new IAM user.
Create a user name (e.g. MyEduUser) and select “Programmatic access” only.
Next, add the user to the group, mentioned above, (e.g. MyEduGroup)


Review and click the “Create user” button and don’t forget to download the csv file, containing the credentials. Add those credentials into a (hidden) file: ~/.aws/credentials file, which should then look like so:

[edu]
aws_access_key_id = AKI...
aws_secret_access_key = A+zgQz...

Btw, the string inside the square brackets is called the profile name.

2.3 Role

Select ‘Roles’ on the left side and then click the “Create new role” button, to create a new role.
Select AWS Lambda
Select the AWSLambdaExecute
Assign a name to the new role (e.g. MyEduLambdaExecRole) and click the “Create role” button.

2.4 Bucket

While in the AWS web interface, click in Services and navigate/find S3. There you will have the chance to create a bucket. Do that. I used ‘edu-bucket’ and clicked through, going with all the defaults.

3. Let’s fire up the IDE

Once again, we are starting with creating a new project in IntelliJ Idea, this time we choose just Java11, name our project EvenOrOddLambda and also create a class in this project, for instance edu.gcccd.csis.LambdaFunc.

3.1 build.gradle

Before moving on, lets add some dependencies to the build.gradle file.
As I’m writing this, these are the current versions of the libraries we need. However, you may want to check at https://mvnrepository.com/ for more current releases.

Make sure you use the profile-name and region, you used in your ~/.aws/.. files and also the name for your S3 bucket.

plugins {
    id 'java'
    id 'com.github.kaklakariada.aws-sam-deploy' version '0.6.1'
}

def stage = 'prod'
def profile = 'edu'         // as defined in ~/.aws/config
def region = 'us-west-2'    // as defined in ~/.aws/config
def bucket = 'edu-bucket'   // the S3 bucket you created earlier.

group 'edu.gcccd.csis'
version '1.0'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    // AWS Lambda
    compile group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.0'
    compile group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '2.2.7'
    // (de-)serialization
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
    // logging
    compile group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.1.0'
    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2'
    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.8.2'
    // Testing
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

// deploying serverless Java apps using AWS Serverless Application Models (SAM) via CloudFormation templates.
serverless {
    activeStage = stage
    defaultAwsProfile = profile
    defaultAwsRegion = region
    defaultDeployBucket = bucket
    stages {
        test {
            // use default values
        }
        prod {
            awsRegion = region
            awsProfile = profile
            deployBucket = bucket
        }
    }
    api {
        stackName = "${project.name}-${stage}"
        samTemplate = file('template.yml')
    }
}

As you see, at the very end the build.gradle file revers to a template.yml file. Let’s add this right now.

3.2 template.yml

Before we finally get to write some Java code, we still need to create the template.yml file mentioned above.
As a sibling to the build.gradle file, this you be in the root of your project directory.
The template.yml file should look something like this. Most importantly however, the classname of the here mentioned Handler needs to match the class name we create above.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Mumbler Serverless App
Resources:
  PostFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ${CodeUri}
      FunctionName: f1
      Handler: edu.gcccd.csis.LambdaFunc
      Runtime: java8
      MemorySize: 1024
      Timeout: 60
      Events:
        PostEvent:
          Type: Api
          Properties:
            Path: /
            Method: post

3.3 Java Code

Now we can finally get to solve the problem: is a given number even or odd?

Let’s make the LambdaFunc class implement RequestHandler, which means it needs a handleRequest method. Long story short the class suddenly looks like this:

package edu.gcccd.csis;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

public class LambdaFunc implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    @Override
    public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, Context context) {
        final APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();        
        responseEvent.setStatusCode(500);
        return responseEvent;
    }
}

It’s still not doing much, but this code would actually compile. It’s worth noting that this particular interface provides and requires JSON encoded information. While the AWS libraries work with the Jackson JSON/XML libs, I much rather use Gson. Therefore, we quickly implement a class that represents the Request that we are expecting and a Response that we want to return.

package edu.gcccd.csis;

public class Request {
    private int input;

    public int getInput() {
        return input;
    }
}
package edu.gcccd.csis;

public class Response {
    final String answer;

    public Response(final Request request) {
        this.answer = String.format("%d %s", request.getInput(), request.getInput() % 2 == 0 ? " is even" : " is odd");
    }
}

With the Request and Response defined, writing the handler function is now pretty straight forward. We also want to make sure we log some information, tho.

    public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
        final APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();
        try {
            log.error("input: " + input.getBody());
            final Gson gson = new Gson();
            final Request request = gson.fromJson(input.getBody(), Request.class);
            final Response response = new Response(request);
            final String body = gson.toJson(response);
            responseEvent.setBody(body);
            responseEvent.setStatusCode(200);
            log.error("handleRequest ok " + body);
        } catch (Exception ex) {
            responseEvent.setBody(ex.toString());
            responseEvent.setStatusCode(500);
            log.error(ex.toString());
        }
        return responseEvent;
    }

Time to deploy ..

Use the Gradle GUI inside of IntelliJ or type ‘gradle deploy’ in to the cli ..

~/CSIS293/EvenOrOddLambda                                                                                                                                                                                                                            
▶ gradle deploy
Starting a Gradle Daemon (subsequent builds will be faster)
JAXB is unavailable. Will fallback to SDK implementation which may be less performant

> Task :uploadZip
Your profile name includes a 'profile ' prefix. This is considered part of the profile name in the Java SDK, so you will need to include this prefix in your profile name when you reference this profile from your Java code.
Uploading 4 MB from file /Users/wolf/CSIS293/EvenOrOddLambda/build/distributions/EvenOrOddLambda-1.0.zip to s3://edu-bucket/EvenOrOddLambda/1.0/8f09e...db6/EvenOrOddLambda-1.0.zip...
Uploaded /Users/wolf/CSIS293/EvenOrOddLambda/build/distributions/EvenOrOddLambda-1.0.zip to s3://edu-bucket/EvenOrOddLambda/1.0/8f09e....db6/EvenOrOddLambda-1.0.zip in PT9.721845S

> Task :deploy
Got status CREATE_PENDING after PT0.000003S
Got status CREATE_IN_PROGRESS after PT2.114126S
Got status CREATE_IN_PROGRESS after PT4.211322S
Got status CREATE_COMPLETE after PT6.316032S
Executing change set arn:aws:cloudformation:us-west-2:178522735890:changeSet/EvenOrOddLambda-prod-15....11
Got status UPDATE_IN_PROGRESS after PT0.000002S
Got status UPDATE_IN_PROGRESS after PT2.11455S
Got status UPDATE_IN_PROGRESS after PT4.239261S
Got status UPDATE_COMPLETE_CLEANUP_IN_PROGRESS after PT6.353389S
Got status UPDATE_COMPLETE after PT8.453475S

BUILD SUCCESSFUL in 43s
5 actionable tasks: 5 executed

4 Back to the AWS Web UI

Let’s see what we did. Log in at https://console.aws.amazon.com/lambda, which for me now looks something like this:

Click on the function name, e.g. ‘f1’

Now click on API Gateway, which will finally expose the URL to the API Endpoint.

For me, this would currently look like this: https://263c832r74.execute-api.us-west-2.amazonaws.com/Prod/

5 Let’s see it in action

We know that we need encode the payload in JSON and also the the name of the input parameter needs to be input. Look at the Request class above, if that isn’t clear to you. I.e. here are two examples how to call the web service:

5.1 curl

curl -d '{"input":17}' -H "Content-Type: application/json" -X POST https://263c832r74.execute-api.us-west-2.amazonaws.com/Prod/

5.2 HTTPie

http POST https://263c832r74.execute-api.us-west-2.amazonaws.com/Prod/ input:=17
Returns:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 23
Content-Type: application/json
Date: Thu, 05 Dec 2019 05:28:04 GMT
Via: 1.1 0931eacdfabebfd9816e3573b4bf15b5.cloudfront.net (CloudFront)
X-Amz-Cf-Id: yPbstCSMSLB1POpw9cyy5z0ViPV7gh-b_biQ7DwIzQmT4UXHg0IR8w==
X-Amz-Cf-Pop: LAX50-C1
X-Amzn-Trace-Id: Root=1-5de89564-46976c63224319a769af93c2;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: ENxHyEatPHcFtIg=
x-amzn-RequestId: 59bc1dff-1c5a-464a-950a-849a8a2061ff
{
"answer": "17 is odd"
}

6 Cloudwatch

Of course it will not always goes as smoothly and eventually the log files will be looked at to figure out what went wrong:

Log in at https://console.aws.amazon.com/cloudwatch/ and click on Logs and Logs/Groups to get to the most recently logged events:

 

Find the code for this kiss (keep it simple stupid) project here: https://github.com/GCCCD-CSIS/evenorodd_lambda

 

Leave a Reply