1 of 77

Serverless

The battle of the Giants

@ChristosMatskas ~ @loige

loige.link/giants-aws-path

Fullstack 2017 - 14th July, London

Cover picture by Antranias

2 of 77

If you are cool, you already have these:

3 of 77

Things to do now, before starting:

  • Be sure you have the last version of the AWS CLI:

pip install --upgrade --user awscli

or pip install --upgrade awscli or pip3 install --upgrade awscli

  • Install lambda-local:

npm install --global lambda-local

  • Login to AWS web console

4 of 77

What the heck is a Lambda Function!

  • a (small) unit of compute. Well… it’s a function!
  • can be written in Node.js, Python, C# or Java
  • is triggered by an event
    • an API call
    • a schedule / cron
    • a change in the file storage
    • a change in the database
    • weird stuff with IoT devices and Alexa
    • notifications or direct invocation by other Lambdas
    • ...

5 of 77

Anatomy of a Lambda (in Node.js)

exports.handler = (

event,

context,

callback) => {

// get input from event and context

// return output or errors with callback

}

6 of 77

Grab the code!

7 of 77

Exercise 01

8 of 77

Our first Lambda, a.k.a. “a boring Hello World”

Trigger:

API -> GET /helloworld

Output:

{"message":"Hello World"}

9 of 77

exports.handler = (event, context, callback) => {

const response = {

statusCode: 200,

headers: {'Content-Type': 'application/json'},

body: JSON.stringify({message: 'Hello World'})

}

callback(null, response)

}

10 of 77

Let’s deploy it on AWS

(through the Web Console)

11 of 77

12 of 77

13 of 77

14 of 77

15 of 77

16 of 77

exports.handler = (event, context, callback) => {

const response = {

statusCode: 200,

headers: {'Content-Type': 'application/json'},

body: JSON.stringify({message: 'Hello World'})

}

callback(null, response)

}

17 of 77

18 of 77

19 of 77

Cool, but...

  • No Local testing
  • very manual process…�(a.k.a “clicky-clicky”)

20 of 77

Hello world REST API - local development & deploy

  • File structure
  • Local testing
  • SAM template
  • Packaging
  • Deploy

21 of 77

Hello world REST API - Files structure

  • src/
    • handler.js
  • sample-event.json
  • template.yml

<- Our code!

<- For local testing

<- For deployment

22 of 77

// src/handler.js

exports.handler = (event, context, callback) => {

const response = {

statusCode: 200,

headers: {'Content-Type': 'application/json'},

body: JSON.stringify({message: 'Hello World'})

}

callback(null, response)

}

23 of 77

// sample-event.json

{}

For real… we don’t care about the input… yet!

<- Just an empty JSON object!

24 of 77

Local testing

lambda-local -l src/handler.js -h handler -e sample-event.json

^ the file we want to run

^ name of the handler function

input event file ^

25 of 77

Serverless Application Model (SAM)

26 of 77

// template.yml

AWSTemplateFormatVersion: '2010-09-09'

Transform: 'AWS::Serverless-2016-10-31'

Resources:

HelloWorldApi:

Type: AWS::Serverless::Function

Properties:

CodeUri: ./src

Handler: handler.handler

Runtime: nodejs6.10

Events:

Endpoint:

Type: Api

Properties:

Path: /helloworld

Method: get

27 of 77

Package and deploy - deployment bucket

Think of a unique name: eg. “<yourname>-loves-unicorns”

aws s3 mb s3://<unique-name>

export BUCKET=<unique-name>

28 of 77

Package and deploy - package

export STACK_NAME=simple-hello-world

aws cloudformation package \

--template-file template.yml \

--s3-bucket $BUCKET \

--output-template-file packaged-template.yml

29 of 77

Package and deploy - package

aws cloudformation deploy \

--template-file packaged-template.yml \

--stack-name $STACK_NAME \

--capabilities CAPABILITY_IAM

If it asks for a region, you can input “eu-west-1” for Ireland, and for output format “json” or “text”

30 of 77

Get the API URL

https://[id].execute-api.[region].amazonaws.com/Prod/helloworld

aws cloudformation describe-stack-resource \

--stack-name $STACK_NAME \

--logical-resource-id ServerlessRestApi

31 of 77

Region

ID

https://wco1kqll3d.execute-api.eu-west-1.amazonaws.com/Prod/helloworld

32 of 77

33 of 77

34 of 77

