Use Case

Last Update : 28 June, 2024 | Published : 28 June, 2024 | 5 Min Read

Building a Scalable Greeting Service with Temporal, FastAPI, Docker, and Traefik

In this post, we’ll walk through building a scalable greeting service using Temporal, a powerful orchestration framework, and FastAPI, a modern web framework for building APIs with Python. We’ll also containerize our application using Docker and set up a reverse proxy with Traefik for secure routing and load balancing. We’ll cover the code structure, key components, containerization, and how they work together to provide a robust and efficient service.

Project Structure

Here’s the structure of our project:

root
├── internal
│   ├── activity
│   │   └── name.py
│   ├── worker
│   │   ├── name.py
│   │   └── run.py
├── main.py
├── Dockerfile
└── docker-compose.yaml

Activities in Temporal

An activity in Temporal is a unit of work that can be executed independently. In our project, we define an activity to say hello in internal/activity/name.py.

from temporalio import activity

@activity.defn
async def say_hello(name: str) -> str:
    return f"Hello {name}!"

Workflows in Temporal

A workflow in Temporal orchestrates the execution of activities. We define a workflow to use our say_hello activity in internal/worker/name.py.

from temporalio import workflow
from datetime import timedelta
from internal.activity.name import say_hello

@workflow.defn
class GreetingWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        return await workflow.execute_activity(
            say_hello, name, start_to_close_timeout=timedelta(seconds=120)
        )

Worker to Execute Workflows

Temporal workers are responsible for polling the Temporal server for tasks and executing workflows and activities. We set up a worker in internal/worker/run.py.

import asyncio
import concurrent.futures

from internal.activity.name import say_hello
from internal.worker.name import GreetingWorkflow
from temporalio.client import Client
from temporalio.worker import Worker

async def main() :
    client = await Client.connect('localhost:7233')

    with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
        worker = Worker(client, task_queue='name-task-queue', workflows=[GreetingWorkflow], activities=[say_hello], activity_executor=executor)
        await worker.run()

if __name__ == "__main__":
    asyncio.run(main())

FastAPI Application

We use FastAPI to expose our greeting service via HTTP endpoints. The main application is defined in main.py.

import logging
from contextlib import asynccontextmanager
from pydantic import BaseModel
from fastapi import FastAPI
from temporalio.client import Client
from internal.worker.name import GreetingWorkflow
import uvicorn

log = logging.getLogger(__name__)

class NameRequest(BaseModel):
    name: str

@asynccontextmanager
async def lifespan(app: FastAPI):
    logging.info("Setting up temporal client")
    app.state.temporal_client = await Client.connect('localhost:7233')
    yield

app = FastAPI(lifespan=lifespan)

@app.get('/', status_code=200, response_model=dict)
def root():
    return {"hello": "world"}

@app.post('/name', status_code=201, response_model=dict)
async def say_hello(request: NameRequest):
    result = await app.state.temporal_client.execute_workflow(
        GreetingWorkflow.run, request.name, id=f"name-workflow-{request.name}", task_queue='name-task-queue'
    )
    return {
        "result": result
    }

if __name__ == "__main__":
    uvicorn.run("main:app", reload=True, port=8000)

Containerization with Docker

We’ll use Docker to containerize our application. The Dockerfile defines the build process for both the FastAPI application and the Temporal worker.

# Use an official Python runtime as a parent image
FROM python:3.11-slim as base

# Set the working directory in the container
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . .

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
    
# Multi-stage build to separate FastAPI and Temporal worker
# FastAPI stage
FROM base as fastapi

# Expose the port that the FastAPI app runs on
EXPOSE 8000

# Command to run FastAPI application
CMD ["python", "main.py"]

# Temporal worker stage
FROM base as worker

# Command to run the Temporal worker
CMD ["python", "internal/worker/run.py"]

Docker Compose for Orchestration

We use Docker Compose to define and run multi-container Docker applications. Our docker-compose.yaml file sets up the FastAPI app, the Temporal worker, and the Traefik reverse proxy.

version: "3.8"

services:
  reverse-proxy:
    image: traefik:v3.0.2
    container_name: "traefik"
    command:
      - --api.insecure=true
      - --providers.docker=true
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.myresolver.acme.tlschallenge=true
      - --certificatesresolvers.myresolver.acme.email=cimomof752@cnurbano.com
      - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - ./letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - traefik-net

  fastapi:
    build:
      context: .
      dockerfile: Dockerfile
      target: fastapi
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.fastapi.rule=Host(`fastapi.localhost.com`)"
      - "traefik.http.routers.fastapi.entrypoints=websecure"
      - "traefik.http.routers.fastapi.tls.certresolver=myresolver"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=web"
      - "traefik.http.routers.redirs.middlewares=redirect-to-https"
    networks:
      - traefik-net

networks:
  traefik-net:
    external: true

Running the Application with Traefik

  1. Start the Temporal Server: Ensure that your Temporal server is running on localhost:7233.
  2. Build and Run the Containers: Use Docker Compose to build and start the containers.
    docker-compose -f docker-compose.yaml up -d
    

Running the Application in local

Temporal Worker with FastAPI

create virtual environment

python -m venv .venv
source .venv/bin/activate

Install requirements

pip install -r requirements.txt

NOTE: TEMPORAL SHOULD BE INSTALLED IN THE VIRTUAL ENVIRONMENT

Run the temporal server in development mode

temporal server start-dev

temporal UI

Set Python Path and Run the Application

When running your scripts, make sure to set the PYTHONPATH so that Python can locate the internal module:

export PYTHONPATH=$(pwd)

Run the Temporal worker:

python internal/worker/run.py

Run the FastAPI application:

python main.py

python UI

Test the API

Test the API using curl or any HTTP client like Postman:

curl -X POST "http://127.0.0.1:8000/name" -H "Content-Type: application/json" -d '{"name": "Suresh"}'

You should receive a response like:

{
  "result": "Hello Suresh!"
}

swagger test

Check the workflows in the Temporal UI
http://localhost:8233/namespaces/default/workflows

workflow overview

Source Code: https://github.com/azar-writes-code/fastapi-traefik-temporal-poc

Conclusion

In this post, we’ve built a greeting service using Temporal for workflow orchestration, FastAPI for exposing our service via HTTP endpoints, Docker for containerization, and Traefik for reverse proxy and load balancing. This setup provides a scalable, secure, and reliable way to handle complex workflows in a microservices architecture. The combination of these technologies makes it a powerful solution for building modern web services.

Looking for Cloud-Native Implementation?

Finding the right talent is pain. More so, keeping up with concepts, culture, technology and tools. We all have been there. Our AI-based automated solutions helps eliminate these issues, making your teams lives easy.

Contact Us