Rust on AWS App Runner - Part1
Published: 2023-08-21 14:36:51

Hey hey hey 🍕!

Over the past few months, I've been advocating for Rust on AWS Lambda a lot!

Here is a quick recap in case you missed it!

- [Live coding](https://www.youtube.com/watch?v=Idys2BAmqIU)  a AWS Lambda in Rust with TDD
- [Lambda-Perf tool](https://github.com/maxday/lambda-perf) where Rust is killing the cold start game

By the way, we're almost at 256 ⭐️ which is 🤯 for a side project! I'm sure we can do it!

- [Rust Linz Meetup](https://www.youtube.com/live/5ptACic_FJQ?feature=share&t=255) where I gave a talk about Serverless + Rust

But as you may know, serverless is NOT only about AWS Lambda. If you need compute, **AWS App Runner** might be a great fit!

## AWS App Runner 

If you see Lambda as `functions as a service` you can see 
App Runner as `containers as a service`.

It means that you can focus on 
- building your app
- containerized it (ie: Docker image)

and AWS will scale it for you, including to zero.

It also means that you will experience a cold start when no containers are up or when you need to handle more traffic.

Similarly to AWS Lambda, Rust would be a great candidate for AWS App Runner if we manage to build a tiny container which starts extremely fast! 

Annnnnnd that's what we are going to try to do in the series of blog post! 

## Ready? Let's gooooooo! 🎉

- **Part1 - Create a simple Rust API - this blog post**
- Part2 - Containerize it! (and make it small)
- Part3 - Deploy it to AWS App Runner

## Part 1 - Create a simple Rust API

In this part, we will focus on building a simple Rust service.

Let's bootstrap the project using `cargo` 
and run `cargo new pizza-rust --bin` 
(oh yeah we're going to talk about 🍕)

You should now have this file structure:

![file structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43q551m441gvqrunlczx.png)

## Dependency
There are multiple web framework crates in Rust but one the most commonly used is `actix_web` so let's use this one!

We're also going to need `serde` to serialize and deserialize our pizzas.

Finally, for testing purpose, we're going to use both `actix-rt` abd `actix-test`

## main.rs
Our main function will look like this:

```rust
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(move || {
        App::new()
        .service(pizza_service)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}
```

Here we've defined a very minimal web server, listening on 8080 and using the not-yet-defined `pizza_service`

## Test first!

Before going too far in the actual code, let's write our unit test first.
When we hit /pizza, we want to receive a JSON array of pizza.

```rust
#[cfg(test)]
mod tests {
    use crate::{pizza_service, Pizza};
    use actix_web::{App, web};

    #[actix_rt::test]
    async fn test() {

        // define a test pizza
        let pizzas = vec![
            Pizza::new("pizzaTest", vec!["topping0", "topping1"], 1234),
        ];

        // define our in-memory db
        let app_data = web::Data::new(pizzas);

        // define our test server
        let srv = actix_test::start(move || 
            App::new()
            .app_data(app_data.clone())
            .service(pizza_service)
        );

        // perform the request
        let req = srv.get("/pizza");
        let mut response = req.send().await.unwrap();
        assert!(response.status().is_success());
        let result_pizza = response.json::<Vec<Pizza>>().await.unwrap();

        // assert the result
        assert!(result_pizza[0].name == "pizzaTest");
        assert!(result_pizza[0].toppings == vec!["topping0", "topping1"]);
        assert!(result_pizza[0].price == 1234);
    }
}
```
## Back to main.rs
Finally, let's modify our code to satisfy our test

```rust
// let's create our pizza structure, making sure we can serialize and deserialize it!
#[derive(Serialize, Deserialize)]
struct Pizza {
    name: String,
    toppings: Vec<String>,
    price: i32,
}

// simple Pizza constructor
impl Pizza {
    fn new(name: &str, toppings: Vec<&str>, price: i32) -> Pizza {
        Pizza {
            name: name.to_string(),
            toppings: toppings.iter().map(|s| s.to_string()).collect(),
            price,
        }
    }
}

// define our GET endpoint
#[get("/pizza")]
async fn pizza_service(data: web::Data<Vec<Pizza>>) -> impl Responder {
    //just returning the JSON representation of our pizzas
    HttpResponse::Ok().json(&data)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    // our fresh pizzas
    let pizzas = vec![
        Pizza::new("Margherita", vec!["tomato", "fior di latte"], 10),
        Pizza::new("Veggie", vec!["green peppers", "onion", "mushrooms"], 12),
    ];

    // in-memory db
    let app_data = web::Data::new(pizzas);

    HttpServer::new(move || {
        App::new()
        .app_data(app_data.clone())
        .service(pizza_service)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}
```

That's it!
Our test is now passing:

```bash
cargo test
   Compiling app-runner-rust v1.0.0 (/rust)
    Finished test [unoptimized + debuginfo] target(s) in 1.27s
     Running unittests src/main.rs (target/debug/deps/app_runner_rust-46a4a4cb852eb0c3)

running 1 test
test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
```

## Final check

Let's run our API with `cargo run` 
and hit our endpoint `curl -i http://localhost:8080/pizza`

```bash
curl -i http://localhost:8080/pizza
HTTP/1.1 200 OK
content-length: 150
content-type: application/json
date: Sun, 20 Aug 2023 00:26:27 GMT

[{"name":"Margherita","toppings":["tomato","fior di latte"],"price":10},{"name":"Veggie","toppings":["green peppers","onion","mushrooms"],"price":12}]
```

Voila! 🍕

Our API is now ready to be containerized and that's the topic of the next blog post in this series! 

👋 Stay tunned for part 2 next week!

You can find me on [LinkedIn](https://www.linkedin.com/in/maxday/), [YouTube](https://www.youtube.com/@maxday_coding) and [Twitter](https://twitter.com/_maxday)!