Conseils pour utiliser Ruby, RVM, Passenger, Rails, Bundler… en développement

2010-08-13

Cet article existe en anglais. Ma propre traduction n’est probablement pas la meilleure, donc si vous avez des améliorations à suggérer, je suis preneur.

Dernière mise à jour le 3 mars 2011 : Rails 3 et Bundler sont en version stable, et les versions de Passenger et REE sont actualisées.

Intro : Pourquoi ces conseils

L’équipe de dev dans laquelle je travaille est composée de 3 personnes qui bossent avec Ruby.
À part moi qui ne fait presque que ça et qui suis de fait un peu le “référent” sur ces questions, mes 2 très chers collègues n’utilisent pas Ruby toute la journée et utilisent donc d’autres environnements de travail (Objective-C/iPhone et PHP/Symfony principalement).

J’ai choisi d’utiliser un certain nombre de technologies un peu “bleeding edge” pour des vraies (j’espère des bonnes) raisons de performance, de fonctionnalités, de confort… mais leur nature “beta” ne les rend pas toujours faciles à comprendre et à manipuler…
C’est surtout le cas quand il y a des bugs, des changements fréquents de fonctionnement, des incompatibilités (temporaires)… et qu’on n’a pas envie (et c’est normal) de se prendre la tête à tout comprendre. Des fois il faut juste que ça marche.

Je vais donc vous présenter rapidement ces différents outils et comment les faire fonctionner ensemble avec le minimum de souffrance et le maximum d’intérêt.
Je précise que je présente ici un usage de ces outils en environnement de développement. Je n’ai pas utilisé RVM en production, bien que ça soit tout à fait possible, et même conseillé.

Les outils du boucher

Ruby

Ah bon, Ruby n’est pas encore stable ?

Si si, mais Ruby est dans une phase de transition entre la version 1.8 et la 1.9 (la 1.9.2 est sortie il y a quelques mois). Et puis il y a plusieurs implémentations (notamment JRuby et Rubinius). Il est donc intéressant de pouvoir facilement tester son code dans ces différentes implémentations, via RVM.

RVM - Ruby Version Manager

Le principe de RVM est de permettre d’installer plusieurs versions de Ruby sur une même machine, dans un environnement hermétique à une éventuelle version installée sur l’OS et de pouvoir basculer de l’une à l’autre facilement.

Il permet aussi, pour chaque Ruby, d’avoir des ensembles de gems bien séparés (Gemset) pour éviter les conflits de versions entre différents projets.

L’outil est très riche et fonctionnellement très puissant, mais il introduit des concepts et des outils pas si simples que ça.
Si on n’est pas complètement en phase avec, on a vite fait de se tromper de version de Ruby et/ou de Gemset.

Heureusement la doc est très bien faite et l’équipe de dev (@wayneeseguin et @Sutto) est super disponible sur IRC.

Ruby on Rails

Je ne présente pas plus en détail Ruby on Rails : c’est un framework de développement d’applications web, écrit en Ruby.

Depuis sa version 3, il apporte énormément de nouveautés, mais aussi certaines incompatibilités par rapport à sa version stable précédente (2.3), surtout au niveau des commandes “rails” et scripts dans les applis. Il n’est donc pas simple du tout d’utiliser Rails 2 et Rails 3 sur une même machine sans utiliser RVM.

Cette version 3 apporte aussi un nouveau système de gestion de dépendances : Bundler, sous forme d’une gem requise.

Bundler

Bundler est une Gem écrite en Ruby, permettant de gérer les dépendances d’un projet écrit en Ruby, par exemple une application Rails.
Beaucoup plus efficace que l’ancien procédé au niveau de la gestion de l’arbre des dépendances il est surtout excellent pour installer rapidement les gems nécessaires à un projet dans un espace limité à ce projet, sans que les éventuelles autres gems installées sur le système ne puisse le perturber.

Sa version stable actuelle est la 1.0 (1.0.10 exactement).

Phusion Passenger

Phusion Passenger sert à connecter des applis Rack ou Rails avec Apache (ou Nginx), permettant d’héberger ces applis presque aussi facilement que des applis/scripts en PHP.

