Oct 26, 2020
A few years back, we published a blog post about deploying a Rust application using Docker and Kubernetes. That application was a Telegram bot. We're going to do something similar today, but with a few meaningful differences:
We're going to be deploying a web app. Don't get too excited: this will be an incredibly simply piece of code, basically copy-pasted from the actix-web documentation.
We're going to build the deployment image on Github Actions
And we're going to be building this using Windows Containers instead of Linux. (Sorry for burying the lead.)
We put this together for testing purposes when rolling out Windows support in our managed Kubernetes product, Kube360® here at FP Complete. I wanted to put this post together to demonstrate a few things:
How pleasant and familiar Windows Containers workflows were versus the more familiar Linux approaches
Github Actions work seamlessly for building Windows Containers
With the correct configuration, Kubernetes is a great platform for deploying Windows Containers
And, of course, how wonderful the Rust toolchain is on Windows
Alright, let's dive in! And if any of those topics sound interesting, and you'd like to learn more about FP Complete offerings, please contact us for more information on our offerings.
Prereqs
Quick sidenote before we dive in. Windows Containers only run on Windows machines. Not even all Windows machines will support Windows Containers. You'll need Windows 10 Pro or a similar license, and have Docker installed on that machine. You'll also need to ensure that Docker is set to use Windows instead of Linux containers.
If you have all of that set up, you'll be able to follow along with most of the steps below. If not, you won't be able to build or run the Docker images on your local machine.
Also, for running the application on Kubernetes, you'll need a Kubernetes cluster with Windows nodes. I'll be using the FP Complete Kube360 test cluster on Azure in this blog post, though we've previously tested in on both AWS and on-prem clusters too.
The Rust application
The source code for this application will be, by far, the most uninteresting part of this post. As mentioned, it's basically a copy-paste of an example straight from the actix-web documentation featuring mutable state. It turns out this was a great way to test out basic Kubernetes functionality like health checks, replicas, and autohealing.
We're going to build this using the latest stable Rust version as of writing this post, so create a rust-toolchain
file with the contents:
Our Cargo.toml
file will be pretty vanilla, just adding in the dependency on actix-web
:
If you want to see the Cargo.lock
file I compiled with, it's available in the source repo. And finally, the actual code in src/main.rs
:
This code creates an application state (a mutex of an i32
), defines a single GET
handler that increments that variable and prints the current value, and then hosts this on 0.0.0.0:8080
. Not too shabby. If you're following along with the code, now would be a good time to cargo run
and make sure you're able to load up the site on your localhost:8080
.
Dockerfile
If this is your first foray into Windows Containers, you may be surprised to hear me say "Dockerfile." Windows Container images can be built with the same kind of Dockerfiles you're used to from the Linux world. This even supports more advanced features, such as multistage Dockerfiles, which we're going to take advantage of here.
There are a number of different base images provided by Microsoft for Windows Containers. We're going to be using Windows Server Core. It provides enough capabilities for installing Rust dependencies (which we'll see shortly), without including too much unneeded extras. Nanoserver is a much lighterweight image, but it doesn't play nicely with the Microsoft Visual C++ runtime we're using for the -msvc
Rust target. NOTE I've elected to use the -msvc
target here instead of -gnu
for two reasons. Firstly, it's closer to the actual use cases we need to support in Kube360, and therefore made a better test case. Also, as the default target for Rust on Windows, it seemed appropriate. It should be possible to set up a more minimal nanoserver-based image based on the -gnu
target, if someone's interested in a "fun" side project. The complete Dockerfile is available on Github, but let's step through it more carefully. As mentioned, we'll be performing a multistage build. We'll start with the build image, which will install the Rust build toolchain and compile our application. We start off by using the Windows Server Core base image and switching the shell back to the standard cmd.exe
:
Next we're going to install the Visual Studio buildtools necessary for building Rust code:
And then we'll modify the entrypoint to include the environment modifications necessary to use those buildtools:
Next up is installing rustup
, which is fortunately pretty easy:
Then we copy over the relevant source files and kick off a build, storing the generated executable in c:output
:
And with that, we're done with our build! Time to jump over to our runtime image. We don't need the Visual Studio buildtools in this image, but we do need the Visual C++ runtime:
With that in place, we can copy over our executable from the build image and set it as the default CMD
in the image:
And just like that, we've got a real life Windows Container. If you'd like to, you can test it out yourself by running:
If you connect to port 8080, you should see our painfully simple app. Hurrah!
Building with Github Actions
One of the nice things about using a multistage Dockerfile for performing the build is that our CI scripts become very simple. Instead of needing to set up an environment with correct build tools or any other configuration, our script:
Logs into the Docker Hub registry
Performs a
docker build
Pushes to the Docker Hub registry
The downside is that there is no build caching at play with this setup. There are multiple methods to mitigate this problem, such as creating helper build images that pre-bake the dependencies. Or you can perform the builds on the host on CI and only use the Dockerfile for generating the runtime image. Those are interesting tweaks to try out another time.
Taking on the simple multistage approach though, we have the following in our .github/workflows/container.yml
file:
I like following the convention of tagging my images with the Git SHA of the commit. Other people prefer different tagging schemes, it's all up to you.
Manifest files
Now that we have a working Windows Container image, the next step is to deploy it to our Kube360 cluster. Generally, we use ArgoCD and Kustomize for managing app deployments within Kube360, which lets us keep a very nice Gitops workflow. Instead, for this blog post, I'll show you the raw manifest files. It will also let us play with the k3
command line tool, which also happens to be written in Rust.
First we'll have a Deployment manifest to manage the pods running the application itself. Since this is a simple Rust application, we can put very low resource limits on this. We're going to disable the Istio sidebar, since it's not compatible with Windows. We're going to ask Kubernetes to use the Windows machines to host these pods. And we're going to set up some basic health checks. All told, this is what our manifest file looks like:
Awesome, that's the most complicated by far of the three manifests. Next we'll put a fairly stock-standard Service in front of that deployment:
This exposes a services on port 80, and targets the http
port (port 8080) inside the deployment. Finally, we have our Ingress. Kube360 uses external DNS to automatically set DNS records, and cert-manager to automatically grab TLS certificates. Our manifest looks like this:
Now that we have our application inside a Docker image, and we have our manifest files to instruct Kubernetes on how to run it, we just need to deploy these manifests and we'll be done.
Launch
With our manifests in place, we can finally deploy them. You can use kubectl
directly to do this. Since I'm deploying to Kube360, I'm going to use the k3
command line tool, which automates the process of logging in, getting temporary Kubernetes credentials, and providing those to the kubectl
command via an environment variable. These steps could be run on Windows, Mac, or Linux. But since we've done the rest of this post on Windows, I'll use my Windows machine for this too.
I told k3
to use the test.az.fpcomplete.com
cluster. On the first k3 kubectl
call, it detected that I did not have valid credentials for the cluster, and opened up my browser to a page that allowed me to log in. One of the design goals in Kube360 is to strongly leverage existing identity providers, such as Azure AD, Google Directory, Okta, Microsoft 365, and others. This is not only more secure than copy-pasting kubeconfig
files with permanent credentials around, but more user friendly. As you can see, the process above was pretty automated.
It's easy enough to check that the pods are actually running and healthy:
Initially, the ingress controller looked like this while it was getting TLS certificates:
And after cert-manager gets the TLS certificate, it will switch over to:
And finally, our site is live! Hurrah, a Rust web application compiled for Windows and running on Kubernetes inside Azure.
NOTE Depending on when you read this post, the web app may or may not still be live, so don't be surprised if you don't get a response if you try to connect to that host.
Conclusion
This post was a bit light on actual Rust code, but heavy on a lot of Windows scripting. As I think many Rustaceans already know, the dev experience for Rust on Windows is top notch. What may not have been obvious is how pleasant the Docker experience is on Windows. There are definitely some pain points, like the large images involved and needing to install the VC runtime. But overall, with a bit of cargo-culting, it's not too bad. And finally, having a cluster with Windows support ready via Kube360 makes deployment a breeze.
If anyone has follow up questions about anything here, please reach out to me on Twitter or contact our team at FP Complete. In addition to our Kube360 product offering, FP Complete provides many related services, including:
If you liked this post, please check out some related posts: