Using Codeship Pro to Test and Deploy Rust Applications

Development

Reading Time: 6 minutes

Rust is one of the most loved programming languages in the entire world right now. Day after day, more companies are starting to adopt and integrate Rust into their technology stacks. With Rust’s booming growth, it’s important to understand how to integrate this new and exciting language into our CI processes. In this article, I want to discuss what it’s like to create a CI process for testing and deploying Rust-based applications with Codeship Pro.

We’ll be using a few other technologies along the way:

  1. Rust Nightly
  2. Docker Community Edition
  3. Codeship’s Jet Command Line Tool
  4. Heroku CLI
  5. Codeship Pro

Before we go too far, I want to discuss the basics of Rust testing.

How Do We Test Rust Applications?

If you’re not entirely familiar with Rust, don’t get nervous. There are only a few things you need to know about testing Rust. Luckily for us, Rust has some incredible documentation on how testing works and flows within the language.

Let’s say I have a main.rs that looks something like this:

fn main() {
    find_average(1, 2, 3);
}

fn find_average(one: i32, two: i32, three: i32) -> i32 {
    let summation = one + two + three;
    return summation / 3;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_average() {
        let avg = find_average(1, 2, 3);
        assert_eq!(2, avg);
    }
}

As seen above, there are a few important attributes to Rust tests:

  1. Scoping the tests under mod test with the macro #[cfg(test_]
  2. Importing the use super::* declaration
  3. Using the macro #[test] with each test.
  4. Writing the tests as functions

With this overview in mind, let’s move on to writing and testing a Rust web application.

Writing a Rust Web Application

Today we’ll be using a framework called Rocket. Rocket is a fairly new Rust-based web framework. It’s often combined with ORMs like Diesel to create a full Rust-based technology stack.

The main value of Rocket lies within its routing, middleware, and view systems. Rocket does not handle aspects such as database queries and calls (that’s where ORMs like Diesel come in).

If you haven’t already, create a new Rust application by running cargo new —-bin rocket_test. To integrate Rocket into our Rust application, we’ll need to modify the Cargo.toml file to look like the following:

Cargo.toml

[package]
name = "rocket_test"
version = "0.1.0"
authors = [Taylor Jones "taylor@jones.com"]

[dependencies]
rocket = { version = "0.2.8", features = ["testing"] }
rocket_codegen = "0.2.8"

Run cargo build and ensure that all the dependencies above agree with your current version of Rust.

Now, we’re ready to start writing the main logic of our application.

src/main.rs

// Enabling macros
#![feature(plugin)]
#![plugin(rocket_codegen)]

// Declaring our intentions to use Rocket
extern crate rocket;

// Our root route - returns a string pointer
#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

// Main method
fn main() {
      // Sets up rocket, ctivates routes, and launches server
    rocket::ignite().mount("/", routes![index]).launch();
}

With our new code, we’ll compile the application again with cargo build.

You should now be able to execute cargo run, visit localhost:8000, and see the words “Hello World” printed on the webpage.

You’ve now created a basic Rocket application. Now, to write tests for it!

Testing a Rocket Application

With a solid base application, let’s support our newly written logic with a test. Try adding the following to the bottom of your application source code.

lib/main.rs

#[cfg(test)]
mod test {
    use super::rocket;
    use rocket::testing::MockRequest;
    use rocket::http::{Status, Method};

    #[test]
    fn index() {
        let rocket = rocket::ignite().mount("/", routes![super::index]);
        let mut req = MockRequest::new(Method::Get, "/");
        let mut response = req.dispatch_with(&rocket);
        assert_eq!(response.status(), Status::Ok);

        let body_str = response.body().and_then(|b| b.into_string());
        assert_eq!(body_str, Some("Hello, world!".to_string()));
    }
}

Execute cargo test and we should see that our singular test passes!

Next up: creating a deployment environment on Heroku.

Rust on Heroku

To create a Rust environment on Heroku, run the following commands:

heroku create
heroku buildpacks:set https://github.com/emk/heroku-buildpack-rust.git

This will create a Heroku application, but also add the Rust buildpack to the deployment process. We’ll also want to create a file called RustConfig with the following content:

RustConfig

VERSION=nightly

This tells Heroku to use Rust Nightly for deployments.

Finally, we’ll want to set our Procfile for use on Heroku. With Rocket, it should look something like this:

Procfile

web: ROCKET_PORT=$PORT ROCKET_ENV=prod ./target/release/rust_blog

!Sign up for a free Codeship Account

Rust on Codeship Pro

With our deployment process in place, we can now move on to writing our Codeship CI process!

Setting up API keys

Before we start writing our Codeship process, we’re going to need two important credentials: our Codeship API key (foundhere) and our Heroku API key (found here). Once obtained, you’ll want them to match the following structure:

codeship.aes

your-codeship-api-key

deployment.env

HEROKU_API_KEY=[your-heroku-api-key]

Please ensure that both of these files are not checked into source control, as they could comprise the security of your application (if exposed).

With both of these files checked out of source control and in place, we’re ready to encrypt them with Jet.

jet encrypt deployment.env deployment.env.encrypted

If successful, this should create an encrypted file called: deployment.env.encrypted. This file can and should be committed into your source control for use in our Codeship CI Process.

Creating a Codeship Pro CI process

Now that we have a local application with passing tests, encrypted credentials, and a deployment server, we need to figure out how to piece it all together. We’ll do this by writing a few Docker-centric files.

First up, our Dockerfile:

Dockerfile

FROM liuchong/rustup:nightly

RUN mkdir -p /app
WORKDIR /app
COPY Cargo.toml ./Cargo.toml
COPY src ./src
RUN cargo build

WORKDIR /code

This file is the basis for the environment Codeship will build in order to test and deploy our application. We take a premade Docker image containing Rust’s nightly channel and mount our application onto our Docker virtual machine.

Next up, we’ll be writing codeship-services.yml:

codeship-services.yml

app:
  build:
    image: liuchong/rustup:nightly
    dockerfile_path: Dockerfile
  cached: true
  volumes:
    - ./:/code
deploy:
    image: codeship/heroku-deployment
    encrypted_env_file: deployment.env.encrypted
    volumes:
      - ./:/deploy

This file defines two services for usage: app and deploy.

The service app is fairly standard. It tells Codeship where the Dockerfile is and where to mount the application. The deploy service takes our encrypted Heroku and Codeship keys and uses them for testing and deployment.

Finally, we’ll be creating codeship-steps.yml:

codeship-steps.yml

- name: rust
  service: app
  command: cargo test
- name: deploy
  tag: master
  service: deploy
  command: codeship_heroku deploy /deploy [your-heroku-app-name]

This file defines two steps for our application to go through. First, a testing step. If the application passes the testing process, it will move on the deployment step.

We’ll also want to replace [your-heroku-app-name] with your actual Heroku app name. Please note that the Heroku app name may be different from your local application name.

Trigger the process

With all of these pieces in place, we can now execute our Codeship CI process. To do so, we’ll simply run git push origin master, and a build should trigger on Codeship. If the testing is successful, we should also be able to see that our application deployed on Heroku!

Final Thoughts

While we’ve spent a little bit of time learning how to set up a CI process, we’ve now streamlined a significant piece of our DevOps process. With this in place, we’re able to spend more time developing and less time setting up complex and sporadic deployments.

You can find all of the source code that we discussed within the following GitHub repo. It should provide you with a solid backbone to build up any future Rust ideas and projects you may have. The best part about all of it is that you now have an awesome means to test and deploy your application!

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.