🪐

Pedal to the Metal with PlanetScale and Rust

When we started building Bend, we knew we wanted our technology choices to support our mission: a greener modern world, supported by a cleaner modern web. To do that, we decided to take a chance on two novel inventions: Rust for our serverless back-end, and PlanetScale for an enterprise-quality database.

Rust is a memory-safe systems programming language that has secured the title of most beloved language on StackOverflow’s annual developer survey for the past six years running. It’s not just hype. Programs written in Rust can run as fast as the processor, with no VM or garbage collector taking up cycles. Then there's PlanetScale, which provides small teams with exceptional cloud database features (like sharding and size-balancing) under a progressive pricing structure.

These two technologies seemed like a match made in heaven. The best in database technology with an unbeatably fast and secure access layer. There was just one problem… 

…no one had ever done it before.

This is our guide to developing a production-quality API on PlanetScale and Rust.

The Tutorial

For the purpose of this guide, we’ll be developing a scaled-down version of Bend's company profile API, or org-service... using PlanetScale, Rust, the Rocket web-framework, and the Diesel ORM. You can view the finished project on GitHub.

Dependencies

  • Rust
    • curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • MySQL
    • brew install mysql-client
  • Diesel
    • cargo install diesel_cli --no-default-features --features mysql
  • PlanetScale’s CLI
    • brew install planetscale/tap/pscale

Let’s generate our project! In your home directory, run: cargo new org-service

This generates a new project directory for our Rust service with a Cargo.toml file and a main.rs entry-point.

Creating a Database

In a dev environment with multiple team members, you'd likely want to have MySQL running locally so that each engineer could connect to their own instance of the test db. We're going to skip that step though, and start developing directly on PlanetScale!

One of PlanetScale's mission drivers is that creating a new database should take no more than 10 seconds. Make an account if you haven't already (it's free!)—the rest we can do from the command line. Run the following from your terminal:

pscale auth login 
⁠pscale database create demo

If you have the PlanetScale dashboard up, you should see a new instance titled "demo" appear under your organization. Otherwise, you can verify it created successfully with:

pscale database list

Our First Migration

Diesel provides a simple but well-integrated system for applying and rolling back database migrations. We’ll just create one migration in this tutorial, but that should give you everything you'd need to know for adding more down the road.

But before we create our migration, we need a user to access the database!

pscale password create demo main local

You should see an output like this:

Diesel connects via a DATABASE_URL environment variable, so inside your org-service directory, create a .env file with the following, replacing the params in square brackets with the output of the pscale command.

DATABASE_URL=mysql://[USERNAME]:[PASSWORD]@[ACCESS HOST URL]/demo

(Don't forget to gitignore your .env file!)

In other cases we might begin with the command `diesel setup`, but since PlanetScale manages schema creation for us, we can jump straight to architecting it. Generate a Diesel migration with:

diesel migration generate init_schema

This creates a set of directories with an up.sql file and a down.sql file. To create our orgs table, let's add the following:

up.sql
<></>
down.sql
<></>

Assuming you maintain your Up and Down SQL correctly, you can easily run, revert, and redo migrations with the Diesel CLI. For now, let's just move on.

diesel migration run
diesel migration list

This will create our orgs table and list the migration as run. It also generates a schema.rs file—each type in the schema corresponds to a Rust type.

Building the API Service

We’re almost ready to write some Rust. Modify your Cargo.toml file to include the following:

<></>

Next, let's write a small helper function to establish connections to our database:

db.rs
<></>

Now we can start interacting with our schema. I like to separate responsibilities into a "data access layer" and routes. We'll start with the former, so create a new directory under src called dal and add the following:

dal/mod.rs
<></>
dal/org.rs
<></>

I'm assuming a basic familiarity with Rust here, so I won't dig too deep into the nuts and bolts here. At a high level, notice that we've implemented basic CRUD functionality, plus a simple getter for a single org by name.

Next we can add our routes module. Add another new directory under src called routes .

routes/mod.rs
<></>
routes/org.rs
<></>

We're using some Rocket magic to streamline these request handlers. In more advanced cases, you'd likely find that you needed to do more data processing before passing things along to the DAL... but that's for another tutorial!

Finally, it all gets sewn together in main.rs.

main.rs
<></>

Boot this thing up with cargo run and test out the endpoints yourself!

Deploying the API Service

The last step, and the one I see most frequently neglected in tutorials like these, is going live. In this walk-through, we'll package the service into a Docker image. From there, you could easily deploy it to Google Cloud Run (or AWS, or Azure)!

There are a couple tricky things to note with our Dockerfile though:

  1. We use cargo-chef to speed up our Docker builds. This caches the dependency layer for faster subsequent builds.
  2. Debian uses MariaDB by default, and Diesel relies on some MySQL specific configuration options to properly initiate connections via SSL. Thus, we tear down the existing MariaDB install in our Debian image and reinstall proper MySQL.

Here's our Dockerfile:

<></>

Build the image with docker build . --tag demo-api-svc

We can provide it with our DATABASE_URL environment variable when we run it like so:

docker run -p 8000:8000 -e DATABASE_URL=mysql://[USERNAME]:[PASSWORD]@[ACCESS HOST URL] demo-api-svc

There's one last problem though... when we build and run this image, and try to hit the org list endpoint... we get the following error:

Code: UNAVAILABLE\nserver does not allow insecure connections, client must use SSL/TLS

Remember when I claimed that no one had ever done this before? You may have rightly wondered, "how do you know?" Well PlanetScale requires SSL connections, and from certain images, it's necessary to manually specify the Certificate Authority roots when you initiate a secure database connection. You can read more about this in PlanetScale's excellent documentation on the subject.

Diesel didn't support specifying CA roots until recently. ...In fact, we had to add the functionality ourselves! The PR was merged just a couple weeks ago.

To deploy this service on Debian—we'll have to update to the latest (unstable) 2.0.0 version of Diesel. Update line 10 of Cargo.toml to the following:

diesel = { git = "https://github.com/diesel-rs/diesel", features = ["mysql", "extras", "chrono"] }

This introduces one breaking change—you'll have to also change the decorator on line 18 of dal/org.rs

#[diesel(table_name = orgs)]

Rebuild the Docker image and rerun with this modification to the environment variable:

docker run -p 8000:8000 -e DATABASE_URL="mysql://[USERNAME]:[PASSWORD]@[ACCESS HOST URL]?ssl_mode=verify_identity&ssl_ca=/etc/ssl/certs/ca-certificates.crt" demo-api-svc

And voila! We have just configured a ready-to-deploy, Dockerized image of our API service, ready to ship with PlanetScale.

Remember to check out the completed tutorial code here, and our full public dataset at bend.green. Don't be a stranger—follow us on Twitter or join our Discord and tell us what you think!

Let's bend the climate curve, one line of code at a time.