Serverless
The battle of the Giants
Fullstack 2017 - 14th July, London
Cover picture by Antranias
If you are cool, you already have these:
Things to do now, before starting:
pip install --upgrade --user awscli
or pip install --upgrade awscli or pip3 install --upgrade awscli
npm install --global lambda-local
What the heck is a Lambda Function!
Anatomy of a Lambda (in Node.js)
exports.handler = (
event,
context,
callback) => {
// get input from event and context
// return output or errors with callback
}
Grab the code!
Exercise 01
Our first Lambda, a.k.a. “a boring Hello World”
Trigger:
API -> GET /helloworld
Output:
{"message":"Hello World"}
exports.handler = (event, context, callback) => {
const response = {
statusCode: 200,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: 'Hello World'})
}
callback(null, response)
}
Let’s deploy it on AWS
(through the Web Console)
exports.handler = (event, context, callback) => {
const response = {
statusCode: 200,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: 'Hello World'})
}
callback(null, response)
}
Cool, but...
Hello world REST API - local development & deploy
Hello world REST API - Files structure
<- Our code!
<- For local testing
<- For deployment
// 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)
}
// sample-event.json
{}
For real… we don’t care about the input… yet!
<- Just an empty JSON object!
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 ^
Serverless Application Model (SAM)
// 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
Package and deploy - deployment bucket
Think of a unique name: eg. “<yourname>-loves-unicorns”
aws s3 mb s3://<unique-name>
export BUCKET=<unique-name>
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
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”
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
Region
ID
https://wco1kqll3d.execute-api.eu-west-1.amazonaws.com/Prod/helloworld
Things we learned
Exercise 02
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
}
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.
Things we learned
Exercise 03
Reading HTTP request body
You can read the body of a request with: event.body
exports.handler = (event, context, callback) => {
console.log(event.body)
}
Return HTTP error responses
You can create any HTTP response object with the following syntax:
callback(null, {
statusCode: XXX,
headers: {},
body: ‘Some content’
})
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’)
})
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:
Fizz Buzz
Exercise: Implement a Fizz Buzz REST API, where:
{“result”: “(Fizz|Buzz|Fizz Buzz|\d+)”}
{“error”: “Invalid body, a positive integer was expected”}
HINT #1: remember to set the endpoint method to POST
In your SAM template:
Events:
Endpoint:
Type: Api
Properties:
Path: /fizzbuzz
Method: post
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.
Things we learned
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
Reading path parameters
You can read a path parameters: event.pathParameters
exports.handler = (event, context, callback) => {
console.log(event.pathParameters.username)
}
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
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())
})
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
Timezone conversion API
Exercise: Implement a timezone conversion REST API, where you receive 3 inputs as path parameters:
The API response is a JSON blob with the converted timestamp. E.g.:
{"timestamp":"2018-05-18T07:13:10+10:00"}
HINT #1: how to convert timestamps across timezones
Use the moment-timezone library:
const resultTimestamp = moment.tz(
timestamp,
sourceTimezone
)
.tz(targetTimezone)
.format()
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
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 ..
Things we learned
Exercise 05
Defining scheduled events
In your SAM template you can use rate or cron expressions:
Events:
Timer:
Type: Schedule
Properties:
Schedule: rate(2 hours)
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”
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
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
// ...
}
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
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
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
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.
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
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(...)
Things we learned
An extra “crazy” example
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.
Closing off
Serverless is fun
Keep learning!
A challenge for you: CAN YOU BUILD SOMETHING COOL?
Thank you!
<3 Special thanks to @Podgeypoos79, @quasi_modal & @andreaman87 for feedback & support
Thanks to @danilop and AWS for the amazing support!