What is AWS Lambda?

AWS Lambda is an Amazon’s service for hosting code without the need to maintain a server to host it on. You simply write your function, upload it to Amazon and use it – let them worry about finding actual resources to run it on.

How does it work?

The way it works is that whenever a request arrives to use your function, Amazon will use one of its own machines to host your piece of code and handle the request. The resources required are assigned dynamically as the request arrives. Once those resources have been allocated to your function, they stay up for some time to handle any other requests. If, for some reason, your code suddenly booms in popularity, Amazon will spin up more contexts and allocate more resources to handle the sudden peak. Once it levels down, the unused contexts and resources will disappear. That way, you have exactly as many resources as is required to handle the current demand for your code.

What are the costs?

So how do you pay for all of that? With Amazon Web Services, you pay only for what you use. In case of Lambda, you are billed for two things: total number of requests and memory used. Luckily, there is a high free tier threshold which renews each month – which means that until you reach that threshold, you won’t be billed a single cent. Exact pricing details are available on Amazon’s official page.

This tutorial will entirely fit in the boundaries of free tier – you won’t have to pay anything. In case you want to be sure Amazon won’t start billing you without your knowledge, you can set up an alarm for whenever your costs exceed $0.01. You can do that by following the official Amazon’s guide.

Let’s get to work!

Before we start, you will need two things: an AWS account and an OpenWeatherMap account (we will use OWM’s API to get data on current weather in various cities – it’s free).

You can setup the first one by following this guide.

Note – even if we only use the free tier, Amazon still requires providing a valid credit card before being able to use any of the services. With the billing alarm set up you are bound to be informed whenever you start being charged, but it’s understandable that you might be reluctant to bind your credit card in the first place. Unfortunately, in such a case you cannot follow this tutorial.

To get an OpenWeatherMap account simply go to their site and sign up – make note of the API key. Here’s a link to their site.

Time to code

We will create a simple function that accepts two parameters – a city name and country code – and outputs current weather data for that city. Both the input and the output will be in JSON format and the function code will be in Java – but you can also write it in Node.js or Python, if you so fancy.

The code for this project will be hosted on my github account so if you get lost at any point, you can look there for reference. Here’s a link to the repository.

First things first – a Maven project. Initiate one using either your favourite IDE or the console tool. Then add the following to the pom.xml:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <dependencies > <dependency > <groupId > com.amazonaws </groupId > <artifactId > aws-lambda-java-core </artifactId > <version > 1.1.0 </version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins </groupId > <artifactId > maven-shade-plugin </artifactId > <version > 2.3 </version > <configuration > <createDependencyReducedPom > false </createDependencyReducedPom > </configuration > <executions > <execution > <phase > package </phase > <goals > <goal > shade </goal > </goals > </execution > </executions > </plugin > </plugins > </build > <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.1.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

We will require the aws-lambda-java-core dependency to make our code work with AWS and the maven-shade-plugin to build a single jar.

Alright. Now let’s create three Java classes:

GetWeatherDataFunction

Request

Response

Leave Request and Response empty for now. The GetWeatherDataFunction should look like this (minus the package and imports):

1 2 3 4 5 6 public class GetWeatherDataFunction implements RequestHandler < Request , Response > { public Response handleRequest ( Request request, Context context ) { return new Response ( ) ; } } public class GetWeatherDataFunction implements RequestHandler<Request, Response> { public Response handleRequest(Request request, Context context) { return new Response(); } }

That’s it. We basically have a working lambda function now – it accepts an empty Request and produces and empty Response. It’s not very thrilling though. Let’s make it do something more. We want to retrieve current weather data from an external API that’s delivered in JSON format, so we need two things: a HTTP library and a JSON parser. Let’s add those to our pom.xml:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > org.javalite </groupId > <artifactId > javalite-common </artifactId > <version > 1.4.11 </version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core </groupId > <artifactId > jackson-core </artifactId > <version > 2.7.5 </version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core </groupId > <artifactId > jackson-databind </artifactId > <version > 2.7.5 </version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core </groupId > <artifactId > jackson-annotations </artifactId > <version > 2.7.5 </version > </dependency > <dependency> <groupId>org.javalite</groupId> <artifactId>javalite-common</artifactId> <version>1.4.11</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.7.5</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.5</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.7.5</version> </dependency>

We will use Jackson, the standard for JSON parsing, and Javalite’s Http library – small and simple, perfect for our use case.

Once we got these, let’s prepare our code to call the appropriate API endpoint. Let’s add the following to our code (the handleRequest function remains unchanged):

1 2 3 4 5 6 private static final String API_KEY = "081cfe3b3ff156db70e355a1ab2abb17" ; private static final String ENDPOINT = "http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s" ; private String buildEndpoint ( String city, String countryCode ) { return String . format ( ENDPOINT, ( countryCode != null ? city + "," + countryCode : city ) , API_KEY ) ; } private static final String API_KEY = "081cfe3b3ff156db70e355a1ab2abb17"; private static final String ENDPOINT = "http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s"; private String buildEndpoint(String city, String countryCode) { return String.format(ENDPOINT, (countryCode != null ? city+","+countryCode : city), API_KEY); }

