Starter template for a todo app.
S3 -- API Gateway -- Lambda -- DynamoDB
Disclaimer: for ease of use, the API will have CORS fully enabled, reveals server error details to the client, and has no authentication. You shouldn't have such a configuration on an actual production system.
-
AWS credentials on your computer:
aws configure
-
Serverless Framework:
npm install -g serverless
Copy starter files from our template:
serverless create --template-url https://github.com/DevOps-Girls/DevOps-Girls-Bootcamp-4/tree/master/serverless-starter-todo
# Serverless: Generating boilerplate...
# Serverless: Downloading and installing "serverless-starter-todo"...
# Serverless: Successfully installed "serverless-starter-todo"
cd serverless-starter-todo
Open the serverless.yml file, which describes a serverless application.
It begins by listing out some basic details:
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
# Replace these square brackets with IAM permissions.
[]
- We'll be running on AWS
- We're using the Node.js JavaScript runtime for our API
- We'll default the application to run in Sydney (ap-southeast-2)
- We'll default the application stage to pre-production (dev)
It also includes a CloudFormation resources section at the end, which is AWS's
way of managing infrastructure as code. We can describe a DynamoDB table, S3
bucket, and much more here, and they will be automatically created/updated
whenever we run serverless deploy
.
resources:
Resources:
# Replace these curly brackets with CloudFormation resources.
{}
Outputs:
# Replace these curly brackets with CloudFormation outputs.
{}
S3 -- API Gateway -- Lambda -- DynamoDB
^^
Have a quick look at the CloudFormation documentation for S3 buckets:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html
Describe the bucket under the resources section:
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
# add properties here
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt:
- WebsiteBucket
- WebsiteURL
Properties that we want to add:
- WebsiteConfiguration: we want to configure the bucket for website hosting, and set the home page to index.html
Add a bucket policy below it:
resources:
Resources:
WebsiteBucket:
# same as above
...
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: '*'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: WebsiteBucket
- /*
Bucket:
Ref: WebsiteBucket
This allows anyone to read the files in the bucket, which is what we want, as the website should be accessible from any device or browser.
Let's run a serverless deploy
to create our S3 bucket:
serverless deploy --verbose
# Service Information
# service: serverless-starter-todo
# stage: dev
# region: ap-southeast-2
# stack: serverless-starter-todo-dev
# api keys:
# None
Verify your changes in the AWS web interface:
What do you see?
Show
You should see a new CloudFormation stack:
And that stack creation should have included an S3 bucket with a public bucket policy:
S3 -- API Gateway -- Lambda -- DynamoDB
^^
Review the output of serverless deploy
:
# Stack Outputs
# WebsiteBucketName: serverless-starter-todo-dev-websitebucket-xxxxxxxxxxxxx
# WebsiteURL: http://serverless-starter-todo-dev-websitebucket-xxxxxxxxxxxxx.s3-website-ap-southeast-2.amazonaws.com/
You'll find the name of your S3 bucket.
Upload the HTML, CSS and JS files in the ui/dist
folder to S3 (use your real
bucket name):
aws s3 sync ui/dist/ s3://serverless-starter-todo-dev-websitebucket-xxxxxxxxxxxxx
Try visiting your website (use your real bucket name as the subdomain):
http://serverless-starter-todo-dev-websitebucket-xxxxxxxxxxxxx.s3-website-ap-southeast-2.amazonaws.com/
What do you see?
Show
You should be able to see the basic layout of the todo app.
However, the todo list would be empty, and you wouldn't be able to add any new todos. This is because we haven't set up a backend that the website can read todos from and write todos to.
S3 -- API Gateway -- Lambda -- DynamoDB
^^^^^^^^^^^^^^^^^^^^^
To create a serverless API with API Gateway and Lambda, we need to fill out the
functions section of our serverless.yml
file.
What do you think this section does?
functions:
TodoApi:
name: serverless-starter-todo-api-${self:provider.stage}
handler: index.handler
environment:
# Replace these curly brackets with environment variables.
{}
events:
- http:
cors: true
method: any
path: /{proxy+}
Show
The functions section describes a Lambda function that can respond to HTTP requests.
index.handler tells a (JavaScript) Lambda function to open a index.js file and call the handler function when a HTTP request is received.
We have configured API Gateway to allow any method, and any path with the {proxy+} path variable. This is Lambda proxy integration.
In short, it means that all types of requests are sent to our Lambda function, and our code is responsible for checking the request method and path, and figuring out what to do.
Run the serverless deploy
command:
serverless deploy --verbose
# Service Information
# service: serverless-starter-todo
# stage: dev
# region: ap-southeast-2
# stack: serverless-starter-todo-dev
# api keys:
# None
# endpoints:
# ANY - https://1234567890.execute-api.ap-southeast-2.amazonaws.com/dev/{proxy+}
# functions:
# TodoApi: serverless-starter-todo-dev-TodoApi
#
# Stack Outputs
# TodoApiLambdaFunctionQualifiedArn: arn:aws:lambda:ap-southeast-2:123456789012:function:serverless-starter-todo-api-dev:1
# ServiceEndpoint: https://1234567890.execute-api.ap-southeast-2.amazonaws.com/dev
# ServerlessDeploymentBucketName: serverless-starter-todo-serverlessdeploymentbuck-abcdefghijkl
Review your changes in the AWS web interface.
What do you see?
Show
You should see that a new API Gateway has been created:
And a Lambda function has been created and automatically hooked up to that API Gateway:
Try out your new API (use your real endpoint, and add /todos
on the end):
https://xxxxxxxxxx.execute-api.ap-southeast-2.amazonaws.com/dev/todos
What do you see?
Show
You should see something like this:
{
"error": "Error: I don't have a TABLE_NAME environment variable, so I don't know where to read and write your todos.",
"message": "error handling request"
}
The issue with our Lambda function is that it's missing the name of the database table to store our todos in. That's because we haven't created the database table yet!
We can also look at the logs to see what happened:
S3 -- API Gateway -- Lambda -- DynamoDB
^^^^^^^^
We now have an API that we can call from our website to read and write todos. The API needs to store the todos somewhere, so that they aren't lost once you close your browser tab, and so you can access them across your laptop, phone, etc.
Have a quick look at the CloudFormation documentation for DynamoDB tables:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
Add a database table to the resources section:
resources:
Resources:
DatabaseTable:
Type: AWS::DynamoDB::Table
Properties:
# more stuff here
Properties that we want to add:
- AttributeDefinitions, KeySchema: we want to create an id primary key in our table
- BillingMode: set this to PAY_PER_REQUEST to avoid paying ongoing costs while the database is not doing anything
Run serverless deploy
to create your database table:
serverless deploy --verbose
Review your changes in the AWS web interface:
https://console.aws.amazon.com/dynamodb/home
What do you see?
S3 -- API Gateway -- Lambda -- DynamoDB
^^
Add permissions for your Lambda function to communicate with your new table:
provider:
...
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DeleteItem
- dynamodb:PutItem
- dynamodb:Scan
Resource:
Fn::Join:
- ''
- - 'arn:aws:dynamodb:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- :table/
- Ref: DatabaseTable
Add the table's name as an environment variable, so your Lambda function knows where to store the todos:
functions:
TodoApi:
...
environment:
TABLE_NAME:
Ref: DatabaseTable
Run serverless deploy
to create the links between your Lambda function and
DynamoDB table:
serverless deploy --verbose
Try out your new API (use your real endpoint, and add /todos
on the end):
https://xxxxxxxxxx.execute-api.ap-southeast-2.amazonaws.com/dev/todos
What do you see?
Show
You should see something like this:
[]
S3 -- API Gateway -- Lambda -- DynamoDB
^^
Click the Set API URL link and paste your API URL into the input field (without /todos
on the end):
Try to add, edit, and delete some todos!
If you've fallen slightly behind, you can copy the completed serverless.yml
for each step below:
serverless.yml
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
# Replace these square brackets with IAM permissions.
[]
functions:
# Replace these curly brackets with a Lambda function.
{}
resources:
Resources:
# Replace these curly brackets with CloudFormation resources.
{}
Outputs:
# Replace these curly brackets with CloudFormation outputs.
{}
serverless.yml
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
# Replace these square brackets with IAM permissions.
[]
functions:
# Replace these curly brackets with a Lambda function.
{}
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument: index.html
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: '*'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: WebsiteBucket
- /*
Bucket:
Ref: WebsiteBucket
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt:
- WebsiteBucket
- WebsiteURL
(no file changes)
serverless.yml
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
# Replace these square brackets with IAM permissions.
[]
functions:
TodoApi:
name: serverless-starter-todo-api-${self:provider.stage}
handler: index.handler
environment:
TABLE_NAME:
# Replace these quotes with a DynamoDB table name.
''
events:
- http:
cors: true
method: any
path: /{proxy+}
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument: index.html
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: '*'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: WebsiteBucket
- /*
Bucket:
Ref: WebsiteBucket
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt:
- WebsiteBucket
- WebsiteURL
serverless.yml
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
# Replace these square brackets with IAM permissions.
[]
functions:
TodoApi:
name: serverless-starter-todo-api-${self:provider.stage}
handler: index.handler
environment:
TABLE_NAME:
# Replace these quotes with a DynamoDB table name.
''
events:
- http:
cors: true
method: any
path: /{proxy+}
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument: index.html
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: '*'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: WebsiteBucket
- /*
Bucket:
Ref: WebsiteBucket
DatabaseTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt:
- WebsiteBucket
- WebsiteURL
serverless.yml
service: serverless-starter-todo
provider:
name: aws
region: ${opt:region, 'ap-southeast-2'}
runtime: nodejs8.10
stackName: serverless-starter-todo-${self:provider.stage}
stage: ${opt:stage, 'dev'}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DeleteItem
- dynamodb:PutItem
- dynamodb:Scan
Resource:
Fn::Join:
- ''
- - 'arn:aws:dynamodb:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- :table/
- Ref: DatabaseTable
functions:
TodoApi:
name: serverless-starter-todo-api-${self:provider.stage}
handler: index.handler
environment:
TABLE_NAME:
Ref: DatabaseTable
events:
- http:
cors: true
method: any
path: /{proxy+}
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument: index.html
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: '*'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: WebsiteBucket
- /*
Bucket:
Ref: WebsiteBucket
DatabaseTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt:
- WebsiteBucket
- WebsiteURL
(no file changes)