Dynamically Generating Dockerfiles for Kubernetes
Hello everyone! Today I am excited to announce that DocQL is open-sourcing a Rust crate (a library) which we built internally called dockerfile. Pretty ingenious name, I know. This new project is used to dynamically generate Dockerfiles which can be directly fed to the docker daemon in order to create new docker containers/images.
What Is Our Use Case
At DocQL we use Kubernetes for pretty much everything. We use it in our local development environments with the docker-for-mac Kubernetes option which Docker ships with these days. There is a Windows version as well. It is a bit more seamless than Minikube, we have observed, and works really well for local development. We also use Kubernetes in our deployment environments.
At DocQL we generate beautiful documentation and user guide websites for GraphQL APIs. Users just add the URL of their GraphQL API, and we do the rest. Having solid API docs along with user guides on how to authenticate and use the various features of your API is critical for the success of any development team, not only for their own internal purposes, but especially for their users. Our use case for the dockerfile crate comes directly from this core driver behind DocQL. Once we have generated the HTML for a user’s API Hub (their GraphQL documentation website), we need to containerize it, vend any of the external dependencies which the API Hub relies on (JS, CSS, etc), and then deploy the API Hub in our Kubernetes cluster in a highly-available manner (thanks Kubernetes for helping out so much).
This has worked out really well for us so far. We have a worker system deployed in Kubernetes which receives requests to build these API Hubs. It has access to the docker daemon running inside of the Kubernetes cluster. It does some work, generates a new Dockerfile using the dockerfile crate, feeds the Dockerfile over to the docker daemon for a build, and then we push the resulting image to our container registry for later deployment.
This is certainly not the only supported use case of the dockerfile project. If you find yourself in need of building containers on the fly in a well structured manner, this project may be for you.
Let’s get started with a simple example on how you might use this crate. First you’ll just add the crate to your
Cargo.toml. Add the line
dockerfile = "0.2" to your dependencies section, or use
cargo add dockerfile . Now you’re ready to rock.
When you send this generated Dockerfile over to the docker daemon for a build, you have the option of specifying
--build-arg RUST_VERSION=1.32 if you want the rust version to actually be 1.32 instead of
1.31, which is the default value we specified in the Dockerfile.
A few notes on the interface;
Dockerfile::base is where you specify the base image from which your Dockerfile will start. This is the only required instruction of a valid Dockerfile.
Dockerfile::base returns a
DockerfileBuilder which allows you to
push any series of instructions into the Dockerfile, and also allows you to call
push_initial_directive which will add
# directive=... instructions respectively to the head of the Dockerfile, which are the only instruction types allowed to precede the initial
Any valid Dockerfile is simply a series of valid build instructions. So, in the dockerfile crate we take the approach of modelling a Dockerfile in quite the same way. We use a type which wraps a
Vec<Instruction> . For those unfamiliar with Rust, that is a vector (think of an array on steroids) of
Instruction type is a Rust enum which represents any one of the valid Dockerfile instructions. The crate supports all available Dockerfile instructions, including parser directives. You add instructions to the Dockerfile by calling the builder’s
push method, and supply any of the valid instructions found in this crate — like
Cmd. Create a concrete instruction instance by using one of the instruction’s associated constructors, like the
Arg::new() constructor, as seen in the example above.
Though Dockerfiles are pretty simple concepts, there are still ways that you can mess things up and cause the docker daemon to spit back errors. Therefore, one of the central objectives of this crate is to help you avoid such issues. This is why we have the various
Instruction types which will generate the appropriate Dockerfile instruction, as well as the
DockerfileBuilder which ensures that your Dockerfile as a whole is structured in a valid way.
.finish() on the builder when you are done to get the
Dockerfile instance. You can write your
Dockerfile instance to a file by calling the
.to_string() method and then writing those bytes to a file. What you do with your generated Dockerfile from there is up to you. Good luck!
Most Dockerfile instructions have a very simple form, and can be represented in Rust code as simple strings. However, some instructions have a bit more of a complex interface. Check out the HEALTHCHECK instruction. It looks like this:
HEALTHCHECK [OPTIONS] CMD command. There is a predefined set of options which can be supplied to this instruction. We want to model things like that as well. The plan is to add more structured constructors for the various instruction types, like the
Healthcheck instruction to ensure that you get the syntax exactly correct when building your Dockerfiles.
All in all, the dockerfile crate is extremely simple, but that simplicity is often refreshing, and hopefully it offers some solid functionality for folks needing to generate Dockerfiles dynamically. Also, I’m a pretty fanatical 🦀 Rustacian 🦀 and any opportunity to contribute back to the community is probably an opportunity I’m gonna take 🙌.
If you happen to be using GraphQL, please head over to https://docql.io and check out our offering. We would be pleased to build some awesome documentation and user guide websites for your GraphQL APIs. All the best!