Of course you have to substitute you api key from OpenWeatherMap here. The one provided here is invalid – I made it up.

OpenWeatherMap’s documentation states that we have to provide a city name, but country code is optional – our buildEndpoint is prepared for that. Of course, we should check whether city is not null and countryCode isn’t a blank string, but let’s skip that for the sake of simplicity.

Now let’s add some actual functionality to our code. Modify handleRequest to do the following:

1 2 3 4 5 6 7 8 9 10 11 12 13 public Response handleRequest ( Request request, Context context ) { Get weatherResponse = Http. get ( buildEndpoint ( "Rzeszow" , "pl" ) ) ; ObjectMapper objectMapper = new ObjectMapper ( ) ; Response response = null ; try { response = objectMapper. readValue ( weatherResponse. text ( ) , Response. class ) ; } catch ( IOException e ) { e. printStackTrace ( ) ; } return response ; } public Response handleRequest(Request request, Context context) { Get weatherResponse = Http.get(buildEndpoint("Rzeszow", "pl")); ObjectMapper objectMapper = new ObjectMapper(); Response response = null; try { response = objectMapper.readValue(weatherResponse.text(), Response.class); } catch (IOException e) { e.printStackTrace(); } return response; }

For now, we have hardcoded the city and country code, but that will change soon. We are almost at the stage where we can test our code locally. We just need to add one more thing – instruct Jackson to ignore unknown fields so it doesn’t throw an error trying to pare the JSON response to an empty Response class (it will simply ignore everything since nothing is defined inside Response). Add this simple annotation on top of Response and a simple field to confirm whether we get any data at all:

1 2 3 4 5 6 7 8 9 10 11 12 13 @JsonIgnoreProperties ( ignoreUnknown = true ) public class Response { private String base ; public String getBase ( ) { return base ; } public void setBase ( String base ) { this . base = base ; } } @JsonIgnoreProperties(ignoreUnknown=true) public class Response { private String base; public String getBase() { return base; } public void setBase(String base) { this.base = base; } }

To test our function locally, we can simply add a main function and run it:

1 2 3 public static void main ( String [ ] args ) { new GetWeatherDataFunction ( ) . handleRequest ( null , null ) ; } public static void main(String[] args) { new GetWeatherDataFunction().handleRequest(null, null); }

You can attach a debugger or add some printing statements to check whether any actual weather data is returned from OpenWeatherMap. If not, perhaps your api key is invalid? Only move further if you confirmed that you get actual data at this point. If everything is okay, the final Response object should have the base field filled with a meaningful string (“cms stations” in my case).

Alright, now let’s model our Request class and get rid of the hardcoded city and country code.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Request { private String city ; private String countryCode ; public String getCity ( ) { return city ; } public void setCity ( String city ) { this . city = city ; } public String getCountryCode ( ) { return countryCode ; } public void setCountryCode ( String countryCode ) { this . countryCode = countryCode ; } } public class Request { private String city; private String countryCode; public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCountryCode() { return countryCode; } public void setCountryCode(String countryCode) { this.countryCode = countryCode; } }

1 Get weatherResponse = Http. get ( buildEndpoint ( request. getCity ( ) , request. getCountryCode ( ) ) ) ; Get weatherResponse = Http.get(buildEndpoint(request.getCity(), request.getCountryCode()));

Let’s also modify our main function to accomodate to the changes. When we’re at it, let’s see if skipping country code won’t break the code:

1 2 3 4 5 public static void main ( String [ ] args ) { Request request = new Request ( ) ; request. setCity ( "Rzeszow" ) ; new GetWeatherDataFunction ( ) . handleRequest ( request, null ) ; } public static void main(String[] args) { Request request = new Request(); request.setCity("Rzeszow"); new GetWeatherDataFunction().handleRequest(request, null); }

