Serverless Framework with custom authoriser locally (episode 2)

Serverless Framework with custom authoriser locally (episode 2)

Background

Booyakasha! 🤜

This is the second episode of the AWS Serverless series. In the first episode, we walked through an example of AWS SAM and a custom authorizer.

Today, we are going to explore another approach - Serverless Framework!
Serverless Framework has serverless-offline and serverless-esbuild plugins, which can help you write Lambda in TypeScript and run it with custom authoriser locally.

TL;DR

Let's walk through it

We are going to implement a /ping API behind an API Gateway and Custom Authoriser using TypeScript.

The architecture diagram as following:

sam-api-gateway-custom-authoriser-example.drawio

If you have read the first episode, you will notice that we are doing the same thing here, but with the Serverless Framework.

We have five steps to implement for this:

  1. Setting Up Serverless Plugins
  2. Creating a Basic API Gateway
  3. Creating a Ping Lambda
  4. Creating an Authoriser Lambda
  5. Verifying the Result

Setting Up Serverless Plugins

We are going to use the following two plugins to create a seamless development experience:

You can use the serverless CLI to generate a new project. However, I recommend cloning the repository serverless-local-authorizer-example to get a workable example.

Once you have a Serverless project, you can install these packages.

$ npm install --save-dev @serverless/typescript 
$ npm install --save-dev serverless-esbuild
$ npm install --save-dev serverless-offline
$ npm install --save-dev ts-node tsconfig-paths typescript

All set, you can now create a {project_root_path}/serverless.ts file with the following content.

import type { AWS } from '@serverless/typescript';

const serverlessConfiguration: AWS = {
  service: 'api',
  frameworkVersion: '3',
  plugins: ['serverless-esbuild', 'serverless-offline'],
  provider: {},
  functions: {},
  package: { individually: true },
  custom: {
    esbuild: {
      bundle: true,
      minify: true,
      sourcemap: true,
      exclude: ['aws-sdk'],
      target: 'node20',
      define: { 'require.resolve': undefined },
      platform: 'node',
      concurrency: 10,
    },
  },
};

module.exports = serverlessConfiguration;

Creating a Basic API Gateway

Let's define a very simple API Gateway and some general settings in the provider field of  {project_root_path}/serverless.ts.

...

const serverlessConfiguration: AWS = {
  service: 'api',
  frameworkVersion: '3',
  plugins: [ ... ],
  provider: {
    name: 'aws',
    region: "ap-southeast-1",
    runtime: 'nodejs20.x',
    apiGateway: {
      shouldStartNameWithService: true,
    },
    environment: {},
  },
  functions: {},
  package: { ... },
  custom: {
    ...
  },
};

...

Creating a Ping Lambda

Now, we are going to add the first Ping Lambda to this API Gateway.

We need the following three files for the Ping Lambda.

  • src/functions/ping/handler.ts a handler returns 200 and a pong message.
import { APIGatewayProxyHandler } from 'aws-lambda';

export const lambdaHandler: APIGatewayProxyHandler = async (_event, _context) => {
  return {
    statusCode: 200,
    body: "pong",
  }
};
  • src/functions/ping/index.ts a serverless definition for the Lambda.
import { handlerPath } from '@libs/handler-resolver';

export default {
  handler: `${handlerPath(__dirname)}/handler.lambdaHandler`,
  events: [
    {
      http: {
        method: 'get',
        path: 'ping',
        authorizer: 'authorizer',
      },
    },
  ],
};
  • src/libs/handler-resolver.ts a helper function to get the current path.
export const handlerPath = (context: string) => {
  return `${context.split(process.cwd())[1].substring(1).replace(/\\/g, '/')}`
};

Now, we've created the Ping Lambda and set the authorizer to a function named authorizer.

Next step, we need to reference this Lambda in the serverless.ts

import type { AWS } from '@serverless/typescript';

import ping from '@functions/ping';

...
const serverlessConfiguration: AWS = {
  ...
  functions: {
    ping,
  },
  ...
}
...

You may have noticed that we use @libs and @serverless while importing stuff. Please follow the tsconfig.json configuration if you fancy this setup.

Creating an Authoriser Lambda

Alright, we are almost there!

We can create an authoriser that does nothing but returning a policy to allow GET/ping request.

  • src/functions/authorizer/handler.ts a handler acts as a Custom Authoriser.
import { APIGatewayTokenAuthorizerHandler, APIGatewayAuthorizerResult } from 'aws-lambda';

export const lambdaHandler: APIGatewayTokenAuthorizerHandler = async (_event, _context) => {
  return generateAdminPolicy();
};

const generateAdminPolicy = () => {
  const authResponse: APIGatewayAuthorizerResult = {
    principalId: `systemadmin`, // you can assign principalId if you want
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Allow',
          Resource: 'arn:aws:execute-api:*:*:*/*/GET/ping', // allow access GET/ping
        },
      ],
    },
  };
  return authResponse;
}
  • src/functions/authorizer/index.ts a serverless definition for the Lambda.
import { handlerPath } from '@libs/handler-resolver';

export default {
  handler: `${handlerPath(__dirname)}/handler.lambdaHandler`,
};

Don't forget to reference this Authorizer Lambda in the serverless.ts.

import type { AWS } from '@serverless/typescript';

import authorizer from '@functions/authorizer';
import ping from '@functions/ping';

...
const serverlessConfiguration: AWS = {
  ...
  functions: {
    authorizer,
    ping,
  },
  ...
}
...

Wicked! You have set up everything. ✌️

Verifying the Result

Let's run the following command to spin up the API Gateway and Lambda locally:

# If you haven't got the Serverless CLI installed
npm install -g serverless

serverless offline start

serverless-offline

Your API Gateway is listening on port 3000 with the path prefix /dev. Use the following command to send a request and see it in action.

curl -H "Authorization: abc" http://127.0.0.1:3000/dev/ping

serverless-offline-response

Cheers! 🍺 You can develop AWS Lambda locally now.

Conclusion

We have now covered AWS SAM and Serverless Framework. Both tools support local development and deployment. The remaining question is, how do you choose between these two options?

Personally, I select the options based on these conditions.

🌟 Serverless Framework

  • Fast local development experience.
  • Ability to write infrastructure as code in TypeScript.
  • Support for other cloud providers.

🌟 AWS SAM

  • Comfort with CloudFormation.
  • Focus on AWS and the ability to configure detailed settings.