C’est un composant logiciel maintenant bien stable, mais son usage en conjonction avec RVM et Bundler n’est pas toujours évident, surtout si on veut qu’il utilise un Gemset (et les gems qui sont dedans) différent pour chaque applis.

Les difficultés rencontrées

Ces derniers mois, on a surtout rencontré des difficultés avec le suivi des versions et les incompatibilités que ça a révélé. Alors que j’arrivais à peut près à suivre le rythme des changements… mes collègues avaient mieux à faire. Des modifs que je mettais en commun rendaient certaines fois les projets inutilisables pour les autres, ou en tous cas difficile à remettre en ordre. Ça débouchait sur des cheveux arrachés, des insultes aux outils :-)…

Recommandations

Je vais décrire une situation de poste de développement (donc pas de production), sous Mac OS X. La situation sous Linux sera probablement très proche, à la différence près que les certains composants ne seront pas pré-installés. Pour Windows, c’est tout autre chose, mais je n’ai aucune info fiable sur ce qui marche ou pas dans cet univers assez hostile au développement web (Ruby en particulier).

Apple fourni Ruby en standard depuis Leopard (peut-être même avant). Cette version pré-installée ne nous sera pas utile, mais il est bien de savoir qu’elle existe, ne serait-ce que pour la différencier des autres qui seront installées via RVM. Actuellement, sous Mac OS X 10.6.4, c’est Ruby 1.8.7-p174 qui est installé.

Si on part d’un système “vierge” au niveau de l’utilisation de Ruby, je conseille de n’installer aucune gem particulière et de ne pas toucher à ce qui est présent. On va tout confier à RVM.

Installer RVM

La procédure d’install de RVM est assez simple et bien documentée. Suivez la, c’est plus sûr que si je la recopie ici.

Un fois cette install faite, vous pouvez commencer à installer d’autres “rubies”. Je conseille particulièrement l’utilisation de Ruby Enterprise Edition :

$ rvm install ree

Je conseille aussi d’en faire la version par défaut, et celle pour Passenger.

$ rvm use ree --default --passenger

Dorénavant, tout nouveau shell utilisera cette version de Ruby et un script “wrapper” permettra à Passenger de l’utiliser.

Pour mettre à jour RVM, il suffit de faire :

$ rvm get latest

Pour utiliser la dernière version Git de RVM plutôt que la dernière version publiée :

$ rvm get head

Utilisation des Gemsets de RVM

Concept des Gemsets

Pour rappel, un Gemset est un environnement hermétique dédié à l’installation et l’utilisation de gems.
Chaque version de Ruby connue de RVM dispose d’au moins 2 Gemsets : default (sans nom, en fait), et global. Par défaut on est dans le Gemset sans nom. Le Gemset global permet de rendre disponibles les gems qui y sont installées dans tous les autres Gemsets de la version courante de Ruby.
On peut ensuite créer autant de Gemsets que l’on souhaite, par exemple 1 pour chaque projet de dev.

Voilà une tentative de représentation visuelle :

/--------------------------|------------------|------------------\
|           Ruby EE        |     Ruby 1.9.2   |    autre Ruby    |
|--------------------------|------------------|------------------|
|           @global        |      @global     |     @global      |
|--------------------------|------------------|------------------|
| @default | @app1 | @appX | @default | @appX | @default | @appX |
\--------------------------|------------------|------------------/

Un Gemset par projet

Pour chacun des sites sur lesquels je travaille, je crée donc un Gemset. Ça permet d’isoler les gems qu’il a à disposition et ainsi de simuler un environnement spécifique à ce projet. Pour les projets qui utilisent Bundler l’intérêt est légèrement moindre, mais pour les autres (Rails 2, autres frameworks ou pas de framework) le bénéfice est immédiat.

Une des fonctionnalités de RVM est de reconnaître la présence d’un fichier .rvmrc dans un répertoire lorsqu’on s’y rend (avec cd par exemple). Si ce fichier de config indique qu’il faut choisir un Ruby et un Gemset, il bascule tout seul.
Chacun de mes projets dispose donc d’un fichier .rvmrc contenant au moins cette ligne :

