Skip to main content

Your First Klotho App

This tutorial demonstrates how to create a Python REST API with FastAPI and use Klotho to transform it into a cloud-native one.

The tutorial will cover 2 Klotho features that give your existing code cloud native capabilities. We call these Klotho capabilities.

Getting Started

Prerequisites

Application Overview

The doggie daycare sample application is a FastAPI app. The application has one endpoint to register a pet and its owner and a second endpoint to retrieve a list of registered pets for the supplied owner as shown below.

REST API Endpoints

  • POST /pets
  • GET /owners/{owner}/pets

This application will utilize the following annotations:

Setting Up

Start by creating a directory for your project.

mkdir klotho-my-first-app  # Fill in wherever you'd like to create your application
cd klotho-my-first-app

Then create a requirements.txt file in your project directory containing the following dependencies:

requirements.txt
uvicorn==0.18.3
pydantic==1.10.2
fastapi==0.75.2
aiocache==0.11.1

Lastly, install these dependencies with pip.

pip install -r requirements.txt

Code

First, create the app.py file in your project directory and opening it in the editor of your choice.

Then create a key-value store for maintaining owner-to-pet mappings using aiocache.Cache and annotate it with @klotho::persist.

app.py
from aiocache import Cache

# @klotho::persist {
# id = "petsByOwner"
# }
petsByOwner = Cache(Cache.MEMORY)

Next, create an instance of FastAPI and annotate it with @klotho::expose.

app.py
from fastapi import FastAPI

# @klotho::expose {
# id = "pet-api"
# target = "public"
# }
app = FastAPI()

Now create the request model for the POST /pets endpoint using Pydantic. It will include two string fields, owner and pet.

app.py
from pydantic import BaseModel

class PetRegistration(BaseModel):
owner: str
pet: str

Finally, add the POST /pets and GET /owners/{owner}/pets endpoints to the application.

app.py
@app.post("/pets")
async def register_pet(registration: PetRegistration):
"""
Associates a pet with its owner
"""
owner = registration.owner
pet = registration.pet

# get the list of the owner's currently registered pets
pets = await petsByOwner.get(owner, default=[])

# adds the pet to the owners list of pets and stores the updated list
pets.append(pet)
await petsByOwner.set(owner, pets)

return f"Added {pet} as {owner}'s pet"
app.py
from fastapi import Response

@app.get("/owners/{owner}/pets")
async def get_owner_pets(owner: str, response: Response):
"""
Gets all pets registered to the supplied owner
"""
pets = await petsByOwner.get(owner)
if pets is None:
response.status_code = 404
return {"message": "Not Found"}
else:
return pets

Testing locally

One of the most powerful aspects of Klotho is letting you code, debug and iterate locally, knowing that once your ready, the cloud version will behave the same way.

This gives you the fastest possible developer experience without changing how you work.

To run the application locally, in one terminal run:

uvicorn app:app --port 3000
ujson module not found, using json
msgpack not installed, MsgPackSerializer unavailable
INFO: Started server process [24982]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:3000 (Press CTRL+C to quit)

Once the application is running, in another terminal test its endpoints with curl:

curl "localhost:3000/owners/Ala/pets"
# -> {"message":"Not Found"} :: We start with no pets registered

curl -X POST "localhost:3000/pets" \
-H 'Content-Type: application/json' \
-d '{"owner": "Ala", "pet": "Noodle"}'
# -> Added Noodle as Ala's pet

curl "localhost:3000/owners/Ala/pets"
# -> ["Noodle"] :: We see that our newly registered pet is now in the key-value store

Once you're done testing, bring the server down (with Ctrl-C in the first terminal).

Compiling with Klotho

Log into Klotho to set up your user profile. This will allow us to support you if you run into any issues, and give you the opportunity to shape the product in this early development stage.

klotho --login # if you haven't already

With the Klotho capabilities added to the application, run Klotho to get the cloud native version of it.

❯ klotho . --app my-first-app --provider aws

██╗ ██╗██╗ ██████╗ ████████╗██╗ ██╗ ██████╗
██║ ██╔╝██║ ██╔═══██╗╚══██╔══╝██║ ██║██╔═══██╗
█████╔╝ ██║ ██║ ██║ ██║ ███████║██║ ██║
██╔═██╗ ██║ ██║ ██║ ██║ ██╔══██║██║ ██║
██║ ██╗███████╗╚██████╔╝ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝

Adding resource input_file_dependencies:
Adding resource exec_unit:main
app.py:19:0: Found 2 route(s) on app 'app' var: app
Adding resource gateway:app.py#app
Adding resource persist_kv:petsByOwner
Adding resource topology:my-first-app
Adding resource infra_as_code:Pulumi (AWS)

The cloud version of the application is saved to the ./compiled directory, and has everything you need to deploy, run and operate the application.

Visualizing the Cloud Version

Helping you understand what Klotho did, open the ./compiled/my-first-app.png diagram created alongside the cloud application:

