Deploy a Serverless API to Amazon Web Services (AWS)

Van Huynh
ITNEXT
Published in
6 min readDec 17, 2018

--

Serverless, Inc., Amazon Web Services

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.

Updated serverless.yml file.

Now, update the ContactRepository and dynamodb.factory.js files with environment variables and parameters to support dynamic naming of the DynamoDB table.

dynamodb.factory.js
contact.repository.js

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.

repository.util.js

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.

Remember that line 24 and 26 are the new lines that need to be applied to all handlers.

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:

.serverless folder

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/contact
HTTP/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/1
HTTP/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/1
HTTP/2 204
...
...
# Nothing is returned.

--

--

Software developer, car enthusiast, gamer, mentor and lifelong learner. I love a good pair-programming session.