$ rvm --create use default@projetX > /dev/null

En résumé, il va utiliser la version par défaut de Ruby (définie plus haut, mais modifiable à tout instant) et le Gemset “projetX” s’il existe. S’il n’existe pas, il est créé automatiquement. Le renvoi sur /dev/null permet que cette opération ne génère aucune sortie, ce qui est le cas sinon et c’est vite casse-pieds lorsqu’on navigue beaucoup via le terminal.

Depuis la version 1.0 (sortie le 22 août, 1 an exactement après le premier commit), RVM va demander confirmation la première fois qu’il rencontre un fichier .rvmrc dans un répertoire. C’est une précaution de sécurité.

Des gems dans le Gemset global

J’installe un certains nombre de gems dans le Gemset global pour les avoir toujours sous la main, quelque soit le projet dans lequel je travail ou si je suis simplement dans un shell hors projet.

  • passenger
  • capistrano + capistrano-ext
  • bundler
  • git_remote_branch
  • awesome_print
  • wirble
  • g

Ces gems seront donc dispo pour tous les Gemsets de la version de Ruby associée au Gemset global en cours.

Couplage harmonieux de RVM et Phusion Passenger

Dans son fonctionnement normal, Passenger utilise une version définie de Ruby et les variables d’environnement “normales” pour savoir où trouver les gems…

Lorsqu’on install Passenger, on doit indiquer au serveur web (ici c’est Apache mais il y a une variante pour Nginx) ces 3 directives :

LoadModule passenger\_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4/ext/apache2/mod\_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4
PassengerRuby /usr/bin/ruby1.8

La clé est d’indiquer à Passenger la bonne version de Ruby et l’emplacement des modules… Voilà ma propre config :

LoadModule passenger\_module /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4/ext/apache2/mod\_passenger.so
PassengerRoot /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4
PassengerRuby /Users/jlecour/.rvm/wrappers/ree-1.8.7-2011.03/ruby

En l’état, Passenger utilisera donc le module présent dans la gem installée dans le Gemset global et la version de Ruby définie par le wrapper passenge_ruby.

C’est bien, mais comment va-t-il utiliser le bon Gemset selon l’application ?

Avec un Passenger >= 2.2.14 et RVM >= 0.1.42 il est désormais possible d’utiliser (en Ruby) une API interne à RVM et forcer les variables d’environnement pour l’application lancée et qu’elle sache où chercher les gems dont elle a besoin. Le procédé est détaillé dans l’article The Path to Better RVM & Passenger Integration.
En résumé, au chargement de l’application Ruby, on demande à RVM quelques sont les variables à utiliser.

Pour le moment, Passenger est limité à un seul interprêteur Ruby. Voir la doc pour plus d’info.

Pour une appli en Rails 3, avec Bundler, voilà mon fichier config/setup_load_paths.rb :

if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
  begin
    rvm_path = File.dirname(File.dirname(ENV\['MY_RUBY_HOME'\]))
    rvm_lib_path = File.join(rvm_path, 'lib')
    $LOAD_PATH.unshift rvm_lib_path
    require 'rvm'
    RVM.use_from_path\! File.dirname(File.dirname(__FILE__))
  rescue LoadError
    # RVM is unavailable at this point.
    raise "RVM ruby lib is currently unavailable."
  end
end

