Deploying Rust code on production
May 5, 2024•767 words
Yep, I said it. I finally published my Rust code on production and it runs smoothly. This post will be a quick one describing my experience on deploying a backend application written in Rust.
Why Rust?
Some people would give out reasons like "performance", or "benefits of the type system". But, that's not the case for me. It's okay if the application that I'm handling is not performant, and I don't really care about the type system. The problem lays on there's little to no users for the application -- the request per second is too low, and this causes headache on maintaining the server. The application that we're talking about is Pesto -- a remote code execution engine, one of my open source project with Teknologi Umum. It consist of 4 modules: frontend (written in Astro), rce (the module for executing remote code, written in Node.js), auth (for authenticating request with API keys, previously written in Go), and registration (for signing up to acquire API keys, previously written in .NET). Everything actually works great, no errors, tracing works, observability works, but it's kind of a burden when everything is idle and it consumes a lot of RAM usage. The registration module usually consumes around 300 - 500 MB of RAM, and the auth module consumes around 20-50 MB of RAM. It's not that much per se, but I deployed everything on a single VM that also have other applications (total of around 20-ish Docker containers). I thought Rust was the way to go just because of its' memory footprint. Another option was C++, I don't want that, so let's make do with Rust.
About Rust ecosystem
Rust is.. not a friendly place to develop anything other than CLI application for Linux, really. Rust development experience on Windows is poor, and it has limited capabilities for cross-compiling even for M1 Macs (the aarch64-darwin architecture). There's been some clashes between the core maintainer and the community, one of the peak of it is the creation of Crablang fork. But still, the worst experience for Rust development is anything that includes async
and async trait
. In every other language, async
is a breeze and supported by the standard libraries/packages, you can even create an async interface like so:
interface Foo {
Task<Bar> GetBarFromDatabase(string search, CancellationToken? cancellationToken);
}
But not in Rust, you have to install an external package (or crate, as they say it) named async_trait
and you have to include it as an annotation above your trait
:
#[async_trait]
trait Foo {
async fn get_bar_from_database(search: String) -> Result<Bar, FooError>
}
Rust is famous for having friendly error message that helps the programmer, yeah that's true, but not with everything related to async
. You'll have error messages whose stacktrace's belongs to the external packages, not your code. I don't have an example about how the error message would look like, but from now on, let's wish good luck to everyone who code async Rust.
Oh, and we'll use Tokio. We won't be using the std::{future, sync, thread, task}
because it's more humane for us. Sometimes I think that it should be renamed to Tokio language instead of Rust, just because how much Tokio is actually carrying the entire async ecosystem for Rust.
Migration process
To make everything runs smoothly, all I need to do is to create separate module while the old Go and .NET app is still running, then we can spawn multiple containers, load balance it, and then it will sometimes try the old container (Go/.NET ones) and some other times, it will try the new container (Rust ones). Since the repository is open source, you can peek on a few PRs:
- feat: auth_rust - Create the
auth_rust
module, provide a Docker image for it - feat: registration_rust - Wait for
auth_rust
to stabilize, then create theregistration_rust
module, also provide a Docker image - fix(auth_rust): TokenValue should be serialized as PascalCase - First known bug about serializing into Redis (that would be read by the other modules)
- fix(registration_rust): don't skip TokenValue deserializing - Another discovered bug, about
serde
behavior - fix(registration_rust): proper email sending - Add new feature on the
registration_rust
module to enable automatic email sending for registered users - build: remove old go auth and dotnet registration packages - Remove the old containers (Go/.NET ones), replace the default with Rust containers
Aftermath
The memory footprint drops very low. Both the auth and registration module that's rewritten to Rust now only consumes around 15 MB of RAM when idle, and that's a very big win.