I've known about serverless functions for a while now, but I've never actually used them. I've always been curious about how they work and how they can be used in real-world scenarios.
I was browsing through this article on creating Azure Functions using custom handlers with languages like Go
or Rust
.
After reading the article, I was intrigued by the idea of using Rust with Azure Functions.
So I try to find something like a tutorial about Rust and Azure Functions if someone built something but I can't find anything,
then I found this tutorial from Mohamad Lawand Youtube video where he created a simple image processing with Azure Functions & Service Bus using .Net Core.
So I was very excited to try to create the same thing but using Rust.
At first, I thought it would be difficult to use Rust with Azure services like Service Bus and Blob Storage, but after doing some research, I've found that there is already an unofficial
Rust Azure SDK which can be used to interact with Azure services.
So I decided to create an image resizing function using Azure Functions and Rust.
The basic idea is simple, first create an API endpoint which makes a POST
request to upload the image to the Azure Blob Storage and send a message to the Azure Service Bus.
Then Azure Functions will be utilized to listen to the Service Bus using Azure Service Bus Queue trigger
and resize the image and save it back to the Blob Storage.
What are Azure Functions?
Azure Functions is a serverless compute service that lets you run event-triggered code without having to explicitly provision or manage infrastructure. You can use Azure Functions to build web APIs, respond to database changes, process IoT data, and more.
Pre-requisites
This tutorial assumes that you have a basic understanding of Rust programming language and Azure Functions. If you are new to Rust, I recommend you to read the official Rust Book.
Also make sure you have Azure account and Azure CLI installed on your machine, if not, you can create a free account on Azure and install Azure CLI on your machine from the above links.
Create API endpoint to upload image
First thing first, we need to create a simple API endpoint to upload the image to the Azure Blob Storage and send a message to the Azure Service Bus. We will use warp for creating the API endpoint.
Let's create a directory called image-resize
and create a new directory called api
and create a new Rust project using cargo
:
bashmkdir image-resize && cd image-resize && mkdir api && cd apicargo init
This will create a new Rust project with the name image-resize
. Now, let's add the required dependencies to the Cargo.toml
file:
toml[package]name = "image-resize"version = "0.1.0"edition = "2021"[dependencies]warp = "0.3"tokio = { version = "1.12", features = ["macros", "fs", "rt-multi-thread"] }futures = { version = "0.3", default-features = false }bytes = "1.0"azure_core = "0.20.0"azure_storage = "0.20.0"azure_storage_blobs = "0.20.0"azure_messaging_servicebus = "0.20.0"serde = "1.0.200"serde_json = "1.0"
Now let's add the code to the main.rs
file:
rust// api/src/main.rsuse warp::{http::StatusCode,multipart::FormData,Filter, Rejection, Reply,};use std::{convert::Infallible};#[tokio::main]async fn main() {let upload_route = warp::path("upload").and(warp::post()).and(warp::multipart::form().max_length(5 * 1024 * 1024)) // Max image size: 5MB.and_then(upload_file);let routes = upload_route.recover(handle_rejection);println!("Server started at http://localhost:3030");warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;}async fn upload_file(form: FormData) -> Result<impl Reply, Rejection> {Ok(format!("Hello, world!"))}async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Infallible> {let (code, message) = if err.is_not_found() {(StatusCode::NOT_FOUND, "Not Found".to_string())} else if err.find::<warp::reject::PayloadTooLarge>().is_some() {(StatusCode::BAD_REQUEST, "Payload too large".to_string())} else {eprintln!("unhandled error: {:?}", err);(StatusCode::INTERNAL_SERVER_ERROR,"Internal Server Error".to_string(),)};Ok(warp::reply::with_status(message, code))}
This will set up a basic API endpoint at http://localhost:3030/upload
which will accept a POST
request with a multipart/form-data
containing the image file.
You can now try to run the API using the following command:
bashcargo run -r
You then should see a message Server started at http://localhost:3030
in the console. Now you can test the API by sending a POST
request to http://localhost:3030/upload
with an image file.
bashCURL -X POST -F 'file=@/path/to/image.jpeg' http://localhost:3030/upload
It should return Hello, world!
for now.
Now let's add the code to read the image file from the FormData
and store it in a Vec<u8>
which we will use to upload the image to the Azure Blob Storage.
rust// api/src/main.rsuse bytes::BufMut;use futures::TryStreamExt;use warp::{http::StatusCode,multipart::{FormData, Part},Filter, Rejection, Reply,};use std::{convert::Infallible};async fn upload_file(form: FormData) -> Result<impl Reply, Rejection> {let uploaded_files: Vec<_> = form.and_then(|mut part: Part| async move {let mut bytes: Vec<u8> = Vec::new();// read the part streamwhile let Some(content) = part.data().await {let content = content.unwrap();bytes.put(content);}// return the part name, filename and bytes as a tupleOk((part.name().to_string(),part.filename().unwrap().to_string(),String::from_utf8_lossy(&*bytes).to_string(),))}).try_collect().await.map_err(|_| warp::reject::reject())?;Ok(format!("Uploaded files: {:?}", uploaded_files))}
Next let's implement the code to upload the image to the Azure Blob Storage, we will use the azure-storage
and azure-storage-blobs
crate for this purpose. Add the following code to the main.rs
file:
rust// api/src/main.rsuse azure_storage::StorageCredentials;use azure_storage_blobs::prelude::ClientBuilder;use bytes::BufMut;use futures::TryStreamExt;use warp::{http::StatusCode,multipart::{FormData, Part},Filter, Rejection, Reply,};use std::{convert::Infallible, env};async fn upload_file(form: FormData) -> Result<impl Reply, Rejection> {let uploaded_files: Vec<_> = form.and_then(|mut part: Part| async move {let mut bytes: Vec<u8> = Vec::new();// read the part streamwhile let Some(content) = part.data().await {let content = content.unwrap();bytes.put(content);}if !bytes.is_empty() {// Azure Blob Storage credentialslet storage_account = env::var("AZURE_STORAGE_ACCOUNT").expect("Missing AZURE_STORAGE_ACCOUNT env var");let storage_access_key = env::var("AZURE_STORAGE_ACCESS_KEY").expect("Missing AZURE_STORAGE_ACCESS_KEY env var");let container_name = env::var("AZURE_STORAGE_CONTAINER").expect("Missing AZURE_STORAGE_CONTAINER env var");let blob_name = part.filename().unwrap().to_string();// create Azure Blob Storage clientlet storage_credentials = StorageCredentials::access_key(storage_account.clone(), storage_access_key);let blob_client = ClientBuilder::new(storage_account, storage_credentials).blob_client(&container_name, blob_name);// upload file to Azure Blob Storagematch blob_client.put_block_blob(bytes.clone()).content_type("image/jpeg").await {Ok(_) => println!("Blob uploaded successfully"),Err(e) => println!("Error uploading blob: {:?}", e),}println!("Uploaded file url: {}", blob_client.url().expect("Failed to get blob url"));}// return the part name, filename and bytes as a tupleOk((part.name().to_string(),part.filename().unwrap().to_string(),String::from_utf8_lossy(&*bytes).to_string(),))}).try_collect().await.map_err(|_| warp::reject::reject())?;Ok(format!("Uploaded files: {:?}", uploaded_files))}
Make sure to have the following environment variables set in your system:
bashexport AZURE_STORAGE_ACCOUNT="your-storage-account-name"export AZURE_STORAGE_ACCESS_KEY="your-storageexport AZURE_STORAGE_CONTAINER="your-container-name"
Also make sure to create a blob container in the Azure Portal and set the container name in the environment variable AZURE_STORAGE_CONTAINER
.
You can also find the access key and storage account name in the Azure Portal.
Now let's create a function to send a message to the Azure Service Bus. Add the following code to the main.rs
file:
rust// api/src/main.rsuse azure_messaging_servicebus::service_bus::QueueClient;... existing imports ...#[derive(Serialize, Deserialize, Debug)]struct Image {filename: String,image_container: String,}async fn upload_file(form: FormData) -> Result<impl Reply, Rejection> {let uploaded_files: Vec<_> = form.and_then(|mut part: Part| async move {... existing code ...if !bytes.is_empty() {... existing code ...let image = Image {filename: part.filename().unwrap().to_string(),image_container: container_name,};send_message_to_queue(image).await;}// return the part name, filename and bytes as a tupleOk((part.name().to_string(),part.filename().unwrap().to_string(),String::from_utf8_lossy(&*bytes).to_string(),))}).try_collect().await.map_err(|_| warp::reject::reject())?;Ok(format!("Uploaded files: {:?}", uploaded_files))}async fn send_message_to_queue(image: Image) {let service_bus_namespace = env::var("AZURE_SERVICE_BUS_NAMESPACE").expect("Please set AZURE_SERVICE_BUS_NAMESPACE env variable first!");let queue_name = env::var("AZURE_QUEUE_NAME").expect("Please set AZURE_QUEUE_NAME env variable first!");let policy_name = env::var("AZURE_POLICY_NAME").expect("Please set AZURE_POLICY_NAME env variable first!");let policy_key = env::var("AZURE_POLICY_KEY").expect("Please set AZURE_POLICY_KEY env variable first!");let http_client = azure_core::new_http_client();let client = QueueClient::new(http_client,service_bus_namespace,queue_name,policy_name,policy_key).expect("Failed to create client");let message_to_send = serde_json::to_string(&image).expect("Failed to serialize image");client.send_message(message_to_send.as_str()).await.expect("Failed to send message");println!("Message sent to Azure Service Bus queue successfully!");println!("Message: {}", message_to_send);}
Here we are sending a message to the Azure Service Bus queue with the image filename and the container name where the image is stored, so we can later use this information to get the correct image from the blob container and resize the image and save it back to the Blob Storage.
Make sure to create a Azure Service Bus and Queue item in your Azure Portal, and also make sure to have all the required environment variables set in your system:
bashexport AZURE_SERVICE_BUS_NAMESPACE="your-service-bus-namespace"export AZURE_QUEUE_NAME="your-queue-name"export AZURE_POLICY_NAME="your-policy-name"export AZURE_POLICY_KEY="your-policy-key"
Now if you run the API and upload an image, if everything is ok you should see the following output:
bashBlob uploaded successfullyUploaded file url: https://storage_account_name.blob.core.windows.net/container_name/filename.jpegMessage sent to Azure Service Bus queue successfully!Message: {"filename":"filename.jpeg","image_container":"container_name"}
Go ahead and check the Azure Blob Storage and Azure Service Bus to see if the image is uploaded and the message is sent to the queue.
If everything is successful, we can now move on to the next step which is to create an Azure Function to listen to the Service Bus queue and resize the image.
Create Azure Function to resize image
Creating a Azure function can be done using both Azure Portal or Azure CLI, also there is easier option which uses VSCode. In this tutorial, I will use Azure CLI to create the Azure Function. Make sure you have Azure CLI installed on your machine.
Navigate to the image-resize
directory and run the following command to create a new Azure Function:
bashmkdir functions && cd functionscargo init
This will create a new Rust project with the name functions
. Now let's add the required dependencies to the Cargo.toml
file:
toml[package]name = "handler" // Azure Function nameversion = "0.1.0"edition = "2021"[dependencies]warp = "0.3"tokio = { version = "1.12", features = ["macros", "fs", "rt-multi-thread"] }futures = { version = "0.3", default-features = false }serde = "1.0.200"serde_json = "1.0"azure_core = "0.20.0"azure_storage = "0.20.0"azure_storage_blobs = "0.20.0"azure_messaging_servicebus = "0.20.0"tracing = "0.1.40"image = "0.25.1"
Now let's create host.json
and local.settings.json
file, this is needed for Azure Functions.
json// host.json{"version": "2.0","logging": {"applicationInsights": {"samplingSettings": {"isEnabled": true,"excludedTypes": "Request"}}},"extensionBundle": {"id": "Microsoft.Azure.Functions.ExtensionBundle","version": "[4.*, 5.0.0)"},"customHandler": {"description": {"defaultExecutablePath": "handler", // make sure this is the same as the binary name"workingDirectory": "","arguments": []}},"concurrency": {"dynamicConcurrencyEnabled": true,"snapshotPersistenceEnabled": true}}// make sure this file is in .gitignore// local.settings.json{"IsEncrypted": false,"Values": {"AzureWebJobsStorage": "","FUNCTIONS_WORKER_RUNTIME": "custom",// make sure to replace with your service bus namespace, and access key"servicebusnamespace_SERVICEBUS": "Endpoint=sb://servicebusnamespace.servicebus.windows.net/;SharedAccessKeyName=SharedAccessKeyName;SharedAccessKey=SharedAccessKey"}}
Now let's create a function using func new
command:
bashfunc new --template "Azure Service Bus Queue trigger" --name "process_image_resize"
This will create a new folder called process_image_resize
with the functions.json
file, let's modify the functions.json
file:
json{"bindings": [{"name": "mySbMsg","type": "serviceBusTrigger","direction": "in","queueName": "queue_name", // replace queue_name with your queue name"connection": "servicebusnamespace_SERVICEBUS" // replace servicebusnamespace with your service bus namespace}]}
Now let's add the following code to the src/main.rs
file:
rustuse azure_messaging_servicebus::service_bus::QueueClient;use azure_storage::StorageCredentials;use azure_storage_blobs::prelude::BlobServiceClient;use serde::{Deserialize, Serialize};use futures::StreamExt;use tracing::trace;use std::{env, io::Cursor};#[derive(Serialize, Deserialize, Debug)]struct ImageNode {filename: String,image_container: String,}#[tokio::main]async fn main() -> azure_core::Result<()> {let service_bus_namespace = env::var("AZURE_SERVICE_BUS_NAMESPACE").expect("Please set AZURE_SERVICE_BUS_NAMESPACE env variable first!");let queue_name = env::var("AZURE_QUEUE_NAME").expect("Please set AZURE_QUEUE_NAME env variable first!");let policy_name = env::var("AZURE_POLICY_NAME").expect("Please set AZURE_POLICY_NAME env variable first!");let policy_key = env::var("AZURE_POLICY_KEY").expect("Please set AZURE_POLICY_KEY env variable first!");let http_client = azure_core::new_http_client();let client = QueueClient::new(http_client,service_bus_namespace,queue_name,policy_name,policy_key).expect("Failed to create client");let received_message = client.receive_and_delete_message().await.expect("Failed to receive message");if received_message.is_empty() {println!("No message received");return Ok(())}println!("Received message: {:?}", received_message);// grab the image from the messagematch serde_json::from_str::<ImageNode>(&received_message) {Ok(image) => {println!("Deserialized image: {:?}", image);// Azure Blob Storage credentialslet storage_account = env::var("AZURE_STORAGE_ACCOUNT").expect("Missing AZURE_STORAGE_ACCOUNT env var");let storage_access_key = env::var("AZURE_STORAGE_ACCESS_KEY").expect("Missing AZURE_STORAGE_ACCESS_KEY env var");let container_name = image.image_container;let blob_name = &*image.filename;// create Azure Blob Storage clientlet storage_credentials = StorageCredentials::access_key(storage_account.clone(), storage_access_key);let service_client = BlobServiceClient::new(storage_account, storage_credentials);let blob_client = service_client.container_client(&container_name).blob_client(blob_name);trace!("Requesting blob");let mut bytes: Vec<u8> = Vec::new();// stream a blob, 8KB at a timelet mut stream = blob_client.get().chunk_size(0x2000u64).into_stream();while let Some(value) = stream.next().await {let data = value?.data.collect().await?;println!("received {:?} bytes", data.len());bytes.extend(&data);}// load the image from the byteslet img = image::load_from_memory(&bytes).expect("Failed to load image");// resize the imagelet resized_img = img.resize(100, 100, image::imageops::FilterType::Triangle);// write the resized image to the bufferlet mut resized_bytes: Vec<u8> = Vec::new();resized_img.write_to(&mut Cursor::new(&mut resized_bytes), image::ImageFormat::Jpeg).expect("Failed to write image");// change the filename to include the word "resized"let new_blob_name = format!("resized_{}", blob_name);let blob_client = service_client.container_client(&container_name).blob_client(&new_blob_name);blob_client.put_block_blob(resized_bytes).content_type("image/jpeg").await.expect("Failed to upload blob");println!("Resized image uploaded successfully");},Err(e) => {println!("Failed to deserialize image: {:?}", e);return Ok(())}};Ok(())}
The above code will listen to the Azure Service Bus queue and receive the message, then it will get the image from the Azure Blob Storage and resize the image and save it back to the Blob Storage with the filename prefixed with resized_
.
Now with everything in place, let's start our server and upload an image:
bash// image-resize/apicargo run -r// upload an imageCURL -X POST -F 'file=@/path/to/image.jpeg' http://localhost:3030/upload
Now let's start the Azure Function by running the following command:
bash// image-resize/functionscargo build -r && cp ./target/debug/handler . && func start
If everything is successful, you should see the following output:
bashReceived message: "{\"filename\":\"example.jpeg\",\"image_container\":\"image_container_name\"}"Deserialized image: ImageNode { filename: "avatar-1.jpeg", image_container: "image_container_name" }received 8192 bytesreceived 8192 bytes......Resized image uploaded successfully
If you go back to the Azure Blob Storage, you should see the resized image with the filename prefixed with resized_
.
Conclusion
This is just a simple example of how you can use Azure Functions with Rust to create a simple image processing function, there are many other possibilities and use cases where you can use Azure Functions with Rust. Overall it was a fun experience to use Rust with Azure Functions, and I learned a lot about Azure Functions and Rust in the process.
Even though Azure SDK for Rust is still in unofficial
state, it is still very good and easy to use, and I hope it will be officially supported by Microsoft in the future.