Building microservices with FastAPI, Docker, and Kubernetes is a game-changer for modern web development. I’ve been experimenting with this stack lately, and I’m excited to share what I’ve learned.
Let’s start with FastAPI. It’s a relatively new Python framework that’s been gaining traction due to its speed and ease of use. What I love about FastAPI is how it leverages Python’s type hints to automatically generate OpenAPI (Swagger) documentation. It’s like magic!
Here’s a simple example of a FastAPI app:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello, World!"}
This creates a basic endpoint that returns a JSON response. But FastAPI can do so much more. Let’s dive into some more advanced features.
One of the coolest things about FastAPI is its built-in support for async/await. This means you can write asynchronous code that can handle many requests concurrently. Here’s an example of an async endpoint:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(1) # Simulate some async operation
return {"message": "This was async!"}
This endpoint will wait for a second before responding, but it won’t block other requests during that time. It’s perfect for I/O-bound operations like database queries or API calls.
Speaking of databases, FastAPI plays well with async database libraries like SQLAlchemy. Here’s a quick example of how you might set up a database connection:
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
app = FastAPI()
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
This sets up an async database session and creates an endpoint to fetch a user by ID. The Depends
function is another great FastAPI feature that handles dependency injection for you.
Now, let’s talk about Docker. Docker is fantastic for containerizing your applications, making them easy to deploy and scale. Here’s a simple Dockerfile for our FastAPI app:
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
This Dockerfile sets up a Python environment, installs our dependencies, copies our code into the container, and starts the FastAPI server using Uvicorn.
To build and run this Docker container, you’d use:
docker build -t my-fastapi-app .
docker run -p 8000:8000 my-fastapi-app
Now your FastAPI app is running in a container! But what if we want to scale this up? That’s where Kubernetes comes in.
Kubernetes is a container orchestration platform that can manage deployments of containerized applications. It’s incredibly powerful, but it can be a bit overwhelming at first. Let’s start with a basic Kubernetes deployment for our FastAPI app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-deployment
spec:
replicas: 3
selector:
matchLabels:
app: fastapi
template:
metadata:
labels:
app: fastapi
spec:
containers:
- name: fastapi
image: my-fastapi-app:latest
ports:
- containerPort: 8000
This Kubernetes manifest creates a deployment with three replicas of our FastAPI app. To expose this deployment to the internet, we’d also need a service:
apiVersion: v1
kind: Service
metadata:
name: fastapi-service
spec:
selector:
app: fastapi
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
This creates a load balancer that distributes traffic among our FastAPI replicas.
To deploy this to Kubernetes, you’d use:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
And just like that, you’ve got a scalable, containerized FastAPI application running on Kubernetes!
But we’re not done yet. Let’s talk about some more advanced FastAPI features that can really level up your microservices.
FastAPI has excellent support for WebSockets, which are perfect for real-time communication. Here’s a simple WebSocket endpoint:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
This creates a WebSocket endpoint that echoes back any messages it receives. You could use this for things like chat applications or real-time updates.
Another powerful feature of FastAPI is its support for background tasks. These are perfect for operations that need to happen after a request is complete, like sending emails or processing data. Here’s an example:
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def process_item(item_id: int):
# This could be a long-running task
print(f"Processing item {item_id}")
@app.post("/items/{item_id}")
async def create_item(item_id: int, background_tasks: BackgroundTasks):
background_tasks.add_task(process_item, item_id)
return {"message": "Item created"}
This endpoint immediately returns a response to the client, but continues processing the item in the background.
Now, let’s talk about testing. FastAPI makes it easy to write tests for your API using the TestClient
. Here’s an example:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}
This tests our root endpoint to make sure it returns the correct response.
When it comes to deploying microservices, one important concept is health checks. These allow Kubernetes to know if your service is ready to receive traffic. FastAPI makes it easy to add health check endpoints:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health_check():
return {"status": "healthy"}
You can then configure your Kubernetes deployment to use this endpoint for readiness and liveness probes.
Another crucial aspect of microservices is logging. FastAPI integrates well with Python’s logging module. Here’s how you might set up logging:
import logging
from fastapi import FastAPI, Request
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Incoming request: {request.method} {request.url}")
response = await call_next(request)
logger.info(f"Outgoing response: {response.status_code}")
return response
This middleware will log every incoming request and outgoing response.
When building microservices, you’ll often need to communicate between services. FastAPI works great with libraries like httpx
for making HTTP requests. Here’s an example of how you might call another service:
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/aggregate")
async def aggregate_data():
async with httpx.AsyncClient() as client:
response1 = await client.get("http://service1/data")
response2 = await client.get("http://service2/data")
# Combine data from both services
combined_data = {**response1.json(), **response2.json()}
return combined_data
This endpoint aggregates data from two other services asynchronously.
As your microservices architecture grows, you might want to consider implementing API gateways and service meshes. While these are beyond the scope of FastAPI itself, tools like Kong or Istio can work alongside your FastAPI services to provide features like rate limiting, authentication, and service discovery.
Speaking of authentication, FastAPI has built-in support for OAuth2 with JWT tokens. Here’s a simple example:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# ... (setup code omitted for brevity)
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
This sets up a login endpoint that returns a JWT token, and a protected endpoint that requires a valid token.
Lastly, let’s talk about documentation. One of the best features of FastAPI is its automatic API documentation. By default, you get Swagger UI at /docs
and ReDoc at /redoc
. You can customize this documentation using docstrings and Pydantic models:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(
title="My Cool API",
description="This API does awesome stuff",
version="1.0.0"
)
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.post("/items/")
async def create_item(item: Item):
"""
Create an item with all the information:
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
"""
return item
This provides detailed documentation for your API, making it easier for others to understand and use your microservices.
In conclusion, FastAPI, Docker, and Kubernetes form a powerful trio for building and deploying microservices. FastAPI provides a fast, easy-to-use framework with great features like async support and automatic documentation. Docker allows you to containerize your applications, making them portable and easy to deploy. And Kubernetes gives you the tools to orchestrate and scale your containers in production.
As you dive deeper into this world of microservices, you’ll discover even more advanced techniques and tools. But with this foundation, you’re well on your way to building robust, scalable applications. Happy coding!