Color theme Dark mode Font size Content width

Rust CLI mit Docker ausliefern

Published on 08/6/2020

Vor kurzem habe ich einen Weg gefunden Rust CLI Programme ├╝ber Docker auszuliefern. F├╝r meinen Arbeitgeber Synoa habe ich in den letzten Monaten ein CLI Tool erstellt, dass mir - und anderen - die Arbeit mit AWS erleichtert. Diese CLI im Team zu verteilen gestaltete sich als schwierig da weder jeder Rust installiert hat noch eine einfache Integration mit Homebrew m├Âglich war da der Code in einem privaten Repository ist. Die einfachste L├Âsung war am Ende, die CLI in einen Docker Container zu packen und so zu verteilen. Wie das geht erkl├Ąre ich in diesem Artikel.

Rust Code

Der folgende Beispiel Code zeigt ein kleines Rust Programm, dass die Argumente ausgibt die w├Ąhrend der Ausf├╝hrung ├╝bergeben wurden.

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

Der Code muss nicht verstanden werden um die Konzepte aus diesem Artikel zu verstehen! Das Muster ist f├╝r alle Binary-Programme gleich, z.B. k├Ânnte genauso eine Go App ├╝ber Docker verteilt werden.

Code kompilieren - in Docker

Den oben gezeigten Code k├Ânnen wir nun kompilieren, dass hei├čt ihn zu einer ausf├╝hrbaren Bin├Ąr-Datei “zusammenf├╝gen”. Hierf├╝r verwenden wir einen “Multi-Stage” Build in Docker. So m├╝ssen wir und andere Entwickler keine vollst├Ąndige Rust Umgebung verwalten und au├čerdem kann jeder Entwickler ├╝ber Docker in der selben Umgebung Binaries kompilieren.

Zun├Ąchst deklarieren wir einen builder Container. Dieser Container wird genutzt um unseren Rust Code zu kompilieren.

FROM clux/muslrust:1.45.0-stable as builder
WORKDIR /volume
COPY . .
RUN cargo build --release

Diese vier Zeilen tun folgendes:

  • Erstelle einen Container auf Basis von clux/muslrust
  • Dem Container wird der “Name” builder gegeben
  • Das Arbeitsverzeichnis wird auf /volume gesetzt, damit wird Docker alle Befehle in diesem Verzeichnis ausf├╝hren
  • Alle Dateien werden aus dem aktuellen Verzeichnis in den Container kopiert
  • Das Kommando cargo build --release wird im Container ausgef├╝hrt und kompiliert unseren Code

Das eigentliche Docker Image erzeugen

Nun k├Ânnen wir im selben Dockerfile unser eigentliches Image erzeugen. Daf├╝r wird das kompilierte Binary aus dem builder container kopiert.

FROM alpine
# Kopiere das kompilierte Binary aus dem builder container
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/docker-cli-sample .
# Alle CLI argumente werden direkt an das Binary ├╝bergeben
ENTRYPOINT [ "/docker-cli-sample" ]

Was geschieht hier?

  • Zuerst erstellen wir einen neuen Docker Container auf basis des Alpine Linux Images.
  • Dann kopieren wir das kompilierte Binary aus dem builder Container in unseren neuen Container
  • Zuletzt sagen wir, dass das Binary als “Startpunkt” verwendet werden soll. Soll hei├čen wenn der Container gestartet wird, dann wird dieses Binary ausgef├╝hrt

Warum Alpine Linux? Alpine Linux ist eine kleine auf Sicherheit fokusierte Linux Distribution. Das Alpine Docker image ist nur ca. 3MB gro├č - kleiner geht kaum!

Alles zusammen sieht unser Dockerfile nun wie folgt aus:

FROM clux/muslrust:1.45.0-stable as builder
WORKDIR /volume
COPY . .
RUN cargo build --release

FROM alpine
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/docker-cli-sample .
ENTRYPOINT [ "/docker-cli-sample" ]

Image bauen und den Container ausf├╝hren

Mit dem oben gezeigten Dockerfile k├Ânnen wir nun ein Image bauen. Hierf├╝r benutzen wir folgenden Befehl:

docker build -t kevingimbel/rust-docker-cli-sample:1.0 .  

Anschlie├čend k├Ânnen wir einen Container ausf├╝hren, der das neu erstellte Image benutzt:

$ docker run --rm kevingimbel/rust-docker-cli-sample:1.0 -hello -world
["/docker-cli-sample", "-hello", "-world"]

Terminal konfiguration

Damit wir diesen Docker Container wie ein “normales” binary ausf├╝hren k├Ânnen m├╝ssen wir im Terminal ein “alias” setzen. Hierf├╝r kommt folgende in die ~/.bashrc bzw. ~/.zshrc.

alias docker-rust-cli='docker run --rm kevingimbel/rust-docker-cli-sample:1.0'

Nun laden wir die Konfigurationsdatei neu oder ├Âffnen ein neues Terminal Fenster und dann kann der Container wie ein normales Script ausgef├╝hrt werden.

