Auction Service Setup
Anatomy of a Serverless Project
In your serverless.yml
file, we have important sections:
service
: where you define metadata about your servicename
gets used in the deployment names
plugins
: serverless plugins that you want to useprovider
: where you give information about your cloud providername
sets providerruntime
sets environment being run inside AWS lambda functionmemorySize
globally sets amount of memory allocated for AWS lambda functionCan be set separately per function
Helps optimize costs
stage
defines whether you're on dev, staging, qa, production, etc.region
sets the zone where you will deploy to the cloud
functions
: where we define functions used by deployment. In each function config, we have the following...handler
is the actual code that will be executed when function is runevents
defines the events that trigger the functionThese can be
http
,sqs
,schedule
, etc.
Note about variables: The stage
property uses a Serverless variable using the syntax by ${variable, fallback}
. In the example above, we access the opt:stage
variable, which is a variable you can create when setting custom options. Then if that variable is undefined, we provide a fallback: 'dev'
.
Note about handler syntax: The handler
property accesses a named variable called handler
in the hello.js
file. (This structure will make sense when we implement middleware.)
custom
section
custom
sectionThe custom
section in your serverless.yml
is the place where you can define your own variables that aren't part of the core Serverless framework. For example, it's commonly used to configure your plugins.
Deploying Application using Serverless
To deploy, just make sure your IAM user information has been configured (multiple ways to do that), and then run serverless deploy -v
.
Note: It's fun to add the -v
flag to see exactly what Serverless is doing.
Viewing results in AWS
In AWS, set your region to one that was set in your
serverless.yml
.Go to CloudFormation service and select "Stacks".
Inside, you'll find your stack! (A stack is a grouping of your applications and the resources they need.)
Inside your stack, you have access to "Events". This tells you what happened during deployment.
You also have "Resources". It tells you all the resources configured.
Notable resource:
ServerlessDeploymentBucket
is where the files containing the code for your lambda functions can be found.LogGroup
is a CloudWatch log group that allows you to see log streams of executions.IamRoleLambdaExecution
sets the policies for the lambda function (what it can and can't do).LambdaFunction
is the function itself. You can view the contents of the function's code here.ApiGatewayRestApi
is where we expose our functions to the internet through endpoints.
Stack Removal
To remove a stack, just type sls remove -v
while in your application root directory.
Creating Our First Lambda Function
We want to build a /createAuctions
API Gateway endpoint that triggers a lambda function.
Configuring serverless.yml
serverless.yml
In serverless.yml
, we write:
Understanding the event
, context
, and response objects in a lambda function
event
, context
, and response objects in a lambda functionThen in src/handlers/createAuction.js
, we have our lambda function:
Pro tip: You can add custom data to both event
and context
via middleware. For example, if the lambda function requires a userId
, you could add it via middleware. (We'll do this later.)
Note: If you only changed the lambda function code itself, you could just re-deploy the lambda function alone. In our case, we have to run sls deploy
because our changes to the serverless.yml
file affect API Gateway and other services as well.
Writing the lambda function code
The following code just creates an auction
object and returns it. We will write it to a database next.
With this change, it's purely lambda function code, so we can redeploy with the command sls deploy -f createAuction
. (Behind the scenes, Serverless just re-deploys the handler file in serverless.yml
.)
What is DynamoDB?
DynamoDB is a fully managed, serverless, noSQL database provided by AWS.
It's fully managed in that it automatically spreads your data and traffic across a sufficient number of servers.
The major parts of the data in a DynamoDB database are:
Tables (like collections)
Items (like documents)
Attributes (like properties)
Query vs. scan
The scan operation scans through an entire table. The query operation allows you to search via a primary key or secondary index.
Between the two, querying is more efficient. Scanning is a last resort.
Primary key
DynamoDB is schema-less except for the primary key in a table. This must be specified when you create a table, and it guarantees a unique identifier.
Types of primary keys:
Partition key (e.g. id)
Composite primary key: partition + sort key (e.g. id + createdAt)
Secondary index
Secondary indices allow for greater flexibility in your queries.
2 types of secondary indices:
Global secondary index: partition + sort key
You can create up to 20 of these per table
Local secondary index: partition + extra sort key
Read consistency
Read consistency has to do with how accurate the data is at the point of read.
There are a few types of read consistency:
Eventually consistent reads: the response doesn't necessarily reflect the results of a recently completed write operation. (This is a result of the fact that DynamoDB spreads your data across zones for better durability and high availability.)
IE: If you just wrote and item to the database and then wanted to read it, you might not get that item back.
Strongly consistent reads: Guarantee that you'll get the most up-to-date reads
Might not be available during network delay or outage
Potential higher latency
Not supported on global secondary indices
More throughput capacity (i.e. more costs)
Pro tip: The type of read you perform should depend on use case. Unless you need to present your user with data you just wrote, you probably want to go with an eventually consistent read.
Read/write capacity modes
There are 2 modes used during reads/writes:
On-demand mode: good for tables where you can't predict the workload you expect
Flexible
Can serve 1000s of requests per second
Pay per request
No need to plan capacity ahead of time
Adapts to workload
Delivery time is single-digit ms latency (SLA)
Provisioned mode: good for when you can predict the workload
Read/write capacity per second must be specified
Can specify auto-scaling rules
Can reserve capacity in advance, which can reduce costs
Capacity measured as Read Capacity Units (RCU) and Write Capacity Units (WCU)
Definitions of RCU and WCU:
RCU:
1 strongly consistent read per second, or
2 eventually consistent reads per second
Maximum 4KB in size
WCU:
1 write per second
Maximum 1KB in size
Note: If you go over size limit, that's not bad. It just means your read/write operation will use more than 1 RCU or 1 WCU.
DynamoDB streams
This is an optional feature that allows you to react to events on creation, update, or deletion.
Example: When a user gets added to the UsersTable
, you can trigger a lambda function that sends them a welcome email.
Adding DynamoDB as a Resource
Since we're practicing IaaS, we create our DynamoDB database in the serverless.yml
file.
This goes under the resources
section and uses CloudFormation syntax (language created by AWS to define resources):
Note: You need to set AttributeDefinitions
and KeySchema
together, or else it won't work. Not sure why.
Inserting Items into a DynamoDB Table
To add items, we need a few things set up:
A way to generate a unique
id
propertyAccess to DynamoDB
Generating a unique id
id
Since we set id
as our primary key, it's required. So in our lambda function, we can use uuid
to generate a unique value:
Writing to DynamoDB
AWS provides an SDK package called aws-sdk
. It allows you to interact with tons of AWS services in your lambda functions.
Pro tips:
It's safe to place your
dynamoDb
client in the outer scope because it's relatively constant.In contrast, you should never place variables in the outer scope that are dynamic.
That's because lambda functions can turn off when not used, destroying memory of those variables.
Every method available in
aws-sdk
uses the callback pattern by default. You can append.promise()
if you want to use the promise pattern instead.
UH OH: If you invoke this lambda function, it will give you an AccessDeniedException
. That's because our lambda function doesn't have write permissions.
Adding IAM role statements (permissions)
When you create a lambda function, it receives an IAM role that defines the access given to the function. By default, Serverless framework only gives us write access to CloudWatch (for logging).
To add DynamoDB, we add a iamRoleStatements
property to our serverless.yml
(either globally under provider
or specifically in your lambda function):
Note about Resource
:
You input your Amazon Resource Name (ARN) here. It's a unique identifier for your specific resource.
Notice
#{AWS::Region}
and#{AWS::AccountId}
? These are pseudo-parameters provided by theserverless-pseudo-parameters
plugin.By making our ARN dynamic, we have flexibility around what region or AWS account we want to deploy our application in.
After re-deploying, now the createAuction
lambda function should work!
Cleanup: Optimizing serverless.yml
serverless.yml
As it stands, everything lives in our serverless.yml
. But what if we keep adding more and more iamRoleStatements
or resources
? Our file could get huge.
How can we separate out the file and make everything more readable?
The file
function
file
functionYAML is a superset of JSON. That means it's just a grouping of objects and their properties.
We can therefore take snippets of YAML and place it in its own file.
Then simply reference it using Serverless framework's built-in file
function:
Note: file
accepts a relative path from your serverless.yml
. Then you access the object attached to the name AuctionsTableIAM
.
Intrinsic functions and custom variables
To make our AWS configuration even more dynamic, we want a few things:
Append the stage name to the table name (like
AuctionsTable-dev
), so we can run a unique table for each stageDynamically obtain the (a) table name and (b) ARN from CloudFormation
Dynamically use the table name in our lambda function
To achieve this, here are a few more tricks we'll use:
Referencing contents of the
serverless.yml
fileCustom variables in the
custom
sectionIntrinsic functions provided by CloudFormation
Setting environment variables
To append the stage to the table name, all we have to do is reference provider.stage
found in the serverless.yml
file. You do this with the keyword self
, which is a reference to that file.
To dynamically obtain the DynamoDB table name and ARN from CloudFormation, we have access to intrinsic functions that we can use as our resources are being deployed. We will set them in the custom
section for easy access.
Now that we have these set, we can use arn
to dynamically set the ARN in our IAM role:
Finally, to dynamically set the table name in our lambda function, we can use custom.AuctionsTable.name
and set it as an environment variable:
Then we just reference the variable in the lambda function:
Note: You can set the environment
either globally inside provider
, or you can set it locally inside your lambda function's configuration.
Is Serverless Offline Worth It?
Serverless Offline emulates AWS Lambda and API Gateway (using Express), allowing you to run your application locally.
Pro tip:
As soon as your application gets more complex and brings in more and more resources, you're going to find you'll have to mock them too.
This puts dependency on community-maintained libraries, which often times don't match how AWS actually works.
These libraries also take a lot of effort to make them work together, turning your configuration into a mess.
Solution: Embrace Serverless framework until Serverless Offline gets better.
Last updated