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