Modus peut il remplacer les Dockerfiles ?
Découvert par hasard, Modus est un langage permettant de créer des images de conteneur Docker/OCI. Modus utilise la programmation logique pour exprimer les interactions entre les paramètres de build, spécifier des workflows de build complexes, paralléliser et mettre en cache automatiquement les builds et aussi permet d’optimiser la taille des images produites.
Attention : c’est un projet encore sujet à des modifications importantes pouvant amener à des dysfonctionnements !
Installation de modus
Il suffit d’utiliser le script du site pour installer modus. Je fais le choix de
l’installer dans le dossier .local/bin
.
curl -sfL 'https://github.com/modus-continens/modus/releases/download/0.1.15/modus-x86_64-linux-musl' -o ~/.local/bin/modus && \chmod +x ~/.local/bin/modus && \
Ecrire un Modusfile
Un Modusfile, est comme un Dockerfile, un ensemble de directives qui spécifient comment créer des images sauf que la syntaxe est totalement différentes puisqu’elle s’appuie sur celle de Datalog ↗ un langage de programmation logique déclaratif.
Exemple de fichier Modusfile (repris du tutoriel de Modus) :
my_app(profile) :- ( from("rust:alpine")::set_workdir("/usr/src/app"), # FROM rust:alpine; WORKDIR /usr/src/app copy(".", "."), # COPY . . cargo_build(profile) # calling into another predicate )::set_entrypoint(f"./target/${profile}/my_app"). # ENTRYPOINT ["./target/release/my_app"]
cargo_build("debug") :- run("cargo build"). # RUN cargo buildcargo_build("release") :- run("cargo build --release"). # RUN cargo build --release
Les commentaires débutent par le #. Dans l’exemple ci-dessus, les instructions équivalentes à un Dockerfile ont été écrites pour plus de clarté à l’aide de commentaires.
Pour le reste tout est expliqué dans la documentation ↗ de Modus (que je ne maitrise pas).
Build d’une image avec Modus
La CLI de modus permet de lancer le build des images, elle est de la forme suivante :
modus build context query
Le contexte est comme avec docker, il indique depuis quel dossier on veut construire l’image. La requête indique quelle image construite avec ses paramètres.
Je vous propose de construire l’image de modus à partir de son projet :
git clone https://github.com/modus-continens/modus.gitsudo apt install rust-allcd moduscargo update
Voici le contenu du builde :
build_env :- (from("rust:alpine")::set_workdir("/usr/src/app"), # musl-dev required for tokio (https://github.com/hound-search/hound/issues/238#issuecomment-336915272), # protobuf-dev required for buildkit-proto -> tonic-build # git required for our build.rs run("apk add --no-cache musl-dev protobuf-dev git")) ::set_env("PROTOC", "/usr/bin/protoc") ::set_env("PROTOC_INCLUDE", "/usr/include/google/protobuf").
run_env :- from("alpine")::set_workdir("/usr/src/app").
cargo_build("debug") :- run(f"cargo build").cargo_build("release") :- run(f"cargo build --release").
build_deps(profile) :- copy("modus-lib/Cargo.toml", "modus-lib/"), copy("modus/Cargo.toml", "modus/"), copy("Cargo.toml", "."), copy("Cargo.lock", "."), copy("rust-toolchain.toml", "."), run("mkdir modus-lib/src modus/src &&\ echo 'pub struct Nothing;' > modus-lib/src/lib.rs &&\ echo 'fn main () {}' > modus/src/main.rs &&\ echo 'fn main () {}' > modus/src/buildkit_frontend.rs"), cargo_build(profile).
build(profile) :- build_env, build_deps(profile), copy(".", "."), # Since cargo use the timestamp of build artifact to determine if it needs to # rebuild, and the source file copied in will be older than the previous build # done by build_deps, we touch all the source files to force a re-build. run("touch modus-lib/src/* modus/src/*"), cargo_build(profile).
frontend_img(profile) :- (run_env, profile_to_target_folder(profile, target_folder), # Work-around for https://github.com/modus-continens/modus/issues/98 # We need to transpile this to Dockerfile to bootstrap the frontend. # Normally just saying f"${target_folder}/..." is good enough. build(profile)::copy(f"/usr/src/app/${target_folder}/buildkit-frontend", "./buildkit-frontend") )::set_entrypoint(["./buildkit-frontend"]).
modus(profile) :- (run_env, profile_to_target_folder(profile, target_folder), build(profile)::copy(f"/usr/src/app/${target_folder}/modus", "./modus") )::set_entrypoint(["./modus"]).
profile_to_target_folder("debug", "target/debug").profile_to_target_folder("release", "target/release").
all("modus", profile) :- modus(profile).all("frontend", profile) :- frontend_img(profile).
all(X) :- all(X, "debug").
On voit ici que ce Modusfile peut construire deux versions de l’image de
conteneur : modus
et fronted
. On peut les construire toutes les deux avec la
requete all.
On voit aussi que pour la version debug on peut ajouter des packages supplémentaires nécessaires à ce mode. Cela permet en effet de réduire la taille dees images de production, mais aussi de limiter la présence de package qui pourrait poser des problèmes de sécurité. Allez on lance le build des images :
modus build . 'all(X)'
Pour builder aussi les images de debug, ajoutons le predicat debug. Ajoutons l’option —json qui va nous permettre de tager les images :
modus build . 'all(X, debug)' --json=build.jsonjq '.[] | [.digest, .predicate + ":" + (.args | join("-"))] | join(" ")' build.json | xargs -I % sh -c 'docker tag %'docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEpython 3.10-slim-bullseye af1108142cf5 12 days ago 126MBdebian 11 dd8bae8d259f 12 days ago 124MBall frontend-release dd18d71e850c 3 weeks ago 15.7MBall modus-debug 93c7afa90409 3 weeks ago 62.7MBall frontend-debug 6a1960fb25da 3 weeks ago 121MBall modus-release 1cc74b931a7e 3 weeks ago 12.2MB
Conclusion
Je trouve que modus offre des possibilités qui permettent d’optimiser la production d’images OCI. C’est un outil en cours de développement et il manque de quelques options comme tager les images directement. Par contre, encore un nouveau langage à apprendre qui est bien différent de ceux que nous utilisons habituellement. Donc à suivre.