Skip to content

Chapter 2: Serverless Deployment (Azure Functions)#

Welcome back! In Chapter 1: Web Interface (Frontend), we saw the friendly face of our project – the part you interact with in your web browser. You learned how it gathers information (like the repo URL) and displays the final tutorial. But what happens after you click that "Generate Tutorial" button? Where does the magic happen, and how is it handled efficiently?

That's where the backend comes in, and in this project, we've chosen a modern, cost-effective approach called Serverless Deployment using Azure Functions.

The Problem: Idle Servers are Expensive!#

Imagine you have a traditional web application. You need a server running all the time just in case someone visits your website or wants to use a feature like generating a tutorial. Most of the time, that server might be doing nothing, just waiting. But you're still paying for it to run, whether it's busy or idle.

Generating a tutorial is a task that doesn't happen constantly. Users request it sometimes, and it might take a while. Keeping a powerful server running 24/7 just for these occasional, potentially long tasks is wasteful.

The Solution: Serverless!#

Serverless doesn't mean there are no servers. It means you don't have to manage the servers yourself. A cloud provider (like Microsoft Azure) handles all the server infrastructure, scaling, and maintenance for you.

You just provide your code, tell the cloud provider when to run it (what "event" should trigger it), and they run it for you. You typically only pay for the time your code is actually running, which is perfect for tasks that happen on demand.

Think of it like paying for electricity. You don't own a power plant; you just plug in your devices and pay for the electricity you consume when you use them. Serverless is like that for your code!

Introducing Azure Functions#

Azure Functions is Microsoft Azure's serverless compute service. It lets you run small pieces of code, called "functions," without worrying about the underlying infrastructure.

