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
- Klotho CLI installed
- Python 3.x
- Node.js 16.x+ (& NPM)
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
/petsGET
/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:
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
- Step-by-Step
- Complete Source
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
.
from aiocache import Cache
# @klotho::persist {
# id = "petsByOwner"
# }
petsByOwner = Cache(Cache.MEMORY)
Next, create an instance of FastAPI
and annotate it with @klotho::expose
.
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
.
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.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"
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
from aiocache import Cache
from fastapi import FastAPI, Response
from pydantic import BaseModel
class PetRegistration(BaseModel):
owner: str
pet: str
# @klotho::persist {
# id = "petsByOwner"
# }
petsByOwner = Cache(Cache.MEMORY)
# @klotho::expose {
# id = "pet-api"
# target = "public"
# }
app = FastAPI()
@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.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:
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
- The
- Pulumi CLI
- Follow the Pulumi installation instructions and set up for local usage:
pulumi login --local
export PULUMI_CONFIG_PASSPHRASE=""
- Follow the Pulumi installation instructions and set up for local usage:
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