samedi 7 septembre 2013

Ansible: l'orchestration facile (2/2)


Cet article est la suite de l'article n°1 présentant Ansible. Celui-ci a pour but de créer notre premier playbook.

En route !

Mon premier Playbook

Objectifs


Maintenant que l'on dispose de notre structure de base, on va configurer la liste des tâches permettant d'installer Tomcat8 sur un Ubuntu 12.04 à savoir:

  1. Installer le paquet OpenJDK 7 (le paquet se nomme openjdk-7-jdk)
  2. Créer un dossier permettant le téléchargement de l'archive de tomcat8 (dans /home/vagrant/archives)
  3. Télécharger tomcat8 depuis le miroir suivant
  4. Créer le dossier /opt/apache-tomcat appartenant à l'utilisateur vagrant
  5. Décompresser notre archive
  6. Créer un lien symbolique de /opt/apache-tomcat/apache-tomcat-8.0.0-RC1 à /opt/apache-tomcat/tomcat8
  7. Modifier le fichier server.xml
  8. Redémarrer tomcat à chaque fois que le fichier catalina.properties est modifié

Résolution

Étape 1: installer un paquet

 

On va créer une tâche permettant l'installation d'un paquet sur notre Ubuntu dans le fichier roles/tomcat8/tasks/main.yml:

- name: install package openJDK 7
  apt: pkg=openjdk-7-jdk update_cache=yes
  sudo: yes

