A beginner's guide to writing your DevOps tools in Rust.
Introduction
In this blog post we'll cover some basic DevOps use cases for Rust and why
you would want to use it.
As part of this, we'll also cover a few common libraries you will likely use
in a Rust-based DevOps tool for AWS.
If you're already familiar with writing DevOps tools in other languages,
this post will explain why you should try Rust.
We'll cover why Rust is a particularly good choice of language to write your DevOps
tooling and critical cloud infrastructure software in.
And we'll also walk through a small demo DevOps tool written in Rust.
This project will be geared towards helping someone new to the language ecosystem
get familiar with the Rust project structure.
If you're brand new to Rust, and are interested in learning the language, you may want to start off with our Rust Crash Course eBook.
What Makes the Rust Language Unique
Rust is a systems programming language focused on three goals: safety, speed,
and concurrency. It maintains these goals without having a garbage collector,
making it a useful language for a number of use cases other languages aren’t
good at: embedding in other languages, programs with specific space and time
requirements, and writing low-level code, like device drivers and operating systems.
The Rust Book (first edition)
Rust was initially created by Mozilla and has since gained widespread adoption and
support. As the quote from the Rust book alludes to, it was designed to fill the
same space that C++ or C would (in that it doesn’t have a garbage collector or a runtime).
But Rust also incorporates zero-cost abstractions and many concepts that you would
expect in a higher level language (like Go or Haskell).
For that, and many other reasons, Rust's uses have expanded well beyond that
original space as low level safe systems language.
Rust's ownership system is extremely useful in efforts to write correct and
resource efficient code. Ownership is one of the killer features of the Rust
language and helps programmers catch classes of resource errors at compile time
that other languages miss or ignore.
Rust is an extremely performant and efficient language, comparable to the speeds
you see with idiomatic everyday C or C++.
And since there isn’t a garbage collector in Rust, it’s a lot easier to get
predictable deterministic performance.
Rust and DevOps
What makes Rust unique also makes it very useful for areas stemming from robots
to rocketry, but are those qualities relevant for DevOps?
Do we care if we have efficient executables or fine grained control over
resources, or is Rust a bit overkill for what we typically need in DevOps?
Yes and no
Rust is clearly useful for situations where performance is crucial and actions
need to occur in a deterministic and consistent way. That obviously translates to
low-level places where previously C and C++ were the only game in town.
In those situations, before Rust, people simply had to accept the inherent risk and
additional development costs of working on a large code base in those languages.
Rust now allows us to operate in those areas but without the risk that C and C++
can add.
But with DevOps and infrastructure programming we aren't constrained by those
requirements. For DevOps we've been able to choose from languages like Go, Python,
or Haskell because we're not strictly limited by the use case to languages without
garbage collectors. Since we can reach for other languages you might argue
that using Rust is a bit overkill, but let's go over a few points to counter this.
- Small executables relative to other options like Go or Java
- Easy to port across different OS targets
- Efficient with resources (which helps cut down on your AWS bill)
- One of the fastest languages (even when compared to C)
- Zero cost abstractions - Rust is a low level performant language which also
gives the us benefits of a high level language with its generics and abstractions.
To elaborate on some of these points a bit further:
OS targets and Cross Compiling Rust for different architectures
For DevOps it's also worth mentioning the (relative) ease with which you can
port your Rust code across different architectures and different OS's.
Using the official Rust toolchain installer rustup
, it's easy to get the
standard library for your target platform.
Rust supports a great number of platforms
with different tiers of support.
The docs for the rustup
tool has a section
covering how you can access pre-compiled artifacts for various architectures.
To install the target platform for an architecture (other than the host platform which is installed by default)
you simply need to run rustup target add
:
$ rustup target add x86_64-pc-windows-msvc
info: downloading component 'rust-std' for 'x86_64-pc-windows-msvc'
info: installing component 'rust-std' for 'x86_64-pc-windows-msvc'
Cross compilation is already built into the Rust compiler by default.
Once the x86_64-pc-windows-msvc
target is installed you can build for Windows
with the cargo
build tool using the --target
flag:
cargo build --target=x86_64-pc-windows-msvc
(the default target is always the host architecture)
If one of your dependencies links to a native (i.e. non-Rust) library, you will
need to make sure that those cross compile as well. Doing rustup target add
only installs the Rust standard library for that target. However for the other
tools that are often needed when cross-compiling, there is the handy
github.com/rust-embedded/cross tool.
This is essentially a wrapper around cargo which does all cross compilation in
docker images that have all the necessary bits (linkers) and pieces installed.
Small Executables
A key unique feature of Rust is that it doesn't need a runtime or a garbage collector.
Compare this to languages like Python or Haskell: with Rust the lack of any runtime
dependencies (Python), or system libraries (as with Haskell) is a huge advantage
for portability.
For practical purposes, as far as DevOps is concerned, this portability means
that Rust executables are much easier to deploy than scripts.
With Rust, compared to Python or Bash, we don't need to set up the environment for
our code ahead of time. This frees us up from having to worry if the runtime
dependencies for the language are set up.
In addition to that, with Rust you're able to produce 100% static executables for
Linux using the MUSL libc (and by default Rust will statically link all Rust code).
This means that you can deploy your Rust DevOps tool's binaries across your Linux
servers without having to worry if the correct libc
or other libraries were
installed beforehand.
Creating static executables for Rust is simple. As we discussed before, when discussing
different OS targets, it's easy with Rust to switch the target you're building against.
To compile static executables for the Linux MUSL target all you need to do is add
the musl
target with:
$ rustup target add x86_64-unknown-linux-musl
Then you can using this new target to build your Rust project as a fully static
executable with:
$ cargo build --target x86_64-unknown-linux-musl
As a result of not having a runtime or a garbage collector, Rust executables
can be extremely small. For example, there is a common DevOps tool called
CredStash that was originally written in Python but has since been
ported to Go (GCredStash) and now Rust (RuCredStash).
Comparing the executable sizes of the Rust versus Go implementations of CredStash,
the Rust executable is nearly a quarter of the size of the Go variant.
Implementation | Executable Size |
Rust CredStash: (RuCredStash Linux amd64) | 3.3 MB |
Go CredStash: (GCredStash Linux amd64 v0.3.5) | 11.7 MB |
Project links:
This is by no means a perfect comparison, and 8 MB may not seem like a lot, but
consider the advantage automatically of having executables that are a quarter of the
size you would typically expect.
This cuts down on the size your Docker images, AWS AMI's, or Azure VM images need
to be - and that helps speed up the time it takes to spin up new deployments.
With a tool of this size, having an executable that is 75% smaller than it
would be otherwise is not immediately apparent. On this scale the difference, 8 MB,
is still quite cheap.
But with larger tools (or collections of tools and Rust based software) the benefits
add up and the difference begins to be a practical and worthwhile consideration.
The Rust implementation was also not strictly written with the resulting size of
the executable in mind. So if executable size was even more important of a
factor other changes could be made - but that's beyond the scope of this post.
Rust is fast
Rust is very fast even for common idiomatic everyday Rust code. And not only that
it's arguably easier to work with than with C and C++ and catch errors in your
code.
For the Fortunes benchmark (which exercises the ORM,
database connectivity, dynamic-size collections, sorting, server-side templates,
XSS countermeasures, and character encoding) Rust is second and third, only lagging
behind the first place C++ based framework by 4 percent.
In the benchmark for database access for a single query Rust is first and second:
And in a composite of all the benchmarks Rust based frameworks are second and third place.
Of course language and framework benchmarks are not real life, however this is
still a fair comparison of the languages as they relate to others (within the context
and the focus of the benchmark).
Source: https://www.techempower.com/benchmarks
For medium to large projects, it’s important to have a type system and compile
time checks like those in Rust versus what you would find in something like Python
or Bash.
The latter languages let you get away with things far more readily. This makes
development much "faster" in one sense.
Certain situations, especially those with involving small project codebases, would
benefit more from using an interpreted language. In these cases, being able to quickly
change pieces of the code without needing to re-compile and re-deploy the project
outweighs the benefits (in terms of safety, execution speed, and portability)
that languages like Rust bring.
Working with and iterating on a Rust codebase in those circumstances, with frequent
but small codebases changes, would be needlessly time-consuming
If you have a small codebase with few or no runtime dependencies, then it wouldn't
be worth it to use Rust.
Demo DevOps Project for AWS
We'll briefly cover some of the libraries typically used for an AWS focused
DevOps tool in a walk-through of a small demo Rust project here.
This aims to provide a small example that uses some of the libraries you'll likely
want if you’re writing a CLI based DevOps tool in Rust. Specifically for this
example we'll show a tool that does some basic operations against AWS S3
(creating new buckets, adding files to buckets, listing the contents of buckets).
Project structure
For AWS integration we're going to utilize the Rusoto library.
Specifically for our modest demo Rust DevOps tools we're going to pull in the
rusoto_core and the
rusoto_s3 crates (in Rust a crate
is akin to a library or package).
We're also going to use the structopt crate
for our CLI options. This is a handy, batteries included CLI library that makes
it easy to create a CLI interface around a Rust struct.
The tool operates by matching the CLI option and arguments the user passes in
with a match
expression.
We can then use this to match on that part of the CLI option struct we've defined
and call the appropriate functions for that option.
match opt {
Opt::Create { bucket: bucket_name } => {
println!("Attempting to create a bucket called: {}", bucket_name);
let demo = S3Demo::new(bucket_name);
create_demo_bucket(&demo);
},
This matches on the Create
variant of the Opt
enum.
We then use S3Demo::new(bucket_name)
to create a new S3Client
which we can
use in the standalone create_demo_bucket
function that we've defined
which will create a new S3 bucket.
The tool is fairly simple with most of the code located in
src/main.rs
Building the Rust project
Before you build the code in this project, you will need to install Rust.
Please follow the official install instructions here.
The default build tool for Rust is called Cargo. It's worth getting familiar
with the docs for Cargo
but here's a quick overview for building the project.
To build the project run the following from the root of the
git repo:
cargo build
You can then use cargo run
to run the code or execute the code directly
with ./target/debug/rust-aws-devops
:
$ ./target/debug/rust-aws-devops
Running tool
RustAWSDevops 0.1.0
Mike McGirr <[email protected]>
USAGE:
rust-aws-devops <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
add-object Add the specified file to the bucket
create Create a new bucket with the given name
delete Try to delete the bucket with the given name
delete-object Remove the specified object from the bucket
help Prints this message or the help of the given subcommand(s)
list Try to find the bucket with the given name and list its objects``
Which will output the nice CLI help output automatically created for us
by structopt
.
If you're ready to build a release version (with optimizations turn on which
will make compilation take slightly longer) run the following:
cargo build --release
Conclusion
As this small demo showed, it's not difficult to get started using Rust to write
DevOps tools. And even then we didn't need to make a trade-off between ease of
development and performant fast code.
Hopefully the next time you're writing a new piece of DevOps software,
anything from a simple CLI tool for a specific DevOps operation or you're writing
the next Kubernetes, you'll consider reaching for Rust.
And if you have further questions about Rust, or need help implementing your Rust
project, please feel free to reach out to FP Complete for Rust engineering
and training!
Want to learn more Rust? Check out our Rust Crash Course eBook. And for more information, check out our Rust homepage.
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.
Do you like this blog post and need help with Next Generation Software Engineering, Platform Engineering or Blockchain & Smart Contracts? Contact us.