Contenu

Consul du pauvre via /etc/hosts pour infrastructure multi-machines sous Terraform

Contenu
Avertissement
Dernière modification de cet article le 2018-12-17, le contenu pourrait être dépassé/obsolète.

Si vous montez une infrastructure de plusieurs machines communiquant en réseau (micro-services, cluster de base de données, frontend et backend, etc.) il faut que chacune connaisse l’adresse IP de ses “collègues”.

Globalement, dans la vie de tous les jours, l’adresse IP a été “remplacée” par des FQDN (noms de domaine). Et même dans une infrastructure non publique et entièrement sous contrôle, l’adresse IP devient rapidement problématique pour différentes raisons :

  • Elle n’est pas toujours parlante : On peut faire une erreur à la saisie et ne pas le remarquer (“server-sql.myapp.net” est mieux que 10.3.45.9… Oups, c’était 10.3.85.9 en réalité)
  • Elle est relativement figée : Si une nouvelle machine vient à remplacer une précédente (ou si un composant change de machine), généralement la nouvelle ne peut pas récupérer l’adresse IP de l’ancienne et donc les composants de l’infrastructure qui utilisant l’ancienne adresse doivent être modifiés pour leur donner la nouvelle adresse IP.

A mon avis, toute infrastructure de plus de 3 composants devrait dépendre de FQDN.

Une solution simple est le fichier texte /etc/hosts (C:\Windows\System32\drivers\etc\hosts sous Windows) qui permet d’associer une adresse IP à un FQDN.

L’autre solution est d’utiliser un serveur DNS, qu’il soit public (celui de votre registraire/registrar ) ou interne (BIND, Dnsmasq, …) que les composants requêterons. Il existe des logiciels de Service Discovery tel que Apache ZooKeeper, Consul et etcd qui font exactement ça (mais en plus pratique).

Quand on débute un petit projet, on a pas forcément envie de faire appel à ce genre d’outil aussi pertinents soient-ils (pour des raisons de temps, de coût, de simplicité, etc.).

Si vous utilisez Terraform pour monter cette infrastructure, je vais vous montrer une petite astuce pour remplir le fichier /etc/hosts de tout vos composants de manière automatique.

L’idée est d’utiliser la liste des adresses IP privées que maintient Terraform, de la passer à une commande bash qui va l’écrire d’une façon précise dans le fichier /etc/hosts des instances.

Pour le fournisseur AWS de Terraform, c’est le tableau aws_instance.cluster_member.*.private_ip qu’on va utiliser. Mais il faut attendre que toutes les instances aient été crées côté AWS (que leurs adresses IP respectives aient été attribuées).

Une des règles/contraintes de mon fichier Terraform est que chaque membre du cluster se voit attribuer un nom composé du préfixe de la variable Terraform cluster_member_name_prefix suivi d’un numéro séquentiel (débutant à 0) : cluster-node-0, cluster-node-1, etc.

Il suffit d’adapter la variable Terraform cluster_member_count pour créer plus/moins d’instances.

Voici le fichier Terraform utilisé :

variable "cluster_member_count" {
  description = "Nombre de membre dans le cluster"
  default = "3"
}
variable "cluster_member_name_prefix" {
  description = "Préfixe pour nommer les membres du cluster."
  default = "cluster-node-"
}
variable "aws_keypair_privatekey_filepath" {
  description = "Chemin vers la clé SSH privée a utiliser pour se connecter en SSH aux instances."
  default = "./secrets/aws.key"
}

# EC2 instances
resource "aws_instance" "cluster_member" {
  count = "${var.cluster_member_count}"
  # ...
}

# Command shell qui provisionne le fichier /etc/hosts sur chaque instance
resource "null_resource" "provision_cluster_member_hosts_file" {
  count = "${var.cluster_member_count}"

  # Le moindre changement sur n'importe quelle instance déclenche l'exécution du provisioner
  triggers {
    cluster_instance_ids = "${join(",", aws_instance.cluster_member.*.id)}"
  }
  # Informations de connexion pour le provisioner
  connection {
    type = "ssh"
    host = "${element(aws_instance.cluster_member.*.public_ip, count.index)}"
    user = "ec2-user"
    private_key = "${file(var.aws_keypair_privatekey_filepath)}"
  }
  provisioner "remote-exec" {
    inline = [
      # Ajoute les adresses IPs de tous les membres du cluster dans le fichier /etc/hosts (sur chaque membre)
      "echo '${join("\n", formatlist("%v", aws_instance.cluster_member.*.private_ip))}' | awk 'BEGIN{ print \"\\n\\n# Cluster members:\" }; { print $0 \" ${var.cluster_member_name_prefix}\" NR-1 }' | sudo tee -a /etc/hosts > /dev/null",
    ]
  }
}

Cela va ajouter les lignes suivantes dans le fichier /etc/hosts de chaque ressource Terraform "aws_instance.cluster_member" (exactement les même lignes sur tous les membres, dans cet ordre précis) :

# Cluster members:
10.0.1.245 cluster-node-0
10.0.1.198 cluster-node-1
10.0.1.153 cluster-node-2

Dans mon cas, le null_resource qui remplit le fichier /etc/hosts était déclenché par l’attachement d’un volume EBS mais un déclencheur `"${join(",", aws_instance.cluster_member.*.id)}" devrait fonctionner tout aussi bien.

De plus, pour le développement local, j’ai ajouté un bloc local-execpour écrire les adresses IPs des membres locallement dans un fichiercluster_ips.txt` :

resource "null_resource" "write_resource_cluster_member_ip_addresses" {
  depends_on = ["aws_instance.cluster_member"]

  provisioner "local-exec" {
    command = "echo '${join("\n", formatlist("instance=%v ; private=%v ; public=%v", aws_instance.cluster_member.*.id, aws_instance.cluster_member.*.private_ip, aws_instance.cluster_member.*.public_ip))}' | awk '{print \"node=${var.cluster_member_name_prefix}\" NR-1 \" ; \" $0}' > \"${path.module}/cluster_ips.txt\""
    # Sortie exemple :
    # membre=cluster-node-0 ; instance=i-03b1f460318c2a1c3 ; private=10.0.1.245 ; public=35.180.50.32
    # membre=cluster-node-1 ; instance=i-05606bc6be9639604 ; private=10.0.1.198 ; public=35.180.118.126
    # membre=cluster-node-2 ; instance=i-0931cbf386b89ca4e ; private=10.0.1.153 ; public=35.180.50.98
  }
}

Et avec la commande shell suivante je peux ajouter les adresses IP privées à mon propre fichier /etc/hosts local :

1
awk -F'[;=]' '{ print $8 " " $2 " #" $4 }' cluster_ips.txt >> /etc/hosts

Exemple :

35.180.50.32 cluster-node-0 # i-03b1f460318c2a1c3
35.180.118.126 cluster-node-1 # i-05606bc6be9639604
35.180.50.98 cluster-node-2 # i-0931cbf386b89ca4e

En espérant que ça puisse servir à d’autre.