Now let’s actually get some meaningful response from our function. Let’s add a new class, Weather, that looks like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @JsonIgnoreProperties ( ignoreUnknown = true ) public class Weather { private String main ; private String description ; public String getMain ( ) { return main ; } public void setMain ( String main ) { this . main = main ; } public String getDescription ( ) { return description ; } public void setDescription ( String description ) { this . description = description ; } } @JsonIgnoreProperties(ignoreUnknown=true) public class Weather { private String main; private String description; public String getMain() { return main; } public void setMain(String main) { this.main = main; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }

Let’s also modify our Response to make use of Weather:

1 2 3 4 5 6 7 8 9 10 11 12 13 @JsonIgnoreProperties ( ignoreUnknown = true ) public class Response { private List < Weather > weather ; public List < Weather > getWeather ( ) { return weather ; } public void setWeather ( List < Weather > weather ) { this . weather = weather ; } } @JsonIgnoreProperties(ignoreUnknown=true) public class Response { private List<Weather> weather; public List<Weather> getWeather() { return weather; } public void setWeather(List<Weather> weather) { this.weather = weather; } }

Now we will get some basic information about the weather. There is much more data coming from OpenWeatherMap but for the sake of simplicity I will not map it in this tutorial. That’s up to you!

Putting it all together

We have a working lambda function but we have only tested it locally. It’s time to push it out to AWS and make it an actual serverless lambda function.

First, let’s get rid of the main function – it’s not needed outside of our local environment. For reference, this is how our final GetWeatherDataFunction should look like – except for the API key, which should be substituted for your own. Request, Response and Weather classes are simple POJOs so I’ll refrain from posting their final looks – you can check them out on the github repo, if you want to make sure everything’s fine.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class GetWeatherDataFunction implements RequestHandler < Request , Response > { private static final String API_KEY = "081cfe3b3ff076ad71e365a1ab2acb37" ; private static final String ENDPOINT = "http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s" ; private String buildEndpoint ( String city, String countryCode ) { return String . format ( ENDPOINT, ( countryCode != null ? city + "," + countryCode : city ) , API_KEY ) ; } public Response handleRequest ( Request request, Context context ) { Get weatherResponse = Http. get ( buildEndpoint ( request. getCity ( ) , request. getCountryCode ( ) ) ) ; ObjectMapper objectMapper = new ObjectMapper ( ) ; Response response = null ; try { response = objectMapper. readValue ( weatherResponse. text ( ) , Response. class ) ; } catch ( IOException e ) { e. printStackTrace ( ) ; } return response ; } } public class GetWeatherDataFunction implements RequestHandler<Request, Response> { private static final String API_KEY = "081cfe3b3ff076ad71e365a1ab2acb37"; private static final String ENDPOINT = "http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s"; private String buildEndpoint(String city, String countryCode) { return String.format(ENDPOINT, (countryCode != null ? city+","+countryCode : city), API_KEY); } public Response handleRequest(Request request, Context context) { Get weatherResponse = Http.get(buildEndpoint(request.getCity(), request.getCountryCode())); ObjectMapper objectMapper = new ObjectMapper(); Response response = null; try { response = objectMapper.readValue(weatherResponse.text(), Response.class); } catch (IOException e) { e.printStackTrace(); } return response; } }

Alright, it’s time to get it out there! Let’s build the project with maven – run mvn package on the root directory, either using your IDE or the console. If successful, the final jar file should be located in the target directory.

The only thing left to do is to create a Lambda function on AWS and upload our code.





Uploading our code to AWS Lambda

First things first – log in to your AWS account and navigate to the Lambda service. If it’s your first time visiting that page, you might hit a welcome screen – simply click “Get started” and move on.

On the next screen, click „Create lambda function”. You will navigate to a list of blueprints. We want to upload our own code without using any blueprints so skip this page. If a page to select a trigger comes up, skip it as well.

On the next page, define the function name (for example GetWeatherDataFunction), fill in the description if you want and select the Java 8 runtime from the dropdown.

Click “Upload” and select your jar file from the target folder – make sure it’s the larger one (the one without original in its name).

Next, define the path to our handle function. If you hover over the “i” icon, you will get information on how to do that. You need to provide a full package and class name and append the function name after a double colon. In my case, it’s: com.github.rskupnik.lambda.GetWeatherDataFunction::handleRequest

Make sure you get this right or you’ll get an error upon testing the function.

Next, select a role. You probably have none defined, so simply select „Create new role from template(s)”. Name your role and leave the policy templates empty.

Leave the “Memory” on 512 and “VPC” on “no VPC”. The memory is actually not only the RAM resource given to your function but also the CPU. AWS ties those two resources closely together under a single “Memory” tag.

Click “Next” and then “Create function”.

After a while you will be redirected to your function’s page. Here you can modify configuration and reupload code if you’ve done any changes. For now, we are interested in the “Test” button on the top. Click it and you will see a popup asking you to define the input. Remember our Request class? Let’s fill in the test request:

1 2 3 4 { "city" : "Rzeszow", "countryCode" : "pl" } { "city" : "Rzeszow", "countryCode" : "pl" }

Feel free to type any valid city and country code here. Click “Save and test” and wait for AWS to complete execution. After a second or two the results of your function will be displayed according to your Response class format.

That’s it, you’ve just run a piece of code on Amazon’s infrastructure without doing any server hosting!

If you want to change the test input, you can do that under “Actions” -> “Configure test event”.

A function that can only be initiated by a click of a button on the test screen is not very useful. The purpose of these functions is to attach them to various other Amazon’s Services. In the next part I’ll show you how to attach API Gateway to make your function react to a HTTP request and effectively be available to anyone on the internet. Until then!