Toutes les tâches nécessitent un nom qui sera affiché lors de l'exécution d'Ansible.
La deuxième ligne précise le module a utiliser à savoir ici le module "apt", on lui passe deux paramètres pkg (qui indique le nom du paquet à installer) et update_cache (qui va déclencher un apt-get update avant l'installation). Mais il existe d'autres options.
Enfin la dernière ligne indique qu'il faut lancer cette commande via sudo.

Testons maintenant notre première tâche via la commande suivante qui va exécuter notre playbook site.yml sur l'ensemble des machines de notre fichier production:

$ ansible-playbook -i production site.yml

Et le résultat (ça peut prendre un peu de temps):
PLAY [application-server] ***************************************************** 
GATHERING FACTS ***************************************************************
ok: [127.0.0.1]
TASK: [install package openJDK 7] *********************************************
changed: [127.0.0.1]
PLAY RECAP ********************************************************************
127.0.0.1 : ok=2 changed=1 unreachable=0 failed=0
Si vous avez des vaches qui s'affichent vous pouvez soit désinstaller le paquer cowsay soit exécuter la commande suivante:
export ANSIBLE_NOCOWS=1

Étape 2: créer un dossier

 

Pour créer un dossier on ajoute juste une tâche à notre fichier main.yml en utilisant le module file:
- name: create archives folder
  file: path=/home/vagrant/archives state=directory
On teste à nouveau et voilà le résultat:
PLAY [application-server] ***************************************************** 
GATHERING FACTS ***************************************************************
ok: [127.0.0.1]
TASK: [install package openJDK 7] *********************************************
ok: [127.0.0.1]
TASK: [create archives folder] ************************************************
changed: [127.0.0.1]
PLAY RECAP ********************************************************************
127.0.0.1 : ok=3 changed=1 unreachable=0 failed=0
On peut voir ici qu'Ansible ne ré-installe pas le package openJDK (statut: ok) mais créé par contre bien notre dossier (statut: changed).

Pour les étapes suivantes je vais vous laisser travailler en ne vous donnant que le nom des modules à utiliser. Le fichier final main est indiqué à la fin de l'article.

Étape 3: télécharger une archive

 

Utiliser pour cela le module get_url.

Étape 4: créer un dossier

 

Voir l'étape 2 en ajoutant les options qui vont bien.

Étape 5: décompresser une archive

 

Utiliser pour cela le module command. Attention à bien utiliser l'option creates pour ne pas décompresser l'archive à chaque exécution d'Ansible.

Étape 6: créer un lien symbolique

 

Encore et toujours le module file avec les options dest et src ce coup-ci.

Étape 7: Modifier le fichier server.xml

 

Pour copier un fichier depuis notre dossier roles/tomcat8/files il suffit d'utiliser le module copy. Pour l'exemple le fichier local copié sur le serveur distant est le fichier server.xml classique auquel ont été enlevé les commentaires:

- name: Copy server.xml
  copy: dest=/opt/apache-tomcat/tomcat8/conf/server.xml owner=vagrant group=vagrant src=roles/tomcat8/files/server.xml

Étape 8: Redémarrer automatiquement Tomcat

 

L'étape précédente nous a permis de modifier facilement la configuration de nos serveurs Tomcat. Néanmoins il faut après une modification de la conf redémarrer le serveur.
Pour réaliser cela nous allons ajouter un handler, une action qui ne sera exécutée que sur notification d'une autre action. Ainsi si le fichier de conf n'est pas modifié Tomcat ne sera pas redémarré.
On ajoute une ligne à notre tâche de l'étape 7 pour notifier notre handler:

- name: Copy server.xml
  copy: dest=/opt/apache-tomcat/tomcat8/conf/server.xml owner=vagrant group=vagrant src=roles/tomcat8/files/server.xml
  notify: restart tomcat
Et on créé le fichier handler correspondant dans roles/tomcat8/handlers/main.yml:

- name: restart tomcat
  shell: /opt/apache-tomcat/tomcat8/bin/shutdown.sh && /opt/apache-tomcat/tomcat8/bin/startup.sh
Voilà normalement lors de la prochaine modification du fichier server.xml notre tomcat sera redémarré. Pour tester nous pouvons rajouter une ligne vide dans le fichier roles/tomcat8/files/server.xml et relancer ansible-playbook:

PLAY [application-server] ***************************************************** 
GATHERING FACTS ***************************************************************
ok: [127.0.0.1]
TASK: [install package openJDK 7] *********************************************
ok: [127.0.0.1]
TASK: [create archives folder] ************************************************
ok: [127.0.0.1]
TASK: [Download Tomcat 8] *****************************************************
ok: [127.0.0.1]
TASK: [Create tomcat directory] ***********************************************
ok: [127.0.0.1]
TASK: [Unzip tomcat] **********************************************************
skipping: [127.0.0.1]
TASK: [Create Tomcat symlink] *************************************************
ok: [127.0.0.1]
TASK: [Copy server.xml] *******************************************************
changed: [127.0.0.1]
NOTIFIED: [restart tomcat] ****************************************************
changed: [127.0.0.1]
PLAY RECAP ********************************************************************
127.0.0.1 : ok=8 changed=2 unreachable=0 failed=0

Fichier Final

 

Aller plus loin

Il est possible de faire encore plus de chose avec Ansible comme par exemple :

Bref aller jeter un oeil à la doc qui bien qu'un peu fouillis contient l'essentiel.


Post 14/52

vendredi 6 septembre 2013

Ansible : l'orchestration facile



L'une des choses qui m'intéresse énormément ces dernières années est le déploiement automatisé et industrialisé des plateformes en particulier via Puppet. Marre des scripts shell !

Présentation

Aujourd'hui j'ai découvert un outil nommé Ansible semblable à bien des aspects à Puppet mais avec quelques différences très intéressantes:

  • Aucun agent à installer sur les machines cibles. Ca permet de se lancer plus rapidement et plus facilement, on installe ansible et c'est parti !
  • Le déploiement et le passage de commande est réalisé via ssh ce qui permet de ne pas tout le temps être en root
  • Possibilité de lancer facilement des commandes shell sur tout ou partie de notre plateforme. Du genre un ping ou un apache status chose qui n'est pas facile via Puppet
  • Ecrit en python plutôt qu'en ruby (mais ça c'est personnel :)
  • Bien plus simple à prendre en main que Puppet

Après l'outil est encore un peu jeune et manque de certaines fonctionnalités dans la version open-source comme:

  • Avoir un dashboard pour consulter le résultat de déploiements réalisés de manière régulières (via cron par exemple). Actuellement on lance ansible il effectue ses actions et affiche le résultat sur la sortie standard. Pas de console centralisée
  • Logs d'exécution. Dans le cas où quelque chose se passe mal, le résultat affiché est très léger et oblige quasiment tout le temps à se logguer sur la machine pour voir de quoi il en retourne alors qu'avoir plus de logs et récupérés sur la machine exécutant ansible serait bien plus pratique quand même !
  • Déploiement sous Windows
Ces fonctionnalités semblent disponibles dans la version enterprise à 100$/machine/an.

Premiers pas

Assez parlé il est temps de jouer un peu avec Ansible !

Installation d'Ansible

L'installation est facile et prévue pour à peu près toutes les distributions Linux et même pour Mac sur la page getting started.

Pour ma part j'ai installé ça via un PPA:

On ajoute ça à nos sources:
deb http://ppa.launchpad.net/rquillo/ansible/ubuntu precise main 

deb-src http://ppa.launchpad.net/rquillo/ansible/ubuntu precise main

Et puis:
sudo apt-get install ansible

Et c'est bon !

Premiers tests

Pour faire nos tests nous allons initier une machine virtuelle via Vagrant:
vagrant init precise64

Et on lance notre vm:
vagrant up

Une fois la machine up nous allons faire un simple test de connexion à notre VM. Tout d'abord créons un dossier ansible à côté de notre Vagrantfile afin de travailler:
mkdir ansible && cd ansible

On créé ensuite le fichier hosts qui va contenir la liste des machines de notre plateforme qui ne contient qu'une machine:
127.0.0.1 ansible_ssh_port=2222 ansible_connection=paramiko

Le paramètre "ansible_ssh_port" permet de spécifier un port différent du port 22 par défaut pour se connecter via SSH. Le port 2222 est une redirection du port 22 de notre VM qui est ouvert automatiquement par Vagrant.

Le paramètre "ansible_connection" a été nécessaire pour moi afin de contourner un bug qui avait pour conséquence que Ansible restait bloqué après la connexion.

Pour info ce type de connexion est le type par défaut à partir d'Ansible 1.3.1 (j'ai fait mes tests avec la v1.2.2).


Et ensuite on lance le module "ping" (inclus dans Ansible) sur l'ensemble des machines de notre plateforme (donc juste en local):
ansible all -i hosts -m ping -u vagrant -k

Les options utilisées:
  • all: permet de préciser à quel groupe/machine appliquer la commande ou le module
  • -i : pour préciser le fichiers hosts à utiliser (par défaut /etc/ansible/hosts)
  • -m: le nom du module à exécuter
  • -u: l'utilisateur utilisé pour la connexion
  • -k: demander le mot de passe sur l'entrée standard
Et le résultat:
localhost | success >> {

    "changed": false, 

    "ping": "pong"

}

Ça marche ! On va maintenant exécuter une commande Unix quelconque, par exemple "uptime". Cela se fait via la commande suivante:
ansible all -i hosts -m shell -a "uptime" -u vagrant -k

Et le résultat:
localhost | success | rc=0 >>

 19:26:06 up 17 min,  2 users,  load average: 0.28, 0.33, 0.32


Tout fonctionne on va maintenant créer un module afin d'organiser un peu notre travail.

Structure d'un rôle

Les modules se nomment dans Ansible des playbook et représente en gros une liste de tâches au format YAML.

D'après la page Best practices nous allons créer quelques fichiers à la racine de notre dossier ansible:
  • Un fichier nommé "production" qui va contenir la liste de nos serveurs de production en l'occurrence uniquement les lignes suivantes:
[application-server]

127.0.0.1 ansible_ssh_port=2222 ansible_connection=paramiko

Nous allons également déclarer un playbook maître qui aura pour seule fonction d'inclure les autres playbooks afin de mieux séparer les choses:
  • site.yml:
- include: appservers.yml

Et notre fichier appservers.yml contenant les différents rôles de nos serveurs d'application:

- hosts: application-server
 
  roles:
  
    - tomcat8


Et maintenant la structure standard d'un rôle:

├── appservers.yml
├── production
├── roles
│   └── tomcat8
│   ├── files
│   ├── handlers
│   ├── tasks
│   └── templates
└── site.yml


Quelques explications:

  • Sous le dossier roles, un dossier pour chaque rôle (tomcat8 par exemple)
  • Ce dossier contient des sous-dossiers:
    • tasks: le fichier principal contenant la liste des tâches associées au rôle tomcat8
    • files: des fichiers destinés à être copié en tant que tels sur les serveurs cibles
    • handlers: les handlers utilisés par nos tâches (on verra un handler en détail un peu plus tard)
    • templates: les templates permettant de générer des fichiers instanciés en fonction des variables du serveur
C'est tout pour cette fois-ci dans le prochain article on réalisera un playbook permettant d'installer Tomcat 8 sur Ubuntu 12.04 !

Edit: l'article 2 vient d'être publié !


Post 13/52

dimanche 1 septembre 2013

[Java] Parallélisme et Immutabilité (1/2)



Ces derniers temps j'ai passé pas mal d'entretiens en Java et je me suis rendu compte qu'il s'agissait toujours des même questions qui revenaient.
Voici donc une petite série sur les questions les plus classiques et comment y répondre.
Pour débuter nous allons parler de parallélisme et d'immutabilité un sujet très classique dans les entretiens.

Souvent le sujet sera introduit de la manière suivante:

Vous voulez paralléliser votre code, comment faites-vous ?

Réponse simple et efficace: en utilisant des threads. Et à partir de là ça se gâte en général !

A quoi faut-il faire attention lors de l'utilisation des Threads ?

Le code utilisé avec des Threads doit être "Thread-Safe" afin d'éviter des incohérences dans le résultat du code.
En particulier lors de l'écriture et de la lecture de valeurs il arrive bien souvent que l'on tombe sur des incohérences telles que:
  1. La valeur lue n'est pas la dernière valeur écrite en mémoire. Cela est du au fait que les threads disposent de leur propre cache et ne le synchronise pas forcément avec la mémoire centrale où est stockée la valeur.
  2. La valeur écrite par un Thread est écrasée par un autre.
Voici un exemple de code pouvant éventuellement (cela dépend beaucoup de la machine, de la JVM et de l'OS) montrer le problème 1:


Résultat: Le script ne se termine jamais car le thread ne voit pas que le champ keepRunning vient de passer à false


Et un autre pour le cas 2:


Résultat:


On voit que le thread 1 se lance et fait ses 10 boucles sans aucun problème.
A la fin de son exécution count vaut 10 ce qui est normal.

Malheureusement le thread 2 ne prend pas la suite car il a déjà démarré il y a quelques temps a récupéré la valeur de count qui valait alors 0 mais n'a pu faire son incrément qu'à la fin de l'exécution du thread 1.

Contrairement à ce qu'on pourrait croire l'opération "count++" n'est pas atomique, il s'agit d'une lecture puis d'une écriture dans le champ count.

Dans le cas présent le thread 2 a pu lire mais pas écrire la valeur dans count. Il ne le fait qu'à la toute fin de la boucle de t1. En fait il écrit sa valeur juste avant que t1 n'écrive "10" dans count.
Cela explique pourquoi t2 continue avec 11 au lieu de 2 s'il avait écrit après la fin de t1.

Dans tous les cas il n'a pu incrémenter correctement count et cela se voit sur le résultat final avec un count valant 19 au lieu de 20 si tout s'était déroulé normalement.


Comment éviter le problème de rafraîchissement de cache d'un Thread ?

Dans le cas présenté ci-dessus notre paramètre "keepRunning" est un paramètre servant à contrôler la fin d'une boucle. Typiquement dans ce cas le plus simple est d'utiliser le mot clé "synchronized".

Pour faire simple ce mot clé permet d'indiquer à la JVM que le champ concerné sera accédé par différents threads et donc de stocker cette valeur dans la mémoire centrale et non dans le cache de chacun des threads.

Le cas d'utilisation typique est de mettre à jour un flag dans un thread et de le lire dans un autre:





Ce mot clé ne permet pas de résoudre le problème n°2 car il ne rend atomique que le get et le set et pas une combinaison des deux (ce qu'est le "count++").

Il n'est pas pertinent non plus d'utiliser le mot clé volatile dans certains cas:
  • Le champ est déclaré "final" (voir la seconde partie sur l'immutabilité)
  • Le champ n'est accédé que par un seul thread
  • Le champ doit être protégé pour des accès complexes (comme dans notre exemple n°2).

Comment éviter le problème d'écrasement d'une valeur par un autre thread ?


La façon la plus simple d'éviter le problème n°2 est d'utiliser le mot clé "synchronized" qui permet de limiter l'exécution d'un bloc de code à un seul thread à la fois:





Le problème de l'utilisation du mot clé synchronized est qu'il induit un lock bloquant les autres threads ce qui est mauvais pour les performances et la scalabilité car il constitue un véritable goulet d'étranglement limitant le passage à un seul Thread à la fois.


Une autre solution est de n'échanger entre les threads que des objets immutables qui évitent les locks et permettent d'obtenir des résultats corrects.

Mais ça on le verra dans la seconde partie de l'article qui arrive bientôt ! (... :)


Post 12/52