Dockeriser et déployer son propre R Archive repo

Il y a plusieurs raisons pour lesquelles vous voudriez déployer votre propre R Archive Repository : vous ne voulez pas dépendre de GitHub pour vos paquets de développement, vous voulez utiliser une méthode plus « confidentielle », ou peut-être (et c’est une bonne raison), vous êtes un nerd et vous aimez l’idée d’héberger votre propre repo. Donc, voici comment.

Qu’est-ce qu’un repo ?

Un point d’archives R / repo est un URL (localisateur de ressources unique) à partir duquel vous pouvez télécharger des packages. Par exemple, lorsque vous faites.. :

install.packages("attempt")

Il y a un argument appelé « repos », qui définit l’endroit sur internet où je veux que R aille chercher le package. Par défaut, vous n’avez pas besoin de spécifier cet argument, car il est défini par : getOption("repos"). Par exemple, en ce moment, sur mon ordinateur portable, j’ai :

getOption("repos")
## CRAN
## "https://cran.rstudio.com/"
## attr(,"RStudio")
## [1] TRUE

Ce qui indique que lorsque j’essaie d’installer un paquet, R ira voir le miroir du CRAN hébergé chez RStudio. Mais je pourrais spécifier n’importe quel autre point :

install.packages(pkgs = "attempt", repos = "http://mirror.fcaglp.unlp.edu.ar/CRAN/", type = "source")

Ici, j’installe {attempt} depuis l’Argentine.

Qu’est-ce qu’un RAN?

À propos de install.packages

Alors, comment ça marche ? Que fait install.packages lorsqu’il est appelé ?