Things we learned

  • Writing our first Lambda Function (web console and local)
  • API Gateway trigger
  • Local testing
  • SAM templates
  • Packaging
  • Deployment

35 of 77

Exercise 02

36 of 77

Reading HTTP query parameters

if the request contains query parameters (e.g. “?name=Podge”):

exports.handler = (event, context, callback) => {

console.log(event.queryStringParameters.name) // Podge

}

37 of 77

Hello World with Query parameters

Exercise: edit the previous exercise to output:

{“message”: “hello <NAME>”}

<NAME> is the value in event.queryStringParameters.name�if no name is available use “world” by default.

38 of 77

Things we learned

  • Reading input from the event object
  • Using query string parameters in REST APIs

39 of 77

Exercise 03

40 of 77

Reading HTTP request body

You can read the body of a request with: event.body

exports.handler = (event, context, callback) => {

console.log(event.body)

}

41 of 77

Return HTTP error responses

You can create any HTTP response object with the following syntax:

callback(null, {

statusCode: XXX,

headers: {},

body: ‘Some content’

})

42 of 77

Return HTTP error responses

For example if you want to return a 404 in an API:

exports.handler = (event, context, callback) =>

callback(null, {

statusCode: 404,

headers: {‘Content-Type’: ‘application/json’},

body: JSON.stringify(‘File Not Found’)

})

43 of 77

Fizz Buzz

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, …

Rules:

Given n (positive integer) as input:

  • If n is divisible by 3 and 5 => Fizz Buzz
  • If n is divisible by 3 => Fizz
  • If n is divisible by 5 => Buzz
  • Otherwise => n

44 of 77

Fizz Buzz

Exercise: Implement a Fizz Buzz REST API, where:

  • The input (n) comes from the request body (request must be POST)
  • The output is:

{“result”: “(Fizz|Buzz|Fizz Buzz|\d+)”}

  • If the input is not valid, return a 400 Bad Request response with body:

{“error”: “Invalid body, a positive integer was expected”}

45 of 77

HINT #1: remember to set the endpoint method to POST

In your SAM template:

Events:

Endpoint:

Type: Api

Properties:

Path: /fizzbuzz

Method: post

46 of 77

HINT #2: the body is a raw string!

event.body contains the raw data (as a string) sent from the web client, it’s not automatically converted into a JSON object.

If you expect to receive a JSON input you will need to use JSON.parse to be able to access the fields in the object.

The body can be the content of a form submit (multipart/form-data or application/x-www-form-urlencoded) or even a binary (e.g. image/png), in all those cases you will need to parse the data yourself.

47 of 77

Things we learned

  • Creating endpoints that accept POST requests
  • Read the body of a POST request
  • Returning custom HTTP status codes

48 of 77

Exercise 04

49 of 77

Defining path parameters

You can map a URL with arbitrary path parameters to your Lambda Function in your SAM template as follows:

Events:

Endpoint:

Type: Api

Properties:

Path: /profile/{username}

Method: get

50 of 77

Reading path parameters

You can read a path parameters: event.pathParameters

exports.handler = (event, context, callback) => {

console.log(event.pathParameters.username)

}

51 of 77

Using external libraries in your Lambda Functions

You can install and use any external library from NPM in your src folder.

cd src

npm init -y

npm install --save lodash

52 of 77

Using external libraries in your Lambda Functions

Then you can simply require the library in your function:

// handler.js

const _ = require(‘lodash’)

exports.handler = (event, context, callback) => callback({

body: JSON.stringify(_.now())

})

53 of 77

Using external libraries in your Lambda Functions

When using external libraries, you need to deploy a zip file containing everything inside your src folder, so you need to change your SAM template as follows:

Resources:

YourLambdaFunction:

Type: AWS::Serverless::Function

Properties:

CodeUri: ./src.zip

54 of 77

Timezone conversion API

Exercise: Implement a timezone conversion REST API, where you receive 3 inputs as path parameters:

  • timestamp is a timestamp in ISO 8601 format �(e.g. 2018-05-17T22:13:10)
  • sourceTimezone the source timezone (e.g. Europe/London)
  • targetTimezone the source timezone (e.g. Australia/Sydney)

The API response is a JSON blob with the converted timestamp. E.g.:

{"timestamp":"2018-05-18T07:13:10+10:00"}

55 of 77

HINT #1: how to convert timestamps across timezones

Use the moment-timezone library:

const resultTimestamp = moment.tz(

timestamp,

sourceTimezone

)

.tz(targetTimezone)

