Azure Rust Function
@Type: Report
@Published:October 27, 2024 at 09:57
@Last Updated:November 12, 2024 at 17:09
@Author: Jakke Korpelainen
Intro
Have been excited about Rust lately (like everyone) and found out that Azure Functions have Rust support by using a customHandler. Having worked with next.js and gone through the trouble of setting it up into AWS Lambda, I wanted to try out a bit similar approach of a serverless function that would listen to all http and serve back html (or json). This article is not a tutorial in the sense that it would contain all steps and it would produce a similar working solution, I'm only attempting to highlight the most important bits below.
I used the Quickstart: Create a Go or Rust function in Azure using Visual Studio Code as a starting point guide for this. However, we will slightly deviate from it to adapt it for serving html.
Fun fact: This blog was initially created into contentful as a sample data source to use in as a data retrieval example for the function.
Source & Demo
Repository: https://github.com/jakke-korpelainen/az-fn-rust-http-html
Demo: https://jakke-fi-function.azurewebsites.net/
Azure Function related configurations
To accept all http into all paths we need to make some changes.
function.json
Set the function route accordingly. Add {*subpath} as route into the first object inside bindings and change the methods if you require e.g. put or patch.
1{ 2 "bindings": [ 3 { 4 "authLevel": "anonymous", 5 "type": "httpTrigger", 6 "direction": "in", 7 "name": "req", 8 "route": "{*subpath}", 9 "methods": [ 10 "get", 11 "post" 12 ] 13 }, 14 { 15 "type": "http", 16 "direction": "out", 17 "name": "res" 18 } 19 ] 20} 21
host.json
In the host, we need to adjust the route prefix to serve all requests.
1{ 2 "version": "2.0", 3 "logging": { 4 "applicationInsights": { 5 "samplingSettings": { 6 "isEnabled": true, 7 "excludedTypes": "Request" 8 } 9 } 10 }, 11 "extensions": { 12 "http": { 13 "routePrefix": "" 14 } 15 }, 16 "extensionBundle": { 17 "id": "Microsoft.Azure.Functions.ExtensionBundle", 18 "version": "[4.*, 5.0.0)" 19 }, 20 "customHandler": { 21 "description": { 22 "defaultExecutablePath": "handler.exe", 23 "workingDirectory": "", 24 "arguments": [] 25 }, 26 "enableForwardingHttpRequest": true 27 } 28} 29
Rust HTTP Server (axum)
The sample used warp, I'm more familiar with axum. I went with the approach below.
main.rs
1use axum::routing::get; 2use axum::Router; 3use std::env; 4use std::net::Ipv4Addr; 5use tower_http::services::ServeDir; 6 7mod home; 8mod posts; 9mod templates; 10 11#[tokio::main] 12async fn main() { 13 http_server().await; 14} 15 16const PORT_KEY: &str = "FUNCTIONS_CUSTOMHANDLER_PORT"; 17 18async fn http_server() { 19 let port: u16 = match env::var(PORT_KEY) { 20 Ok(val) => val.parse().expect("Custom Handler port is not a number!"), 21 Err(_) => 3000, 22 }; 23 24 let router = Router::new() 25 .route("/", get(home::home_route)) 26 .route("/posts", get(posts::posts_route)) 27 .route("/posts/:post", get(posts::post_route)) 28 .nest_service("/assets", ServeDir::new("public")); 29 30 let listener = tokio::net::TcpListener::bind(format!("{}:{}", Ipv4Addr::LOCALHOST, port)) 31 .await 32 .unwrap(); 33 axum::serve(listener, router).await.unwrap(); 34} 35
home.rs
Example of compiling a template for rendering. Templates accept rendering parameters.
1pub async fn home_route() -> impl IntoResponse { 2 HtmlTemplate(templates::HomeTemplate {}) 3} 4
Templating (askama)
Askama is compiling the html and is able to detect misuse of variables and templating syntax, though the errors may be fairly confusing.
Template Declaration
Templates are be defined & typed in rust.
templates.rs
1pub struct HtmlTemplate<T>(pub T); 2 3impl<T> IntoResponse for HtmlTemplate<T> 4where 5 T: Template, 6{ 7 fn into_response(self) -> Response { 8 match self.0.render() { 9 Ok(html) => Html(html).into_response(), 10 Err(err) => ( 11 StatusCode::INTERNAL_SERVER_ERROR, 12 format!("Failed to render template. Error: {err}"), 13 ) 14 .into_response(), 15 } 16 } 17} 18 19#[derive(askama::Template, Debug)] 20#[template(path = "home.html")] 21pub struct HomeTemplate {} 22 23#[derive(askama::Template, Debug)] 24#[template(path = "posts.html")] 25pub struct BlogPostsTemplate { 26 pub posts: Vec<BlogPost>, 27} 28 29#[derive(askama::Template, Debug)] 30#[template(path = "post.html")] 31pub struct BlogPostTemplate { 32 pub post: Option<BlogPost>, 33} 34
Templating Syntax
Example of using rust pattern matching and a loop inside a template.
1{% match data.tags %} 2{% when Some with (tags) %} 3<p> 4 Tags: 5 {% for tag in tags %} 6 <span>{{ tag }}</span> 7 {% endfor %} 8</p> 9{% when None %} 10{% endmatch %} 11
templates/_base.html
1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Blog Posts</title> 7 <script src="https://cdn.tailwindcss.com"></script> 8 </head> 9 <body> 10 <nav class="bg-gray-800 p-4"> 11 <div class="container mx-auto flex justify-between items-center"> 12 <a href="/" class="text-white text-lg font-semibold">Home</a> 13 <div class="space-x-4"> 14 <a href="/" class="text-gray-300 hover:text-white">Home</a> 15 <a href="/posts" class="text-gray-300 hover:text-white">Blog</a> 16 </div> 17 </div> 18 </nav> 19 {% block main %} 20 <!-- main --> 21 {% endblock %} 22 </body> 23</html> 24
templates/home.html
An example of a page that is using the partial defined above.
1{% extends "_base.html" %} 2 3<!-- Main --> 4 5{% block main %} 6<main class="container mx-auto"> 7 <div class="flex items-center mt-10 gap-10"> 8 <img class="max-h-52 rounded-full" src="/assets/image.jpg"/> 9 <div> 10 <h1 class="text-xl">Hello World!</h1> 11 <p>We're also able to serve images, however they'll be compiled into the binary.</p> 12 </div> 13 </div> 14</main> 15{% endblock %} 16
Thanks for reading ✌️