Nous n’entrerons pas dans les détails précis, mais résumons :

  • install.packages va à l’url, et cherche « url/src/contrib ».
  • dans ce dossier, R recherche un fichier appelé PACKAGES.
  • R analyse ce fichier, isole les éléments donnés dans `pkgs’ , ajoute ce qui est nécessaire au téléchargement (numéro de version et autres….).
  • R télécharge et installe le package .

C’est « aussi » simple : si vous avez un dossier « src/contrib », si à l’intérieur de ce dossier il y a un fichier PACKAGES bien rempli, et si tous les tar.gz sont là, vous pouvez install.packages(pkgs = "mypkg", repos = "myrepo", type = "source").

Le fichier PACKAGES.

Dans ce fichier, vous aurez besoin d’une entrée pour chaque package dans votre repo. Chacun d’eux devrait être décrit comme suit :

Package: craneur # Le nom
Version: 0.0.0.9000 # La version
Imports: attempt, desc, glue, R6, tools # Les Imports
Suggests: testthat # Les suggests
License: MIT + file LICENSE # La licence
MD5sum: e3ef1ff3d829c040c9bafb960fb8630b # La MD5sum
NeedsCompilation: no # Si votre package doit ou non être compilé

Avec {craneur}

Faire cela à la main peut être lourd, c’est pourquoi j’ai développé ce package pour le faire automatiquement, appelé{craneur}., que vous pouvez installé avec :

remotes::install_github("ColinFay/craneur")

Voici comment on l’utilise:

library(craneur)
colin <- Craneur$new("Colin")
colin$add_package("../craneur_0.0.0.9000.tar.gz")
colin$add_package("../jekyllthat_0.0.0.9000.tar.gz")
colin$add_package("../tidystringdist_0.1.2.tar.gz")
colin$add_package("../attempt_0.2.1.tar.gz")
colin$add_package("../rpinterest_0.4.0.tar.gz")
colin$add_package("../rgeoapi_1.2.0.tar.gz")
colin$add_package("../proustr_0.3.0.9000.tar.gz")
colin$add_package("../languagelayeR_1.2.3.tar.gz")
colin$add_package("../fryingpane_0.0.0.9000.tar.gz")
colin$add_package("../dockerfiler_0.1.1.tar.gz")
colin$add_package("../devaddins_0.0.0.9000.tar.gz")
colin
## package path
## 1 craneur ../craneur_0.0.0.9000.tar.gz
## 2 jekyllthat ../jekyllthat_0.0.0.9000.tar.gz
## 3 tidystringdist ../tidystringdist_0.1.2.tar.gz
## 4 attempt ../attempt_0.2.1.tar.gz
## 5 rpinterest ../rpinterest_0.4.0.tar.gz
## 6 rgeoapi ../rgeoapi_1.2.0.tar.gz
## 7 proustr ../proustr_0.3.0.9000.tar.gz
## 8 languagelayeR ../languagelayeR_1.2.3.tar.gz
## 9 fryingpane ../fryingpane_0.0.0.9000.tar.gz
## 10 dockerfiler ../dockerfiler_0.1.1.tar.gz
## 11 devaddins ../devaddins_0.0.0.9000.tar.gz

Sauvegarder le RAN avec :

colin$write()

Vous avez maintenant un dossier que vous pouvez copier et coller sur votre serveur . Ce serveur peut être votre propre ftp, un serveur universitaire, un git…. n’importe où vous pouvez pointer avec une url !

Note : d’autres package existent pour le faire. Notamment[drat]].(https://github.com/eddelbuettel/drat),[{cranlike}]].(https://github.com/r-hub/cranlike) ou[{packrat}]].(https://github.com/rstudio/packrat).

Créer un serveur

Avec Digital Ocean

Pour les besoins de cet article, je vais utiliser un serveur sur Digital Ocean. Si vous voulez essayer DO, voici uncoupon de 10$ (Note : c’est un lien affilié, et j’obtiendrai un crédit de 10$ si jamais vous y dépensez 25$).

Comme ce n’est pas un tuto de déploiement DO, je vais sauter cette partie et supposer que vous avez réussi à installer un serveur (en gros, c’est juste « créer un droplet avec ubuntu », et accéder avec ssh en utilisant le mot de passe que vous recevez par mail). Vous pouvez toujours vous référer à la doc si vous avez besoin de plus d’informations.

J’ai donc lancé mon serveur DO via ssh (avec le mot de passe reçu par email), et installé Docker, en suivant ce tutoriel.

J’ai maintenant une machine avec Docker dessus.

Le Dockerfile

Ecrivons le Dockerfile pour notre RAN. Fondamentalement, nous aurons besoin de:

  • un webserver – qui sera lancé avec {servr} (tant qu’à faire, faisons tout avec R).
  • le ran repo créé plus tôt.

Cette simple Dockerfile créerait un RAN :

library(dockerfiler)
dock <- Dockerfile$new()
dock$RUN("mkdir usr/ran/src/contrib/ -p")
dock$COPY("src/contrib", "usr/ran/src/contrib")
dock$RUN("Rscript -e 'install.packages(\"httpuv\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'install.packages(\"jsonlite\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'install.packages(\"servr\", repos = \"https://cran.rstudio.com/\")'")
dock$EXPOSE(8000)
dock$CMD("Rscript -e 'servr::httd(\"usr/ran/src/contrib\", host = \"0.0.0.0\", port = 8000)'")
dock
FROM rocker/r-base
RUN mkdir usr/ran/src/contrib/ -p
COPY src/contrib usr/ran/src/contrib
RUN Rscript -e 'install.packages("httpuv", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'install.packages("jsonlite", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'install.packages("servr", repos = "https://cran.rstudio.com/")'
EXPOSE 8000
CMD Rscript -e 'servr::httd("usr/ran/src/contrib", host = "0.0.0.0", port = 8000)'

Mais il manque quelque chose : que faire si je veux régénérer un RAN à chaque fois que j’ai un nouveau package ? Eh bien, écrivons un fichier Docker différent pour faire cela.

Un fichier docker actualisable

Tout d’abord, je vais copier tous les paquets sources dans un dossier pkg>.

pkg <- list.files("../", pattern = "tar.gz", full.names = TRUE)
file.copy(pkg, "pkg")
list.files("pkg")
## [1] "attempt_0.2.1.tar.gz" "craneur_0.0.0.9000.tar.gz"
## [3] "devaddins_0.0.0.9000.tar.gz" "dockerfiler_0.1.1.tar.gz"
## [5] "fryingpane_0.0.0.9000.tar.gz" "jekyllthat_0.0.0.9000.tar.gz"
## [7] "languagelayeR_1.2.3.tar.gz" "prenoms_0.1.0.tar.gz"
## [9] "proustr_0.3.0.9000.tar.gz" "rgeoapi_1.2.0.tar.gz"
## [11] "rpinterest_0.4.0.tar.gz" "tidystringdist_0.1.2.tar.gz"
  • Je vais ensuite créer un craneur.R (file.create("craneur.R"))qui va automatiquement lancer et écrire avec {craneur} à partir d’un dossier. Il contiendra le code suivant :
library(craneur)
colin <- Craneur$new("Colin")
lapply(list.files("usr/pkg", pattern = "tar.gz", full.names = TRUE), function(x) colin$add_package(x))
colin$write(path = "usr/ran")

Comme je veux que l’utilisateur puisse faire http://urlseulement, et comme mon index RAN est dans src/contrib, je vais créer un html qui fait simplement la redirection :

file.create("index.html")

avec à l’intérieur : &lt;body onload="window.location = 'src/contrib/index.html'"&gt;

  • Et voici la nouvealle Dockerfile :
dock <- Dockerfile$new()
# Install the packages
dock$RUN("Rscript -e 'install.packages(\"httpuv\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'install.packages(\"jsonlite\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'install.packages(\"servr\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'install.packages(\"remotes\", repos = \"https://cran.rstudio.com/\")'")
dock$RUN("Rscript -e 'remotes::install_github(\"ColinFay/craneur\")'")
# Create the dir
dock$RUN("mkdir usr/ran -p")
dock$RUN("mkdir usr/pkg -p")
# Move some stuffs
dock$COPY("craneur.R", "usr/pkg/craneur.R")
dock$COPY("pkg", "usr/pkg")
# Copy the index.html
dock$COPY("index.html", "usr/ran/index.html")
# Create the folders
dock$RUN("Rscript usr/pkg/craneur.R")
# Open port
dock$EXPOSE(8000)
# Launch server
dock$CMD("Rscript -e 'servr::httd(\"usr/ran/\", host = \"0.0.0.0\", port = 8000)'")
dock
FROM rocker/r-base
RUN Rscript -e 'install.packages("httpuv", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'install.packages("jsonlite", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'install.packages("servr", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'install.packages("remotes", repos = "https://cran.rstudio.com/")'
RUN Rscript -e 'remotes::install_github("ColinFay/craneur")'
RUN mkdir usr/ran -p
RUN mkdir usr/pkg -p
COPY craneur.R usr/pkg/craneur.R
COPY pkg usr/pkg
COPY index.html usr/ran/index.html
RUN Rscript usr/pkg/craneur.R
EXPOSE 8000
CMD Rscript -e 'servr::httd("usr/ran/", host = "0.0.0.0", port = 8000)'
dock$write()

Je peux la build avec

docker build -t ran .

Puis:

docker run -d -p 80:8000 ran

Maintenant, si je vais sur http://127.0.0.1/ dans mon browser, je trouve la liste de tous les packages dispo.

Puis:

install.packages("attempt", repos = "http://127.0.0.1/", type = "source")

Sur le serveur

Copions tout cela dans le dossier ran de notre serveur :

scp torun.R [email protected]:/usr/ran/
scp craneur.R [email protected]:/usr/ran/
scp Dockerfile [email protected]:/usr/ran/
scp -r pkg/ [email protected]:/usr/ran/
scp index.html [email protected]:/usr/ran/

Rendez-vous sur notre machine virtuelle, construisons la Dockerfile, puis :

docker run -d -p 80:8000 ran

ET tadaaa : http://206.189.28.254.

Vous pouvez désormais installer depuis ce serveur :

install.packages("attempt", repos = "http://206.189.28.254", type = "source")

Mettre à jour le serveur

Donc maintenant, je peux mettre à jour mon serveur de paquets si jamais je retire ou j’ajoute un nouveau tar.gz : Je n’aurai qu’à reconstruire mon image Docker.

Aller plus loin

Mise à jour efficace

Ici, pour être vraiment efficace, il faudrait que je divise mes images Docker en deux : une avec toute l’installation, et une avec la génération {craneur} : de cette façon, je n’aurais pas à recompiler mon image docker à partir de zéro à chaque fois que j’ai une modification dans la liste des paquets.

DNS

http://206.189.28.254 n’est pas une bonne adresse à partager ou à retenir, donc nous pourrions acheter un domaine et le pointer vers notre serveur . Mais…. c’est pour un autre jour 😉


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker



Also read