A little story about serverless
Since 2014, my day job has been helping folks build and maintain stuff on Amazon Web Services (AWS). I've helped organizations from the most niche startups to household names on everything from real-time inventory management to machine learning on pizzas and everything in between.
I've seen containers get popular. I've seen the word Serverless get used so much that I am not even sure what it means anymore. One thing does remain for sure though, the real world Serverless systems I've been part of creating that have handled billions of transactions are some of the most pleasant to maintain and operate of any I've seen in my career.
So why do I love serverless, why does it matter? Y'all remember the Spectre/Meltdown insanity in early 2018 where chip level vulnerabilities were discovered, and everyone was freaking out and scrambling to fix it? The serverless environments I operated were patched before most orgs even had the conference rooms booked to build the response plan.
Some of my most fun tickets and replies of all time were along the lines of "Hello, we are from Company X's security team, and we need a full plan for maintenance and audits for the environment you maintain for us ASAP to address the recent security issues." Nothing like saying "already handled" with a link to the AWS patch notes completed before the vulnerability was even public.
At the end of the day, you want to deliver business value. You, too, can leverage the operations of the best and brightest in computing for pennies on the dollar by using serverless practices. Instead of worrying about server patching, networking, ssh keys, and the ilk, you get to focus on delivering your core value.
Tools of the Trade
There are a lot of options out there today from the Serverless Framework to Apex Up and many other providers and frameworks (many of them focusing on a niche use-case or language).
Most of my new projects kick off with AWS Amplify CLI these days. AWS Amplify CLI is somewhat of a wrapper on the complexities of the cloud, offering opinionated solutions while still offering customizability where you need.
Make sure you have Node.js 8.11.x or later and an AWS Account (don't worry about configuring anything yet, we simply need the account), and away we go.
Our project today is going to be a blast from the past, with a little twist. Remember those small "visitor counter" badges everyone used to have at the bottom of their website, usually right next to an animated gif of their countries flag?
Let's do that, but better... we are going to make it real-time™!
Start by installing the Amplify CLI with npm install -g @aws-amplify/cli
and running amplify configure
in your terminal. You'll be walked through various steps in a combination of your terminal and browser window, at the end of which you will have created and configured a new IAM user.
$ npm install -g @aws-amplify/cli
$ amplify configure
Follow these steps to set up access to your AWS account:
Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue
Specify the AWS Region
? region: us-east-1
Specify the username of the new IAM user:
? user name: captain-counter
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=captain-counter&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess
Press Enter to continue
Enter the access key of the newly created user:
? accessKeyId: AKIAWTXIHO**********
? secretAccessKey: AfGA3kTlGyv6F0GMyzQS********************
This would update/create the AWS Profile in your local machine
? Profile Name: captain-counter
Successfully set up the new user.
Let's set up our project directory and initialize our amplify app.
$ mkdir counter && cd counter
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project counter
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using none
? Source Directory Path: src
? Distribution Directory Path: dist
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use captain-counter
⠙ Initializing project in the cloud...
These steps (and mostly using default answers) will end up with a base amplify app running in your AWS account. It doesn't do much now, but let's keep coding. Do note; I opted not to use any particular framework. I want to keep this extremely lightweight since we'll be loading the script in other websites and there is no need for a framework where we are going.
Adding the API
Our little project wouldn't be very successful if we didn't have a way to track hits. We are going to leverage a GraphQL API with a service called AWS AppSync. AppSync is a fully managed GraphQL solution that allows you to quickly and easily map to various data sources, I've personally used it for many things, and it's everything it says on the label and then some.
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: counter
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name Counter
Creating a base schema for you...
Great, now let's open up amplify/backend/api/counter/schema.graphql
and change the schema.
type Counter
@model
{
id: String!
hits: Int
}
Now for the fun part, let's deploy our API. Behind the scenes, Amplify compiles your schema to various queries and mutations, updates your CloudFormation templates to manage all the resources you need for your API, code generates a small client to access your API, and finally deploys everything via CloudFormation to your AWS account.
$ amplify push
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | ----------------- |
| Api | counter | Create | awscloudformation |
? Are you sure you want to continue? Yes
GraphQL schema compiled successfully.
Edit your schema at /Users/jshort/Work/counter/amplify/backend/api/counter/schema.graphql or place .graphql files in a directory at /Users/jshort/Work/counter/amplify/backend/api/counter/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠦ Updating resources in the cloud. This may take a few minutes...
....sometime later
GraphQL endpoint: https://ol2t5s4qlvbidcx2mwmigeya4m.appsync-api.us-east-1.amazonaws.com/graphql
GraphQL API KEY: da2-rbk5t2xpuvevlm6qza4onbly7m
At the end of the process, you'll get a GraphQL endpoint and API key. You can use these to start playing with and exploring your API immediately. Using something like GraphQL Playground or Insomnia may be the fastest way to play around with things.
If you check out your API, you will notice a TON of pre-built functionality for normal CRUD operations (Create, Read, Update, Delete). For our use-case, we don't need any of it, and we are going to substitute our own.
Change your amplify/backend/api/counter/schema.graphql
to reflect this more locked down schema. We are taking away nearly all of the CRUD operations, renaming some of the operations, and adding a filtered subscription method. If you want to learn more about this, check out the AWS Amplify docs for GraphQL Transforms.
type Counter
@model(
queries: { get: "counter" },
mutations: { create: "hit" },
subscriptions: null
)
{
id: String!
hits: Int
}
type Subscription {
hits(id: String!): Counter
@aws_subscribe(mutations: ["hit"])
}
Behind the scenes, Amplify is managing a DynamoDB table for us. A managed NoSQL database that can handle a tremendous load (In my experience one of the best serverless databases).
Next, We are going to customize our GraphQL resolver to take advantage of atomic updates within Amazon DynamoDB, meaning that every "hit" mutation for a counter, we increment the "hits" column by adding 1.
Amplify gives us a convenient way to override default resolver implementations with a resolvers
folder in amplify/backend/api/counter/resolvers
. Create a file call Mutation.hit.req.vtl
and pop in the Velocity Template Language code below.
$util.qr($context.args.input.put("__typename", "Counter"))
#if( $util.isNullOrBlank($context.args.input.id) )
$util.error("You MUST pass an `id` parameter")
#else
{
"version": "2017-02-28",
"operation": "UpdateItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id)
},
"update": {
"expression": "SET #typename = :typename ADD hits :one",
"expressionNames": {
"#typename": "__typename"
},
"expressionValues": {
":one": $util.dynamodb.toDynamoDBJson(1),
":typename": $util.dynamodb.toDynamoDBJson("Counter")
}
}
}
#end
Another quick amplify push
(go ahead and agree to the prompts) grab yourself a beverage of choice and come back to a shiny new API ready for us to use.
Go ahead and try it out in your GraphQL Editor.
mutation{
hit(input: {
id: "test"
}){
id
hits
}
}
And you should get a response similar to this.
{
"data": {
"hit": {
"id": "test",
"hits": 118
}
}
}
Awesome. Next order of business, make ourselves a little counter.
cd src
from your project root. You will see some other files and folder in there, like aws-exports.js
and graphql
. Create a new file called package.json
with the following content.
{
"name": "counter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "parcel index.html"
},
"author": "",
"license": "ISC",
"devDependencies": {
"parcel-bundler": "^1.12.3"
},
"dependencies": {
"aws-amplify": "^1.1.28"
},
"browserslist": [
"last 2 Chrome versions"
]
}
Once you've got the file in place and saved run npm install
, this may take a few minutes. As part of this, we are installing the aws-amplify Javascript SDK, as well as Parcel, a zero-config bundler so we can bundle up our module and leverage the SDK as well as making it easy to develop on later.
Alright, the final two bits before the big payoff.
First, create an index.html
file in src
.
<!DOCTYPE html>
<html>
<head>
<title>Counter Widget</title>
</head>
<body>
<div data-counter-id="test">Loading...</div>
<script type="text/javascript" src="/index.js"></script>
</body>
</html>
We need this for Parcel to hook in to. Please note, you could be doing most of this in React, Vue, Svelte, and using WebPack or whatever floats your boat. Amplify SDK was leveraged and the rest of the code was written to keep things simple and illustrate the power behind the scenes, I am not trying to proselytize any particular frontend approach.
Finally, we have arrived at the big payoff. Let's create index.js
in src
as well.
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import awsmobile from './aws-exports';
Amplify.configure(awsmobile);
import * as mutations from "./graphql/mutations";
import * as subscriptions from "./graphql/subscriptions";
/*
Find all the unique counter id on the page.
Send a single hit request for each unique ID.
Subscribe to all updates for each one as well.
*/
const init = function createUpdateCounters() {
const countersToUpdate = document.querySelectorAll(`[data-counter-id]`);
const counterHitIdSet = new Set();
countersToUpdate.forEach((counter) => {
counterHitIdSet.add(counter.dataset.counterId);
})
counterHitIdSet.forEach((id) => {
hitCounter(id);
});
}
/*
Send a mutation to your GraphQL to let it know we hit it.
This also means we get back the current count, including our own hit.
*/
async function hitCounter(id) {
const counter = await API.graphql(graphqlOperation(mutations.hit, { input: { id } }));
updateText(counter.data.hit)
subscribeCounter(id)
}
function updateText(counter) {
const countersToUpdate = document.querySelectorAll(`[data-counter-id=${counter.id}]`);
countersToUpdate.forEach(function (elem) {
elem.innerHTML = counter.hits;
})
}
/*
Subscribe via WebSockets to all future updates for the counters
we have on this page.
*/
function subscribeCounter(id) {
const subscription = API.graphql(
graphqlOperation(subscriptions.hits, { id })
).subscribe({
next: (counter) => updateText(counter.value.data.hits)
});
}
// On dom loaded, kick things off
document.addEventListener("DOMContentLoaded", function () {
init();
});
The comments contain most the hints about what is going on, but Amplify and GraphQL do a significant amount of the heavy lifting for us.
Go ahead and run npm start
in the terminal and visit the URL it says your local dev server is started on. If everything works as expected, you should be able to see a counter after a brief Loading...
message.
So, we made it! There are a couple of important takeaways to think about here. What we've done is running on production ready, massively scalable services. This can easily scale to thousands of requests per second (after lifting some default limits). Our actual code written was quite minimal, allowing Amplify to do the heavy lifting, not to mention everything is actually "infrastructure as code" which means we can create whole new app instances and environments on demand.
The mindset of serverless is all about offloading "undifferentiated heavy lifting", leveraging tools and services like AWS Amplify and AppSync get us closer to producing business value and further from managing infrastructure.
Top comments (3)
Very instructive post! Thanks for writing this up.
I wonder what the perfect serverless dev ops website would look like in a web browser.
Thanks for sharing this content