Slide 1

Slide 1 text

from Big Band Web App to Serverless Bebop @AnjanaVakil JazzCon.Tech 2018

Slide 2

Slide 2 text

Hello! I’m @AnjanaVakil Recurse Center Non-Graduate Mozilla TechSpeaker & Outreachy Alumna Engineering Learning & Development Lead, Mapbox

Slide 3

Slide 3 text

location data platform for developers maps, search, navigation web, iOS, Android, Unity... @AnjanaVakil JazzCon.Tech 2018

Slide 4

Slide 4 text

@AnjanaVakil JazzCon.Tech 2018

Slide 5

Slide 5 text

we use to ◍ get user stats ◍ monitor/test unpacker uploads ◍ ask yoda who’s on call ◍ see who’s out tmrw ◍ pick a random teammate Place your screenshot here @AnjanaVakil JazzCon.Tech 2018

Slide 6

Slide 6 text

user- limits out unpacker yoda stats Express slack i/o auth our slack-commands app node @AnjanaVakil JazzCon.Tech 2018

Slide 7

Slide 7 text

codebase slack-commands/ ├─ readme.md ├─ cloudformation/ #app & cmd config │ └─ slack-commands.template.js ├─ index.js #express app ├─ commands/ │ ├─ out.js │ ├─ stats.js │ └─ ... └─ test/ ├─ index.test.js ├─ out.test.js ├─ stats.test.js └─ ... @AnjanaVakil JazzCon.Tech 2018

Slide 8

Slide 8 text

All commands in a single app: Downsides Security ◍ Permissions? Secrets? ◍ Least privilege Maintenance ◍ Many different teams involved ◍ Ownership? Support? ◍ Code gumbo Cost ◍ Always running ◍ No per-command breakdown @AnjanaVakil JazzCon.Tech 2018

Slide 9

Slide 9 text

enough about computers let’s talk about jazz @AnjanaVakil JazzCon.Tech 2018

Slide 10

Slide 10 text

“ While swing music tended to feature orchestrated big band arrangements, bebop music highlighted improvisation. @AnjanaVakil JazzCon.Tech 2018 wikipedia

Slide 11

Slide 11 text

Big Band Glenn Miller Orchestra, c. 1940

Slide 12

Slide 12 text

Big Band out unpacker status yoda Express slack i/o auth user- limits @AnjanaVakil JazzCon.Tech 2018

Slide 13

Slide 13 text

“ I kept thinking there's bound to be something else. I could hear it sometimes. I couldn't play it.... I found that by using the higher intervals of a chord as a melody line and backing them with appropriately related changes, I could play the thing I'd been hearing. It came alive. - Charlie Parker @AnjanaVakil JazzCon.Tech 2018 wikipedia

Slide 14

Slide 14 text

Bebop Tommy Potter, Charlie Parker, Max Roach, Miles Davis, & Duke Jordan, NYC c. 1945

Slide 15

Slide 15 text

“ As bebop was not intended for dancing, it enabled the musicians to play at faster tempos. Bebop musicians explored advanced harmonies, complex syncopation, altered chords, extended chords, chord substitutions, asymmetrical phrasing, and intricate melodies. @AnjanaVakil JazzCon.Tech 2018 wikipedia

Slide 16

Slide 16 text

Bebop out unpacker status yoda user-limits @AnjanaVakil JazzCon.Tech 2018

Slide 17

Slide 17 text

“there’s bound to be something else” ◍ Assign a single owner/gatekeeper? ◍ Multiple single-command apps? ◍ Separate commands from router app @AnjanaVakil JazzCon.Tech 2018

Slide 18

Slide 18 text

“serverless” functions to the rescue AWS Lambda @AnjanaVakil JazzCon.Tech 2018

Slide 19

Slide 19 text

what do you mean “serverless” Actual Server You own a computer You put code on it You run it constantly (and you pay constantly) You keep it healthy Cloud Server AWS owns a computer (or 2) You put code on it AWS runs it constantly (and you pay constantly) You tell AWS how to keep it healthy “No” Server AWS owns a computer You put code on it AWS runs it when you ask (and you pay only then) AWS keeps it healthy @AnjanaVakil JazzCon.Tech 2018

Slide 20

Slide 20 text

◍ Only concern: Input -> Output ◍ No state maintained between calls ◍ Can be side-effecting, though (e.g. API call, database write) ◍ Limited resources & exec time (5m) what do you mean “function” @AnjanaVakil JazzCon.Tech 2018

Slide 21

Slide 21 text

