Serverless API Deployment

Background

Previously we provided an overview of the Serverless API and ran its operations locally. Next we will explain how this API is deployed to the AWS cloud, to run as lambda functions.

Serverless Deployment Stack

Our Cloud API is implemented using the Serverless Framework and lambda functions. In this post we will deploy the API to the AWS API Gateway, and our overall cloud architecture will then use the whole Serverless stack:

The Serverless API will provide data to SPA, mobile and desktop clients. Although the final Single Page Application uses secure cookies, the API will only need to deal with JWT access tokens, in all three cases.

Cloud API URLs

Previously we hosted our Final SPA at the below CloudFront URL:

Next the Serverless API will be deployed to this AWS API Gateway URL:

Prerequisite Setup

In earlier posts we ensured that prerequisites were in place, and these are needed in order for the below API deployment to work:

Deployment Overview

The API has a number of commands in its package.json file, which point to scripts that run Serverless Framework deployment commands:

{
  "scripts": {
    "lint": "npx eslint . --ext .ts,.tsx",
    "build": "rm -rf dist && tsc",
    "buildRelease": "rm -rf dist && tsc --sourceMap false",
    "start": "./start.sh",
    "test": "./start.sh",
    "deploy": "./deploy.sh",
    "remove": "sls remove --stage deployed"
  }
}

The deployment script first runs ‘sls package‘ to build a ZIP file, then runs ‘sls deploy‘ to push resources to the cloud. This creates lambda functions, configures AWS API Gateway endpoints, and enables Cloudwatch logging.

The Serverless.yml file points to a custom domain that needs to be precreated before the first deployment. The sample uses a single AWS deployment stage of ‘deployed‘, though there is also a ‘local‘ stage used when running lambdas locally:

service: serverlessapi

provider:
  name: aws
  runtime: nodejs20.x
  region: eu-west-2
  stage: ${self:custom.config.settings.stage}
  apiGateway:
    shouldStartNameWithService: true

custom:
  customDomain:
    domainName: ${self:custom.config.settings.apiHostName}
    certificateName: '*.${self:custom.config.settings.certificateDomainName}'
    basePath: investments
    stage: ${self:custom.config.settings.stage}
    endpointType: regional

API Configuration

The Serverless API uses the same JSON based approach to configuration as earlier APIs, and uses these settings when deployed to AWS:

{
    "api": {
        "useProxy": false,
        "proxyUrl": ""
    },
    "logging": {
        "apiName": "SampleApi",
        "prettyPrint": false,
        "performanceThresholdMilliseconds": 500
    },
    "oauth": {
        "issuer":           "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_CuhLeqiE9",
        "audience":         "",
        "algorithm":        "RS256",
        "scope":            "https://api.authsamples.com/investments",
        "jwksEndpoint":     "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_CuhLeqiE9/.well-known/jwks.json"
    },
    "cache": {
        "isActive": true,
        "region": "eu-west-2",
        "tableName": "OAUTH_CACHE",
        "claimsCacheTimeToLiveMinutes": 15
    }
}

Building Code

Our earlier Express API was run on a development computer using ts-node, then built to Javascript for release builds. Lambdas must always be built to Javascript, so tsc is always used, with the following tsconfig.json settings:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "lib": ["ES2022"],
    "module":"ES2022",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "outDir": "dist",
    "sourceMap": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

Deployment Package

The deployment package is output to a .serverless folder, and files that should not be deployed are excluded via the Serverless.yml file. Only  production Node.js dependencies are included, and those in the package.json devDependencies section do not get uploaded to AWS:

The size of each lambda is around 1MB, including dependencies for JWT libraries and to make HTTP requests to the Authorization Server. Cloud Formation is also generated, to automate the creation of AWS resources.

Configure the API Subdomain

The main hosting domain of authsamples.com was created in the earlier Cloud Domain Setup. An API custom domain must also be pre-created in AWS API Gateway. This points to the wildcard certificate generated earlier, and is linked to the deployment stage under API Mappings:

In Route 53 / Hosted Zones, the above API Gateway Domain Name must then be mapped to an A Record for the custom domain name, pointing to the generated value:

API Deployment

The deployment script then continues by deploying the ZIP file to AWS, which will create Cloud HTTPS Endpoints that route to the API’s lambda functions:

Once deployed our Sample API will be created in API Gateway, and we can view results in the AWS Console:

The deployment configures an Integration Request for each incoming HTTP request, to invoke the relevant lambda:

Our API has 3 simple endpoints that exist at the below internet URLs, and the third of these is parameterised via path segments:

The deployed API then provides data for all of this blog’s final UI code samples, as summarised in this blog’s Quickstart page.

Deployed Lambdas

In the AWS Console, I can then view the deployed lambda functions, which run in the London region:

The compiled Javascript of each lambda can also be inspected. I actually deploy all API code for each individual lambda, since this is the simplest deployment model, and it enables code sharing.

Lambdas can be packaged individually if preferred, and it is possible to use Serverless Plugin Scripts to run custom packaging logic. This might involve excluding certain folders or dependencies for certain lambdas.

DynamoDB Resources

In order to perform OAuth related caching, the Serverless.yml file also creates some DynamoDB resources:

resources:
  Resources:

    CacheTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: OAUTH_CACHE
        AttributeDefinitions:
          - AttributeName: CACHE_KEY
            AttributeType: S
        KeySchema:
          - AttributeName: CACHE_KEY
            KeyType: HASH
        TimeToLiveSpecification:
          AttributeName: TTL_VALUE
          Enabled: true
        BillingMode: PAY_PER_REQUEST

This creates a table that contains key value pairs, for both the JWKS data and custom claims, looked up when a Cognito access token is first received:

Although this works, it is less optimal than the in-memory caching used previously for cloud native APIs.

API Immediate Logs

In Cloudwatch, log groups have been added for each of the lambdas, and log retention is configured to lasts only a few days. The role of these logs is similar to that of the immediate log files in our earlier Node.js APIs.

Log entries can be viewed in the AWS console, but Cloudwatch is only used for immediate logging. In a real company setup, logs would therefore need to be aggregated to a more powerful log analysis system.

In order to perform the above logging, the API’s code needs access to the full request and response details. Therefore I would recommend always using the REST API option, since any API should have access to this data:

Troubleshooting Lambda Startup

Errors that prevent AWS from calling the lambda entry point are not reported in Cloudwatch logs. Instead you must activate API Gateway Logging when needed. First define an IAM role with log permissions:

From API Gateway, the extra logging can then be temporarily activated when needed. An extra Cloud Watch Log Group is then available, with further info on the startup error

A common cause of this type of error is when a Node.js dependency is include in devDependencies when it should be in dependencies. In this case the dependency is not deployed and the lambda fails to start.

Where Are We?

We have ported our earlier Node.js API to run with low cost and zero maintenance in AWS. Serverless lambdas are interesting to know about, and in the next post we will explain more about the OAuth implementation.

Next Steps