Deploy a Serverless API to Amazon Web Services (AWS)
In a previous article, I wrote about building a serverless contacts API. I walked through setting it up for local development and did not talk about deployments to AWS once the API is finished.
In this article, I will close that loop by showing you how to deploy the same API to your AWS account.
AWS credentials
Let’s start by logging into AWS and creating a serverless-agent account to use for deployments. Give the agent user the recommended permissions per the referenced gist in the Serverless link above.
Once you’ve created the user and downloaded the csv
file containing the access and secret keys, create a folder at your user directory level called .aws
to store the keys.
$ nano ~/.aws/credentials# default for local development
[default]
aws_access_key_id=fake-aws-key
aws_secret_access_key=fake-aws-secret# serverless-agent
[serverless]
aws_access_key_id=key-from-downloaded-csv-file
aws_secret_access_key=secret-from-downloaded-csv-file
Project updates
# These are the files that will need to be updated or added.* package.json
* serverless.yml
* dynamodb.factory.js
* repository.util.js (new)* .env (local only)* Handlers: list.js, get.js, add.js, update.js and delete.js* contact.seeder.js
* runner.js
Besides storing the credentials for AWS, we will also need to update a few files in our API project so that it can properly run once deployed.
Let’s start with the package.json
file. In the original article, I had dotenv
listed in the devDependencies
, it should be listed in dependencies
since it is used by the functions.
We also need to add package
and deploy
scripts to our package.json
file so we can use them to deploy our API to AWS.
# package.json{
...
"scripts": {
"deploy": "serverless deploy --aws-profile serverless",
"package": "serverless package"
},
...
"dependencies": {
"dotenv": "^6.2.0"
},
...
}
Next, update the serverless.yml
file with settings to allow the functions to access DynamoDB and exclude project files that aren’t necessary to run.
Now, update the ContactRepository
and dynamodb.factory.js
files with environment variables and parameters to support dynamic naming of the DynamoDB table.
Finally, create a new utility called repository.util.js
to abstract out the creation of the ContactRepository
and update the handlers to use the utility to create instances of the repository.
The ContactRepository
in the handlers will need to have a table name set during construction to work properly. Since we just created a repository utility to do just that, let’s update the handler code to leverage it.
Test locally
Before we go ahead and deploy this API out to AWS, let’s test to make sure that our changes still work locally.
Update the .env
file to include an entry for the CONTACTS_TABLE
environment variable.
# .envAWS_ENDPOINT='http://localhost:8000'
AWS_REGION='us-west-2'# these keys are no longer needed
# AWS_ACCESS_KEY_ID='fake-access-key'
# AWS_SECRET_ACCESS_KEY='fake-secret-key'# the name of the contact table
CONTACTS_TABLE='contacts-api-dev-contacts'
Make sure DynamoDB is running locally. Then run npm start
to run the API locally, in offline mode.
$ npm start
> contacts-api@1.0.0 start ./contacts_api
> sls offline start
Serverless: Starting Offline: dev/us-west-2.
Serverless: Routes for list:
Serverless: GET /contacts
...
...Serverless: Offline listening on http://0.0.0.0:3000# from a different terminal window, run$ curl -i localhost:3000/contactsHTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 411
accept-ranges: bytes
Date: Sun, 16 Dec 2018 04:08:48 GMT
Connection: keep-alive
[{"firstName":"Luke","lastName":"Skywalker", ..., ... }]
NOTE: You might need to update the contact.seeder.js
and runner.js
files in the /seed
folder if you need to reseed the table.
# seed/contact.seeder.js# Update the constructor to accept a 'tableName' parameter
# and assign it to 'this._tableName' to replace the hardcoded
# 'contacts' string....
constructor(dynamodb, docClient, tableName) {
...
...
//this._tableName = 'contacts';
this._tableName = tableName;
}
...# seed/runner.js# Extract the 'CONTACTS_TABLE' environment from process.env and
# use it in the instantiation of the 'ContactSeeder'.
# Optionally, update the 'log' statements with 'CONTACTS_TABLE'
# instead of using the hard coded 'contacts' string.const { CONTACTS_TABLE } = process.env;
...
...
const contactSeeder =
new ContactSeeder(dynamo, doclient, CONTACTS_TABLE);# running 'npm run seed' should now work properly.
Packaging
We should do one final verification by packaging the API before we actually deploy to AWS.
Serverless includes a package
command that will create the deployment package locally in a temp .serverless
folder for us to inspect.
Run the npm run package
script we added earlier to the package.json
file, then open up the .serverless
folder.
$ npm run package> contacts-api@1.0.0 package ./contacts_api
> serverless package
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Open the .serverless
folder and extract the contacts-api.zip
file. Expand the folders and make sure that only node_modules
and src
are included in the zip.
The contacts-api
folder should look like this:
Deploy to AWS
Now that we have confirmed that all of our changes work locally, let’s deploy to AWS and see if it works there.
Notice that the npm run deploy
script calls, serverless deploy --aws-profile serverless
. This matches the AWS credentials we set up at the beginning of this article.
$ npm run deploy
> contacts-api@1.0.0 deploy /contacts_api
> serverless deploy --aws-profile serverless
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (15.45 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.............................................................
Serverless: Stack update finished...
Service Information
service: contacts-api
stage: dev
region: us-west-2
stack: contacts-api-dev
api keys:
None
endpoints:
GET - https://<auto-gen>.amazonaws.com/dev/contacts
GET - https://<auto-gen>.amazonaws.com/dev/contact/{id}
POST - https://<auto-gen>.amazonaws.com/dev/contact
PUT - https://<auto-gen>.amazonaws.com/dev/contact/{id}
DELETE - https://<auto-gen>.amazonaws.com/dev/contact/{id}
functions:
list: contacts-api-dev-list
get: contacts-api-dev-get
add: contacts-api-dev-add
update: contacts-api-dev-update
delete: contacts-api-dev-delete
layers:
None
We can now call each of the endpoints to test if they work, starting with list.
$ curl -i https://<auto-gen>.amazonaws.com/dev/contactsHTTP/2 200
content-type: application/json
content-length: 2
...
...# An empty array because we haven't posted or seeded data.
[]
Posting data (add).
$ curl -i \
-H 'Content-type: application/json' \
-X POST \
-d '{"id": "1", "firstName": "Jin", "lastName": "Erso"}' \
https://<auto-gen>.amazonaws.com/dev/contactHTTP/2 201
content-type: application/json
content-length: 0
...
...
Fetching contacts list again to verify (list).
$ curl -i https://<auto-gen>.amazonaws.com/dev/contactsHTTP/2 200
content-type: application/json
content-length: 48
...
...# There's the data we posted to the POST endpoint!
[{"id":"1","firstName":"Jin","lastName":"Erso"}]
Fetch by id (get).
$ curl -i https://<auto-gen>.us-west-2.amazonaws.com/dev/contact/1HTTP/2 200
content-type: application/json
content-length: 46
...
...# Notice that it's the contact and not an array of contacts like
# when we called the GET /contacts endpoint.
{"id":"1","firstName":"Jin","lastName":"Erso"}
Updating a contact (update).
$ curl -i \
-H 'Content-type: application/json' \
-X PUT \
-d '{"id": "1", "firstName": "Jin-updated", "lastName": "Erso-updated"}' \
https://<auto-gen>.amazonaws.com/dev/contact/1HTTP/2 200
content-type: application/json
content-length: 62
...
...# The updated contact is returned.
{"id":"1","firstName":"Jin-updated","lastName":"Erso-updated"}
Finally, delete.
$ curl -i \
-X DELETE \
https://<auto-gen>.amazonaws.com/dev/contact/1HTTP/2 204
...
...# Nothing is returned.
The source branch can be found here:
The original article can be found here:
I mentioned running AWS/DynamoDB out of a Docker container which can be found here:
Here is the detailed guide to Serverless around AWS configurations and deployment: