Depuis le début de l'ère internet, le nombre de systèmes et d'applicatifs déployés par les entreprises ne cesse de croître de manière exponentielle, et l'arrivée du cloud et du Big Data n'a fait qu'accélérer la tendance. En outre, là où il y a vingt ans un administrateur système gérait une douzaine de machines, le même administrateur en gère aujourd'hui parfois plusieurs centaines. Or, il devient de plus en plus compliqué de proposer une supervision adaptée qui puisse supporter une telle mise à l'échelle.
Évitons les longs discours et prenons un exemple concret. Un serveur applicatif héberge un serveur Apache HTTPd en frontal d'une paire d'instances du serveur applicatif JEE, Wildfly. Le serveur Apache reçoit les requêtes HTTP à leur intention et effectue une simple balance de charge, entre les deux instances, à l'aide d'un module dédié, mod_cluster. Maintenant, voyons comment superviser ce système.
1. Introduction
1.1 Petit exemple concret
La première chose à superviser est bien évidemment la disponibilité du système, donc on va souhaiter naturellement effectuer une commande ping à intervalles réguliers, pour vérifier si le système est toujours accessible. Il nous faut ensuite superviser la disponibilité de Apache - à minima, avec une requête régulière vers une page de statut. En outre, il serait appréciable de suivre le processus associé à Apache, et, s'il vient à « disparaître », de le redémarrer immédiatement.
Sur la partie Wildfly, on doit non seulement surveiller la disponibilité du service, mais aussi quelques indicateurs clés. Partons du principe que les deux instances hébergent des applicatifs Web ; il faut donc vérifier leurs disponibilités, à travers Apache HTTPd (sur le port 80) mais aussi sur les ports spécifiques de chaque instance (8080 et 8180), pour pouvoir déterminer si, quand un problème survient, ce sont les instances Wildfly ou Apache HTTPd en lui-même qui en sont à l'origine.
On ne peut néanmoins pas s'arrêter à ces seules métriques. En effet, les instances Wildfly s'exécutent chacunes sur une machine virtuelle Java et il est donc essentiel de bien surveiller certains indicateurs clés spécifiques à cette dernière. Les premiers qui viennent à l'esprit sont bien évidemment la consommation mémoire et la durée d'exécution du « ramasse-miettes » (garbage collection).
Mais il est aussi important de surveiller le nombre de sous-processus (threads) et le taux d'utilisation des connexions aux bases de données (connection pool). Enfin, si les développeurs des applicatifs ont bien fait leur travail, on devrait trouver des URL dédiées à la supervision et peut-être même des données et opérations exposées par JMX (Java Management Extensions) [1].
En outre, à toutes ces métriques finalement très « fonctionnelles », il faut ajouter de nombreuses métriques techniques (utilisation du CPU, surveillance des systèmes de fichiers, etc).
1.2 Trop de métriques tue les métriques
Comme on vient de l'illustrer, il y a beaucoup d'informations à surveiller sur un tel système, et si l'on multiplie ces derniers par des centaines de systèmes - et il existe plusieurs grands comptes chez qui on peut trouver ce genre de déploiement - la seule masse des données (et leur fréquence d'émission) peut suffire à faire s'écrouler de nombreuses solutions de supervision.
On peut bien évidemment y remédier en déployant plusieurs instances, ou en mettant à profit la capacité de la solution à monter en charge d'une manière ou d'une autre. On peut aussi déployer une instance de supervision par environnement et agréger les résultats sur une instance « maître ». Toutes ces solutions sont envisageables mais ne font qu'augmenter la complexité du système ainsi que le nombre de machines à utiliser.
Pour s'en convaincre, faisons un rapide calcul. S'il faut un serveur de supervision pour surveiller vingt systèmes, il faudra probablement dix autres serveurs de supervision pour surveiller deux cents systèmes. Bref, la supervision a rapidement un coût considérable en terme de machines.
En outre, presque toutes les solutions de supervision ont besoin, pour être complètes, de déployer un agent sur le système cible. Ces agents peuvent être relativement consommateurs en ressource, et nécessitent parfois, comme c'est le cas pour Tivoli ou RHQ [2], leur propre machine virtuelle Java. Sur de gros systèmes hébergeant de nombreux applicatifs Java, la mémoire dédiée à ces agents peut atteindre jusqu'à plusieurs Gb. Toujours autant de ressources de consommées, juste à des fins de supervision.
1.3 Comment s'en sortir ?
Depuis l'émergence du Cloud, mais aussi de solutions distribuées comme Git, il apparaît de plus en plus évident que la meilleure approche pour tenir une charge est d'avoir un système distribué, dont la capacité croît au fur et à mesure que le nombre d'instances augmente.
Regardons justement le cas de Git. Avant l'apparition de Git, les serveurs SVN des entreprises devenaient rapidement des goulots d'étranglement dans le processus de développement. Les fusions de code étaient compliquées, le déclenchement de processus pré ou post commit ralentissaient le système et ne pouvaient donc pas être utilisées de manière systématique. En outre, retrouver le commit coupable d'une régression ne pouvait pas être fait en exécutant simplement un test unitaire sur un sous-ensemble d'entrées dans l'historique de la base de code. Bref, aussi bien en terme de montée en charge qu'en terme de fonctionnalités, le modèle centralisé de SVN avait atteint ses limites. Et le succès de Git, et des DVCS en général, en a découlé naturellement.
En effet, Git a déporté la plupart des traitements à effectuer du serveur central au poste du développeur, ne laissant à l'éventuelle machine dédiée à héberger le « point de référence » commun, que la simple tâche d'accepter une série de changements (ou non). Tout le travail difficile est géré sur le poste du développeur. Plus ils sont nombreux, plus chacune de leurs machines travaille, sans surcharger le référentiel. Il reste bien évidemment une limite physique - un certain nombre de connexions concurrentes peuvent toujours rendre le serveur « référentiel » indisponible, mais la limite est largement plus haute qu'avant, et au pire des cas, le développeur peut continuer son travail, et simplement repousser l'envoi des changements à un moment où le serveur sera de nouveau disponible.
2. Supervision distribuée
2.1 Architecture de haut niveau
Fort de ce constat, voyons comment nous pourrions appliquer la même stratégie à la supervision. Au lieu de centraliser la remontée des métriques, nombreuses et coûteuses en espace, vers un serveur central, nous allons tout d'abord réduire le rôle de ce dernier au minimum en déléguant le travail au système cible en lui-même.
Dans cette vision, le système central se limite à vérifier que le serveur est toujours up (en gros qu'il répond à la commande ping, plus quelques tests SNMP pour être sûr que le système d'exploitation n'a pas « planté »). Le reste est délégué à un processus local.
Le processus local, finalement voisin de l'agent évoqué plus haut, se charge de plusieurs tâches :
- Surveiller la disponibilité des processus applicatifs et remonter une alerte si ces derniers ne répondent plus ;
- Relever les différentes métriques systèmes (CPU, espace disque...) et applicatifs (mémoire de la JVM, nombre de connexions à la base de données utilisées...) ;
- Redémarrer les processus indisponibles ou autre action similaire en cas de détection d'une anomalie.
2.2 Limitations
On peut tout de suite relever quelques limites évidentes à cette approche. Tout d'abord, l'absence de centralisation des données. Si l'on souhaite connaître la consommation moyenne en CPU d'un serveur, il faut s'y connecter pour regarder les données collectées. C'est un faible prix à payer, quand on réfléchit à la quantité de données à envoyer pour bénéficier du confort d'une approche « centralisée ».
Dans le même ordre d'idée, il n'y a aucune vision d'ensemble de type reporting. Par exemple, on ne peut pas facilement, en accédant au serveur central, répondre à la question « Quelle est la consommation moyenne de mémoire des serveurs Java ». Mais là encore, est-il pertinent d'épuiser l'ensemble des systèmes en mettant en place une lourde infrastructure de supervision pour répondre à une question finalement épisodique ? Ne serait-il pas plus judicieux, lorsque la question se pose, d'effectuer un relevé et de calculer cette moyenne ?
Un autre désavantage dans la décentralisation de cette approche réside dans le fait que les réactions à certains événements ne peuvent être que locales. Par exemple, on peut imaginer à l'aide d'outils tels que RHQ, si la charge de plusieurs systèmes augmente, pouvoir démarrer des instances supplémentaires. Dans notre cas, ce n'est pas possible.
Nous verrons un peu plus loin comment compenser ces limites. Pour le moment, place à la pratique, et essayons de construire, à l'aide d'une solution Open Source bien sûr, une telle supervision distribuée.
3. Supervision locale avec Monit
Pour démontrer la validité de cette approche, nous allons mettre en place un outil Open Source, du nom de Monit [3], qui permet justement de surveiller des processus locaux, mais aussi d'effectuer des relevés de métriques systèmes et de redémarrer des services si nécessaire.
On notera néanmoins que Monit a une limite claire par rapport à des agents Java, comme celui de RHQ évoqué plus haut. Monit n'est pas aussi portable que ce dernier et il ne fonctionnera donc que sur des systèmes Linux. Cette opinion n'engage que l'auteur de cet article, mais ce n'est pas une limite très importante dans le cas de la gestion de serveurs. En outre, il semble exister des alternatives, telles que Munin [4].
Un autre avantage d'utiliser un agent dédié à la plate-forme, plutôt qu'un applicatif portable s'exécutant sur la JVM est bien évidement la moindre consommation de ressource système.
3.1 Puppet
En plus d'expliquer la configuration de Monit, nous allons, au fur et à mesure de l'article, réaliser la configuration Puppet [5] associée. Le propos de cet article n'étant pas de décrire Puppet, on laissera le soin au lecteur de se reporter à la documentation existante sur cet outil, si nécessaire, ou sinon de simplement ignorer ces ajouts qui ne sont pas nécessaires à la compréhension du sujet de cet article.
3.2 Installation de Monit
3.2.1 Installation Manuelle
C'est une étape assez triviale, le paquet est très souvent disponible dans la plupart des distributions. Pour un système RHEL, il suffit d'ajouter le dépôt logiciel EPEL [6] adapté à sa version :
# yum install -y http://ftp.uni-bayreuth.de/linux/fedora-epel/6/i386/epel-release-6-8.noarch.rpm
On notera néanmoins que, pour ne pas compromettre son système, et s'assurer qu'on n'installe pas, par erreur, des paquets logiciels issus de ces dépôts plutôt que ceux fournis par Red Hat, il ne faudra pas oublier de désactiver les dépôts EPEL après l'installation de Monit :
# sed -i /etc/yum.repos.d/epel.repo -e 's/enabled=.*$/enabled=0/'
C'est déjà prudent mais en fait, on peut même faire mieux et simplement ajouter, à la définition du dépôt logiciel, une instruction includepkgs qui limitera le périmètre des paquets à installer et à mettre à jour depuis ce dépôt :
/etc/yum.repos.d/epel.repo
[epel] name=Extra Packages for Enterprise Linux 6 - $basearch mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$amp;basearch failovermethod=priority
enabled=1
gpgcheck=1
includepkgs=puppet*,monit*
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6
Une fois le dépôt logiciel en place, il suffit donc d'installer le paquet logiciel associé à Monit :
# yum install -y monit
3.2.2 Mise en place avec Puppet
Le premier élément à mettre en place est bien évidemment la définition du dépôt logiciels EPEL :
//modules/epel/manifests/init.pp:
class epel::repo {
$gpgkey_epel='/etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6'
yumrepo { 'epel':
name => 'Extra Packages for Enterprise Linux 6 - $basearch',
enabled => 1,
failovermethod => 'priority',
mirrorlist => 'https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch',
gpgcheck=> 1,
includepkgs => 'puppet*,monit*',
gpgkey => "file://$gpgkey_epel",
require => File[$gpgkey_epel]
}
file { $gpgkey_epel:
ensure => present,
source => puppet://modules/epel/RPM-GPG-KEY-EPEL-6,
}
}
Une fois la définition du dépôt ajoutée, il suffit de demander à Puppet de garantir la présence du paquet monit :
// manifests/site.pp:
node 'myhost' {
include epel::repo
package { 'monit':
ensure => installed,
require => Yumrepo['epel'],
}
}
3.3 Mise en place du service
3.3.1 Mise en place manuelle
monit est un service comme un autre et il faut donc le démarrer de manière tout à fait traditionnelle:
# service monit status monit is stopped
# service monit start Starting monit: [ OK ]
# service monit status monit (pid 1586) is running...
Il est bien sûr important que ce service soit lancé dès le démarrage de la machine, donc n'oublions surtout pas d'exécuter la commande suivante :
# chkconfig monit --level=345 on
3.3.2 Mise en place avec Puppet
La mise en place du service avec Puppet est assez triviale, mais nous allons tout de même, pour garder la configuration la plus propre possible, déplacer toutes les ressources spécifiques à Monit dans un module dédié - ce qui nous servira par la suite de toute manière :
// modules/monit/manifests/init.pp
class monit::monit($yumrepo='epel') {
package { 'monit':
ensure => installed,
require => Yumrepo[$yumrepo],
}
service { 'monit':
ensure => running,
enable => true,
require => Package['monit'],
}
}
Nous avons ajouté l'option de redéfinition du dépôt Yum dont dépend Monit pour faciliter le déploiement de Monit sur d'autres systèmes n'utilisant pas le dépôt EPEL.
Regardons maintenant à quoi ressemble notre fichier manifeste :
// manifests/site.pp:
node 'myhost' {
include epel::repo
include monit::monit
}
3.4 Configuration du service
3.4.1 Configuration par défaut
Sans surprise, le service Monit se configure simplement à l'aide de fichiers déployés dans le répertoire /etc/monit.d/ - comme tout bon service « à la Unix » qui se respecte. Lors de l'installation du service, le fichier présent dans ce répertoire est le fichier logging qui définit simplement dans quel fichier Monit journalise ses actions :
$ cat /etc/monit.d/logging
# log to monit.log
set logfile /var/log/monit
Et voici le contenu habituel de ce fichier :
# cat /var/log/monit
[CEST Aug 17 12:59:06] info : monit: generated unique Monit id 8a8cc0a7d85fb3e85f9d91deaec81970 and stored to '/root/.monit.id'
[CEST Aug 17 12:59:06] info : 'myhost' Monit started
3.4.2 Ajout du serveur de mail
Cette configuration est minimale et il lui manque un élément crucial : l'adresse du serveur STMP à partir duquel Monit pourra, le cas échéant, envoyer des messages d'alerte. Commençons donc par ajouter un fichier mailserver à notre configuration :
# cat /etc/monit.d/mailserver
set mailserver smtp.gmail.com port 587 username "monit@gmail.com" password "isupervisesoftware" using tlsv1
Note : par souci de simplicité, nous avons utilisé comme STMP le service de Google GMail.
3.4.3 Définition du destinataire des alertes
Maintenant que Monit peut envoyer des mails d'alerte, il reste encore à définir à qui - ainsi que le format du mail. Ceci se fait très aisément avec l'ajout d'un autre fichier dans notre répertoire de configuration :
$ cat /etc/monit.d/alert
set mail-format {
from: $HOST@gmail.com
subject: Monit a effectué un redémarrage du service $SERVICE exécuté sur $HOST
message: Vous êtes notifié par ce message d'une action entreprise automatiquement par Monit sur le service $SERVICE sur $HOST: $DESCRIPTION
}
set alert belaran@gmail.com
À ce stade, il est prudent de redémarrer le service et de vérifier, dans son fichier journal, qu'il n'y a pas d'erreur dans la configuration et que Monit a pu démarrer sans incident :
# service monit restart
Shutting down monit: [ OK ]
Starting monit: [ OK ]
# tail -f /var/log/monit
[CEST Sep 23 16:17:05] info : 'myhost' Monit started
Il nous reste un dernier élément à mettre en place dans la configuration générale de Monit. Monit expose une partie de son API via un Web Service et vient donc avec un (petit) serveur Web. Ce dernier permet aussi d'accéder à des données de supervision sur l'hôte, comme nous le verrons plus loin.
Bref, cette fonctionnalité est fort pratique, et nous allons donc l'activer, en ajoutant un autre fichier au répertoire /etc/monit.d/ :
// /etc/monit.d/monit.web
set httpd port 2812 and use address localhost
allow localhost
On notera que l'on autorise uniquement les accès locaux (allow localhost) pour des raisons de sécurité. Néanmoins, si votre réseau est déjà sécurisé, vous pouvez souhaiter autoriser certaines adresses IP ou sous réseau, pour vous connecter à distance à Monit.
3.4.4 Configuration avec Puppet
Nous allons étendre notre module Puppet pour inclure une paire de fichiers patrons (templates) qui permettront de générer une configuration appropriée selon les environnements :
// modules/monit/templates/mailserver.erb
set mailserver <%= @smtp_host %> port <%= @smtp_port %> username "<%= @smtp_user %>" password "<%= @smtp_pass %>"
<% if (defined(@smtp_use_tlsv1)) %>
using tlsv1
<% end if %>
// modules/monit/templates/alert.erb
set mail-format {
from: $HOST@gmail.com
subject: Monit a effectué un redémarrage du service $SERVICE exécuté sur $HOST
message: Vous êtes notifié par ce message d'une action entreprise automatiquement par Monit sur leservice $SERVICE sur $HOST: $DESCRIPTION
}
set alert <%= @alert_recipient %>
// modules/monit/templates/monit.web.erb
set httpd port <%= @httpd_port %> and use address <%= @monit_bind_address %>
allow <%= @allowed_host %>
On ajoute ensuite une classe, que l'on peut configurer si besoin est, mais définie avec les paramètres par défaut appropriés à notre module Monit :
// modules/monit/manifests/init.pp:
...
class monit::alert($mail_receipient='belaran@gmail.com', $smtp_host='smtp.gmail.com', $smtp_port='587', $smtp_user='monit@gmail.com', $smtp_pass='isupervisesoftware',
$smtp_use_tlsv1='true', $httpd_port='2812', $monit_bind_address='localhost', $allowed_host='localhost') {
file { '/etc/monit.d/alert':
content => template("monit/alert.erb'),
require => Package['monit'],
notify => Service['monit'],
}
file { '/etc/monit.d/mailserver':
content => template('monit/mailserver.erb'),
require => Package['monit'],
notify => Service['monit'],
}
file { '/etc/monit.d/monit.web':
content => template('monit/monit.web.erb'),
require => Package['monit'],
notify => Service['monit'],
}
}
Ce qui donne, dans le fichier manifeste :
// manifests/site.pp:
node 'myhost' {
include epel::repo
include monit::monit
include monit::alert
}
On en profite d'ailleurs pour généraliser encore plus notre manifeste, en définissant un node monit-node dont notre serveur de test, myhost, va hériter :
node 'monit-node' {
include epel::repo
include monit::monit
include monit::alert
}
node 'myhost' inherits 'monit-node' {}
4. Mise en place des services sous le contrôle de Monit
4.1 Apache HTTPd
4.1.1 Installation du serveur
Le temps où l'on recompilait soi-même son serveur Apache étant révolu (enfin, on l'espère pour le lecteur), l'installation d'un serveur Apache est relativement simple :
# yum install -y httpd # sur RHEL
N'oublions pas d'installer mod_proxy, pour permettre à Apache d'effectuer une balance de charge entre les deux instances Wildfly, et aussi le mod_status, qui permet de bénéficier d'informations supplémentaires sur l'état du serveur. Sur RHEL, ils le sont par défaut, mais si vous utilisez une autre distribution il sera peut être nécessaire d'installer des paquets logiciels supplémentaires.
Quid de mod_jk et mod_proxy ?
Si le lecteur a déjà expérimenté avec Wildfly, ou même des versions antérieures, quand le serveur s'appelait encore JBoss, il aura sans doute entendu parler de mod_jk et mod_cluster qui sont des modules dédiés à l'utilisation de Apache HTTPd avec justement des serveurs Web Java. Néanmoins, par souci de simplicité et pour rester au cœur du sujet, nous utiliserons le module mod_proxy.
En effet, ce dernier est un reverse proxy dont le fonctionnement est simple - les requêtes sont dirigées vers les instances Wildfly, et il ne nécessite donc pas, à l'inverse des modules cités à l'instant, d'explication supplémentaire.
4.1.2 Configuration de Apache HTTPd
Il n'y a que peu de configuration à effectuer sur le serveur, tout du moins à ce stade. En effet, il suffit, comme toujours, de proprement définir son nom (ServerName) – ne serait-ce que pour retirer le désagréable message d'erreur à ce sujet au démarrage du service, et d'activer la page /server-status :
// /etc/httpd/conf/httpd.conf
...
ServerName myhost:80
...
<Location /server-status>
SetHandler server-status
</Location>
4.1.3 Mise sous contrôle de Monit
Pour que Monit contrôle le processus de HTTPd, il est nécessaire d'ajouter un fichier décrivant ce nouveau service à surveiller au sein du répertoire /etc/monit.d/:
/etc/monit.d/httpd.service:
check process httpd with pidfile /var/run/httpd/httpd.pid
start program = "/sbin/service httpd start"
stop program = "/sbin/service httpd stop"
if failed host localhost port 80 protocol HTTP request / then restart
if 5 restarts within 5 cycles then timeout
Redémarrons le service monit pour s'assurer que les configurations sont correctes et que le service est désormais sous contrôle :
# service monit reload # vérifie que la configuration est correcte, sans interrompre le service
# service monit restart # prend en compte la nouvelle configuration
4.2 Vérification du bon fonctionnement de Monit
Nous allons maintenant simplement simuler un crash de HTTPd en exécutant la commande kill sur son PID :
# kill -9 28497
Regardons maintenant le journal de Monit :
$ tail -f /var/log/monit.log
On constate bien, sans surprise, que Monit a détecté l'absence du processus et a redémarré automatiquement le serveur HTTPd. Assurons-nous maintenant que Monit cesse bien de redémarrer le serveur si celui-ci est constamment en échec. Pour démontrer ceci, nous allons créer un petit script en charge d'éteindre le service, toutes les secondes, pour forcer Monit à le redémarrer :
/root/keep-service-down.sh
#!/bin/bash
readonly SERVICE_NAME=${1:-'httpd'}
readonly SCAN_INTERVAL=${2:-'1'}
while true
do
echo "Kill processes ${SERVICE_NAME} and wait for ${SCAN_INTERVAL}s."
service "${SERVICE_NAME}" 'stop'
sleep "${SCAN_INTERVAL}"
done
Une fois ce script lancé observons le journal de Monit :
[CEST Sep 25 16:27:07] info : 'myhost' Monit started
[CEST Sep 25 16:28:10] error : 'apache' process is not running
[CEST Sep 25 16:28:12] info : 'apache' trying to restart
[CEST Sep 25 16:28:12] info : 'apache' start: /sbin/service
[CEST Sep 25 16:28:42] error : 'apache' failed to start
[CEST Sep 25 16:29:43] error : 'apache' process is not running
[CEST Sep 25 16:29:43] info : 'apache' trying to restart
[CEST Sep 25 16:29:43] info : 'apache' start: /sbin/service
...
[CEST Sep 25 16:39:14] error : 'apache' failed to start
[CEST Sep 25 16:40:14] error : 'apache' process is not running
[CEST Sep 25 16:40:14] info : 'apache' trying to restart
[CEST Sep 25 16:40:14] info : 'apache' start: /sbin/service
[CEST Sep 25 16:40:44] error : 'apache' failed to start
[CEST Sep 25 16:41:44] error : 'apache' service restarted 5 times within 5 cycles(s) - unmonitor
Comme on peut le voir, après plusieurs essais dans le temps imparti, Monit cesse de surveiller le processus (action qu'il désigne sous le nom de unmonitor). Bien évidemment, Monit envoie une notification pour indiquer ceci.
À partir de maintenant, nous pouvons librement éteindre ou démarrer HTTPd, Monit ne surveillant plus le processus. Ce comportement peut sembler étrange de prime abord, mais il s'agit en fait d'une fonctionnalité cruciale. En effet, redémarrer sans cesse un service peut avoir de fâcheuses conséquences. Par exemple, certains logiciels au démarrage créent un ensemble de jeux de données de seulement quelques méga-octet, dans un répertoire temporaire, généré automatiquement dans /tmp. Si on redémarre le logiciel à l'infini, on peut aisément consommer tout l'espace disponible sur cette partition et faire purement et simplement s'effondrer le système !
Ceci étant dit, arrêtons notre script « tueur de service » et demandons à Monit de surveiller de nouveau HTTPd :
# monit monitor httpd start
# tail -f /var/log/monit
[CEST Sep 26 10:09:22] info : 'myhost' Monit started
[CEST Sep 26 10:10:17] debug : monitor service 'httpd' on user request
[CEST Sep 26 10:10:17] info : monit daemon at 21451 awakened [CEST Sep 26 10:10:17] info : Awakened by User defined signal 1
[CEST Sep 26 10:10:17] info : 'apache' monitor action done
4.3 Mise en place avec Puppet
Là encore, la configuration de Apache avec Puppet est triviale :
// modules/httpd/manifests/init.pp
class httpd::httpd {
package { 'httpd': ensure => installed, }
file { '/etc/httpd/conf/httpd.conf':
content => template('httpd/httpd.conf.erb'),
require => Package['httpd'],
notify => Service['httpd'],
}
service { 'httpd':
ensure => running,
enable => true,
require => File['...'],
}
}
// modules/httpd/templates/httpd.conf.erb
...
ServerName <%= @hostname %>:80
...
<Location /server-status>
SetHandler server-status
</Location>
...
La précédente configuration ne tient compte néanmoins que d'une installation « standard » d'Apache. On ne mentionne nulle part la mise sous contrôle de ce service par Monit. Pour ce faire, nous allons rajouter, dans notre module Monit, une nouvelle définition de ressource :
// modules/monit/manifests/init.pp
...
define monit::service($service_name=$name,$pid_file, $http_port=80, $bind_ip='localhost'
$nb_failed_restart=5, $nb_cycles=5) {
file { "/etc/monit.d/$service_name":
content => template('monit/service.erb'),
require => Package['monit'],
}
}
...
// modules/monit/templates/service.erb
check process <%= @service_name %> with pidfile <%=@pid_file %>
start program = "/sbin/service <%= @service_name %>
start" stop program = "/sbin/service <%= @service_name %> stop"
if failed host <%= @bind_ip %> port <%= @http_port %> protocol HTTP request / then restart
if <%= @nb_failed_restarts §> restarts within <%= @nb_cycles %> cycles then timeout
Une fois ce module défini, il ne nous reste plus qu'à mettre à jour notre fichier manifeste, et à utiliser notre nouvelle définition de ressource :
// manifests/site.pp:
node 'myhost' {
include epel::repo
include monit::monit
include monit::alert
include httpd::httpd
monit::service { 'httpd':
pid_file => '/var/run/httpd/httpd.pid',
require => Service['httpd'],
}
}
4.4 Installation de Wildfly
4.4.1 Préparation du système
Wildfly étant un serveur d'applications Java/Java EE, la première étape à effectuer est bien évidemment l'installation d'un compilateur et d'une machine virtuelle Java. Si, pendant de longues années, la machine virtuelle de référence était celle fourni par Sun (HotSpot), désormais fournie par Oracle, son équivalent Open Source OpenJDK a désormais bien rattrapé son prédécesseur et est largement aussi performante. Nous allons donc installer cette dernière (avec son compilateur) :
# yum install -y openjdk-devel
4.4.2 Installation du serveur
Sur RHEL, si vous disposez d'une souscription pour JBoss EAP, vous pouvez simplement utiliser yum pour effectuer l'installation :
# yum install jbossas
Sinon, comme peu de distributions fournissent des paquets logiciels pour les logiciels Open Source, pour des raisons qui nécessiteraient probablement un article à part à expliciter, il faudra effectuer une installation à partir de l'archive ZIP fournie sur le site de Widfly :
# wget http://download.jboss.org/wildfly/8.1.0.Final/wildfly-8.2.0.Final.tar.gz
Il suffit ensuite de décompresser cette archive dans un répertoire. Comme le RPM fourni par Red Hat sur les systèmes RHEL installe ce serveur dans /usr/share/jbossas, nous ferons de même pour permettre, pour le reste de l'article, d'avoir la même configuration.
# tar xvzf wildfly-8.1.0.Final.tar.gz
wildfly-8.2.0.Final/
wildfly-8.2.0.Final/.installation/
wildfly-8.2.0.Final/appclient/
...
wildfly-8.2.0.Final/standalone/tmp/auth/
Une fois l'archive décompressée (ou le RPM installé), il suffit de mettre en place ce serveur comme un service. Fort heureusement, un script est fourni à cet effet avec l'archive : bin/init.d/jboss-as-standalone.sh. Ce script utilise par défaut, comme fichier de configuration du serveur et de sa machine virtuelle, le fichier situé dans le même répertoire que lui bin/init.d/jboss-as.conf. On peut bien évidemment, et nous allons le faire, redéfinir ce fichier :
# grep -e 'jboss-as.conf' bin/init.d/jboss-as-standalone.sh
config: /etc/jboss-as/jboss-as.conf
JBOSS_CONF="/etc/jboss-as/jboss-as.conf"
Commençons déjà par utiliser le script fourni, pour vérifier que le serveur a été proprement installé et démarre sans problème :
# ln -s /usr/share/jbossas/bin/init.d/jboss-as-standalone.sh /etc/init.d/wildfly
# service wildfly start # service wildfly status jboss-as is running (pid 1460)
4.4.3 Mise en place de la seconde instance
Comme décrit plus haut, nous souhaitons disposer des deux instances de Wildfly, chacune s'exécutant dans une machine virtuelle Java séparée et dédiée - et utilisant une série de ports différents. Pour ce dernier point, c'est très aisé à faire puisque le serveur dispose d'un argument permettant de décaler de manière consistante l'ensemble des ports qu'il utilise.
Pour ce qui est de démarrer une seconde instance, il est nécessaire, d'une part de placer les données de chaque instance dans des répertoires distincts, et d'autre part de disposer de services distincts du point de vue système. Néanmoins, plutôt que de dupliquer les fichiers de services, et devoir maintenir manuellement les deux instances pourtant identiques, nous allons avoir recours à une astucieuse utilisation des liens symboliques.
/etc/init.d/wildfly
#!/bin/bash
readonly JBOSS_USER='jboss'
readonly JBOSS_CONF='/etc/jbossas/jbossas.conf'
export JBOSS_CONF
readonly JBOSS_HOME='/usr/share/jbossas/'
readonly SERVICE_CONF_DIR="${JBOSS_HOME}/standalone/configuration"
if [ "${0}" == '/etc/init.d/wildfly' ]; then
echo 'This script can not be invoked directly. Please create a symlink, with an ID value:'
echo "# ln ${0} ${0}-1"
exit 1
fi
readonly INSTANCE_ID=$(echo ${0} | sed -e 's/^.*-//')
readonly SERVICE_BASE_DIR="/usr/share/jbossas-${INSTANCE_ID}/"
readonly SERVICE_LOG_DIR="/var/log/jbossas/${INSTANCE_ID}"
readonly JBOSS_PIDFILE="/var/run/jboss-as/wildfly-${INSTANCE_ID}.pid"
readonly JBOSS_CONSOLE_LOG="${SERVICE_LOG_DIR}/console.log"
readonly PROG="wildfly-${INSTANCE_ID}"
readonly PORT_OFFSET=$(expr 100 * $(expr "${INSTANCE_ID}" - 1) )
export JAVA_OPTS="-Djboss.server.base.dir=${SERVICE_BASE_DIR} \ -Djboss.server.log.dir=${SERVICE_LOG_DIR} \ -Djboss.server.config.dir=${SERVICE_CONF_DIR}\ -Djboss.socket.binding.port-offset=${PORT_OFFSET}" "${JBOSS_HOME}/bin/init.d/jboss-as-standalone.sh" ${@}
Ce script de démarrage est relativement simple à comprendre. En essence, on se sert d'un numéro, ajouté au nom du lien symbolique, pour déterminer le décalage de port, le nom du service et où se situent les répertoires de données de l'instance. On modifie aussi la définition du fichier de configuration qui pointe désormais sur le fichier /etc/jbossas/jboss.conf, et dont le contenu est vide.
On notera qu'il ne faut pas oublier de créer l'utilisateur jboss, et les répertoires associés aux instances. En outre, il faudra attribuer à cet utilisateur la propriété de ces répertoires. Plus bas nous réaliserons cette tâche avec Puppet, mais ici nous le ferons tout d'abord à l'aide d'une série de commandes Bash :
# export JBOSS_USER='jboss'
# useradd -s /sbin/nologin "${JBOSS_USER}"
# mkdir /usr/share/jbossas-{1,2}
# mkdir /var/log/jbossas/{1,2}
# mkdir /var/run/jboss-as/
# chown -R "${JBOSS_USER}:${JBOSS_USER}" /usr/share/jbossas-* /var/log/jbossas/* /var/run/jboss-as/
Il ne reste plus maintenant qu'à créer les deux liens symboliques vers le fichier de service, et démarrer nos instances :
# ln -s /etc/init.d/wildfy /etc/init.d/wildfy-1
# ln -s /etc/init.d/wildfy /etc/init.d/wildfy-2
# service widlfly-1 start # service widlfly-2 start
# service widlfly-1 status wildfly-1 is running (pid 1890)
# service widlfly-1 status wildfly-2 is running (pid 1895)
# curl -s http://$(hostname):8080 # pour vérifier l'accessibilté de la première instance # curl -s http://$(hostname):8180 # pour vérifier l'accessibilité de la seconde instance
Et le fameux Domain Mode ?
Introduit depuis JBoss AS 7, soit quelques versions avant le renommage du projet en Wildfly, ce mode de fonctionnement - très inspiré par celui du concurrent propriétaire du projet, WebLogic, permet de gérer plusieurs instances, qu'elles soient localisées sur une même machine ou sur plusieurs. Il forme donc un excellent moyen de mettre en place plusieurs instances sur une machine mais il nécessiterait plus d'explications spécifiques pour en décrire le fonctionnement, ce qui nous éloignerait de beaucoup du propos de l'article.
4.4.4 Mise en place avec Puppet
Bien que certains le pensent - et malheureusement parfois s'en servent ainsi, Puppet n'est pas un outil de déploiement, mais un outil destiné à maintenir un serveur dans un état défini. Ainsi, Puppet ne prend pas réellement en charge l'installation d'un logiciel - son déploiement pour être clair - mais utilise seulement les outils à sa disposition selon le système (ici yum pour gérer les RMS) pour vérifier la présence ou l'absence d'un paquet logiciel.
Tout ceci pour dire que si vous ne disposez pas d'un RPM pour Wildfly, la première étape est d'en réaliser un. Il s'agit d'un paquet sans architecture (noarch) et sans dépendance, plus complexe que la machine virtuelle Java que l'on souhaite utiliser. C'est plutôt trivial, et il existe plusieurs exemples en ligne - à commencer par le Git de l'auteur de cet article [7]. Nous n'allons donc pas nous écarter du sujet de l'article et supposer que, soit vous disposez d'un tel RPM, soit en tant que client de Red Hat vous avez accès au RPM fourni par la société.
La mise en place de Wildlfy/JBoss CAP par Puppet est donc assez « classique » et ressemble à ce que l'on a pu voir jusqu'à maintenant. Nous allons juste définir une ressource dynamique pour faciliter la mise en place des différentes instances :
class widlfly::wildfy($jboss_user='jboss') {
user { $jboss_user:
ensure => present,
shell => '/sbin/nologin',
}
package { 'jbossas':
ensure => installed,
require => User[$jboss_user],
}
file { '/var/run/jboss-as/':
ensure => directory,
require => Package['jbossas'],
}
file { '/etc/init.d/wildfly':
source => 'puppet:///modules/wildfly/wildfly',
mode => 644, owner => 'root', group => 'root',
require => Package['jbossas'],
}
file { '/etc/jbossas/jboss.conf':
content => '', # le fichier est vide, car la logique est placé dans le script de démarrage
owner => root, group => root, mode => 755,
require => Package['jbossas'],
}
}
define wildfly::instance($id=$name, $jboss_home='/usr/share/jbossas', $jboss_log_dir='/var/log/jbossas/' $jboss_user='jboss') {
file { "$jboss_home-$id":
ensure => directory,
owner => $jboss_user,
group => $jboss_user,
}
file { "$jboss_log_dir":
ensure => directory,
}
file { "$jboss_log_dir/$id":
ensure => directory,
require => File[$jboss_log_dir],
}
$service="wildfly-$id"
file { "/etc/init.d/$service":
ensure => link,
target => '/etc/init.d/wildfly',
require => File['/etc/init.d/wildfly'],
}
service { "wildfly-$id":
ensure => running,
enable => yes,
require => File["/etc/init.d/$service"],
}
}
Mettons maintenant à jour notre fichier manifeste pour y déclarer les deux instances de Wildfly :
// manifests/site.pp:
node 'myhost' {
include epel::repo
include monit::monit
include monit::alert
include httpd::httpd
include widlfly::wildfy
wildfly::instance { ['1','2']: }
monit::service { 'httpd':
pid_file => '/var/run/httpd/httpd.pid',
require => Service['httpd'],
}
}
4.4.5 Mise sous contrôle de Wildfly par Monit (avec Puppet)
Grâce à la ressource que nous avons défini précédemment, il est très aisé de mettre sous contrôle de Monit nos instances Wildfly. Ajoutons donc maintenant une paire de nouvelles ressources à notre fichier manifeste :
// manifests/site.pp:
node 'myhost' {
include epel::repo
include monit::monit
include monit::alert
include httpd::httpd
include widlfly::wildfy
wildfly::instance { ['1','2']: }
monit::service { 'httpd':
pid_file => '/var/run/httpd/httpd.pid',
require => Service['httpd'],
}
monit::service { 'wildfly-1':
pid_file => '/var/run/jboss-as/wildfly-1.pid',
http_port => 8080,
}
monit::service { 'wildfly-2':
pid_file => '/var/run/jboss-as/wildfly-2.pid',
http_port => 8180,
}
}
4.5 Console de supervision
Bien que notre stratégie de supervision distribuée ne permette pas, par définition, de disposer d'une console de supervision globale, Monit propose néanmoins une simple console Web, locale, dont nous avons configuré l'interface et le port d'écoute plus haut. Nous allons maintenant rapidement présenter cette dernière.
Pour y accéder, il suffit donc de se connecter au port 2812 sur l'hôte localhost. On pourrait bien évidemment exposer cette console vers l'extérieur, mais comme cette dernière permet d'effectuer quelques opérations - comme par exemple désactiver le monitoring, ce n'est pas une approche recommandée.
Sécuriser l'accès à la console de Monit
Si l'on souhaite pourvoir accéder à la console de Monit depuis une machine distante, sans recours à des tunnels SSH - ou plus simplement si on souhaite mettre à disposition certaines de ses opérations à des utilisateurs authentifiés, il est recommandé de placer une instance Apache - ou un serveur Web plus léger, comme nginx [8] ou lighttpd [9] en frontal. Ces serveurs sont dotés des fonctionnalités nécessaires pour implémenter une authentification forte, et peuvent donc faire le relais entre la console graphique, disponible seulement en local, et le monde extérieur.
Le premier écran de la console donne une vision globale du système. Elle indique l'utilisation de ses ressources (CPU, Mémoire), mais aussi l'état des services que Monit supervise (voir figure 1) . Si l'on clique sur le lien associé au système, on obtient quelques informations plus détaillées, mais surtout un bouton permettant d'activer ou désactiver la console, comme le montre la figure 2. On peut aussi accéder aux informations, à la disposition de Monit, sur l'état d'un processus, comme par exemple, à une instance de JBoss EAP (voir figure 3). Le plus utile est sans doute, non pas les informations sur le processus en tant que tel, mais la possibilité d'effectuer des opérations sur le processus à travers Monit. Ceci peut se révéler très pratique, pour permettre à des développeurs ou des testeurs d'effectuer des tâches de maintenance sur un système applicatif sans pour autant leur donner les accès privilégiés (root).
Fig. 1 : Vision globale du système.
Fig. 2 : Informations détaillées et bouton d'activation/désactivation de la console.
Fig. 3 : État d'un processus (instance JBoss EAP).
Conclusion
Après un tour d'horizon, très technique, de notre cas d'utilisation, voici venu le temps de tirer quelques conclusions. La première étant que nous avons déjà démontré la faisabilité d'une telle supervision distribuée, à l'aide de Monit.
Une première limite, qui n'est pas liée au concept de supervision distribuée, mais plutôt à l'implémentation utilisée, chez Monit, est l'utilisation d'email pour remonter les alertes. S'il est relativement aisé aujourd'hui d'avoir une infrastructure de transfert de message résistante et hautement disponible, il n'en reste pas moins qu'une alerte ne peut jamais quitter le système en panne.
Mais là encore, il est important de noter que l'idée n'est pas de supprimer le système de supervision centrale, mais d'en limiter le périmètre au maximum. On peut donc laisser à ce dernier la charge de vérifier que le système cible est toujours en marche et que le serveur de mail utilisé est toujours accessible.
À l'inverse, comme l'a montré l'article, mettre en place Monit pour superviser des processus est très, très simple, surtout avec une gestion de configuration déléguée à un outil comme Puppet. Cette supervision est non seulement robuste, et capable de réagir à des changements d'état du service, mais elle tiendra également la mise à l'échelle, puisque chaque système supplémentaire déployé prendra en charge lui-même le coût de sa supervision.
Références
[1] Java Management Extensions : http://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html
[2] RHQ : http://rhq-project.github.io/rhq/
[3] Monit : https://mmonit.com/monit/
[4] Munin : http://munin-monitoring.org/
[5] Puppet : http://puppetlabs.com/
[6] Dépôts logiciels EPEL: https://fedoraproject.org/wiki/EPEL
[7] RPM JBoss : https://github.com/rpelisse/eap6-rpm-infra
[8] Nginx : http://nginx.org/
[9] Lighttpd : http://www.lighttpd.net/