twitter.com/@johncmckim
johncmckim.me
medium.com/@johncmckim
Software Engineer at A Cloud Guru
John McKim
@johncmckim
Contribute to Serverless Framework
https://acloud.guru
https://serverless.com
FaaS + The Herd
A Serverless Architecture is an event driven system that utilises FaaS and other fully managed services for logic and persistence.
Benefits
For fun and learning
Caring for my Garden
How It works
Protocols
Authentication
Fake Device
const awsIot = require('aws-iot-device-sdk');
const device = awsIot.device({
'keyPath': './certificates/private.pem.key',
'certPath': './certificates/certificate.pem.crt',
'caPath': './certificates/verisign-ca.pem',
'clientId': 'garden-aid-client-test-js',
'region': 'ap-southeast-2'
});
device
.on('connect', function() {
const topic = 'garden/soil/moisture';
const message = JSON.stringify({
DeviceId: 'test-js-device',
Recorded: (new Date()).toISOString(),
Level: level
});
device.publish(topic, message, {});
});
Fake Device
Message Selection & Transformation
SQL Statement
SELECT DeviceId, Recorded, Level FROM 'garden/soil/moisture'
Actions
Lambda
DynamoDB
ElasticSearch
SNS
SQS
Kinesis
CloudWatch
Republish to another MQTT topic.
IoT Rule in serverless.yml
SensorThingRule:
Type: AWS::IoT::TopicRule
Properties:
TopicRulePayload:
RuleDisabled: false
Sql: "SELECT DeviceId, Recorded, Level FROM '$/garden/soil/moisture'"
Actions:
-
DynamoDB:
TableName: { Ref: MoistureData }
HashKeyField: "ClientId"
HashKeyValue: "${clientId()}"
RangeKeyField: "Timestamp"
RangeKeyValue: "${timestamp()}"
PayloadField: "Data"
RoleArn: { Fn::GetAtt: [ IotThingRole, Arn ] }
-
Lambda:
FunctionArn: { Fn::GetAtt: [ checkMoistureLevel, Arn ] }
Amazon Simple Queue Service (SQS)
Benefits
Drawbacks
Fully Managed message queuing service.
Amazon Kinesis Streams
Benefits
Drawbacks
Capture and store streaming data.
Amazon Simple Notification Service (SNS)
Benefits
Drawbacks
Full managed messaging and Pub/Sub service
Check Level
const AWS = require('aws-sdk');
const sns = new AWS.SNS();
const publish = (msg, topicArn, cb) => {
sns.publish({
Message: JSON.stringify({
message: msg
}),
TopicArn: topicArn
}, cb);
};
module.exports.checkLevel = (event, context, cb) => {
if(event.Level < 2.5) {
const msg = 'Moisture level has dropped to ' + event.Level;
const topicArn = process.env.mositureNotifyTopic;
publish(msg, topicArn, cb);
cb(null, { message: msg, event: event });
return;
}
cb(null, { message: 'No message to publish', event: event });
}
Slack Notifier
const BbPromise = require('bluebird');
const rp = require('request-promise');
const util = require('util');
const notify = (msg) => {
return rp({
method: 'POST',
uri: process.env.slackWebHookUrl,
json: true,
body: {
text: msg,
},
});
}
module.exports.notify = (event, context, cb) => {
console.log(util.inspect(event, false, 5));
const promises = [];
event.Records.forEach(function(record) {
if(record.EventSource !== 'aws:sns') {
console.warn('Recieved non sns event: ', record);
return;
}
const notification = JSON.parse(record.Sns.Message);
promises.push(notify(notification.message));
});
return BbPromise.all(promises)
.then(() => cb(null, { message: 'success' }))
.catch(cb);
};
Slack Notifications
Web Client
Web Backend
API Gateway
What is it?
Auth0 Authentication
Authentication with GraphQL
const networkInterface = createNetworkInterface(GRAPHQL_URL);
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
// get the authentication token from local storage if it exists
const idToken = localStorage.getItem('idToken') || null;
if (idToken) {
req.options.headers.Authorization = `Bearer ${idToken}`;
}
next();
},
}]);
Custom Authorizer
const utils = require('./auth/utils');
const auth0 = require('./auth/auth0');
const AuthenticationClient = require('auth0').AuthenticationClient;
const authClient = new AuthenticationClient({
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
});
module.exports.handler = (event, context, cb) => {
console.log('Received event', event);
const token = utils.getToken(event.authorizationToken);
if (!token) {
return cb('Missing token from event');
}
const authInfo = utils.getAuthInfo(event.methodArn);
return authClient.tokens.getInfo(token)
.then((userInfo) => {
if (!userInfo || !userInfo.user_id) {
throw new Error('No user_id returned from Auth0');
}
console.log(`Building policy for ${userInfo.user_id} with: `, authInfo);
const policy = new AuthPolicy(userInfo.user_id, authInfo.accountId, authInfo);
policy.allowMethod(AuthPolicy.HttpVerb.POST, '/graphql');
const result = policy.build();
console.log('Returning auth result: ', result, result.policyDocument.Statement);
return result;
})
.catch((err) => {
console.log(err);
return 'Unauthorized';
});
};
Dashboard
type Project {
name: String
stars: Int
contributors: [User]
}
{
project(name: "GraphQL") {
stars
}
}
{
"project": {
"stars": 4462
}
}
Schema
Results
Query
import gql from 'graphql-tag';
import { connect } from 'react-apollo';
import MoistureChart from '../../pres/Moisture/Chart';
export default connect({
mapQueriesToProps({ ownProps, state }) {
return {
moisture: {
query: gql`{
moisture(hours: ${ownProps.hours}, clientId: "${ownProps.clientId}") {
date, moisture
}
}`,
variables: {},
pollInterval: 1000 * 30, // 30 seconds
},
};
},
})(MoistureChart);
const graphql = require('graphql');
const tablesFactory = require('./dynamodb/tables');
const MoistureService = require('./services/moisture');
const tables = tablesFactory();
const moistureService = MoistureService({ moistureTable: tables.Moisture });
const MoistureType = new graphql.GraphQLObjectType({
name: 'MoistureType',
fields: {
date: { type: graphql.GraphQLString },
moisture: { type: graphql.GraphQLFloat },
}
});
const schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'Root',
description: 'Root of the Schema',
fields: {
moisture:
name: 'MoistureQuery',
description: 'Retrieve moisture levels',
type: new graphql.GraphQLList(MoistureType),
args: {
clientId: {
type: graphql.GraphQLString,
},
hours: {
type: graphql.GraphQLInt,
defaultValue: 1
},
},
resolve: (source, args, root, ast) => {
const hours = args.hours > 0 ? args.hours : 1;
return moistureService.getLastHours(args.clientId, hours);
}
}
}
})
});
module.exports = schema;
const graphql = require('graphql');
const schema = require('./schema');
module.exports.handler = function(event, context, cb) {
console.log('Received event', event);
const query = event.body.query;
return graphql.query(schema, event.body.query)
.then((response) => {
cb(null, response)
})
.catch((error) => {
cb(error)
});
}
GraphQL Query
Single Lambda Design
Lambda Tree Design
My Experiences
IoT Service
Device Shadows
Notifications Service
Web Services
Many things
Code + Reading
Frameworks & Tools
twitter.com/@johncmckim
johncmckim.me
medium.com/@johncmckim