# bash
source ~/.bashrc
# zsh
source ~/.zshrc

Danach k├Ânnen wir den Container mit dem Befehl docker-rust-cli starten.

$ docker-rust-cli hello from docker
["/docker-cli-sample", "hello", "from", "docker"]

Fortgeschritten: Volumes

Wir k├Ânnten hier fertig sein, aber eine wichtige Funktion fehlt noch: Volumes. Wenn unser CLI tool Dateien erstellt w├╝rden diese sonst im Docker container bleiben und der wird standardm├Ą├čig gel├Âscht da wir --rm verwenden.

Der alias wird also mit einem Volume angepasst.

alias docker-rust-cli='docker run --rm -v $(pwd):/cmd-root-dir kevingimbel/rust-docker-cli-sample:1.0'

Mit -v $(pwd):/cmd-root-dir sagen wir Docker, dass das aktuelle Verzeichnis ($(pwd)) im Container als Pfad /cmd-root-dir gemounted werden soll. Jetzt m├╝ssen wir nur noch unserem Image sagen, dass es Dateien auch in diesem Verzeichnis ablegen soll. Das geht indem wir in der Dockerfile die WORKDIR setzen.

Das Dockerfile sieht nun wie folgt aus.

FROM clux/muslrust:1.45.0-stable as builder
WORKDIR /volume
COPY . .
RUN cargo build --release

FROM alpine
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/docker-cli-sample .
WORKDIR /cmd-root-dir
ENTRYPOINT [ "/docker-cli-sample" ]

WORKDIR erstellt das Verzeichnis wenn es nicht existiert, wir m├╝ssen es also nicht selbst erstellen. Um diese Anpassung zu testen k├Ânnen wir unser Script eine Log Datei schreiben lassen. Dazu ver├Ąndern wir den Rust Code wie folgt.

use std::env;
use std::fs;

fn main() -> std::io::Result<()> {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
    fs::write("docker-cli-sample.log", format!("Args: {:?}", args))?;
    Ok(())
}

Mit fs::write schreiben wir nun alle Argumente auch in die Datei docker-cli-sample.log statt sie nur im Terminal anzuzeigen. Jetzt muss das Verzeichnis nur noch wie oben geschrieben gemounted werden:

alias docker-rust-cli='docker run --rm -v $(pwd):/cmd-root-dir kevingimbel/rust-docker-cli-sample:1.0'

Wichtig sind hierbei die einfachen Anf├╝hrungszeichen (') - ohne diese w├╝rde $(pwd) nur ein Mal ausgef├╝hrt werden statt bei jedem Aufruf!

Wenn wir jetzt den Befehl ausf├╝hren wird eine Log Datei in das aktuelle Verzeichnis geschrieben:

$ docker-rust-cli
["/docker-cli-sample", "hello", "world"]
$ cat docker-cli-sample.log
Args: ["/docker-cli-sample", "hello", "world"]

Fortgeschritten: Versionierung

F├╝r etwas mehr Komfort k├Ânnen wir eine Variable f├╝r den “Docker Tag”, also die Version unseres Images, nutzen. So kann man sp├Ąter einfach updaten ohne den eigentlichen Befehl anpassen zu m├╝ssen.

export MY_CLI_VERSION="1.0"
alias docker-rust-cli='docker run --rm -v $(pwd):/cmd-root-dir kevingimbel/rust-docker-cli-sample:$MY_CLI_VERSION'

Soll nun Version 1.1 verwendet werden muss lediglich die Variable MY_CLI_VERSION auf 1.1 ge├Ąndert werden. Jeder mit Zugriff auf das Docker Image kann nun den Code in die ~/.bashrc oder ~/.zshrc kopieren und das CLI Programm nutzen.

Zusammenfassung

  • Wir k├Ânnen mit Multi-Stage Builds code in Docker kompilieren
  • Rust binaries k├Ânnen in kleinen Containern wie z.B. Alpine oder “blanken” Container ausgef├╝hrt werden
  • Mit einem alias k├Ânnen wir bequem und komfortable Docker Container ausf├╝hren als w├Ąren es “installiere” Binaries
  • Indem wir WORKDIR und Volumes nutzen k├Ânnen wir Dateien aus dem Container heraus speichern

Der Quellcode f├╝r dieses Tutorial kann auf GitHub unter kevingimbel/docker-cli-sample gefunden werde. Ein funktionierendes Docker Image gibt es auf Docker Hub unter kevingimbel/rust-docker-cli-sample.

Um das Docker Image zu nutzen kann folgender Befehl ausgef├╝hrt werden:

docker run --rm kevingimbel/rust-docker-cli-sample:1.0 hello from docker

Categories

Tags

Like this?

If this content has helped you or your company, consider making a donation to a good cause.

For example, consider donating to the following causes:

A Selfie of me

Kevin Gimbel

is a DevOps Engineer and avid (Video) Gamer. He’s also interested in Sci-Fi, Cyberpunk, and dystopian books.

Wearing a mask is a good idea!

You can find out more about me if you wish.