Introduction

I recently developed a small API that I chose to deploy on AWS Lambda. I also had to make it secure so that only authorized users could send requests. In this article, I am going to show how I managed to deploy my application.

Prerequisites

Set up the Python environment

First, we need a Python virtual environment. It will be packaged with the application later and uploaded to the Lambda function so it has all the required dependencies.

python -m virtualenv venv
source venv/bin/activate

Next, we install the required packages:

pip install fastapi uvicorn mangum
  • FastAPI: The framework I use to build my API.
  • uvicorn: ASGI web server implementation for Python.
  • mangum: An adapter for running ASGI applications in AWS Lambda.

Install AWS CLI

This step is optional. We can do all the operations manually from the AWS console. However, it is faster to use the command-line tool. It let us update the code of the Lambda function in one command instead of multiple steps we would have to do manually. To install the client in Linux, run the following commands:

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

Configure AWS CLI

To use the AWS command-line tool, we need to save our credentials and some configurations. The easiest way to configure the application is to run the aws configure command:

aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json

The FastAPI application

Our application is going to have this architecture. It allows us to scale up by adding endpoints and to manage multiple versions of our API.

project/
    |_ venv/
    |_ app/
        |_ __init__.py
        |_ main.py
        |_ api/
            |_ __init__.py
            |_ api_v1/
                |_ __init__.py
                |_ api.py
                |_ endpoints/
                    |_ __init__.py/
                    |_ myapp.py

app/main.py

from fastapi import FastAPI

from app.api.api_v1.api import router as api_router
from mangum import Mangum

app = FastAPI()

app.include_router(api_router, prefix='/api/v1')

# Wrap the app in a handler for the Lambda function
handler = Mangum(app)

app/api/api_v1/api.py

from fastapi import APIRouter

from .endpoints import myapp

router = APIRouter()

router.include_router(myapp.router, prefix='/myapp', tags=['Myapp'])

app/api/api_v1/endpoints/myapp.py

from fastapi import APIRouter

router = APIRouter()

@router.get('/')
async def get():
    return "It works!"

Test the application

We can now test the application. - Start the webserver with uvicorn

uvicorn main:app --host 0.0.0.0 --port 8000
  • Send a GET request to the endpoint
curl http://0.0.0.0:8000/api/v1/myapp/
"It works!"

AWS setup

Our application is (almost) ready. We can start working on our AWS resources.

Create a Lambda application

First, we create a lambda function that will run our code.

  • Navigate to the Lambda page and click Create function.
    Click Create function

  • Select Author from scratch.

  • Enter the function's name.
  • Choose the runtime (Python3.9).
  • Click the Create function button.
    Function creation

At this point, we are on the Lambda function page. We need to update the handler to point to the one we created in our code (i.e. handler = Mangum(app) in main.py). - Scroll down to Runtime settings and click the Edit button.
- Set Handler to app.main.handler:
Set Lambda handler

Create an API Gateway

Now that our Lambda function exists, we need an API Gateway to run our API. - Navigate to the API gateway page.
- Choose the API type (REST API) and click the Build button. Choose REST API
- Select Rest, New API, and enter the API name. Create API Gateway - Click the Create API button.

Create a proxy method for all other requests

  • Click Actions and select Create Resource.
  • Check Configure as proxy.
  • CLick Create Resource.
    Configure as proxy

We can see that a new resource has been added (/{proxy+}). There is also an ANY. These are the methods that are accepted by the API. ANY means that all the methods are accepted. We select the method and set the Lambda function called by the API.
API resource

API Gateway setup

Add permission to the Lambda function when prompted.

Deploy API

Our API Gateway is ready. We can deploy it. - In Actions menu, select Deploy API
Deploy API
- Select New Stage.
- Enter the new stage name.
New stage
- Click Deploy.

We are redirected to the stage editor page. At the top of the page, we can see the Invoke URL that is pointing to our API:
Invoke URL

We will have to send our requests to that URL. But before we can do it, we need to upload our application code to the Lambda function.

Upload the application to Lambda

Package the virtual environment

The Lambda function will need all the dependencies required to run our application. We create a zip file that will contain all the packages we installed in our virtual environment:

cd venv/lib/python3.9/site-packages/
zip -r9 ../../../../myapp.zip .

We add our application source code:

cd ../../../../
zip -g ./myapp.zip -r app

Update the Lambda function

Using the AWS CLI, we can update the Lambda function's code directly.

aws lambda update-function-code --function-name myapp-lambda --zip-file fileb://myapp.zip --publish

Test the API

Everything should be working now. We send a GET request to our endpoint to test:

curl https://ab1ah50v54.execute-api.us-east-1.amazonaws.com/prod/api/v1/myapp/
"It works!"

Setup an authorizer to secure our application

Our application is up and running. Currently, anyone with the URL of our API can send requests. This poses some serious security concerns. We need to make our API secure.
To do so, we are going to create another Lambda function that will work as an authorizer. Any request sent to the API will have to contain an authorization token. The API Gateway will verify with the authorizer that the token is valid.
If the token is authorized, the request is forwarded to the API endpoint.

Create a Lambda authorizer function

  • Navigate to Lambda function and Click the Create function button.
  • Select Author from scratch.
  • Enter the function's name.
  • Select the runtime.
  • Click Create function.
    Create auth function

The code required for the verification of the token is simple. We can type it directly in the Lambda function.
The code needs to extract the authorizationToken from the event. It then compares it with the valid token. If the tokens are the same, then the authorization is set to Allow. If the two tokens are different, the authorization is set to Deny.
After validating the event's token, we build a response that is formatted like an AWS policy document.

import os

APITOKEN = os.environ.get('APITOKEN')

def lambda_handler(event, context):
    # Validate the token provided with the request
    try:
        event_token = event['authorizationToken']
    except Exception:
        response = {'status': 400, 'body': 'Bad Request'}
        return response

    if event_token == APITOKEN:
        auth = 'Allow'
    else:
        auth = 'Deny'

    # Construct and return the response
    response = {
        "policyDocument": { 
            "Version": "2012-10-17", 
            "Statement": [
                {
                    "Action": "execute-api:Invoke", 
                    "Resource": ["arn:aws:execute-api:<region>:<account_number>:<api_gateway_id>/*/*/*"], 
                    "Effect": auth
                }
            ] 
        }        
    }

    return response

In this code, you will have to replace <region>, <account_number>, and <api_gateway_id> with the correct values.

  • Click the Deploy button to save your changes.

Save Environment Variables

In the authorizer script, we get the API token from an environment variable: APITOKEN = os.environ.get('APITOKEN').
For this to work, we need to save our token as an environment variable.
From you authorizer Lambda function page:
- Open the Configuration tab and select Environment variables in the left menu.
- Click the Edit button
Env Var
- Click the Add environment variable button
Add env var

  • Set the key to APITOKEN and enter a long random alphanumeric string in the value field.
  • Click Save
    Set env var

Setup API Gateway to use the authorizer

We need to set our API Gateway to call our authorizer before processing the request. - Navigate to the API Gateway page.
- Select Authorizers in the left menu and click Create New Authorizer
Create new authorizer

  • Enter the authorizer's name.
  • Select type (Lambda).
  • Enter the Lambda authorizer function's name.
  • Under Lambda Event Payload select Token.
  • In Token Source enter authorizationToken.
  • Disable caching if it is not required.
  • Click the Create button
    Set new authorizer

Now that our authorizer is set, we need to tell our API Gateway to use it when handling requests.
- Select Resources in the left menu, then ANY and Method Request.
Enable select request
- In the settings, expand the Authorization dropdown and select the Lambda authorizer function
Enable select authorizer

For those changes to be effective, we need to re-deploy the API. Open Actions and select Deploy API to update the API Gateway

Test our application

# Testing without authorizationToken
curl 'https://ab1ah50v54.execute-api.us-east-1.amazonaws.com/prod/api/v1/myapp/'
{"message":"Unauthorized"}
# Testing with wrong auhorizationToken
curl -H 'authorizationToken: foo'  'https://ab1ah50v54.execute-api.us-east-1.amazonaws.com/prod/api/v1/myapp/'
{"Message":"User is not authorized to access this resource"}
# Testing with correct authorizationToken
curl -H 'authorizationToken: mysecrettoken' 'https://ab1ah50v54.execute-api.us-east-1.amazonaws.com/prod/api/v1/myapp/'
"It works!"

It works! We have deployed our API on the cloud and made it secure.