Lose the server if it’s... ◍ small ◍ short-lived ◍ self-contained ◍ needed occasionally to lambda or not to lambda Keep the server if it’s... ◍ heavy ◍ long-running ◍ interdependent ◍ needed constantly @AnjanaVakil JazzCon.Tech 2018

Slide 22

Slide 22 text

HTTP API endpoint AWS load balancer EC2s on ECS JS JS JS old architecture @AnjanaVakil JazzCon.Tech 2018

Slide 23

Slide 23 text

HTTP API endpoint router new architecture stats out unpacker yoda @AnjanaVakil JazzCon.Tech 2018

Slide 24

Slide 24 text

old codebase slack-commands/ ├─ readme.md ├─ cloudformation/ #app & cmd config │ └─ slack-commands.template.js ├─ index.js #express app code ├─ commands/ #cmd code │ ├─ out.js │ ├─ stats.js │ └─ ... └─ test/ #app & cmd tests ├─ index.test.js ├─ out.test.js ├─ stats.test.js └─ ... @AnjanaVakil JazzCon.Tech 2018

Slide 25

Slide 25 text

new codebase slack-commands/ ├─ readme.md ├─ cloudformation/ #app config │ └─ slack-commands.template.js ├─ index.js #express app code ├─ commander.js #invoke lambdas └─ test/ ├─ commander.test.js └─ index.test.js slack-command-{cmd}/ ├ readme.md ├ cloudformation/ #cmd config │ └ slack-command-{cmd}.template.js ├ index.js #cmd code └ test/ └ index.test.js @AnjanaVakil JazzCon.Tech 2018

Slide 26

Slide 26 text

what does a function look like? @AnjanaVakil JazzCon.Tech 2018

Slide 27

Slide 27 text

Place your screenshot here user stats commmand @AnjanaVakil JazzCon.Tech 2018

Slide 28

Slide 28 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } @AnjanaVakil JazzCon.Tech 2018

Slide 29

Slide 29 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } handler function (we tell Lambda its name) @AnjanaVakil JazzCon.Tech 2018

Slide 30

Slide 30 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } execution environment we can configure @AnjanaVakil JazzCon.Tech 2018

Slide 31

Slide 31 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } input { args: [ “vakila”, “1/1”, “3/22”] } @AnjanaVakil JazzCon.Tech 2018

Slide 32

Slide 32 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } aws runtime info (e.g. time remaining - ignored here) @AnjanaVakil JazzCon.Tech 2018

Slide 33

Slide 33 text

