In this module, you deploy your application code to AWS Lambda, a serverless computing service. With AWS Lambda, you don’t need to worry about server management or capacity planning. You simply upload the code you want to run, and AWS runs the code whenever a configured event trigger occurs.

One of the event triggers you can use with Lambda is Amazon API Gateway. Amazon API Gateway lets you configure HTTP endpoints that forward requests to your AWS Lambda functions. Between Lambda and API Gateway, you can build a fully functioning web application without provisioning or managing any servers. Further, both Lambda and API Gateway are billed on a pay-per-use basis, meaning you only pay for what you use.

In the following steps, you walk through setting up a Lambda function and configuring HTTP routes with API Gateway using the AWS Command Line Interface (AWS CLI). If you are building an application using Lambda and API Gateway, use a tool such as AWS Serverless Application Model (AWS SAM) or the Serverless Framework to manage your serverless applications using infrastructure-as-code.

Time to Complete Module: 20 Minutes


  • Step 1. Creating your network resources

    In an earlier module, you configured your ElastiCache instance so that it could be accessible from your Cloud9 development environment. ElastiCache instances must be accessed from inside a VPC rather than over the public internet.

    In this module, you deploy your application code to run on AWS Lambda. For your AWS Lambda function to access your ElastiCache instance, it must be located in a subnet in your VPC and configured with a security group that can access your ElastiCache instance.

    Your Lambda function also needs to access your Amazon Aurora Serverless database using the Data API. The Data API is a publicly available endpoint for your database. For a Lambda function in a private subnet to have access to the public internet, you need to configure a NAT Gateway that translates private network traffic into public internet traffic.

    The internal details of the required network resources are beyond the scope of this tutorial. For now, you can provision the needed resources using the create-networking.sh script in the scripts/ directory.

    You can execute this script with the following command:

    bash scripts/create-networking.sh

    You should see the following output (note this may take a while):

    Fetching VPC Id
    Fetching subnet Id
    Creating Elastic IP address
    Creating NAT Gateway
    Waiting for NAT Gateway to be ready...
    Creating private subnet
    Creating route table
    Creating route
    Associating route table with subnet
    Networking resources created!
    
  • Step 2. Packaging your code and deploying it to Lambda

    Now, you need to package up your function code and deploy it to AWS Lambda. Lambda expects you to upload a ZIP file containing all of your application code. Additionally, you specify a runtime to use and the file and function that serves as the entry point to your code. This entry point is called a handler and is called whenever an incoming event triggers your code.

    In addition to the function code, you also need to provide an AWS Identity and Access Management (IAM) role for your Lambda function. This role is assumed by your function upon execution so that it has permissions to access AWS resources, such as reading or writing from a database, sending messages to a queue, or logging output to Amazon CloudWatch.

    In the scripts/ directory, there is a file called create-lambda.sh. This script does four things:

    1. Zips up the contents of the application/ directory for use in your Lambda function.
    2. Creates an IAM role to be assumed by your Lambda function.
    3. Adds an IAM policy to your IAM role that allows your Lambda function to access the Data API in your Amazon Aurora Serverless database.
    4. Creates the Lambda function by uploading your zip file.

    If you want to see the AWS CLI commands used to run these steps, open the scripts/create-lambda.sh file in your file explorer.

    To execute the script, run the following command in your terminal:

    bash scripts/create-lambda.sh

    You should see the following output:

    Building zip file
    Creating IAM role
    Adding policy to IAM role
    Sleeping for IAM role propagation
    Creating Lambda function
    Lambda function created with ARN <functionArn>
    
  • Step 3. Creating and configuring an API Gateway REST API

    Now that you have deployed your Lambda function, you can make it accessible over HTTP using Amazon API Gateway. API Gateway provides a powerful access layer to your backend services. It is highly configurable and provides for authentication, validation, rate-limiting, and more.

    In your scripts/ directory, there is a file called create-rest-api.sh that provisions an API Gateway REST API and connects it to your Lambda function. This script handles a number of things, including:

    1. Creates a REST API in API Gateway.
    2. Configures a proxy resource and method in the REST API to send all incoming request paths and methods to a single Lambda function.
    3. Adds the permissions for the REST API to trigger your Lambda function.

    API Gateway has a lot of configuration options, and this tutorial cannot cover all of the details. For additional detail, see Amazon API Gateway Concepts.

    Run the following command in your terminal to execute the script and configure your REST API:

    bash scripts/create-rest-api.sh

    You should see the following output in your terminal:

    Creating REST API
    Fetching root resource
    Creating proxy resource
    Creating method
    Adding integration
    Creating deployment
    Fetching account ID
    Adding lambda permission
    REST API created
    
    Your API is available at: https://<id>.execute-api.<region>.amazonaws.com/prod

    You’ve now configured your REST API to connect to your Lambda function. The end of the output included the base URL to access your REST API.

    The value of your base URL was added to the env.sh file. Source that file now to set the BASE_URL environment variable in your terminal.

    source env.sh
  • Step 4. Testing your API endpoint

    Now that your API is live, let’s test it out. You’ll walk through a quick explanation of the code before testing one of your endpoints.

    The web application is built using Express.js, a popular Node.js web application framework. You don’t need to use an existing web framework like Express.js when building applications on AWS Lambda, but it can ease the learning curve if you have experience with Express.

    The core of the Express application is located in application/app.js. Open the file in the file explorer.

    // application/app.js
    const express = require('express')
    const bodyParser = require('body-parser')
    const { fetchUserScores, addUserScore, fetchTopScores } = require('./data')
    const { createCognitoUser, login, verifyToken } = require('./auth')
    const { validateCreateScore } = require('./validate')
    
    const app = express()
    app.use(bodyParser.json())
    
    function wrapAsync(fn) {
      return function(req, res, next) {
        fn(req, res, next).catch(next);
      };
    }
    // Login
    app.post('/login', wrapAsync(async (req, res) => {
      const idToken = await login(req.body.username, req.body.password)
      res.json({ idToken })
    }))
    
    // Create user
    app.post('/users', wrapAsync(async (req, res) => {
      await createCognitoUser(req.body.username, req.body.password, req.body.email)
      res.json({ username: req.body.username })
    }))
    
    // Fetch user scores
    app.get('/users/:username', wrapAsync(async (req, res) => {
      const limit = req.query.limit || 10;
      const scores = await fetchUserScores(req.params.username, limit)
      res.json(scores)
    }))
    
    // Add new score
    app.post('/users/:username', wrapAsync(async (req, res) => {
      const validated = validateCreateScore(req.body)
      if (!validated.valid) {
        throw new Error(validated.message)
      }
      const token = await verifyToken(req.header('Authorization'))
      if (token['cognito:username'] != req.params.username) {
        throw new Error('Unauthorized')
      }
      const score = await addUserScore(req.params.username, req.body.level, req.body.score)
      res.json(score)
    }))
    
    // Fetch top scores
    app.get('/scores/:date', wrapAsync(async (req, res) => {
      const scores = await fetchTopScores(req.params.date)
      res.json(scores)
    }))
    
    app.use(function(error, req, res, next) {
      res.status(400).json({ message: error.message });
    });
    
    module.exports = app

    At the top of this file, you import express and other dependencies, including the authentication helper functions and data access functions that you reviewed in previous modules. Then, you configure the various routes you want in your function, such as /login for a user to login and fetch an ID token or /users/:username to fetch the top scores for a particular user. Finally, at the bottom of the file, the script exports the Express application.

    Next, look at application/handler.js. This is the file with the entrypoint method that you tell Lambda to invoke on an incoming request. The contents of this file are as follows:

    // application/handler.js
    const awsServerlessExpress = require('aws-serverless-express')
    const app = require('./app')
    const server = awsServerlessExpress.createServer(app)
    
    exports.handler = (event, context) => { awsServerlessExpress.proxy(server, event, context) }
    

    This handler is using the aws-serverless-express package. This package converts an incoming API Gateway request into the standard request expected by the Express.js framework. This makes it easy to run Express.js code using Lambda and API Gateway.

    Let’s test your endpoint using the same fetchUser function you saw in an earlier module. This time you go through your web application rather than calling the function directly.

    Run the following command in your terminal:

    curl -X GET ${BASE_URL}/users/ubecker

    You should see the following output in your terminal:

    [{"game_id":101,"username":"ubecker","gamedate":"2019-11-06 09:00:37","score":9090,"level":84},{"game_id":148,"username":"ubecker","gamedate":"2019-11-10 07:53:27","score":8428,"level":30},{"game_id":146,"username":"ubecker","gamedate":"2019-11-06 13:28:49","score":8052,"level":86},{"game_id":33,"username":"ubecker","gamedate":"2019-11-08 08:05:11","score":7218,"level":18},{"game_id":5,"username":"ubecker","gamedate":"2019-11-07 17:56:25","score":6983,"level":91},{"game_id":252,"username":"ubecker","gamedate":"2019-11-05 18:57:45","score":5403,"level":8},{"game_id":245,"username":"ubecker","gamedate":"2019-11-10 06:16:58","score":5230,"level":75},{"game_id":51,"username":"ubecker","gamedate":"2019-11-07 10:44:46","score":5043,"level":2},{"game_id":282,"username":"ubecker","gamedate":"2019-11-07 02:58:57","score":4884,"level":17},{"game_id":106,"username":"ubecker","gamedate":"2019-11-07 07:56:40","score":4394,"level":17}]

    Success! You hit your API endpoint. This triggered your Lambda function, which fetched the top scores for a user from your Amazon Aurora Serverless database using the Data API.


In this module, you deployed your Lambda function and configured your REST API with API Gateway. Then, you saw how your application was working and invoked an endpoint to test it.

In the next module, you walk through all of your application functionality. You create a new user and use your credentials to log in. Then, you record some game scores for your user, check the top scores for your user, and check the top scores overall.