Here are the key concepts you need to know:

  • Function: A single piece of code that performs a specific task (e.g., processing a request, reacting to a database change).
  • Trigger: The event that starts a function running. This could be an HTTP request, a message arriving in a queue, a timer, etc.
  • Bindings: A way to easily connect your function to other services (like databases, storage, queues) without writing a lot of connection code. (We'll see input and output bindings implicitly used with the queue and storage).

How Our Tutorial Generation Works with Azure Functions#

Our project uses Azure Functions to handle the backend processing. Here's the basic flow:

  1. You submit the form on the Frontend (Chapter 1).
  2. The Frontend sends an HTTP request to an Azure Function.
  3. This first Azure Function (start-job) receives the request and doesn't start the long generation process immediately.
  4. Instead, start-job takes the request details and puts them into a Queue. Think of the queue as a waiting list for jobs.
  5. Another Azure Function (generate) is constantly watching this Queue.
  6. When a new job message appears in the Queue, the generate function is automatically triggered.
  7. The generate function reads the job details from the queue message and starts the heavy lifting: fetching the code, analyzing it, talking to the AI, and generating the tutorial content. (Workflow Engine (Pocket Flow) will explain how this internal processing happens).
  8. Once the generate function is done, it saves the resulting tutorial files to a Storage Account (specifically, Azure Blob Storage).
  9. Later, when you visit the output page on the Frontend, the Frontend makes HTTP requests to other Azure Functions (get-output-structure, get-output-content) to read the saved tutorial files from the Storage Account and display them to you.

This queuing pattern is great because: * It makes the initial request (start-job) very fast, so the user gets quick feedback ("Job accepted!"). * It separates the request reception from the heavy processing, making the system more robust. If the generate function is busy or fails, the message stays in the queue and can be processed later. * It allows Azure Functions to scale the generate function automatically. If many jobs arrive in the queue, Azure can start multiple instances of the generate function to process them in parallel.

Let's visualize this with a simple diagram:

sequenceDiagram
    participant User
    participant Frontend as Web Interface
    participant StartJobFunc as "start-job" Function (HTTP Trigger)
    participant JobsQueue as Jobs Queue
    participant GenerateFunc as "generate" Function (Queue Trigger)
    participant BlobStorage as Tutorial Storage (Blob Storage)

    User->>Frontend: Submit Tutorial Request
    Frontend->>StartJobFunc: HTTP POST /start-job (Request details)
    StartJobFunc->>JobsQueue: Send message (Job details)
    StartJobFunc-->>Frontend: HTTP 202 Accepted
    Frontend->>User: Show "Generating..." / Redirect

    Note over JobsQueue: Message is waiting...

    JobsQueue-->>GenerateFunc: Trigger (New message arrived)
    GenerateFunc->>GenerateFunc: Process Tutorial (Code fetching, AI, etc.)
    GenerateFunc->>BlobStorage: Save Tutorial Files
    GenerateFunc-->>JobsQueue: Message processed (removed from queue)

    Note over User: Later, User visits output page

    User->>Frontend: View Tutorial Output
    Frontend->>BlobStorage: Read Tutorial Files (via other functions)
    BlobStorage-->>Frontend: Tutorial Content
    Frontend-->>User: Display Tutorial

This shows how the initial request triggers a function that queues the real work, and then a separate function is triggered by the queue to do the processing and save the result. Other functions then serve the saved result.

Looking at the Code#

The core Azure Functions logic for our backend is in the function_app directory, primarily within the function_app.py file.

Let's look at simplified snippets.

First, the start-job function that receives the HTTP request:

# function_app/function_app.py (Simplified start_job)
import azure.functions as func
import json
import os
from azure.storage.queue import QueueClient # Need this!

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.function_name(name="start_job")
@app.route(route="start-job", methods=["POST"])
def start_job(req: func.HttpRequest) -> func.HttpResponse:
    try:
        req_body = req.get_json()
        # Get request data (like repo_url, keys, etc.)
    except ValueError:
        # Handle bad JSON
        pass # Simplified

    # Connect to the queue
    queue_connection_string = os.getenv('AzureWebJobsStorage') # Connection string from settings
    queue_client = QueueClient.from_connection_string(queue_connection_string, queue_name="jobsqueue")

    # Send the request data as a message to the queue
    job_message = json.dumps(req_body)
    queue_client.send_message(job_message)

    print(f"Message sent to queue: {job_message}") # Logging

    # Respond quickly that the job was accepted
    return func.HttpResponse(
        json.dumps({"message": "Job accepted."}),
        status_code=202, # 202 Accepted status
        mimetype="application/json"
    )
  • The @app.route decorator tells Azure Functions that this Python function should run when an HTTP request arrives at the /api/start-job path (the /api part is added by the Azure Functions host).
  • We read the incoming request body using req.get_json().
  • We get the connection information for our Azure Storage Account (where the queue lives) from environment settings.
  • We connect to the specific queue named jobsqueue.
  • We convert the request data into a JSON string and send it as a message to the queue using queue_client.send_message().
  • Finally, we return an HTTP response with status 202 Accepted to the frontend, indicating that the request was received and queued.

Next, the generate function that is triggered by the queue:

# function_app/function_app.py (Simplified generate)
import azure.functions as func
import json
# import the core generation logic
# from main import generate_tutorial_content # Need this!

@app.function_name(name="generate")
@app.queue_trigger(arg_name="msg", queue_name="jobsqueue", connection="AzureWebJobsStorage")
def generate(msg: func.QueueMessage) -> None:
    """Triggered by a message in the queue."""
    try:
        # Read the message content (which is JSON)
        req_body = json.loads(msg.get_body().decode('utf-8'))
    except Exception as e:
        print(f"Error reading queue message: {e}")
        # Log and exit if message is bad
        return

    try:
        # Extract parameters from the message
        repo_url = req_body.get('repo_url')
        # ... get other parameters like keys, patterns, etc.

        # Call the main generation logic (which is defined in main.py)
        # This function handles code fetching, AI calls, saving output etc.
        # generate_tutorial_content(...) # Simplified call

        print(f"Successfully processed job for repo: {repo_url}")

    except Exception as e:
        print(f"Error during generation: {e}")
        # Log the error
        # save_error_log(...) # Simplified error handling
        return
  • The @app.queue_trigger decorator tells Azure Functions that this function should run whenever a message appears in the jobsqueue. The message content will be passed into the function as the msg argument.
  • We read the message body (msg.get_body()) and decode the JSON string back into a Python dictionary (req_body).
  • We extract the necessary information (like repo_url) from the dictionary.
  • Crucially, we then call the main tutorial generation logic, which is separated into the generate_tutorial_content function within the main.py file. This keeps the Azure Function focused on the trigger and the core application logic separate.
  • The generate_tutorial_content function (which we'll look at in later chapters like Workflow Engine (Pocket Flow)) performs all the complex steps and saves the results.
  • If the function completes without error, Azure automatically removes the message from the queue. If it fails, the message becomes visible again after a delay, allowing for retries.

The generated tutorial output (Markdown files, structure JSON) needs to be stored somewhere the frontend can access it later. This is done in Azure Blob Storage. Think of Blob Storage as a simple online file system. The generate_tutorial_content function saves the files there.

Then, the frontend needs to retrieve this data. This is handled by two other Azure Functions: get-output-structure and get-output-content. These are triggered by HTTP GET requests from the frontend.

# function_app/function_app.py (Simplified get_output_structure)
import azure.functions as func
import json
import os
from azure.storage.blob import BlobServiceClient # Need this!

@app.function_name(name="get_output_structure")
@app.route(route="output-structure/{repo_name}", methods=["GET"])
def get_output_structure(req: func.HttpRequest) -> func.HttpResponse:
    repo_name = req.route_params.get('repo_name') # Get repo name from the URL path

    if not repo_name:
        # Handle missing repo name
        pass # Simplified

    # Connect to Blob Storage
    connection_string = os.environ.get("AzureWebJobsStorage")
    blob_service_client = BlobServiceClient.from_connection_string(connection_string)
    container_client = blob_service_client.get_container_client("tutorials") # Our container name

    try:
        # List blobs that match the repo name prefix (e.g., "my-repo/...")
        # This finds all files belonging to this tutorial
        # blobs = list(container_client.list_blobs(name_starts_with=f"{repo_name}/")) # Simplified

        # Process blob list to build structure dictionary
        structure = {"chapters": []} # Simplified structure building

        return func.HttpResponse(
            json.dumps(structure), # Return the structure as JSON
            status_code=200,
            mimetype="application/json"
        )

    except Exception as e:
        # Handle errors accessing storage
        pass # Simplified
  • @app.route sets up an HTTP GET trigger that expects a repo_name in the URL path (e.g., /api/output-structure/my-repo).
  • We get the repo_name from the request's route parameters.
  • We connect to Azure Blob Storage using the connection string and specify the container (tutorials) where the output is stored.
  • We list the blobs (files) within that container that start with the given repo_name as a prefix (e.g., my-repo/chapter_1/intro.md).
  • The full code then processes this list to figure out the chapter and lesson structure, formats it, and returns it as a JSON response.

And the get-output-content function to fetch a specific file:

# function_app/function_app.py (Simplified get_output_content)
import azure.functions as func
import json
import os
from azure.storage.blob import BlobServiceClient # Need this!

@app.function_name(name="get_output_content")
@app.route(route="output-content/{repo_name}/{*file_path}", methods=["GET"])
def get_output_content(req: func.HttpRequest) -> func.HttpResponse:
    repo_name = req.route_params.get('repo_name') # Get repo name
    file_path = req.route_params.get('file_path') # Get the rest of the URL path as file_path

    if not repo_name or not file_path:
        # Handle missing parameters
        pass # Simplified

    # Connect to Blob Storage
    connection_string = os.environ.get("AzureWebJobsStorage")
    blob_service_client = BlobServiceClient.from_connection_string(connection_string)
    container_client = blob_service_client.get_container_client("tutorials")

    # Construct the full path to the blob (file)
    blob_path = f"{repo_name}/{file_path}"

    try:
        # Get the specific blob client
        blob_client = container_client.get_blob_client(blob_path)

        if not blob_client.exists():
            # Handle file not found
             return func.HttpResponse(json.dumps({"error": "File not found"}), status_code=404, mimetype="application/json") # Simplified

        # Download the content
        content = blob_client.download_blob().readall().decode('utf-8')

        return func.HttpResponse(
            json.dumps({"content": content}), # Return content in a JSON object
            status_code=200,
            mimetype="application/json"
        )

    except Exception as e:
        # Handle errors accessing storage
        pass # Simplified
  • @app.route handles a URL path like /api/output-content/my-repo/chapter_1/intro.md. It extracts the repo_name and the file_path (the rest of the path).
  • We connect to Blob Storage and construct the full path to the specific file (my-repo/chapter_1/intro.md).
  • We use the blob_client for that specific file to download its content (download_blob().readall()).
  • The content is decoded (since it's stored as bytes) and returned as a JSON response.

There's also a fetch-patterns function triggered by HTTP POST (/api/fetch-patterns) that the frontend uses to suggest file patterns for filtering. Its code is structured similarly, receiving data via HTTP and returning a JSON response after performing its task (analyzing GitHub file paths using the GitHub API, which requires requests).

Finally, the host.json file contains configuration settings for the Azure Functions host itself, such as timeouts:

// function_app/host.json
{
  "version": "2.0",
  "functionTimeout": "00:10:00", // This allows functions to run for up to 10 minutes
  "logging": {
    // ... logging settings ...
  },
  "extensionBundle": {
    // ... bundle settings ...
  }
}

The functionTimeout is important for our generate function, as fetching, analyzing, and calling the AI can take several minutes for larger repositories. Setting it to "00:10:00" allows functions to run for up to 10 minutes by default.

Benefits of this Approach#

Using Azure Functions and this queue-based pattern gives us several advantages:

Feature Benefit Why it matters here
Serverless No server management needed. Reduces operational overhead for the project maintainer.
Cost-Effective Pay only for execution time and storage used. Cheaper than a dedicated server if tutorial generation is not happening constantly.
Scalable Azure automatically scales up/down based on demand. Handles sudden spikes in tutorial requests without manual intervention.
Decoupling Queue separates job submission from processing. Improves reliability; front-end is responsive, processing can be retried if needed.
Asynchronous Processing happens in the background. User doesn't have to wait for generation to finish on the initial request.

Conclusion#

By leveraging Azure Functions, our project's backend is set up to be efficient, scalable, and cost-effective. HTTP-triggered functions handle initial requests and serving saved output, while a queue-triggered function handles the main, potentially long-running tutorial generation process asynchronously. The results are stored reliably in Azure Blob Storage, ready to be retrieved by the frontend.

This serverless architecture allows us to run the complex codebase analysis and tutorial generation only when it's needed, without the overhead of managing dedicated servers.

In the next chapter, we'll dive deeper into the generate function and explore how it orchestrates the multiple steps required to create a tutorial using a special tool called Pocket Flow.

Next Chapter: Workflow Engine (Pocket Flow)


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/function_app.py), 2(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/host.json), 3(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/main.py), 4(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/test/test_fetch_patterns.py), 5(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/test/test_function.py), 6(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/test/test_output_content.py), 7(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/test/test_output_structure.py), 8(https://github.com/hieuminh65/Tutorial-Codebase-Knowledge/blob/be7f595a38221b3dd7b1585dc226e47c815dec6e/function_app/test/test_start_job.py)