I heard a lot of great things about Rust.  The promise of a C-like language with guaranteed memory safety sounds too good to pass up.  It's super fast, and seemed to be a useful language to learn given you can write code for embedded systems, command line applications, and front-end and back-end web applications.

What is Rust?

On their website, Rust states their intentions are to create a language that empowers everyone to build reliable and efficient software.  It's major features include memory and thread safety, which is a common area for bugs to show up in code.  Rust is created and supported by Mozilla and can be found here: https://www.rust-lang.org/.  Another helpful resource is their thorough tutorial they affectionately call "The Book": https://doc.rust-lang.org/stable/book/title-page.html.

Rust on AWS Lambda

All that being said, why not set up an AWS Lambda to run my Rust code! Rust is not a supported runtime for AWS Lambda, so this will also be good experience working with custom runtimes (https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html).

Luckily, AWS Labs has done a lot of the leg work getting Rust to run on Lambda.  Check out the links below for more information.  Be wary, as I chose to use the master branch of lambda instead of the current release (v0.3.0) in my code sample. The latest release is missing an important recent update at the time of writing this for using shared data between lambda runs. However, you can use the v0.3.0 release without this update using the AWS Labs Github examples.

Let's get to the code!

Let's take a quick walk-through of the code linked at the bottom of this article.  The file structure is below

.
├── Cargo.lock
├── Cargo.toml
├── events // folder to hold Lambda JSON events
│   ├── google.json
│   └── turnip-exchange.json
├── README.md
├── run.sh // script to run Lambda code locally 
├── src
│   ├── client
│   │   └── mod.rs // surf client code
│   └── main.rs // AWS Lambda Rust runtime code
└── template.yaml // SAM template for Lambda 

main.rs has the code the sets up the AWS Lambda Rust runtime.  There are more examples and explanations on this code at the links in the previous section.  

mod client; // module for surf client wrapper

use client::RustLambdaHttpClient;
use lambda::{handler_fn, Context}; // AWS Lambda runtime for Rust
use serde_derive::{Deserialize, Serialize};

type Error = Box<dyn std::error::Error + Send + Sync + 'static>;

/// The custom incoming Lambda event structure
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CustomEvent {
    host: String,
    path: String,
    http_verb: String,
    #[serde(default)]
    post_data: String,
}

/// The custom outgoing Lambda event structure
#[derive(Serialize)]
struct CustomOutput {
    message: String,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    // shared client to be used across lambda invocations
    let client = RustLambdaHttpClient::new();
    let client_ref = &client;
    
    // entry point for the lambda event
    lambda::run(handler_fn(
        move |event: CustomEvent, _ctx: Context| async move {
            dbg!(&event);
            let url = format!("{host}{path}", host = event.host, path = event.path);
            let body = client_ref
                .clone()
                .send(event.http_verb, url, event.post_data)
                .await?;

            Ok::<CustomOutput, Error>(CustomOutput {
                message: format!("Body: {}", body),
            })
        },
    ))
    .await?;

    Ok(())
}

Making an HTTPS request

A working sample of Rust running on Lambda is available on my Github. It parses an incoming event to make a simple HTTP request using surf.  You can run the lambda locally in a docker container using the run.sh script available in the repository.  Deployment to your AWS environment can be done through the SAM CLI tool from AWS.  

I've separated the client code to a client module.  The code for that is below

use crate::Error;
use surf::http::Method;
use surf::Client;
use surf::Url;

/// Wrapper used to share the surf client struct
#[derive(Clone)]
pub struct RustLambdaHttpClient {
    client: Client,
}

impl RustLambdaHttpClient {
    pub fn new() -> Self {
        RustLambdaHttpClient {
            client: surf::Client::new(),
        }
    }

    pub async fn send(
        self,
        http_verb: String,
        url: String,
        post_data: String,
    ) -> Result<String, Error> {
        let mut res = self.client
            .send(
                surf::RequestBuilder::new(parse_http_method(&http_verb), Url::parse(&url)?)
                    .body(post_data)
                    .header(
                        "User-Agent",
                        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0",
                    )
                    .build(),
            )
            .await?;
        let body = res.body_string().await?;
        dbg!(&body);
        Ok(body)
    }
}

/// Parses HTTP method codes from the Lambda Event
fn parse_http_method(input_method: &str) -> Method {
    match input_method {
        "GET" => Method::Get,
        "POST" => Method::Post,
        _ => panic!("No matching HTTP method for {}", input_method),
    }
}

Source Code

My source code is below!  Feel free to use it as a template for your own needs.  Hopefully it helps.  Since I'm new to Rust, any feedback is appreciated.

mitchgollub/rust-lambda-https
Contribute to mitchgollub/rust-lambda-https development by creating an account on GitHub.