topology diagram showing a lambda that uses a DynamoDB table and is exposed by an API Gateway

The expose capability created and connected an API Gateway to the FastAPI app running on a Lambda execution unit. The persist capability replaced the in-memory cache backend with one that's backed by DynamoDB.

Deploying the Application

Deployment Dependencies

Now that the last-section has produced infrastructure-as-code, it is possible to deploy the cloud-native version of the application. The AWS account set up on your computer will be used.

But first, we need to install and set up a few more dependencies:

  • Docker
  • an AWS account, set up with either:
    • The AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY environment variables for a user
    • OR, $HOME/.aws/credentials (eg. via AWS CLI: aws configure) setup
  • Pulumi CLI
    • Follow the Pulumi installation instructions and set up for local usage:
      pulumi login --local
      export PULUMI_CONFIG_PASSPHRASE=""

Deploying with Pulumi

# region should be an aws region, for example us-west-1

pulumi config set aws:region <region> --cwd 'compiled/' -s my-first-app

If this is your first time deploying this application, you will be prompted to create the my-first-app stack. Just press ENTER and the stack will be created for you:

The stack 'my-first-app' does not exist.
If you would like to create this stack now, please press <ENTER>, otherwise press ^C:

Now that the Pulumi stack is created, simply install the project dependencies and deploy.

cd compiled
npm install
pulumi up

You'll see a preview of the changes to be applied to your AWS account: (you'll be able to delete these later on)

Previewing update (my-first-app)

Type Name Plan
+ pulumi:pulumi:Stack my-first-app-my-first-app create.
+ ├─ awsx:ecr:Repository my-first-app create
+ pulumi:pulumi:Stack my-first-app-my-first-app create..
+ ├─ aws:ecr:Repository my-first-app create
+ ├─ aws:dynamodb:Table KV_my-first-app create
+ │ └─ aws:sqs:Queue my-first-app-kv-queue create
+ ├─ aws:iam:Role my-first-app_0d6e4_LambdaExec create
+ │ └─ aws:iam:Policy my-first-app-main-exec create
+ │ └─ aws:iam:Policy my-first-app-main-exec create
+ ├─ aws:cloudwatch:LogGroup main-function-api-lg create
+ │ └─ aws:iam:RolePolicyAttachment my-first-app-main-exec create
+ │ ├─ aws:apigateway:Resource app-py-apppets/ create
+ pulumi:pulumi:Stack my-first-app-my-first-app create
+ │ │ └─ aws:apigateway:Integration lambda-POST-pets-45aa7 create
+ │ ├─ aws:apigateway:Resource app-py-appowners/ create
+ │ ├─ aws:apigateway:Resource app-py-appowners/{owner}/ create
+ │ ├─ aws:apigateway:Resource app-py-appowners/{owner}/pets/ create
+ │ │ └─ aws:apigateway:Method GET-pets-8030f create
+ │ │ └─ aws:apigateway:Integration lambda-GET-pets-8030f create
+ │ └─ aws:apigateway:Deployment app.py#app-deployment create
+ │ └─ aws:apigateway:Stage app.py#app-stage create
+ ├─ aws:iam:RolePolicyAttachment my-first-app-main-lambdabasic create
+ ├─ aws:s3:Bucket create
+ ├─ aws:lambda:Function main create
+ ├─ aws:lambda:Permission get-ownersownerpets-permission create
+ └─ aws:lambda:Permission post-pets-permission create

Outputs:
apiUrls : [
[0]: output<string>
]
deploymentPortal: "None - Opted out of topology upload by default"

Resources:
+ 26 to create

Do you want to perform this update? [Use arrows to move, enter to select, type to filter]
> yes
no
details

Respond with yes when you're ready and wait for your resources to be created. This usually takes 3 minutes. When it's done deploying, you'll see the completion status and the AWS provided API Gateway URL for your API:

Outputs:
apiUrls : [
[0]: "https://qxy99avspl.execute-api.us-east-1.amazonaws.com/stage/"
]

Testing the Cloud Version

After you deploy the application, you'll have a publicly accessible endpoint. Test that the Klotho-powered version behaves the same as the local one rerunning the same tests from before, only this time point them at the deployed version:

APP_URL=<app url from the pulumi output> # including the /stage
curl "$APP_URL/pets"
# -> {} :: We start with no pets registered

curl -X POST "$APP_URL/pets" \
-H 'Content-Type: application/json' \
-d '{"owner": "Ala", "pet": "Noodle"}'
# -> Added Noodle as Ala's pet

curl "$APP_URL/owners/Ala/pets"
# -> ["Noodle"] :: We see that our newly registered pet is now in the key-value store

Cleanup

To clean up your AWS deployment simply run:

cd compiled
pulumi destroy

You will see a Pulumi preview of all the resources pending deletion. Response with yes to begin deleting.

What next?

  • Join Discord and chat with us. What went well, what went poorly, what are you looking forward to?
  • Read through the Python API docs