AWS Lambda Tutorial

AWS Lambda Tutorial

·

6 min read

The first time I heard about AWS lambda it make me doubt my career. "Less" is an adjective suffix meaning “without” so I thought serverless is a way of getting rid of the servers. I'm a backend developer so I imagine this is a way of generating backend code. In reality serverless is just a way of making your backend easier to scale. Actually, you don't have to worry about how to scale it.

A few weeks ago I read again about it, it was something about how to build a SaaS with 0$, and lambda was mentioned because you get 1 000 000 free calls. I liked the idea, seeing some benefits in it and I thought it would be useful to try all sorts of things, so I will need an easy deployment process that I could use to quickly run side projects. In order to follow this, you will need an AWS account and aws cli working. I will be using java but it should be easy to adapt to any other language because I will deploy it using Docker container. I did this using the command line because it is easier to replicate.

How lambda works

In theory, things are simple. You write a function that is ran based on some event. In this tutorial, the event is a REST call. Each such function will be executed in a container. This means lambda has its own isolated execution environment. The container is created when the function is executed for the first time. After some time the container is destroyed. Because of this, you might get into cold start issues. This happens because your container may take some time to start. Sometimes AWS will reuse your container so you won't notice it but if your function is rarely called you might see it takes a lot of time to execute.

Build the project

I used the IntelliJ Idea and created an empty maven project. I added the AWS lambda java core dependency to the project and build a simple method that handles a request.

The java code looks like this:

package bq;

import com.amazonaws.services.lambda.runtime.Context;

public class App {
    public static Object handleRequest(Object arg, Context context) {
        return arg;
    }
}

The maven pom to package this is:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>bq</groupId>
    <artifactId>bq</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <version>3.1.2</version>
                    <configuration>
                        <includeScope>runtime</includeScope>
                    </configuration>
                    <executions>
                        <execution>
                            <id>copy-dependencies</id>
                            <phase>package</phase>
                            <goals>
                                <goal>copy-dependencies</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>

        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
        </dependency>
    </dependencies>
</project>

We need maven-dependency-plugin because we will copy all the jar files into the docker container. This plugin will make sure we have them in the target/dependency directory. Maybe there is a way to push everything as a jar but this is good enough to start.

Make sure everything works

$ mvn package

This is the project structure:

.
├── Dockerfile
├── lambda-iam.json
├── lambda.iml
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── bq
│   │   │       └── App.java
│   │   └── resources
│   └── test
│       └── java
└── target
    ├── bq-1.0-SNAPSHOT.jar
    ├── classes
    │   └── bq
    │       └── App.class
    ├── dependency
    │   └── aws-lambda-java-core-1.2.1.jar
    ├── generated-sources
    │   └── annotations
    ├── maven-archiver
        └── pom.properties
    //removed the rest

Push the container

As previously mentioned the lambda will start a docker container. This uses AWS base images for Lambda. More details about it here gallery.ecr.aws/lambda/java The usage tab also shows you how to build your lambda container if you need more instructions.

The docker file:

FROM public.ecr.aws/lambda/java:11

COPY target/classes ${LAMBDA_TASK_ROOT}
COPY target/dependency/* ${LAMBDA_TASK_ROOT}/lib/

CMD [ "bq.App::handleRequest" ]

RUN chmod 644 $(find . -type f)
RUN chmod 755 $(find . -type d)

Build the container

mvn package
docker build . -t java-aws-echo:1

And tested it:

docker run  -p 9000:8080 java-aws-echo:1
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

The docker image will be hosted in Elastic Container Registry (ecr) After I saw everything worked on localhost I created an ecr repository and pushed the image:

aws ecr create-repository --repository-name java-aws-echo

aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com
docker tag java-aws-echo:1 YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com/java-aws-echo:1
docker push YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com/java-aws-echo:1

Create the lambda

Before we create the lambda we need to create a role that lambda will assume. This is the minimal policy I found somewhere in AWS docs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
aws iam create-role --role-name lambda --assume-role-policy-document file://lambda.json

Creating the lambda from the command line is fairly simple.

aws lambda create-function --function-name java-lambda --code ImageUri=YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com/java-aws-echo:1 --role arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/lambda --package-type Image

And voila that is all for your function. The only thing is that is not yet exposed on the internet. You can give it a try from AWS console UI and trigger a test.

Exposing the lambda into internet.

This was the part I wasn't expecting to be that complicated. Once you have your lambda deployed you are probably asking yourself, where do I find the URL? My assumption was that once you created it by default it will receive a default URL. It is not the case and you need to do a few more steps and your function will be exposed using the AWS API Gateway. Here is a bash script I created that will create an API GW for your lambda:

#!/usr/bin/env bash
NAME="Lambda API GW"
METHOD="POST"
#you can find the arn if you go on Lambda > Functions > java-lambda ->Copy ARN
LAMBDA_ARN="PROVIDE THE LAMBDA ARN!!!!"
REGION="us-west-2"

aws apigateway create-rest-api --name "$NAME" > apigateway-create-rest-api.tmp

API_ID=$(cat apigateway-create-rest-api.tmp |jq ".id" -r)
echo $API_ID

ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID|jq '.items|first.id' -r)
echo $ROOT_ID

#request
aws apigateway put-method --rest-api-id $API_ID --resource-id $ROOT_ID --http-method $METHOD --authorization-type NONE --no-api-key-required
#Response
aws apigateway put-method-response --rest-api-id $API_ID --resource-id $ROOT_ID --http-method $METHOD --status-code 200 --response-models "{\"application/json\": \"Empty\"}"

echo "aws apigateway put-method --rest-api-id $API_ID --resource-id $ROOT_ID --http-method $METHOD"

echo "Integrations"
URI="arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/$LAMBDA_ARN/invocations"
echo $URI
aws apigateway put-integration --rest-api-id $API_ID --resource-id $ROOT_ID --type AWS --http-method $METHOD --integration-http-method $METHOD --uri $URI

aws apigateway put-integration-response --rest-api-id $API_ID --resource-id $ROOT_ID --http-method $METHOD --status-code 200 --content-handling CONVERT_TO_TEXT

echo "Permissions"
FUNCTION_NAME=$(aws lambda get-function --function-name $LAMBDA_ARN|jq '.Configuration.FunctionName' -r)
ACCOUNT_ID=$(aws sts get-caller-identity|jq '.Account' -r)
SOURCE_ARN="arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/$METHOD/"

echo "$FUNCTION_NAME $SOURCE_ARN"
aws lambda add-permission --function-name $FUNCTION_NAME --statement-id STTT --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn $SOURCE_ARN

aws apigateway create-deployment --rest-api-id $API_ID --stage-name atm1

I then searched for the lambda URL in the AWS console. Log into your account and select API Gateway. The URL for it is: us-west-2.console.aws.amazon.com/apigateway.. Then click on the newly created API and it should show the Resources and the POST method. In the left menu, there should be "Stages", click on it and then select the atm1.
On a blue background, there should be "Invoke URL" in the stage editor. Give it a test:

curl -XPOST "Your Invoke URL" -d '{}'

If you followed this tutorial and you have any issues please let me know. Hope this helps!