Déploiement D'un Cluster Docker Swarm Mode via Terraform Sur Scaleway 3/3 : Déployer Le Cluster

Contexte

Je vous propose dans cette série d’articles, d’installer un cluster docker Swarm Mode via Terraform chez l’hébergeur Scaleway.

Celle-ci comprend 3 articles :

Déploiement via Terraform du cluster Docker Swarm Mode

organisation des fichiers terraform

Les fichiers terraform, (qui regroupent les ressources de l’infrastructure voulue), sont les suivants :

terraform.tf

Il s’agit du fichier de configuration principal. Il décrit l’infrastructure voulue, sous forme de ressources.
Ces ressources sont fournies par des providers, et mis en place avec l’aide de provisioners afin de déployer des fichiers sur les serveurs, ou d’exécuter des commandes (locales au poste qui lance le déploiement ou distantes sur les serveurs cibles).

le fichier est organisé via les sections suivantes :

  • définition des providers (ici scaleway et cloudflare)
  • définition de la ressource “manager_init“, qui correspond au premier serveur scaleway servant à initialiser le cluster swarm (un noeud manager) :
    Ce premier serveur initiant le cluster est nécessaire ; il génère les tokens de sécurité pour les autres noeuds ayant le rôle de manager ou worker.

La récupération des tokens de sécurité Docker Swarm mode se fait via des appels distants vers le premier Manager sur le port 2375.

Une solution plus sûre pourrait passer plutôt par l’écriture du token sur un fichier sur le premier serveur lors de sa génération, une récupération en locale, puis un upload en scp sur les serveurs nécessitant ces tokens lors de la connexion à ceux-ci juste après leur création via des provisioners Terraform.

Veuillez noter aussi que le cluster Docker Swarm mode écoute sur le port 2375, qui n’est pas sécurisé. Une installation plus sûre utiliserait le port 2376 couplé à une authentification cliente par certificat SSL supportée nativement par le Docker Engine. Cette configuration dépasse largement le cadre de cet article.

  • définition des autres managers du cluster :
    il est recommandé, pour avoir un cluster robuste, d’avoir au moins 3 managers présents. Une plus grande robustesse aurait induit la répartition des noeuds sur plusieurs centres de données (Scaleway propose un centre de données français, et un centre de données néerlandais pour l’instant).
  • définition des workers du cluster
  • définition de l’IP statique de référence et association au serveur d’initialisation :

Cela permet que le DNS pointe toujours vers une IP valide, même si le serveur associé change.
À noter qu’un problème persiste ici : lorsque le serveur tombe, il n’y a pas encore de mécanisme de rétro-contrôle qui associe l’IP avec un autre serveur en vie du cluster.

  • ajout d’un sous-domaine wildcard (*) au DNS, pointant vers l’IP statique
    Cela permet au cluster de gérer des sous-domaines de façon autonome, via traefik par exemple
  • définition des groupes de sécurité, avec leur règles associées :
    Comm indiqué précédemment, cela n’apporte qu’une sécurité limitée, au vu du fonctionnement des groupes de sécurité de Scaleway.

Voici le contenu du fichier terraform.tf :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
provider "scaleway" {
organization = "${var.scaleway_organization}"
token = "${var.scaleway_token}"
region = "${var.scaleway_region}"
}
provider "cloudflare" {
email = "${var.cloudflare_email}"
token = "${var.cloudflare_token}"
}
resource "scaleway_server" "manager_init" {
count = 1
name = "swarm-manager-${count.index + 1}"
image = "${var.ubuntu_x86_64_image}"
type = "${var.scaleway_type}"
dynamic_ip_required = "true"
security_group="${scaleway_security_group.internal.id}"
provisioner "remote-exec" {
inline = [
"mkdir -p /etc/systemd/system/docker.d",
"mkdir -p /etc/docker"]
}
provisioner "file" {
source = "daemon_manager.tpl"
destination = "/etc/docker/daemon_manager.tpl"
}
provisioner "remote-exec" {
inline = [
"sed -e 's/SWARM_MANAGER_PRIVATE_IP/${self.private_ip}/g' /etc/docker/daemon_manager.tpl > /etc/docker/daemon.json",
"systemctl daemon-reload",
"systemctl restart docker",
"docker swarm init --advertise-addr ${self.private_ip} --listen-addr ${self.private_ip}:2377 "
]
}
}
resource "scaleway_server" "manager_join" {
count = 2
name = "swarm-manager-${count.index + 2}"
image = "${var.ubuntu_x86_64_image}"
type = "${var.scaleway_type}"
dynamic_ip_required = "true"
security_group="${scaleway_security_group.internal.id}"
provisioner "remote-exec" {
inline = [
"mkdir -p /etc/systemd/system/docker.d",
"mkdir -p /etc/docker"
]
}
provisioner "file" {
source = "daemon_manager.tpl"
destination = "/etc/docker/daemon_manager.tpl"
}
provisioner "remote-exec" {
inline = [
"sed -e 's/SWARM_MANAGER_PRIVATE_IP/${self.private_ip}/g' /etc/docker/daemon_manager.tpl > /etc/docker/daemon.json",
"systemctl daemon-reload",
"systemctl restart docker",
"docker swarm join ${scaleway_server.manager_init.0.private_ip}:2377 --token $(docker -H ${scaleway_server.manager_init.0.private_ip}:2375 swarm join-token -q manager)"
]
}
depends_on = [
"scaleway_server.manager_init"
]
}
resource "scaleway_server" "worker" {
count = 2
name = "swarm-worker-${count.index + 1}"
#Docker
image = "${var.ubuntu_x86_64_image}"
type = "${var.scaleway_type}"
dynamic_ip_required = "true"
security_group="${scaleway_security_group.internal.id}"
provisioner "remote-exec" {
inline = [
"mkdir -p /etc/systemd/system/docker.d",
"mkdir -p /etc/docker"
]
}
provisioner "file" {
source = "daemon_worker.json"
destination = "/etc/docker/daemonjson"
}
#connection to the first manager to get the cluster token
provisioner "remote-exec" {
inline = [
"systemctl daemon-reload",
"systemctl restart docker",
"docker swarm join ${scaleway_server.manager_init.0.private_ip}:2377 --token $(docker -H ${scaleway_server.manager_init.0.private_ip}:2375 swarm join-token -q worker)"
]
}
depends_on = [
"scaleway_server.manager_init"
]
}
resource "scaleway_ip" "external_ip" {
server = "${scaleway_server.manager_init.0.id}"
}
# Add a record to the domain
resource "cloudflare_record" "domain" {
domain = "${var.cloudflare_domain}"
name = "*"
value = "${scaleway_ip.external_ip.ip}"
type = "A"
ttl = 3600
}
resource "scaleway_security_group" "internal" {
name = "cluster_inside"
description = "security group to configure access from inside the cluster"
}
#accept in
variable "input_ports_from_cluster" {
#TCP port 2377 for cluster management communications
#TCP and UDP port 7946 for communication among nodes
#TCP and UDP port 4789 for overlay network traffic
type = "list"
default = [2377,4789,7946]
}
resource "scaleway_security_group_rule" "internal_in_accept_2375" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "2375"
}
resource "scaleway_security_group_rule" "internal_in_accept_2376" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "2376"
}
#TCP port 2377 for cluster management communications
resource "scaleway_security_group_rule" "internal_in_accept_2377" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "2377"
}
#TCP and UDP port 4789 for overlay network traffic
resource "scaleway_security_group_rule" "internal_in_accept_overlay_tcp" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "4789"
}
#TCP and UDP port 4789 for overlay network traffic
resource "scaleway_security_group_rule" "internal_in_accept_overlay_udp" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "UDP"
port = "4789"
}
#TCP and UDP port 7946 for communication among nodes
resource "scaleway_security_group_rule" "internal_in_accept_inter_nodes_tcp" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "7946"
}
resource "scaleway_security_group_rule" "internal_in_accept_inter_nodes_udp" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "UDP"
port = "7946"
}
#If you are planning on creating an overlay network with encryption (--opt encrypted),
#you will also need to ensure protocol 50 (ESP) is open.
resource "scaleway_security_group_rule" "internal_in_accept_esp_encryption" {
security_group = "${scaleway_security_group.internal.id}"
action = "accept"
direction = "inbound"
ip_range = "10.0.0.0/8"
protocol = "TCP"
port = "50"
}
resource "scaleway_security_group" "external" {
name = "cluster_outside"
description = "security group to configure access from outside the cluster"
}
variable "input_ports_from_external" {
#If you are planning on creating an overlay network with encryption (--opt encrypted),
#you will also need to ensure protocol 50 (ESP) is open.
type = "list"
default = [80,443,22]
}
resource "scaleway_security_group_rule" "external_in_accept" {
security_group = "${scaleway_security_group.external.id}"
action = "accept"
direction = "inbound"
ip_range = "0.0.0.0/0"
protocol = "TCP"
port = "${element(var.input_ports_from_external, count.index)}"
count = "${length(var.input_ports_from_external)}"
}
#drop in
resource "scaleway_security_group_rule" "any_in_drop" {
security_group = "${scaleway_security_group.external.id}"
action = "drop"
direction = "inbound"
ip_range = "0.0.0.0/0"
protocol = "TCP"
}

daemon_manager.tpl

Ce fichier est un template pour configurer le démon docker des noeuds manager du cluster. la variable SWARM_MANAGER_PRIVATE_IP sera remplacée au démarrage pour l’adresse IP de l’interface réseau privée du serveur.

1
2
3
4
5
6
7
{
"experimental" : true,
"storage-driver" : "overlay2",
"labels" : ["provider=scaleway"],
"mtu": 1500,
"hosts": ["unix:///var/run/docker.sock","tcp://SWARM_MANAGER_PRIVATE_IP:2375"]
}

daemon_worker.json

Ce fichier permet de configurer le démon des noeuds worker du cluster Docker Swarm mode. Il ne contient pas l’adresse IP
de l’interface réseau privée du serveur, car les noeuds worker ne peuvent piloter le cluster (sauf si on les transforme en manager).

1
2
3
4
5
6
7
{
"experimental" : true,
"storage-driver" : "overlay2",
"labels" : ["provider=scaleway"],
"mtu": 1500,
"hosts": ["unix:///var/run/docker.sock"]
}

variables.tf

Le fichier terraform.tf, fait référence à des variables. Celles-ci sont décrites dans le fichier variables.tf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
variable "scaleway_organization" {
description = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
variable "scaleway_token" {
description = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
variable "scaleway_region" {
description = "scaleway datacenter 'par1' for paris(France) or 'ams1' fr Amsterdam (Nederlands)"
default="par1"
}
variable "scaleway_type" {
description = "type of server"
default="C2S"
}
variable "cloudflare_email" {
description = "your email scaleway account identifier"
}
variable "cloudflare_token" {
description = ""
}
variable "cloudflare_domain" {
description = "your DNS domain managed by cloudflare"
}
variable "ubuntu_x86_64_image" {
description = "your custom image ID"
}

terraform.tfvars

Ce fichier contient les valeurs des variables référencées dans le fichier terraform.tf, et définies dans le fichier variables.tf.

1
2
3
4
5
6
7
8
scaleway_organization = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
scaleway_token = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
scaleway_region = "par1"
scaleway_type = "C2S"
cloudflare_email = "myname@email.com"
cloudflare_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
cloudflare_domain = "mondomaine.com"
ubuntu_x86_64_image = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

terraform.tfstate

Ce fichier est généré par Terraform, et contient l’état de l’infrastructure après application des changements.

déclenchement de Terraform

  • créer un plan d’exécution

l’exécution de la commande terraform plan, permet d’inspecter l’infrastructure actuelle, et l’infrastructure cible,
afin de générer un plan d’exécution des changements nécessaires.

  • créer l’infrastructure

Cette création se fait via la commande :
terraform apply

  • détruire l’infrastructure

La destruction se fait via la commande terraform destroy (après confirmation via l’invite de commandes).

résultat

Après avoir crée via terraform apply, votre infrastructure (et patienté quelques minutes), en se connectant sur un des noeuds ayant le rôle de manager, vous pouvez lancer la commande suivante :

1
2
3
4
5
6
7
root@swarm-manager-1:~# docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
b37dxtsbn573m4xhas0j7b4do * swarm-manager-1 Ready Active Leader
cvv0iyfy7tybsev0k55iyif4i swarm-manager-2 Ready Active Reachable
llrhva62q0byzhruu6lu29gyx swarm-worker-2 Ready Active
p4mcus11v15can4ut0lqajfqd swarm-worker-1 Ready Active
pi2wxsmibs64y9d4xwnuxvew8 swarm-manager-3 Ready Active Reachable

Vous pouvez donc constater la bonne mise en place de votre cluster Docker Swarm Mode.

conclusion

Cet article vous permet de vous familiariser avec Terraform, un outil de plus en plus présent pour automatiser la création de vos infrastructures immutables via du code.
Il vous permet aussi de vous essayer à la création d’un cluster Docker Swarm Mode.
J’attire néanmoins votre attention sur la non-sécurisation de l’image et du réseau mis en place.

Une mise en production d’un environnement de ce type, demanderait au préalable, d’autres opérations de sécurisation qui sortent du cadre de cet article.

Déploiement D'un Cluster Docker Swarm Mode via Terraform Sur Scaleway 2/3 : Génération D'une Image Scaleway

Contexte

Je vous propose dans cette série d’articles, d’installer un cluster docker Swarm Mode via Terraform chez l’hébergeur Scaleway.

Celle-ci comprend 3 articles :

génération de l’image docker

Il est nécessaire d’utiliser une image, pour que le serveur Scaleway créé via Terraform puisse exécuter un système d’exploitation.
Scaleway propose de réutiliser le format de docker (Dockerfile), avec quelques adaptations, pour définir sa propre image.

Pour cela, Scaleway fournit un repository github appelé image builder, permettant de construire une image iso suivant ses besoins, via un serveur Scaleway.
Des images sont fournies par la communauté et validées par l’hébergeur pour être disponibles pour tous, mais on ne peut pas dire que Scaleway soit d’une grande célérité pour les valider (ubuntu et la dernière version de docker ici), ou pour les fournir.

À ce jour (avril 2017), l’image fournie ne contient que docker 1.12.2, soit 3 versions de retard…
Il est donc nécessaire de construire sa propre image.

création du serveur permettant de construire son image

Un préalable est bien sûr d’avoir un compte Scaleway.
La création d’un serveur de construction d’image peut être fait simplement via la ligne de commande de Scaleway appelée scw.
Rappelons que la création de cette image est spécifique à cet hébergeur.

scw a été pensé pour ressembler à la ligne de commande docker. Vous retrouverez de nombreuses sous-commandes docker qui ont été reprises dans celle-ci.

Identifiez-vous au préalable sur Scaleway via scw :
scw login

scw images vous permet de lister les images disponibles, tant celles de la communauté scaleway que les vôtres.

Vous retrouverez donc l’image dédiée à la construction d’images personnalisées appelée image-builder.

scw run vous permet de créer un serveur et le démarrer. Vous lancerez donc un nouveau serveur de création d’image via :

scw run --name="mon-image-perso" image-builder

À noter que la construction du serveur n’est pas instantané (quelques minutes), il faudra donc être patient.

Cette commande vous donnera de plus, une connexion ssh sur votre serveur nouvellement crée.

Sur ce serveur, exécutez :

image-builder-configure

puis identifiez-vous via votre identifiant (mail) et votre mot de passe.
Cette étape préalable permettra, une fois l’image construite, de pousser l’image dans l’infrastructure Scaleway pour qu’elle soit disponible (uniquement pour notre compte), lors de la création de nouveaux serveurs (le but de ce second article).

Le serveur nouvellement crée contient 2 fichiers d’exemple qui nous intéressent : Makefile.sample et Dockerfile.sample.

Créez un répertoire dédié :

mkdir myimage
copiez les fichiers d’exemple :

cp Makefile.sample myimage/Makefile

puis

cp Dockerfile.sample myimage/Dockerfile

et rentrez dans le répertoire:

cd myimage

Makefile

Ce fichier permet de définir :

  • le nom de l’image à construire
  • la version
  • le titre
  • la description
  • l’architecture
  • la taille du volume (espace disque) associé
  • le nom du script à exécuter lors du démarrage du noyau linux

Modifiez le fichier Makefile avec le descriptif, le nom et la version voulue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NAME = my-image
VERSION = latest
VERSION_ALIASES = 1.2.3 1.2 1
TITLE = My image
DESCRIPTION = My image with Ubuntu and MySQL
DOC_URL =
SOURCE_URL = https://github.com/scaleway-community/...
VENDOR_URL =
DEFAULT_IMAGE_ARCH = x86_64
IMAGE_VOLUME_SIZE = 50G
IMAGE_BOOTSCRIPT = stable
IMAGE_NAME = My image
## Image tools (https://github.com/scaleway/image-tools)
all: docker-rules.mk
docker-rules.mk:
wget -qO - https://j.mp/scw-builder | bash
-include docker-rules.mk

Dockerfile

Ce fichier permet de décrire les étapes d’installation de votre serveur, comme un fichier Dockerfile classique.
Néanmoins, des commentaires sont présents dans ce fichier, qui sont indispensables pour créer l’image sur plusieurs architectures : n’y touchez pas.

contenu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
FROM scaleway/ubuntu:amd64-16.10
# following 'FROM' lines are used dynamically thanks do the image-builder
# which dynamically update the Dockerfile if needed.
#FROM scaleway/ubuntu:armhf-16.10 # arch=armv7l
#FROM scaleway/ubuntu:arm64-16.10 # arch=arm64
#FROM scaleway/ubuntu:i386-16.10 # arch=i386
#FROM scaleway/ubuntu:mips-16.10 # arch=mips
# Prepare rootfs
RUN /usr/local/sbin/scw-builder-enter
# Add your commands here (before scw-builder-leave and after scw-builder-enter)
RUN sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
RUN sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
RUN apt-get update && sudo apt-get install -y docker-ce
RUN mkdir -p /etc/systemd/system/docker.d
COPY docker.conf /etc/systemd/system/docker.service.d/override.conf
RUN systemctl enable docker
# Clean rootfs
RUN /usr/local/sbin/scw-builder-leave

base de l’image

Cette image dérive d’une image Ubuntu relativement récente (ici 16.10).

Un choix plus conservateur serait Ubuntu 16.04 LTS (LTS pour Long Term Support).

Notez qu’il est important de choisir une distribution ayant un noyau assez récent, car Docker s’appuie sur des fonctionnalités du noyau Linux qui se sont stabilisées que depuis peu, et évolue à la vitesse d’un cheval au galop.

Une autre alternative, qui va au bout de la démarche de “conteneurisation”, serait de choisir un système d’exploitation reposant uniquement sur des conteneurs tels rancherOS, CoreOS, Photon ou Atomic. Cela fera peut-être partie d’un prochain article. L’arrivée de LinuxKit, essayant d’être le dénominateur commun technique de ces initiatives, en est l’illustration.

description

Ce fichier Dockerfile comprend l’ajout des utilitaires nécessaires à l’ajout de la clé gpg de Docker, ainsi que le repository, puis l’installation du package docker-ce lui-même.

Le fichier docker.conf est ajouté aussi pour surcharger la configuration par défaut de docker dans systemd (en ne définissant aucune option), afin de permettre de définir par la suite (dans le troisième article), via le fichier /etc/docker/daemon.json (fichier dont l’usage est maintenant recommandé par Docker), les différentes options du démon docker, dont les ports.

Cette étape n’aurait pu être incluse dans l’image générée et donc dans ce Dockerfile, car nous incluons dans le fichier /etc/docker/daemon.json l’adresse IP privée du serveur crée, qui est dynamique.

docker.conf :

1
2
3
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

lancer la création de l’image

exécuter la commande suivante pour créer l’image :
make image_on_local

Après l’exécution de la commande précédente, Vous devriez voir votre nouvelle image (à la première ligne) dans la liste de celles disponibles.
via la commande :

scw images

1
2
3
4
5
6
7
REPOSITORY TAG IMAGE ID CREATED REGION ARCH
user/My_image latest 667077c2 7 minutes [ par1] [x86_64]
user/My_image_With_docker latest fd23ff8e 2 weeks [ par1] [x86_64]
user/My_image_With_docker latest ecaf8b3b 2 weeks [ par1] [x86_64]
Ubuntu_Yakkety latest d53fafe4 6 months [ams1 par1] [arm x86_64]
Mattermost latest 0644b229 9 months [ams1 par1] [x86_64]
Ubuntu_Xenial latest 656de689 12 months [ams1 par1] [arm x86_64]

Votre image apparait avec 8 caractères, c’est-à-dire un identifiant tronqué (ici 667077c2).

Pour connaître l’identifiant complet, qui sera utilisé lors de la création des serveurs, exécutez la commande suivante :

scw inspect 667077c2

Celle-ci retourne :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[{
"id": "667077c2-1d1b-465b-aff4-f22e58a61149",
"name": "My image",
"creation_date": "2017-04-22T22:39:56.884833+00:00",
"modification_date": "2017-04-22T22:39:56.884833+00:00",
"root_volume": {
"id": "d586f059-3c79-470a-9511-fffaad2a8406",
"size": 50000000000,
"name": "x86_64-my-image-latest-2017-04-22_22:34",
"volume_type": "l_ssd"
},
"default_bootscript": {
"bootcmdargs": "LINUX_COMMON ip=:::::eth0: boot=local",
"initrd": "http://169.254.42.24/initrd/initrd-Linux-x86_64-v3.12.4.gz",
"kernel": "http://169.254.42.24/kernel/x86_64-4.10.8-std-1/vmlinuz-4.10.8-std-1",
"architecture": "x86_64",
"id": "8fd15f37-c176-49a4-9e1d-10eb912942ea",
"organization": "11111111-1111-4111-8111-111111111111",
"title": "x86_64 4.10.8 std #1 (stable)",
"public": true
},
"organization": "00000000-0000-5000-9000-000000000000",
"arch": "x86_64"
}]

L’identifiant complet est donc 667077c2-1d1b-465b-aff4-f22e58a61149.

L’image est prête à être utilisée pour créer nos serveurs.

l’image n’est pas sécurisée

Veuillez noter que cette image n’est pas du tout sécurisée. En effet, la configuration SSH n’est pas durcie :

Voici une liste non exhaustive des manquements quant à sa sécurisaition :

  • la désactivation de l’authentification par mot de passe
  • le changement de port SSH
  • l’activation de la séparation des privilèges
  • la désactivation des algorithmes faibles
  • le ssh port knocking
  • etc …

Une liste complète pourra être consultée sur le site de l’anssi.

De plus, des mécanismes de sécurité ne sont pas installés (fail2ban par exemple).

Enfin, le fonctionnement des règles des groupes de sécurité réseau de Scaleway sont pour le moins particulières (ou j’ai loupé quelque chose) :

Même si ces règles contiennent les plages IP depuis lesquels accepter le traffic (ici des IP internes 10.0.0.0/8 bien que cela ne nous prémunisse pas des autres clients Scaleway), les ports sont quand mêmes accessibles via l’extérieur….

En bref, la mise en place dans l’image (ce qui n’est pas le cas ici), d’un pare-feu (ufw, ou directement netfilter par exemple), n’acceptant que des connexions des serveurs du cluster est nécessaire au delà des quelques minutes de test de cet article.

Je regrette de plus, que scaleway ne bénéficie pas du réseau privé disponible sur l’infrastructure de la maison mère online.net (appelé RPN).

Conclusion

La définition et la construction d’une image de serveur sont facilitées par les outils fournis par Scaleway, inspirés de ceux de Docker. Malheureusement, cette image ne sera pas utilisable directement chez d’autres hébergeurs. Le troisième et dernier article réutilisera l’image créée ici, pour créer des serveurs et y déployer le cluster Docker Swarm Mode.

Déploiement D'un Cluster Docker Swarm Mode via Terraform Sur Scaleway 1/3 : Intérêts

Contexte

Je vous propose dans cette série d’articles, d’installer un cluster docker Swarm Mode via Terraform chez l’hébergeur Scaleway.

Celle-ci comprend 3 articles :

Caractéristiques générales de la solution

Dans ce premier article, voyons les caractéristiques de cette solution (n’hésitez pas à passer directement au deuxième article si vous êtes allergiques au bla-bla) :
il s’agit d’une infrastructure auto-descriptive, flexible, immutable, à forte densité, résistante aux pannes.

une infrastructure auto-descriptive

Au lieu d’opérer des opérations d’installation et de configuration, de façon manuelle, et non documentée, il est indispensable de nos jours, d’utiliser des outils automatisés, reposant sur des informations textuelles, que l’on peut gérer via des systèmes de gestion de version (git par exemple).
La seule lecture de fichiers doit pouvoir décrire dans son intégralité la solution mise en place.

L’infrastructure déployée doit être automatisée via du code (bye-bye les clickodromes non reproductibles).
Cette tendance est appelée infrastructure as code en Anglais.

une infrastructure immutable

L’”immutabilité” (en vrai français l’immuabilité), est particulièrement à la mode en ce moment, tant au niveau de la programmation (Erlang, Scala …), que des infrastructures.

Concernant la fourniture de serveurs, dès qu’un changement doit être opéré, au lieu de modifier celui-ci, nous allons le détruire pour en recréer un autre, de façon automatisée. Cela évitera dans le temps, de ne plus maîtriser l’état du serveur ; en effet, l’installation, la mise à jour, la suppression, puis la réinstallation d’une nouvelle version d’un logiciel sur un serveur n’est souvent pas équivalent à l’installation directe de cette nouvelle version. L’état du serveur devient de moins en moins maîtrisable au cours du temps, il se dégrade.

Ainsi, des outils forts pratiques tels Ansible, Puppet ou Chef, pour ne citer que les plus connus, qui automatisent beaucoup de tâches, sont de moins en moins utilisés pour déployer des infrastructures (serveurs, DNS, réseaux etc..), car leur utilisation répétée sur ces mêmes ressources ne permet plus de maîtriser précisément leurs états au bout d’un certain temps. Néanmoins, ils restent présents concernant la finalisation quelquefois complexe des serveurs.

Cette évolution est traduite en anglais par l’expression “pets vs cattle”, c’est-a-dire la comparaison de serveurs à des animaux de compagnie (“pets”), auquel on porte une grande attention (pour faire le parallèle à la grande complexité actuelle pour maîtriser l’état de nos serveurs), à opposer au bétail (“cattle”), auquel on n’attache pas une grande importance (on n’hésite pas à recréer un serveur au lieu de le faire évoluer).

une infrastructure flexible

Concernant la flexibilité, l’appel d’APIs de fournisseurs d’infrastucture à la demande (IAAS ou infrastructure as a service) s’impose de plus en plus.
Les hébergeurs peuvent être internes aux grandes entreprises avec des déploiements de centre de données VMWare par exemple, ou externes avec des fournisseurs tels Amazon Web Services (AWS), Digital Océan, ou Scaleway dans cette série d’articles.
Cette flexibilité permet d’ajuster son infrastructure en temps réel au gré de ses besoins.

une infrastructure à haute densité

L’usage tendant à se généraliser des conteneurs, principalement Docker, permet de faciliter et standardiser l’installation de logiciels, et de densifier les serveurs.
En effet, l’installation de multiples conteneurs sur un même serveur, permet de rentabiliser au mieux l’usage des serveurs : de nombreux logiciels se côtoient sans effets de bords notables (mise à part une concurrence accrue sur l’usage des ressources serveurs).

infrastructure résistante aux pannes

Une infrastructure résistante au pannes sera distribuée et redondée sur plusieurs serveurs (voire plusieurs centre de données), afin de pallier à une défaillance de l’un d’entre eux.

et concrètement du côté de la technique ?

Docker Swarm Mode

L’usage de conteneurs Docker tend à se généraliser pour standardiser le déploiement d’applications.
Les différents conteneurs devant intéragir pour former des applications (serveur web, base de données etc…), différentes solutions d’orchestration existent.
J’ai choisi pour cet article une solution récente et simple :
docker swarm mode.

Elle n’est pas aussi mûre que Mesos, ni la plus complète (Kubernetes), mais est une des plus simples (avec Cattle un des orchestrateurs supportés par Rancher).

Terraform

Nous utiliserons Terraform, une solution de description et de construction d’infrastructure immutable. Celle-ci comprend de nombreux fournisseurs (providers), dont scaleway pour les serveurs, et cloudflare pour le DNS.

Scaleway

J’ai choisi l’hébergeur français Scaleway, filiale de Online, pour son coût modique, et la présence d’un “provider” Terraform.
Cette solution est encore assez jeune, et fournit un niveau très basique d’équipement à la demande, contrairement à sa maison mère (pas de réseau privé virtuel RPN ici par exemple).

Cloudflare

J’ai choisi d’utiliser cloudflare pour gérer le nom de domaine, car celui-ci fournit ce service gratuitement, et permet d’inclure une étoile (wildcard), comme sous-domaine, permettant que tous les sous-domaines soient redirigés vers le cluster Docker Swarm Mode.

Conclusion

Les différentes propriétés décrites dans ce premier article, permettent je l’espère de vous faire comprendre l’intérêt des outils mis en oeuvre.

Passons maintenant à la lecture du deuxième article pour passer à la mis en oeuvre.

Kafkacat : Lire Et Écrire Dans Kafka en Ligne De Commande Sans JVM

Cette série d’articles essaie de vous faire découvrir des outils bien utiles autour de Apache Kafka.

Le premier outil est kafkacat, un outil en ligne de commande qui permet facilement et rapidement de lire et d’écrire dans des topics kafka.

outils fournis par apache kafka

Le projet open source Apache Kafka propose une série d’outils pour intéragir avec cette plateforme de streaming distribuée ultra-performante :
ceux-ci font partie intégrante du projet.

Pour les utiliser, il est nécessaire de récupérer le projet sur github, et de lancer ces scripts qui lancent des machines virtuelles Java. Ces outils sont les plus aboutis et complets. Néanmoins, pour des besoins simples, il est pratique d’utiliser des interfaces en ligne de commande (CLI).

intérêt de kafkacat

Kafkacat est une interface en ligne de commande. Elle permet donc d’être chaînée, afin de filtrer les messages lus, rediriger le flux vers un fichier, ou vers d’autres outils Linux.

#installation

Des packages sont disponibles dans la plupart des distributions Linux.
Sous Ubuntu, il faut lancer :

sudo apt-get install kafkacat

un horizon des commandes kafkacat

afficher l’aide

Pour afficher l’aide, il suffit de lancer :

kafkacat -h

afficher la version

Pour afficher la version, il suffit de lancer :

kafkacat -V

structure d’une commande kafkacat

une commande kafkacat s’exécute toujours de la façon suivante :

kafacat -b host:port -mode -modeoptions

Il est indispensable de spécifier évidemment un ou des brokers kafka auxquels se connecter, via l’attribut -b.

Celui-ci doit être complété par le mode avec les valeurs suivantes :

  • -L pour lire les méta-données du cluster kafka (topics, nombre de partitions etc..)
  • -C pour lire les messages
  • -P pour produire les messages

options générales

les options suivantes sont présentes dans tous les modes :

  • -b pour spécifier l’hôte et le port des brokers kafka de la forme host1:port1;host2:port2,host3:port3
  • -t pour spécifier le topic
  • -c pour limiter le nombre de message produits ou lus
  • -p pour spécifier la partition
  • -G pour spécifier le groupe de consommateurs
  • -X pour spécifier des options à la librairie libdrdkafka C sous-jacente à kafkacat
  • -K pour spécifier le délimiteur de la clé (optionnelle) d’un message
  • -D pour spécifier le délimiteur de la valeur d’un message
  • -q pour que kafkacat soit silencieux
  • -d pour permettre le débugging via la librairie libdrdkafka

lire les métadonnées du cluster

Lister les topics présent dans un broker se fera via la commande suivante :

kafkacat -b 127.0.0.1:9092 -L

et donnera ce type de résultat :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Metadata for all topics (from broker 1: 127.0.0.1:9092/1):
1 brokers:
broker 1 at 127.0.0.1:9092
3 topics:
topic "Topic2" with 1 partitions:
partition 0, leader 1, replicas: 1, isrs: 1
topic "test-compact" with 1 partitions:
partition 0, leader 1, replicas: 1, isrs: 1
topic "Topic1" with 1 partitions:
partition 0, leader 1, replicas: 1, isrs: 1

Indiquant :

  • la liste des brokers avec leur identifiant
  • la liste des topics
  • pour chaque topic, les partitions
  • pour chaque partition le broker hébergeant la partition leader, les brokers hébergeant les réplicas, et le nombre de réplicas synchronisés (isrs)

lire des messages

Kafka stocke ses messages dans des topics, qui contiennent une ou plusieurs partitions.
Les message sont toujours lus du plus ancien au plus récent, et ont un ordre garanti au sein d’une même partition.

kafkacat -b mybroker:9092 -C -t mytopic

Cette commande permet de lister les messages présents dans le topic mytopic, mais ne se terminera pas : kafkacat restera en écoute de nouveaux messages.

lire des messages et quitter

Afin de terminer la commande après avoir lu les messages, il faut ajouter l’option -e -pour exit) :
kafkacat -b mybroker:9092 -C -t mytopic -e

lire des messages depuis un offset donné

Par défaut, kafkacat lit les messages du topic depuis les offsets les plus récents de chaque partition. Néanmoins, on peut spécifier à partir de quel offset on souhaite lire le topic via l’argument -o.
Les valeurs possibles sont :

  • beginning
  • end
  • stored
  • une valeur positive
  • une valeur négative

beginning

kafkacat -b host:port -C -t mytopic -o beginning -e

lira les messages du topic depuis le plus ancien message encore stocké pour chaque partition (le message peut disparaître plus ou moins rapidement en fonction des règles de suppression configurées dans kafka).

end

kafkacat -b host:port -C -t mytopic -o end

lira les futurs messages à partir du dernier message stocké pour chaque partition au moment de l’interrogation de kafka.

Y coupler -e n’aura pas trop de sens, car la commande sortira avant d’avoir pu récupérer un nouveau message.

stored

A la place de spécifier en option l’offset à partir duquel lire les messages, kafkacat peut lire l’offset de début de lecture pour chaque partition via une autre source de données :

  • de façon locale, via un répertoire :

kafkacat -b mybroker.example.org -C -t mytopic -e -o stored -c 1000 -X offset.store.method=file -X topic.auto.commit.interval.ms=250 -X topic.offset.store.path=/tmp/kafka-offsets

Permettra de lire les messages de ce topic depuis les offsets désignés pour chaque partition. Si le répertoire est vide, kafkacat créera les fichiers nécessaires pour stocker ces offsets (de la forme topicname-partition*.offset). Le dernier offset lu sera sauvegardé toutes les 250 millisecondes.

  • de façon distribuée dans kafka lui-même :
    kafkacat -b mybroker.example.org -C -t mytopic -e -o stored -c 1000 -X offset.store.method=broker -X topic.auto.commit.interval.ms=250 -X group.id=mygroup

L’offset est stocké dans un topic spécial, utilisé par kafka pour connaître les derniers offsets de chaque partition de chaque topic, pour chaque groupe de consommateurs.
Il est donc nécessaire de passer à kafka l’identifiant du groupe de consommateurs pour qu’il puisse identifier quel dernier message vous avez lu.

valeur positive

kafkacat -b host:port -C -t mytopic -o 31559 -e

lira les messages depuis l’offset 31559 de toutes les partitions jusqu’à la fin du topic au moment de l’interrogation de kafka.

valeur négative

Il est possible de lire (approximativement), par exemple les 1500 derniers messages d’un topic (sur l’ensemble des partitions), en lançant la commande suivante :

kafkacat -b host:port -C -t mytopic -o -1500 -e

lire des messages sur une partition donnée

Il est possible de lire sur une partition donnée via -p, avec la commande suivante :

kafkacat -b host:port -C -t mytopic -o -1500 -p 4 -e

Ne seront retournés que les messages du topic stockés dans la partition numéro 4.

lire des messages avec un groupe de consommateurs donné

Il est possible de lire des messages en tant que membre d’un groupe de consommateurs avec la commande suivante :

kafkacat -b host:port -C -t mytopic -G mygroup -e

Kafka nous assignera des partitions du topic à lire (toutes les partitions si nous sommes le seul membre actif du groupe), et nous délivrera les messages à partir des derniers offsets lus pour chaque partition.

lire des messages avec un format de sortie personnalisé

Les options de formattage sont :

  • %t pour le topic
  • %p pour la partition
  • %o pour l’offset
  • %k pour la clé
  • %S pour la taille du message
  • %s pour le cotnenu du message

kafkacat -b broker:port -t mytopic -e -f 'Topic %t[%p], offset: %o, key: %k, payload: %S bytes: %s\n'

lire des messages avec une enveloppe JSON en sortie

L’option -J permet d’avoir une enveloppe JSON encadrant la clé, et le message.

kafkacat -b host:port -C -t mytopic -e -J

écrire des messages

écrire des messages depuis la sortie standard

Pour écrire un message depuis la sortie standard :

kafkacat -b 127.0.0.1:9092 -t Topic2 -P -c 1 -e

écrire son message et appuyez sur la touche entrée pour terminer le message.

écrire des messages depuis un fichier contenant plusieurs messages

Il est possible de produire des messages depuis un fichier les contenant délimités par des caractères séparateurs, via l’argument -l.

kafkacat -b 127.0.0.1:9092 -t Topic2 -P -e -l file1

Cette option n’est possible que pour un fichier.

écrire des messages depuis des fichiers

Il est possible de produire des messages depuis plusieurs fichiers, chacun contenant un seul message.
Même si ces fichiers contiennent des séparateurs, le contenu de chaque fichier sera considéré comme un seul message.

kafkacat -b 127.0.0.1:9092 -t Topic2 -P -e file1 file2 file3

écrire des messages depuis des fichiers via des séparateurs personnalisés

Il est possible de définir les séparateurs de messages via l’option -D, et les séparateurs des clés des messages via l’option -K :

kafkacat -b 127.0.0.1:9092 -t Topic2 -P -e -D% -K/ file1 file2 file3

Un fichier pourra avoir la structure suivante :

7892/monmessage%7893/deuxiememessage%7894/troisiememessage

compatibilité des versions de kafka avec kafkacat

Comme évoqué auparavant, kafkacat repose sur la librairie C libdrdkafka.

Certaines versions de cette librairie ont pour comportement par défaut d’intéragir avec le protocole de kafka dans une certaine version;
ce comportement par défaut ne correspond pas toujours à la version de kafka que vous utilisez. Il faut donc spécifier via des paramètres libdrdkafka (option -X)le protocole à utiliser. Ainsi, quand on lit un kafka 0.10.1.0 avec kafkacat 1.3.0-1 on peut obtenir l’erreursuivante :

1
2
3
4
5
%4|1480463638.946|PROTOERR|rdkafka#consumer-1| 127.0.0.1:9092/9092: Protocol parse failure at rd_kafka_fetch_reply_handle:3864 (incorrect broker.version.fallback?)
%4|1480463638.946|PROTOERR|rdkafka#consumer-1| 127.0.0.1:9092/9092: expected 10370775 bytes > 44 remaining bytes
%4|1480463639.551|PROTOERR|rdkafka#consumer-1| 127.0.0.1:9092/9092: Protocol parse failure at rd_kafka_fetch_reply_handle:3864 (incorrect broker.version.fallback?)
%4|1480463639.551|PROTOERR|rdkafka#consumer-1| 127.0.0.1:9092/9092: expected 10370775 bytes > 44 remaining bytes
%4|1480463640.156|PROTOERR|rdkafka#consumer-1| 127.0.0.1:9092/9092: Protocol parse failure at rd_kafka_fetch_reply_handle:3864 (incorrect broker.version.fallback?)

Le paramétrage suivant permettra de résoudre ce problème :

kafkacat -b mybroker:9092 -C -t mytopic -e -X api.version.request=true

Les compatibilités des versions et les options à activer en fonction sont listées dans la page suivante :

page de compatibilité

limites de kafkacat

Kafkacat ne permet pas toutes les intéractions permises par les outils intégrés dans le projet Kafka.

La principale cause est l’absence de toutes les fonctionnalités de ces outils dans le protocole d’interaction avec Kafka.
De nombreuses avancées arrivent de version en version, mais des manques persistent.

création et suppression de topics

Kafka 0.10.1.0 intègre dans son protocole la création et la suppression de topics.
Kafkacat 1.3.0 n’intègre pas cette fonctionnalité qui est prévue pour la version 1.4.0.

recherche par timestamp

Cette fonctionnalité est aussi apparue dans la version 0.10.1.0 de kafka. Elle n’est pas encore supportée par kafkacat.

support de la compression lz4

La compression lz4, ajoutée en complément des compressions gzip et snappy, a été implémentée dans kafkacat mais n’est pas encore présent dans le paquet distribué par ubuntu.

Il conviendra donc de compiler depuis les sources kafkacat, avec une version plus récente de la librairie libdrdkafka pour supporter ce nouvel algorithme.

conclusion

Kafkacat me semble être un bon outil en ligne de commande, permettant d’interagir de façon performante avec kafka, et de s’intégrer aux autres lignes de commandes Linux via le chaînage des commandes.

Néanmoins, il s’avère dans certains cas indispensable d’avoir une version récente de kafkacat, ou de passer par les outils fournis avec kafka.

Webappender : Personnaliser Les Logs Affichées Dans Son Navigateur

Cette série de deux articles décrit le fonctionnement de la librairie de visualisation de logs webappender.

Le premier article expose sa mise en place, et les différentes façons de visualiser les logs suivant son navigateur.

Le deuxième article décrit la façon de choisir le contenu et la taille des logs à afficher.

Personnaliser le contenu des logs

La librairie webappender, permet de personnaliser le contenu de chaque log. par défaut, toutes les informations disponibles via logback sont incluses.

Néanmoins, certaines informations peuvent être longues à récupérer. Webappender permet donc de ne pas aller chercher et inclure ces informations, si le header HTTP X-wa-verbose-logs=false est transmis.

Ainsi, seront exclues de chaque log, les informations suivantes :

  • le numéro de ligne
  • le chemin du fichier hébergeant la classe java
  • le temps
  • le temps relatif
  • le nom du thread
  • la classe appelante
  • la méthode appelante
  • le Mapped Diagnostic context (MDC)
  • le ThrowableProxy (permettant de récupérer les informations d’une exception)
  • le nom du contexte
  • les données du code appelant (CallerData), c’est-à-dire la stackTrace
  • le marqueur

Avec cette configuration, le temps d’exécution des requêtes est donc moins impactés lorsque le webappender est activé.

Filtrer les logs

Webappender, supporte les filtres logback, pour réduire les traces applicatives (logs) transmises à votre navigateur.

Veuillez noter que ces filtres ne sont appliqués que dans le contexte de webappender, c’est-à-dire dans les informations transmises dans la réponse HTTP.

Ce filtrage ne s’applique, comme toujours, qu’aux traces liées aux requêtes faites par votre navigateur.
Les traces ne sont pas filtrées dès leur création, comme pourrait le faire les filtres turbo.

Webappender : Pour Visualiser Les Logs Logback Dans Son Navigateur

Cette série de deux articles décrit le fonctionnement de la librairie de visualisation de logs webappender.

Le premier article expose sa mise en place, et les différentes façons de visualiser les logs suivant son navigateur.

Le deuxième article décrit la façon de choisir le contenu et la taille des logs à afficher.

Le problème

Pour des applications web, il est courant de d’effectuer la recette des fonctionnalités sur des serveurs distants.

Quand un comportement non attendu survient, il est nécessaire de retracer le fonctionnement précis de l’application, notamment via l’inspection des traces applicatives aussi appelées logs.

Le serveur étant distant, il est souvent nécessaire :

  • de se connecter en ssh sur le bon serveur
  • d’identifier le répertoire hébergeant les logs
  • d’identifier le bon fichier de logs
  • de démêler les logs correspondants à son test, des autres logs

Ces opérations ne sont pas instantanées et rébarbatives.

Webappender

Afin d’éviter toutes ses étapes, j’ai créer le webappender, afin que les traces serveur issues de vos requêtes HTTP arrivent directement dans votre navigateur.

Ainsi, les autres logs des requêtes exécutées de façon concurrente sur le serveur, ne sont pas envoyées vers votre navigateur. Le développeur regarde donc uniquement les informations qui sont liées à son test.

Mise en place dans votre application web JEE

Pré-requis

Webappender est compatible avec les applications JEE utilisant la librairie de logs logback.
L’adhérence entre webappender et JEE est minime. Une compatibilité plus large est donc possible.

Installation en 2 étapes

Maven

Ajouter à votre fichier Maven pom.xml cette dépendance :

1
2
3
4
5
<dependency>
<groupId>com.clescot</groupId>
<artifactId>webappender</artifactId>
<version>1.4</version>
</dependency>

Notez qu’un filtre de servlet, installé par annotation, est fourni avec la librairie. Son urlPatterns correspond à toutes les requêtes (/*).

Activer le webappender

Par défaut, webappender est désactivé .

Permettre de visualiser les logs de chacun (par une interception du réseau), peut être très risqué et dangereux dans des environnements de production.

Donc, pour prévenir toute erreur de configuration, nous désactivons par défaut le webappender.

Pour l’activer, vous devez mettre ce paramètre sur la ligne de commande qui lance votre serveur d’applications :
-Dwebappender=true.

Visualisation des logs

La librairie webappender permet de visualiser les logs de sa requête dans tous les navigateurs.
Les visualisations de logs dans les configurations suivantes, impliquent une installation de webappender dans votre application JEE utilisant logback, avec un serveur d’applications java actif.

Vos logs dans Chrome via ChromeLogger

Vous devez installer le plugin chrome intitulé ChromeLogger.

Malheureusement, Chrome logger ne transmet par défaut aucun header l’identifiant, contrairement à d’autres plugins.

Or, Webappender transmet les logs à votre navigateur, quand les requêtes contiennent un header spécial : X-ChromeLogger.

Pour réparer cela, vous devez installer une extension comme Modify Headers for Google Chrome.

Veuillez noter que cette extension envoie à chaque requête, que vous soyiez sur votre site web avec webappender ou non, les entêtes spécifiques que vous avez rajouté. La publication des entêtes spécifiques vers les serveurs est donc très large.

Une alternative à Modify Headers for Google Chrome, est l’extension ModHeader, qui permet par site de définir les headers à modifier ou rajouter.

header à ajouter pour activer chrome logger

Quand l’installation et la configuration de cette nouvelle extension est effective, appuyez sur la touche F12 de votre clavier, pour visualiser le panel console de Chrome. Cela vous montrera les logs engendrées par votre navigation sur la webapp.

logs affichées dans la console chrome logger

Voici le détail de ce qu’une log contient :

détail d'une logs affichée dans la console chrome logger

Vos logs dans Firefox via FireLogger

Vous devez installer le plugin firefox intitulé FireLogger.

Quand cela est fait, appuyez sur la touche F12, pour visualiser le panneau firebug ; vous devriez voir un nouvel onglet intitulé logger.
logs affichées dans la console firelogger

Il vous montrera vos logs, suivant leur niveau. Les logs sont transmis à votre navigateur, quand les requêtes contiennnent une entête spéciale construite par le plugin ‘FireLogger’ : X-FireLogger.

détail de log dans la console firelogger

Vos logs dans n’importe quel navigateur

Si vous ne pouvez ou ne voulez pas installer un plugin firefox ou chrome, ou si vous n’avez aucun de ces navigateurs, vous pouvez tout de même visualiser vos logs dans n’importe quel navigateur. Dans ce cas, vous devez configurer un body formatter.

Pour cela, il faut :

  • transmettre au serveur une entête spéciale : X-BodyLogger, via une extension comme modify headers (disponible sur chrome ou firefox), ou tout autre plugin compatible avec votre navigateur ayant la possibilité d’ajouter à façon une entête spéciale.

Pour Internet Explorer, une solution gratuite comme Fiddler permet de modifier les entêtes HTTP.

header HTTP activant un bodyformatter

  • installer au début de vos JSP, la declaration de la taglib, et à la fin de vos JSP, le tag webappender. Si vous utilisez une librairie de templating (comme sitemesh par example), une unique insertion dans un decorateur global aura un effet sur toutes vos JSP.
1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="debug" uri="https://github.com/clescot/webappender-tag"%>
<html>
<head>
<title>hello from test.jsp page</title>
</head>
<body>
test.jsp
</body>
</html>
<debug:webappender/>

Avantages de cette méthode :

  • Les logs sont présentes dans le corps de la requête et non l’entête, (2Gb semble être la limite pour le corps d’une requête HTTP) ; il n’y a donc pas de réel problème de taille de logs
  • cela fonctionne sur tous les navigateurs

Inconvénients de cette méthode :

  • cela modifie le corps de votre requête
  • il n’y a pas de visualisation soignée de ces logs contrairement aux plugins présentés précédemment
  • vous ne pouvez pas filtrer vos logs du côté navigateur

Test avec une webapp exemple

Vous pouvez tester l’application de démo, qui illustre les fonctionnalités de la librairie webappender.

Pré-requis du test

Installer la démo

Tapez ces commandes dans votre terminal :

1
2
3
git clone git@github.com:clescot/webappender.git
cd webappender/webappender-war-example
mvn org.apache.tomcat.maven:tomcat7-maven-plugin:2.2:run-war -Dwebappender=true

Aller à l’adresse suivante http://127.0.0.1:8080/webappender , et visualiser les logs en fonctions du navigateur choisi, et des entêtes transmises.

Conclusion

La librairie webappender, permet relativement simplement, de visualiser dans son navigateur, les logs générées par sa requête côté serveur.
Webappender peut être installée, sur des applications JEE utilisant logback.

Git Pre-Push : Un Hook Pour Empêcher La Propagation Des Bugs

Etat des lieux

Les applications développées comportent inévitablement des bugs.
L’erreur étant humaine, cet état de fait ne surprend plus grand monde.

Pour parer à cette situation, sont développés en parallèle toutes sortes de tests, dont les tests unitaires. Nécessaires mais non suffisants, ceux-ci sont généralement les plus rapides à s’exécuter, permettant d’avoir un feeback rapide.
Leur intérêt n’est plus à démontrer.

Il est navrant en 2014, que des bugs détectés en amont par des tests se propagent.

Une vérification par une chaîne d’intégration continue centralisée ?

Le premier réflexe pour régler ce problème, est d’utiliser les outils déjà en place. Un des outils qui s’est généralisé, est une chaîne d’intégration continue centralisée, type jenkins. Cela permet de détecter un bug quand l’exécution des tests unitaires du build échoue.

Cette solution permet d’éviter de propager des bugs en production, l’équipe ne livrant que si le traitement sur la chaîne d’intégration continue s’exécute avec succès.

Néanmoins, la vérification par la chaîne d’intégration centralisée ne prévient pas la propagation du bug aux autres membres de l’équipe qui travaillent sur la même branche”, car le code a été poussé sur le repository distant. Les autres développeurs et jenkins se synchronisent ensuite sur ce même repository déjà buggé.

Le feedack vers l’équipe est donc trop tardif !
Essayons de trouver une autre approche, permettant d’avoir un feedback beaucoup plus court.

Une vérification par une chaîne d’intégration continue décentralisée !

Le but de cette approche, est de contrôler au plus tôt le code, avant de le propager au sein de l’équipe. Ce contrôle doit s’effectuer avant tout partage.

Un hook sur push

J’utilise depuis quelques semaines, une solution chez un client permettant d’exécuter les tests unitaires via Maven (mvn clean test) lors de la commande git push.

Depuis la version 1.8.2 de git apparue en mars 2013, le déclenchement d’un hook est maintenant possible sur un push.
Un Hook (crochet en français ?), est un moyen d’exécuter un script personnalisé à certains moments (commit, push, merge, etc..). Il s’exécute avant l’action en question, et conditionne l’exécution de celle-ci.

Au passage, vous pouvez connaître votre version de git en exécutant la commande git --version.

Les hooks sont situés dans le sous-répertoire hooks du répertoire caché .git de votre repository. Des exemples de hook, finissant par sample sont déjà présents.

J’utilise le hook défini par itty bitty labs, en exécutant les tests unitaires via maven :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
CMD="mvn clean test" # Command that runs your tests
# Check if we actually have commits to push
commits=`git log @{u}..`
if [ -z "$commits" ]; then
exit 0
fi
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
$CMD
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "failed $CMD"
exit 1
fi
exit 0

Placez ce script intitulé pre-push dans le répertoire hooks.

Ayant des tests d’intégration qui peuvent être longs, seuls les tests unitaires sont exécutés à chaque push.
Ainsi, {“le Hook sur push permet d’éviter la propagation des bugs vers les autres membres de l’équipe, mais bloque le développement lors de son exécution”}.

Pour résumer, cette solution :

  • ne s’enclenche que sur un git push
  • est bloquante

Elle s’adapte bien à mon usage, car il peut m’arriver d’effectuer des commit sur une branche, en cassant des tests unitaires de façon transitoire lors de refactorings. Néanmoins, il existe des situations où il est préférable de désactiver un hook.

Git permet cela sur tous les hooks git via l’option no-verify :

1
git push origin mabranche --no-verify

Un hook sur commit avec un repository local caché

David Gageot, proposait déjà cette approche de chaîne d’intégration continue décentralisée sur son blog en 2009, mais via une autre solution.

Sa solution est à base de hook git sur un commit. Ce hook clone le repository local dans un répertoire caché , et exécute les tests avant de valider le commit. En cas de succès, il propage le code sur le repository local initial.

Le hook sur commit permet une détection des bugs très tôt, et ne perturbe pas la fluidité du développement.

Pour résumer, cette solution :

  • est basée sur un hook déclenché sur un commit
  • est non-bloquante

Ainsi, elle permet de ne pas attendre le résultat des tests pour continuer à développer.

La solution de David Gageot, permet un contrôle plus tôt par rapport au hook sur git push, avec pour contrepartie la nécessité d’effectuer des commits plus rigoureux.

Il ne faut donc pas sortir des clous lors de chaque commit, quitte à exceptionellement désactiver le hook via l’option no-verify citée plus haut (option valide sur tous les hooks).

Effectivement, les commits sous git étant fréquents, il pourrait être désagréable d’être bloqué en attendant le résultat de tests unitaires de nombreuses fois par jour. La solution du clone du repository local dans un répertoire caché est une solution élégante, car elle permet de ne pas être bloqué dans ses développements.

Conclusion

Si vous êtes très rigoureux, utilisez donc la solution proposée par David Gageot. Si vous n’êtes pas en situation de le faire sur votre projet, peut-être que l’approche hook sur git push répondra à votre besoin.
J’espère qu’un de ces hooks vous permettra d’éviter la propagation de bugs vers vos collègues.

Metrics, Pour Mesurer Efficacement Les Performances : Intégration Avec JDBC, Logback Et Jersey

Présentation de Metrics

Je vous propose dans cette série de 4 articles, de vous présenter la librairie Metrics,
initié par la société Yammer.
Celle-ci permet de fournir des métriques au niveau applicatif et JVM.

Ce quatrième article, présente l’intégration de Metrics avec les drivers JDBC, logback et jersey.

avec les drivers JDBC

La librairie JDBCMetrics intégre JDBC avec Metrics. Cela permet :

  • d’avoir une vision globale de la charge de la base de données issue de votre application
  • d’avoir une vision précise du nombre et des performances des requêtes SQL pour chaque requête HTTP

Pour rajouter ce module à votre application, il faut rajouter la dépendance suivante à votre fichier maven pom.xml:

1
2
3
4
5
<dependency>
<groupId>com.soulgalore</groupId>
<artifactId>jdbcmetrics</artifactId>
<version>1.1</version>
</dependency>

La vision globale de la charge de la base de données induite par l’application est possible via la configuration du driver JDBC, soit via un Datasource (la librairie jouant le rôle de proxy), soit via le DriverManager.

Au passage DriverManager est une classe dépréciée, ayant un comportement incohérent au niveau du chargement du driver. Préférez donc le Datasource.

La vision précise de la charge au niveau base de données par requête HTTP, est permise de façon optionnelle via l’installation d’un servlet filter :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
filter>
<filter-name>JDBCMetricsFilter</filter-name>
<filter-class>
com.soulgalore.jdbcmetrics.filter.JDBCMetricsFilter
</filter-class>
<init-param>
<param-name>use-headers</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>request-header-name</param-name>
<param-value>jdbcmetrics</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>JDBCMetricsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Pour avoir la mesure de la charge occasionnée par requête HTTP, il faut dans celle-ci mettre le header suivant :

jdbcmetrics=yes

La requête HTTP devra donc comporter cette entête supplémentaire, afin d’avoir une réponse incluant des entêtes spécifiques
à JDBC.

voici un exemple de requête HTTP ayant cette entête (via l’extension Firefox RESTClient), ainsi que les entêtes de la réponse :

Les Métriques JDBC sont bien sûr visualisables via les reporters (ici via JMX) :

L’application exemple de cette série d’articles, intègre Metrics et Guice, ainsi que JDBCMetrics.

Voici un exemple de module Guice, permettant d’installer le proxy JDBCMetrics entre le pool de connexions de la base et l’application :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.clescot.rest;
import com.google.inject.Binder;
import com.google.inject.Module;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCMetricsModule implements Module {
public static final String JDBC_H2_URL = "jdbc:h2:mem:test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
public static final String CREATE_TABLE_FOR_AUDIT = "create table ACTIVITY (ID INTEGER auto_increment,STARTTIME datetime, ENDTIME datetime, ACTIVITY_NAME VARCHAR(200),PRIMARY KEY (ID) )";
@Override
public void configure(Binder binder) {
org.apache.tomcat.jdbc.pool.DataSource h2DataSource = new org.apache.tomcat.jdbc.pool.DataSource();
h2DataSource.setUrl(JDBC_H2_URL);
h2DataSource.setUsername(USERNAME);
h2DataSource.setPassword(PASSWORD);
h2DataSource.setDriverClassName(org.h2.Driver.class.getName());
try(Connection connection = h2DataSource.getConnection()){
PreparedStatement preparedStatement = connection.prepareStatement(CREATE_TABLE_FOR_AUDIT);
preparedStatement.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
com.soulgalore.jdbcmetrics.DataSource metricsDataSourceProxy = new com.soulgalore.jdbcmetrics.DataSource(h2DataSource);
binder.bind(DataSource.class).toInstance(metricsDataSourceProxy);
}
}

avec Logback

Metrics fournit une librairie d’intégration avec logback, pour remonter des informations concernant la fréquence des évenements logués suivant le niveau de log.

Pour intégrer Metrics et logback, il faut rajouter la dépendance suivante dans votre fichier pom.xml :

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-logback</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>

Voici le code à utiliser pour lier Metrics à logback.

1
2
3
4
5
6
7
final LoggerContext factory = (LoggerContext) LoggerFactory.getILoggerFactory();
final Logger root = factory.getLogger(Logger.ROOT_LOGGER_NAME);
final InstrumentedAppender metrics = new InstrumentedAppender(registry);
metrics.setContext(root.getLoggerContext());
metrics.start();
root.addAppender(metrics);

Une application concrète de cette intégration pourrait être une surveillance d’évenements logués en erreur ou warning, afin de réagir rapidement quand ceux-ci surviennent avec une fréquence importante.

Voici comment installer une mesure concernant les logs ayant le niveau error dans logback :

1
registry.meter("ch.qos.logback.core.Appender.error");

A noter qu’une intégation avec log4J existe aussi.

avec Jersey

Pour intégrer les mesures de Metrics avec les services REST exposés via Jersey et Spring, il est nécessaire d’intégrer le module suivant à votre fichier pom.xml :

1
2
3
4
5
<dependency>
<groupId>com.yammer.metrics</groupId>
<artifactId>metrics-jersey</artifactId>
<version>3.0.1</version>
</dependency>

Sérialisation du registre Metrics en JSON via jackson

Le module maven metrics-json, permet de sérialiser facilement les mesures au format JSON, via des modules Jackson dédiés.

l’ajout de la dépendance suivante dans votre pom.xml permet de les utiliser :

1
2
3
4
5
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>3.0.1</version>
</dependency>

De plus, vous devez créer une ressource REST, qui va exposer la représentation JSON de votre registre Metrics comme dans l’exemple suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Path("/")
public class FooResource {
private static final ObjectMapper mapper = new ObjectMapper().registerModules(
new com.codahale.metrics.json.MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, false),new HealthCheckModule());
private MetricRegistry registry;
@Inject
public FooResource(MetricRegistry registry) {
this.registry = registry;
}
@Path("/metrics")
@GET
@Produces(MediaType.APPLICATION_JSON)
public String serializeMetricsRegistryInJSON() throws JsonProcessingException {
return mapper.writeValueAsString(registry);
}
}

Ainsi, cette ressource JAX-RS exposera sur l’url http://monhost:8080/metrics en GET une représentation JSON du registre Metrics.

Une exposition de ces métriques peut être utile, pour par exemple, une page de supervision habillant ces métriques avec du javascript.

conclusion

La librairie Metrics est très pratique. Son usage s’est largement répandu, ce qui se traduit par la présence de librairies tierces afin d’enrichir son usage. Les 4 articles de cette série vous ont permis j’espère, de vous familiariser avec cette librairie.
J’ai mis en place cette solution chez un de mes clients, en envoyant les informations de Metrics vers un serveur Graphite, pour une historisation pérenne, et un travail à postériori sur les métriques techniques ou fonctionnelles remontées.

Afin de distinguer les métriques des différents environnements (poste de développement, recette, pre-production, production…), remontées vers le même serveur, j’ai mis en place un ServletContextListener qui configure au démarrage de l’application le reporter Graphite en fonction de variables positionnées au lancement du serveur. Les métriques seront donc présentes dans graphite dans des arborescences séparées, via un préfixe différent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.clescot.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.concurrent.TimeUnit;
import com.yammer.metrics.reporting.GraphiteReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MetricsGraphiteContextListener implements ServletContextListener {
private static final String DEFAULT_GRAPHITE_HOST = "graphite";
private static final String DEFAULT_GRAPHITE_PORT = "2003";
private static final String DEFAULT_GRAPHITE_PREFIX = "default.graphite.prefix'";
private static final String DEFAULT_GRAPHITE_PERIOD = "1";
private static final Logger LOGGER = LoggerFactory.getLogger(MetricsGraphiteContextListener.class);
private static final TimeUnit DEFAULT_GRAPHITE_TIME_UNIT = TimeUnit.MINUTES;
private static final String GRAPHITE_HOST_SYSTEM_PROPERTY_KEY = "graphite.host";
private static final String GRAPHITE_PERIOD_SYSTEM_PROPERTY_KEY = "graphite.period";
private static final String GRAPHITE_TIME_UNIT_SYSTEM_PROPERTY_KEY = "graphite.time.unit";
private static final String GRAPHITE_PORT_SYSTEM_PROPERTY_KEY = "graphite.port";
private static final String GRAPHITE_PREFIX_SYSTEM_PROPERTY_KEY = "graphite.prefix";
private static final String JAVA_SYSTEM_PARAMETER_PREFIX = " '-D";
@Overridemetrics-example
public void contextInitialized(final ServletContextEvent sce) {
final String servletContextName = sce.getServletContext().getServletContextName();
String graphiteHost = System.getProperty(GRAPHITE_HOST_SYSTEM_PROPERTY_KEY, DEFAULT_GRAPHITE_HOST);
Long period = Long.parseLong(System.getProperty(GRAPHITE_PERIOD_SYSTEM_PROPERTY_KEY, DEFAULT_GRAPHITE_PERIOD));
TimeUnit timeUnit = TimeUnit
.valueOf(System.getProperty(GRAPHITE_TIME_UNIT_SYSTEM_PROPERTY_KEY, DEFAULT_GRAPHITE_TIME_UNIT.name()));
int graphitePort = Integer
.parseInt(System.getProperty(GRAPHITE_PORT_SYSTEM_PROPERTY_KEY, DEFAULT_GRAPHITE_PORT));
String graphitePrefix = System.getProperty(GRAPHITE_PREFIX_SYSTEM_PROPERTY_KEY, DEFAULT_GRAPHITE_PREFIX);
GraphiteReporter.enable(period, timeUnit, graphiteHost, graphitePort, graphitePrefix +"."+servletContextName);
LOGGER.info(
"graphite reporter enabled : period='{}', timeUnit='{}', graphite host='{}', graphite port='{}', metricsprefix='{}'",
period, timeUnit, graphiteHost, graphitePort, graphitePrefix +"."+servletContextName);
LOGGER.info("to customize graphite options listed above, put in the java command line some of these ones (without simple quotes):");
LOGGER.info(JAVA_SYSTEM_PARAMETER_PREFIX + GRAPHITE_PERIOD_SYSTEM_PROPERTY_KEY + "=yourCustomGraphitePeriod'");
LOGGER.info(
JAVA_SYSTEM_PARAMETER_PREFIX + GRAPHITE_TIME_UNIT_SYSTEM_PROPERTY_KEY + "=yourCustomGraphiteTimeUnit'");
LOGGER.info(JAVA_SYSTEM_PARAMETER_PREFIX + GRAPHITE_HOST_SYSTEM_PROPERTY_KEY + "=yourCustomGraphiteHost'");
LOGGER.info(JAVA_SYSTEM_PARAMETER_PREFIX +GRAPHITE_PORT_SYSTEM_PROPERTY_KEY+"=yourCustomGraphitePort'");
LOGGER.info(JAVA_SYSTEM_PARAMETER_PREFIX +GRAPHITE_PREFIX_SYSTEM_PROPERTY_KEY+"=yourCustomGraphitePrefix'");
}
@Override
public void contextDestroyed(final ServletContextEvent sce) {
}
}

Références

Metrics, Pour Mesurer Efficacement Les Performances : Intégration Avec Spring Et Guice

Présentation de Metrics

Je vous propose dans cette série de 4 articles, de vous présenter la librairie Metrics,
initié par la société Yammer.
Celle-ci permet de fournir des métriques au niveau applicatif et JVM.

Ce troisième article, présente l’intégration de Metrics avec les librairies d’injection de dépendances Spring et Guice.

Metrics avec Spring et Guice

Le deuxième article de cette série consacrée à Metrics, présentait une intégration “legacy” de Metrics dans une application JEE.

Néanmoins, Il existe des librairies qui intègrent facilement Metrics à Spring ou Guice.
L’avantage de ces librairies, est qu’elles permettent de simplifier l’usage de Metrics, via des annotations.
Les applications utilisant maintenant principalement les containers d’injection de dépendances, votre configuration ressemblera plutôt à un des exemples présentés ci-après.

Avec Spring

Une librairie tierce permet l’intégration facile de Metrics avec les applications utilisant Spring (cette librairie supporte, au moment où j’écris l’article, la version 3.x de Metrics, contrairement à la librairie intégrant Metrics et Guice).

Au choix, soit la configuration via un fichier XML, soit par des annotations permet d’intégrer les deux librairies. Cette configuration nous permettra d’annoter nos classes hébergeant les métriques.

Il faut rajouter la dépendance suivante à votre fichier maven pom.xml :

1
2
3
4
5
<dependency>
<groupId>com.ryantenney.metrics</groupId>
<artifactId>metrics-spring</artifactId>
<version>3.0.1</version>
</dependency>

la configuration Spring via xml

La configuration en xml, de l’intégration des deux librairies se fait comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:metrics="http://www.ryantenney.com/schema/metrics"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.ryantenney.com/schema/metrics
http://www.ryantenney.com/schema/metrics/metrics-3.0.xsd">
<!-- registry and reporters should be defined in only one context xml file -->
<metrics:metric-registry id="registry" name="springMetrics" />
<metrics:reporter type="console" metric-registry="registry" period="1m" />
<!-- annotation-driven must be included in all context files -->
<metrics:annotation-driven metric-registry="registry" />
<!-- beans -->
</beans>

Comme indiqué dans les commentaires du xml précédent (issu de la documentation du module d’intégration), la déclaration du registre et des reporters doit être réalisée dans un seul fichier xml, tandis que la balise liée aux annotations doit être présente dans chaque fichier xml spring.

la configuration Spring via des annotations

De façon alternative, l’intégration se réalise en javaConfig comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableMetrics
public class SpringConfiguringClass extends MetricsConfigurerAdapter {
@Override
public MetricRegistry getMetricRegistry() {
return SharedMetricRegistries.getOrCreate("springMetrics");
}
@Override
public void configureReporters(MetricRegistry metricRegistry) {
ConsoleReporter.forRegistry(registry)
.outputTo(output)
.build()
.start(1, TimeUnit.MINUTES);
}
}

Cette intégration permet de :

  • créer les métriques sur les méthodes des beans via les annotations @Timed, @Metered, @ExceptionMetered, and @Counted.
  • créer des jauges sur les membres des beans spring annotés avec @Gauge
  • injecter via spring les variables représentant les métriques via l’annotation @InjectMetric
  • insérer, dans le registre spécifique des healthchecks, toute classe étendant la classe HealthCheck
  • créer des reporters Metrics et les lier au cycle de vie de Spring

Une fois l’intégration de Metrics via Spring réalisée, il devient très aisé via des annotations de rajouter des métriques à notre application.

Exemple de classe annotée :

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClass {
@Timed
public void timedMethod(){
}
@Metered
public void meteredMethod() {
}
@Counted
public void countedMethod() {
}
}

Avec Guice

l’application web exemple liée à cet article, montre l’intégration de Metrics avec une application utilisant le framework d’injection de dépendances Guice.

L’application utilise Metrics 3.0.1, via une librairie metrics-guice qui n’a pas encore de version stable (à l’heure où j’écris cette article) supportant Metrics 3.x. J’ai donc forké le repository de metrics-guice pour juste upgrader la dépendence de Metrics vers la version 3.0.1. Un repository metrics-guice dépendant de Metrics 3.0.1 a été crée pour cette occasion.

Classiquement, l’intégration de Guice dans une webapp se réalise via l’installation d’un servletListener étendant GuiceServletContextListener.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.clescot.rest;
import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.servlets.HealthCheckServlet;
import com.codahale.metrics.servlets.MetricsServlet;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
import com.palominolabs.metrics.guice.InstrumentationModule;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.util.concurrent.TimeUnit;
public class GuiceContextListener extends GuiceServletContextListener {
private ServletContext servletContext;
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
servletContext = servletContextEvent.getServletContext();
super.contextInitialized(servletContextEvent);
}
@Override
protected Injector getInjector() {
Injector injector = Guice.createInjector(new HealthCheckModule(), new RESTModule(), new InstrumentationModule(),
new JDBCMetricsModule()
);
//register registries in ServletContext
MetricRegistry metricRegistry = injector.getInstance(MetricRegistry.class);
HealthCheckRegistry healthCheckRegistry = injector.getInstance(HealthCheckRegistry.class);
servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY, metricRegistry);
servletContext.setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, healthCheckRegistry);
//configure reporters
JmxReporter.forRegistry(metricRegistry).build().start();
ConsoleReporter.forRegistry(metricRegistry).build().start(10, TimeUnit.SECONDS);
return injector;
}
}

Un HealthCheckModule a été crée :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.clescot.rest;
import com.codahale.metrics.health.HealthCheck;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.servlet.ServletModule;
import com.palominolabs.metrics.guice.servlet.AdminServletModule;
public class HealthCheckModule extends ServletModule{
@Override
protected void configureServlets() {
install(new AdminServletModule());
Multibinder<HealthCheck> healthChecksBinder = Multibinder.newSetBinder(binder(), HealthCheck.class);
healthChecksBinder.addBinding().to(DatabaseHealthCheck.class);
}
}

Les deux autres modules (cf les sources de l’application exemple) correspondent à :

  • l’enregistrement des ressources REST dans Guice (RESTModule)
  • la configuration de la base de données et son monitoring (JDBCMetricsModule)
  • un module pour lier Metrics et Guice (InstrumentationModule).

Vous noterez l’intégration d’un JMXReporter, et d’un ConsoleReporter dans cette configuration, qui envoie dans la sortie standard toutes les 10 secondes (à des fins de test), un descriptif des métriques.

Les ressources REST créées et les servlets Metrics disponibles seront référencées dans le fichier web.xml (ou de façon alternative via des annotations pour des containers plus récents).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>com.clescot.rest.GuiceContextListener</listener-class>
</listener>
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>AdminServlet</servlet-name>
<servlet-class>com.codahale.metrics.servlets.AdminServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>healthcheck</servlet-name>
<servlet-class>com.codahale.metrics.servlets.HealthCheckServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>ping</servlet-name>
<servlet-class>com.codahale.metrics.servlets.PingServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>metrics</servlet-name>
<servlet-class>com.codahale.metrics.servlets.MetricsServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>threads</servlet-name>
<servlet-class>com.codahale.metrics.servlets.ThreadDumpServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>AdminServlet</servlet-name>
<url-pattern>/admin</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>healthcheck</servlet-name>
<url-pattern>/admin/healthcheck</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ping</servlet-name>
<url-pattern>/admin/ping</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>metrics</servlet-name>
<url-pattern>/admin/metrics</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>threads</servlet-name>
<url-pattern>/admin/threads</url-pattern>
</servlet-mapping>
</web-app>

Les exemples de code d’une application Guice ayant ces annotations sont présents dans le dernier article (les annotations sont portées par des ressources Jersey).

conclusion

L’intégration de la librairie Metrics avec Spring ou Guice se réalise sans réelles difficultés majeures. Le dernier article de cette série, porte sur l’intégration de librairies tierces, comme Jersey, les drivers JDBC ou logback.

Références

Metrics Pour Mesurer Efficacement Les Performances : Intégration Avec JEE

Présentation de Metrics

Je vous propose dans cette série de 4 articles, de vous présenter la librairie Metrics,
initié par la société Yammer.
Celle-ci permet de fournir des métriques au niveau applicatif et JVM.

Ce deuxième article, présente l’intégration de Metrics dans une application web JEE.
A noter que l’intégration proposée n’inclut pas, à des fins pédagogiques, une intégration facilitée via Spring ou Guice, comme c’est le cas dans le troisième article de la série.

Intégration dans une webapp

Un exemple d’application web monitoré avec Metrics est fourni en complément de cet article.

Enregistrement du registre Metrics dans le scope application

Lors du démarrage de l’application JEE, il est nécessaire d’enregistrer dans le scope application (i.e le servletContext), le registre Metrics, pour qu’il soit disponible pour toutes les classes.
Nous utiliserons pour cette tâche, un ContextListener, qui est justement exécuté au démarrage de l’application.

Pour faciliter le partage du registre, Metrics fournit un module de classes utilitaires pour les applications JEE, intitulé metrics-servlet.

Installez-donc celui-ci dans votre fichier maven pom.xml.

1
2
3
4
5
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-servlet</artifactId>
<version>3.0.1</version>
</dependency>

Une fois cette dépendance installée, vous pouvez étendre le InstrumentedFilterContextListener pour vous faciliter la tâche comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.clescot.rest;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.servlet.InstrumentedFilterContextListener;
public class MetricsListener extends InstrumentedFilterContextListener {
private final static MetricRegistry METRIC_REGISTRY = new MetricRegistry();
@Override
protected MetricRegistry getMetricRegistry() {
return METRIC_REGISTRY;
}
}

L’installation de la classe MetricsListener effectuée dans votre fichier web.xml comme suit, vous pouvez utiliser les différentes métriques listées dans le premier article de cette série (jauge, compteur, mesure, histogramme, timer etc…).

1
2
3
4
5
6
7
...
...
<listener>
<listener-class>com.clescot.rest.MetricsListener</listener-class>
</listener>
...
...

Veuillez noter que le module metrics-servlet, n’est pas à confondre avec metrics-servlets évoqué plus loin dans cet article.

Une alternative, présentée dans le troisième article de cette série, est l’initialisation et l’injection du registre via un container d’injection de dépendances comme Spring ou Guice (l’application web exemple utilise Guice).

Health check

Metrics permet aussi d’intégrer un système de “Health check”, afin de surveiller, via des appels du répartiteur de charge (load-balancer), les composants externes sur lesquels repose l’application, tels la base de données, ou le moteur de recherche par exemple.
Cela permet d’avoir une idée précise, de la fiabilité de ces composants, et donc de travailler sur la tolérance de l’application aux pannes de ceux-ci.

Cette intégration est réalisée par le module maven intitulé metrics-healthchecks.
Rajoutez donc dans votre fichier maven pom.xml, la dépendance suivante :

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-healthchecks</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>

Puis il faut étendre la class HealthCheck pour en créer un :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.clescot.rest;
import com.codahale.metrics.core.HealthCheck;
import javax.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DatabaseHealthCheck extends HealthCheck {
private static final String DATABASE_HEALTH_CHECK_NAME = "database";
private DataSource datasource;
@Inject
public DatabaseHealthCheck(DataSource datasource) {
super("com.clescot.rest.DatabaseHealthCheck");
this.datasource = datasource;
}
@Override
protected Result check() throws SQLException {
HealthCheck.Result result;
try (Connection connection = datasource.getConnection()) {
Statement statement;
ResultSet resultSet;
statement = connection.createStatement();
resultSet = statement.executeQuery("select 1 from dual");
if (resultSet.next()) {
result = Result.healthy("'select 1 from dual' : OK");
} else {
result = Result.unhealthy("la requête 'select 1 from dual' retourne un résultat vide : KO");
}
} catch (Throwable t) {
result = HealthCheck.Result.unhealthy("'select 1 from dual' : KO ", t);
}
return result;
}
}

La création et l’enregistrement dans le registre de Metrics du healthcheck peut se faire dans la classe MetricsListener présentée précédemment. Le datasource pourra être récupérée si besoin, via JNDI.

1
registry.register("database", new DatabaseHealthCheck(datasource));

Enfin, il faut enregistrer dans le fichier web.xml, la servlet appelant les différents healthChecks enregistrés dans le registre, c’est-à-dire la healthCheckServlet :

1
2
3
4
5
6
7
8
9
10
11
12
13
...
<servlet>
<servlet-name>healthcheck</servlet-name>
<servlet-class>com.codahale.metrics.servlets.HealthCheckServlet<</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
...
<servlet-mapping>
<servlet-name>healthcheck</servlet-name>
<url-pattern>/healthcheck</url-pattern>
</servlet-mapping>
...

Le load balancer appelera donc l’url http://monapplication:8080/healthcheck à intervalles réguliers sur chaque instance de webapp pour s’assurer du bon fonctionnement de tous les noeuds du cluster.

En cas de défaillance, l’instance en question sortira du pool d’instances utilisé pour répondre aux requêtes.

Exposition des mesures

Une fois les mesures posées, il faut les exposer, pour pouvoir les visualiser, et donc les analyser. Ce sont les reporters dans Metrics qui ont cette fonction d’exposition des métriques.

Via JMX

L’exposition des mesures Metrics, peut être réalisée via JMX (non conseillé par Metrics en production) par l’installation et l’initialisation d’un reporter spécifique de la façon suivante :

1
2
final JmxReporter reporter = JmxReporter.forRegistry(registry).build();
reporter.start();

Cette initialisation pourra aussi être effectuée dans le MetricsListener évoqué précédemment.

L’utilisation d’outils tels jconsole, ou visualVM, vous permettra de visualiser les mesures exposées sous forme de MBeans.

Voici un exemple de visualisation des métriques JMX via visualVM :
{% img [center] /images/visualvm.png [métriques JMX via visualVM[métriques JMX via visualVM]] %}

Via HTTP

Metrics fournit par défaut, pour les applications JEE, des servlets permettant de sérialiser en HTTP le contenu du registre Metrics.

Pour obtenir ces servlets prêtes à l’emploi, il faut importer le module maven dans votre pom.xml :

1
2
3
4
5
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-servlets</artifactId>
<version>3.0.1</version>
</dependency>

Le module dédie expose les servlets suivantes :

  • MetricsServlet: expose les mesures via un objet JSON

Voici un exemple du rendu de cette servlet :
{% img [center] /images/metrics-http.png [métriques JSON[métriques JSON via MetricsServlet]] %}

  • HealthCheckServlet : répond aux requêtes GET en exécutant tous les healthChecks enregistrés dans le registre. Cette classe est utile pour les loadBalancers, pour ne rediriger les requêtes des clients que vers les serveurs en bonne santé. Un code HTTP 200 est retourné en cas de succès, un code HTTP 500 est retourné dans le cas inverse.
  • ThreadDumpServlet : renvoie une représentation textuelle de tous les threads en cours sur la JVM, c’est-à-dire leur état, leur stack trace, les verrous présents etc… Cette servlet est utile à des fins de diagnostic.
  • AdminServlet agrège les services des servlets précédemment listées.

Les servlets listés sont certes très utiles pour exploiter une application ou aider au diagnostic d’un problème, mais elles ne sauraient être accessibles à tous.

Il est donc nécessaire, soit via votre serveur web (apache, nginx etc..), soit via votre reverse proxy, ou votre firewall, d’empêcher un accès direct depuis l’extérieur.

via d’autres canaux

d’autres reporters sont fournis par la librairie Metrics, pour exporter les métriques suivant différents canaux donc ceux-ci :

  • ConsoleReporter pour un export sur la sortie standard
  • Slf4jReporterpour un export via la façade de log SLF4J
  • GraphiteReporter pour stocker de façon scalable et pseudo temps-réel les métriques

Conclusion

L’intégration de Metrics dans une application JEE est aisée. Nous verrons dans le prochain article de la série, une intégration facilitée via Spring ou Guice.

Références