module.exports.command = (event, context, callback) => { const { ApiCoreUrl, MapboxToken } = process.env; const user = event.args[0]; const from = new Date(event.args[1] || (+new Date - 864e5*30)); // 30d const to = new Date(event.args[2] || (+new Date)); if (isNaN(from)||isNaN(to)) return callback(new Error('invalid date')); const requrl = getStatsUrl(user, from, to, ApiCoreUrl, MapboxToken); request(requrl, (err, res, body) => { if (err||res.statusCode !== 200) return callback(err||res.statusCode); try { const data = JSON.parse(body); } catch(err) { return callback(err); } return callback(null, formatStatsMessage(user, data)); }); } “ok, I’m done” function (AWS passes this when executing) error data @AnjanaVakil JazzCon.Tech 2018

Slide 34

Slide 34 text

we wrote a function! yay @AnjanaVakil JazzCon.Tech 2018

Slide 35

Slide 35 text

do we get it up there? how @AnjanaVakil JazzCon.Tech 2018

Slide 36

Slide 36 text

Cloud::Formation templates ◍ JSON template - “just code” ◍ define your AWS stack components ◍ upload -> AWS builds your stack @AnjanaVakil JazzCon.Tech 2018

Slide 37

Slide 37 text

Cloud::Formation templates { "AWSTemplateFormatVersion": "2010-09-09", "Description": "slack-command-stats", "Parameters": { "ApiCoreUrl": {...}, "MapboxToken": {...} "Resources": { "Command": { "Type": "AWS::Lambda::Function", "Properties": {...} }, "CommandRole": { "Type": "AWS::IAM::Role", "Properties": {...} } }, "Outputs": { ... } } @AnjanaVakil JazzCon.Tech 2018

Slide 38

Slide 38 text

Cloud::Formation templates { "AWSTemplateFormatVersion": "2010-09-09", "Description": "slack-command-stats", "Parameters": { "ApiCoreUrl": {...}, "MapboxToken": {...} "Resources": { "Command": { "Type": "AWS::Lambda::Function", "Properties": {...} }, "CommandRole": { "Type": "AWS::IAM::Role", "Properties": {...} } }, "Outputs": { ... } } stack input params @AnjanaVakil JazzCon.Tech 2018

Slide 39

Slide 39 text

Cloud::Formation templates { "AWSTemplateFormatVersion": "2010-09-09", "Description": "slack-command-stats", "Parameters": { "ApiCoreUrl": {...}, "MapboxToken": {...} "Resources": { "Command": { "Type": "AWS::Lambda::Function", "Properties": {...} }, "CommandRole": { "Type": "AWS::IAM::Role", "Properties": {...} } }, "Outputs": { ... } } aws permissions for function @AnjanaVakil JazzCon.Tech 2018

Slide 40

Slide 40 text

Cloud::Formation templates { "AWSTemplateFormatVersion": "2010-09-09", "Description": "slack-command-stats", "Parameters": { "ApiCoreUrl": {...}, "MapboxToken": {...} "Resources": { "Command": { "Type": "AWS::Lambda::Function", "Properties": {...} }, "CommandRole": { "Type": "AWS::IAM::Role", "Properties": {...} } }, "Outputs": { ... } } actual lambda function the good stuff @AnjanaVakil JazzCon.Tech 2018

Slide 41

Slide 41 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018

Slide 42

Slide 42 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 function name helps find it later

Slide 43

Slide 43 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 code to run the good stuff

Slide 44

Slide 44 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 execution environment can pass in params

Slide 45

Slide 45 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 permissions from earlier

Slide 46

Slide 46 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 what to run it on totally not a server

Slide 47

Slide 47 text

Cloud::Formation templates "Command": { "Type": "AWS::Lambda::Function", "Properties": { "FunctionName": "slack-command-stats-production", "Code": { "S3Bucket": {...}, "S3Key": {...} }, "Handler": "index.command", "Environment": { "Variables": { "ApiCoreUrl": { "Ref": "ApiCoreUrl" }, "MapboxToken": { "Ref": "MapboxToken" } } }, "Role": { "Fn::GetAtt": ["CommandRole","Arn"] }, "Runtime": "nodejs4.3", "MemorySize": 128, "Timeout": 60, "Tags": [ { "Key": "Team", "Value": "EngOps"} ] } } @AnjanaVakil JazzCon.Tech 2018 arbitrary tags e.g. who owns this?

Slide 48

Slide 48 text

Mapbox open-source AWS helpers github.com/mapbox/ Command-line tools lambda-cfn create & deploy Node Lambda functions cfn-config configure/start/update CFN stacks JS libraries cloudfriend easily assemble CFN templates in JS decrypt-kms-env use secret environment vars @AnjanaVakil JazzCon.Tech 2018

Slide 49

Slide 49 text

we deployed a function! yay @AnjanaVakil JazzCon.Tech 2018

Slide 50

Slide 50 text

do we call it? how @AnjanaVakil JazzCon.Tech 2018

Slide 51

Slide 51 text

testing in the AWS console (manually) @AnjanaVakil JazzCon.Tech 2018

Slide 52

Slide 52 text

in Node with AWS SDK const AWS = require('aws-sdk'); const runCommand = (req, res, next) => { const [commandName, ...args] = = req.slackText; const params = { FunctionName: `slack-command-${commandName}-production`, // our convention Payload: JSON.stringify({ args: args }), // the `event` Lambda receives }; const lambda = new AWS.Lambda({ region: 'us-east-1' }); lambda.invoke(params).promise() .catch((err) => err.message) // pass on error message as response data .then((data) => res.json(formatForSlack(data))); }; @AnjanaVakil JazzCon.Tech 2018

Slide 53

Slide 53 text

why did we do all that? @AnjanaVakil JazzCon.Tech 2018

Slide 54

Slide 54 text

Before (single app) ◍ Many secrets in one stack ◍ Updating your code updates whole stack ◍ No fine-grained cost analysis Refactoring to Lambda: Benefits After (multiple Lambdas) ◍ Each stack only knows its own secrets ◍ Updating your code leaves others untouched ◍ Each stack/fn can be tagged & cost-monitored @AnjanaVakil JazzCon.Tech 2018

Slide 55

Slide 55 text

HTTP API endpoint router next steps stats out unpacker yoda @AnjanaVakil JazzCon.Tech 2018

Slide 56

Slide 56 text

HTTP API endpoint next steps stats out unpacker yoda @AnjanaVakil JazzCon.Tech 2018 router

Slide 57

Slide 57 text

Merci! @AnjanaVakil [email protected] ✌ Team Mapbox Young Hahn, Emily McAfee, Kelly Young, Jake Pruitt, Andrew Evans JazzCon.Tech Organizers Images from Wikimedia Template by SlidesCarnival.com