ENV['BUNDLE\_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'

Pour une application Rails, sans Bundler, il suffit de supprimer les 2 dernières lignes.

De cette manière, j’arrive à héberger avec Passenger des applis Rails 2, Rails 3 et Rack sur le même serveur web, sans conflit de version.

Utiliser Bundler pour la gestion des dépendances

Je résume une fois de plus ; Bundler permet de spécifier les gems (et leur version) nécessaires à un projet, de les installer (avec prise en charge des dépendances) et de ne “proposer” au projet en question que ces gems là. Le résultat est donc : facilité d’installation, confiance dans les versions utilisées, absence de conflits.

Si vous avez utilisé une version de Bundler <= 0.9.6, je ne peux que vous conseiller de désinstaller les version actuelles (sauf si vous en avez explicitement besoin), installer la version 1.0.rc puis recalculer les dépendances.

$ gem uninstall bundler
$ gem install bundler
$ bundle install

La version 1.0 (même depuis sa première beta) a apporté de gros changement, dont certains sont incompatibles, mais elle simplifie la gestion des déploiements en production…

Lorsque dans un terminal, à la racine d’un projet géré avec Bundler, on fait un bundle install, le fichier de config Gemfile est analysé pour déterminer l’arbre des dépendances et un fichier Gemfile.lock est créé. Il est ensuite utilisé pour connaître l’exacte version de chaque gem à installer/utiliser.
Tant que ce fichier n’est pas modifié, le projet d’utilisera que ces gems là, ce qui garanti une cohérence entre l’environnement de développement (y compris d’un développeur à l’autre) et ceux de test, pre-production, production…

Un développeur qui souhaite mettre à jour une gem ou en installer une nouvelle doit modifier le fichier Gemfile en conséquence, puis faire un bundle install. Il peut enfin versionner ces changements pour les rendre disponibles aux autres développeurs.

Un développeur qui ne veut pas gérer les changements de version de gems du projet en cours ne doit juste pas toucher aux fichiers Gemfile et Gemfile.lock. Pour s’assurer qu’il dispose en local des gems nécessaires à l’exécution de l’application ou bien si après une mise à jour du code il rencontre un “plantage” indiquant une erreur de gem, le 1er réflexe doit être de faire un bundle install depuis la racine du projet. Ainsi il s’assure d’être à jour au niveau des gems et de leur version.

L’utilisation de RVM rend ces processus encore plus faciles, rapides et fiables. En effet en mode normal, Bundler va installer au niveau système les gems nécessaires, mais comme le niveau système qu’il voit est en fait un Gemset, il ne pollue pas les autres projets.
En mode déploiement, il installe les gems dans un dossier de cache au sein de l’application, ce qui revient à peut près au même qu’un Gemset. Il est possible d’optimiser cette phase avec Capistrano.

Bundler et Capistrano pour un déploiement rapide et facile

Une appli gérée avec Bundler n’utilise plus les mécanismes classiques de require pour charger ses gems, c’est Bundler qui s’en charge à l’initialisation de l’appli. Il faut donc que les gems soient installées et accessibles à Bundler sur le serveur d’application. Il faut donc vérifier que tout est OK après chaque déploiement, et si possible mutualiser les gems d’une fois sur l’autre pour éviter des téléchargements et une consommation inutiles.

Depuis la version 1.0.0.rc.5, Bundler dispose d’une tache pour Capistrano (en réalité c’est depuis la rc.4 mais elle contenait des bugs). Il suffit d’ajouter require 'bundler/capistrano' dans sa recette de déploiement (deploy.rb). On peut éventuellement spécifier l’emplacement d’installation des gems, mais par défaut, c’est dans shared/bundle.

Autres paquets majeurs

Pour la gestion courante des paquets additionnels non fournis par Apple, je conseille d’une manière générale l’utilisation de Homebrew. Contrairement à MacPorts, il ne réinstalle pas toutes les dépendances systématiquement. S’il trouve son bonheur dans les paquets et librairies installés par Apple, il va les utiliser. Ça assure une plus grande cohérence, rapidité d’install et légèreté d’ensemble.

Homebrew installe aussi tout ce qui lui est nécessaire dans l’environnement utilisateur, sans nécessité d’utiliser sudo. C’est un peu déroutant pour un sysadmin, mais dans un contexte de développement, c’est plus pratique et plus simple.

J’ai utilisé Homebrew sur mon Mac pour installer MySQL, Git, ImageMagick, MongoDB… et nombre de petits outils tels que tree, wget, ack…, avec beaucoup de satisfaction.

Crédits

En priorité, je souhaite remercier les développeurs de ces outils géniaux et l’armée de contributeurs qui a participé.

Merci à Wayne E Seguin et John Mettraux pour une méticuleuse relecture, Thibaut Barrère et Sébastien Gruhier pour leur soutien et des astuces additionnelles