.format()

56 of 77

HINT #2: path parameters containing a slash? �Escape them!

When deploying this API your url will look like:

/convert/{timestamp}/{sourceTimezone}/{targetTimezone}

since source and target timezone might contain a “/” in between (e.g. “Europe/Dublin”), you will need to escape it (e.g. “Europe%2FLondon”)

E.g.

/convert/2018-05-17T22:13:10/Europe%2FLondon/Australia%2FSydney

57 of 77

HINT #3: beware on your zip

The deployable src.zip file should contain only the files and folders inside the “src” directory and shouldn’t include the src folder itself.

NO

YES

rm -f src.zip && cd src && zip -r ../src.zip . && cd ..

58 of 77

Things we learned

  • Define and access path parameters in REST APIs
  • Using external libraries from NPM
  • Deploying multiple files with a zip package

59 of 77

Exercise 05

60 of 77

Defining scheduled events

In your SAM template you can use rate or cron expressions:

Events:

Timer:

Type: Schedule

Properties:

Schedule: rate(2 hours)

61 of 77

Using environment variables

In your SAM template you can reference generic parameters, that need to be passed when deploying the function:

Parameters:

ApiKey:

Description: The API key of the service

Type: String

Default: “ABCD”

62 of 77

Using environment variables

You can pass one or more parameters as environment variable to a function:

Resources:

MyFunction:

Properties:

Environment:

Variables:

API_KEY: !Ref ApiKey

63 of 77

Using environment variables

Then you can use the environment variables in your code as in any other piece of Node.js code:

exports.handler = (event, context, callback) => {

const API_KEY = process.env.API_KEY

// ...

}

64 of 77

Using environment variables

When you deploy your function you can specify the value for your parameters with the option --parameter-overrides:

aws cloudformation deploy ... --parameter-overrides ApiKey=$API_KEY

65 of 77

Using Dynamo DB with your Lambda Functions

If your Lambda needs to write and read data you can easily associate a Dynamo DB table to it in your SAM template.

To create a table:

Resources:

MyTable:

Type: AWS::Serverless::SimpleTable

66 of 77

Using Dynamo DB with your Lambda Functions

Then to reference the name of the table as an environment variable:

Resources:

MyFunction:

Type: AWS::Serverless::Function

Properties:

Policies: AmazonDynamoDBFullAccess

Environment:

TABLE_NAME: !Ref MyTable

67 of 77

Weather forecast scraper

Exercise:

Implement a function that retrieves weather forecast for a specific area every 2 hours and stores the results in a Dynamo DB table.

68 of 77

HINT #1: Use Open Weather Map APIs as data source

With this API you can simply issue an HTTP request:

const url = `http://api.openweathermap.org/data/2.5/forecast`

const qs = { q: LOCATION, appid: API_KEY }

request({url, qs}, (err, {body}) => {

const data = JSON.parse(body)

// …

}

You will need to register for an API Key

69 of 77

HINT #2: Use dynamise to deal with Dynamo DB

Dynamo DB official SDK is… not the easiest!�Wrapper libs like dynamise can make your life easier:

const db = require('dynamise')

const items = [{id: 1, name: “Podge”}, {id: 1, name: “Luciano”}]

const client = db()

const TABLE_NAME = process.env.TABLE_NAME

client.table(TABLE_NAME).multiUpsert(items)

.then(...)

70 of 77

Things we learned

  • Define schedule events
  • Use template parameters and environment variables with SAM
  • Use Dynamo DB tables

71 of 77

An extra “crazy” example

72 of 77

Synchronise an S3 Bucket with Azure Storage

Example: lessons/06

This example implements a Lambda Function that is triggered when a new file is created in an S3 bucket of our choice.

When the lambda is triggered the new file is read and streamed into an Azure Storage share directory.

73 of 77

Closing off

74 of 77

Serverless is fun

  • It’s very easy to create an API or a product and deploy it in the cloud
  • Don’t need to worry (a lot) about infrastructure
  • If you don’t like SAM/Cloudformation there are alternatives:

75 of 77

Keep learning!

76 of 77

A challenge for you: CAN YOU BUILD SOMETHING COOL?

Talk with Christos and Luciano if you need IDEAS or advice and send them your creations :)

77 of 77

<3 Special thanks to @Podgeypoos79, @quasi_modal & @andreaman87 for feedback & support

Stay in touch:

@ChristosMatskas @loige

Thanks to @danilop and AWS for the amazing support!