Note importante : Vous devez avoir lu et effectué les manipulations de la Partie 1 de Terraform avant de commencer cette section.

Attention : Cet article contient des éléments non-testables sur OVH, notamment les étapes 2, 4, 5 et 6.

Etape 1 - Créons un réseau

Dans OpenStack, un réseau est un circuit interne sur lequel nos instances vont pouvoir dialoguer entre elles. Hormis s'ils sont reliés à un routeur, un réseau OpenStack interne n'est pas accessible au reste du monde. De base, OpenStack dispose d'un réseau, qu'il n'est pas possible de supprimer : le réseau public. Lorsqu'une machine est connectée sur ce réseau, elle devient accessible sur Internet. L'objectif de créer des réseaux internes est de permettre à des machines de communiquer entre elles sans être exposées au monde entier, et de filtrer les connexions provenant de l'extérieur.

Assez parler, créons notre premier réseau interne. Créez un fichier network.tf (si vous avez lu la partie précédente, vous pouvez supprimer le fichier instances.tf pour ne pas claquer du crédit inutilement) ;

# Internal Network
resource "openstack_networking_network_v2" "first_network" {
  name           = "first_network"
  admin_state_up = "true"
}

# Network Subnet
resource "openstack_networking_subnet_v2" "first_network" {
  name            = "first_network-subnet"
  network_id      = "${openstack_networking_network_v2.first_network.id}"
  cidr            = "192.168.10.0/24"
  ip_version      = 4
}

Note OVH : Il faut en premier activer le Réseau Privé sur l'interface OVH Public Cloud : Network -> Private Network.

Ici, nous créons un réseau nommé "first_network", et nous lui allons une plage IP de fonctionnement (un subnet). En l'occurence, 192.168.10.0/24, ce qui signifie que le réseau aura pour adresse 192.168.10.0, son broadcast 192.168.10.255, le réseau disposera donc de 254 IP adressables, de 192.168.10.1 à 192.168.10.254. Par question de simplicité, nous gardons un réseau adressé en IPv4 comme vous pouvez le voir, il est cependant à noter qu'OpenStack supporte très bien l'IPv6. Lançons un terraform apply, et rendons-nous dans la partie Network -> Network Topology: Nous constatons que nous avons 2 réseaux, le réseau Public (ou Ext-Net), et le réseau first_network fraîchement créé! Il est désormais temps de faire quelque chose de ce réseau.

Etape 2 - Rattachons-le à un routeur !

Sur OpenStack, les routeurs dont des éléments primordiaux du réseau : Ils permettent de relier plusieurs réseaux virtuels entre eux, pour permettre aux machines de communiquer entre elles, même sur des réseaux différents. Par la même occasion, cela signifie qu'il est également possible de rendre des machines accessibles depuis Internet alors qu'elles ne sont pas directement sur le réseau public.

Ouvrons notre fichier network.tf, et ajoutons :

# Public Network
data "openstack_networking_network_v2" "public" {
  name = "public"
}

# Router on Public Network
resource "openstack_networking_router_v2" "global" {
  name                = "global"
  admin_state_up      = true
  external_network_id = "${data.openstack_networking_network_v2.public.id}"
}

Ici, nous effectuons plusieurs choses intéressantes.

  • Les trois premières lignes permettent de dire à Terraform qu'une ressource, nommée "public" et de type "openstack_networking_network_v2" existe déjà sur OpenStack, et qu'il doit la récupérer pour s'en servir. Cela nous permet de récupérer l'identifiant et les informations du réseau public créé par OpenStack par défaut.
  • Les lignes d'après permettent de créer un routeur, nommé "global", sur le réseau "public".

Un petit terraform apply, et on devrait voir le routeur apparaître sur Horizon, dans la partie "Network Topology". Il est donc temps de relier le réseau précédemment créé. Encore une fois, éditons network.tf, et ajoutons :

# Interface between Router and Subnet
resource "openstack_networking_router_interface_v2" "first_network" {
  router_id = "${openstack_networking_router_v2.global.id}"
  subnet_id = "${openstack_networking_subnet_v2.first_network.id}"
}

Encore un terraform apply, et, ce coup-ci nos réseaux seront reliés par le routeur! Cela signifie que n'importe quelle instance ayant une IP Flottante pourra être appellée depuis Internet, et que les machines branchées sur "public" et sur "first_network" peuvent communiquer entre elles. C'est pour cela que nous avons besoin d'un outil pour filtrer ces connexions... Et cet outil, c'est le principe des "groupes de sécurité".

Etape 3 - Sécurisons les connexions

Par défaut, l'ensemble des machines peuvent communiquer entre-elles sur le même réseau, sur tout les ports. Pour toute communication extérieure, il faut explicitement ouvrir les ports via les groupes de sécurité. Créons un fichier security_groups.tf :

# Security group HTTP & HTTPS
resource "openstack_networking_secgroup_v2" "www" {
  name        = "http"
  description = "allow HTTP/S"
}

# Allow port 80
resource "openstack_networking_secgroup_rule_v2" "http" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = "80"
  port_range_max    = "80"
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = "${openstack_networking_secgroup_v2.www.id}"
}

# Allow port 443
resource "openstack_networking_secgroup_rule_v2" "https" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = "443"
  port_range_max    = "443"
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = "${openstack_networking_secgroup_v2.www.id}"
}

Ici, nous allons créer un groupe de sécurité nommé "http" sur OpenStack, et ce dernier va avoir deux règles : Une première, qui va ouvrir le port 80 sur le réseau 0.0.0.0/0 (donc pour toute IP), et l'autre qui va ouvrir le port 443 de la même manière. On l'ouvre en ingress, ce qui signifie que la communication via ces ports peuvent s'effectuer dans les deux sens, et en IPv4 uniquement.

Maintenant, il faut ajouter les groupes de sécurités aux instances que vous voulez autoriser. Passons donc aux instances!

Etape 4 - Attachons une instance

Reprenons notre instance de l'article précédent, et éditons le fichier instances.tf :

# Joli groupe
resource "openstack_compute_instance_v2" "hello_world" {
  name            = "hello${count.index + 1}"
  image_name      = "Centos 7"
  flavor_name     = "b2-7"
  security_groups = ["${openstack_networking_secgroup_v2.www.name}", "default"]

  user_data = "${file("conf/data.yml")}"

  network {
    uuid = "${openstack_networking_network_v2.first_network.id}"
  }

  count = "2"
}

Ici, nous créons deux instances hello (une fois de plus), et nous l'attachons au réseau first_network que nous avons précédemment créé. Nous leurs faisons faire la configuration de base comme dans l'article précédent (vous trouverez le contenu du fichier dans l'article précédent sur OpenStack). Ainsi, nous allons pouvoir vérifier que les deux machines communiquent bien entre elles. Une fois les deux machines connectées, il suffit d'essayer de se connecter sur le serveur Nginx de l'autre serveur. Si la page est récupérée, c'est un succès! (vous pouvez notamment le faire via curl ou wget).

Cependant, il est encore impossible d'afficher la page Web depuis chez nous :(. Ajoutons donc une IP Flottante a notre instance. Editons le fichier instances.tf :

# Joli groupe
resource "openstack_compute_instance_v2" "hello_world" {
  name            = "hello"
  image_name      = "Centos 7"
  flavor_name     = "b2-7"
  security_groups = ["${openstack_networking_secgroup_v2.www.name}", "default"]

  user_data = "${file("conf/data.yml")}"

  network {
    uuid = "${data.openstack_networking_network_v2.global.id}"
  }

}

# Floating IP 
resource "openstack_networking_floatingip_v2" "publication" {
  pool = "public"
}

# Association Floating IP
resource "openstack_compute_floatingip_associate_v2" "publication" {
  floating_ip = "${openstack_networking_floatingip_v2.publication.address}"
  instance_id = "${openstack_compute_instance_v2.hello_world.id}"
}

Ici, nous allouns une IP Flottante sur le réseau public, et nous l'affectons à notre VM. Une fois le petit terraform apply usuel affecté, vous devriez pouvoir accéder depuis votre navigateur à votre serveur Nginx via l'IP Flottante affectée (que vous pourrez voir dans Horizon). Avec l'IP flottante, il est désormais possible d'y accèder directement depuis chez vous. Maintenant que nous avons réussis à interconnecter deux machines entre-elles, un autre soucis se pose : Lorsque nous créons les machines, les IP, qu'elles soient flottantes ou autres, ont une IP définie aléatoirement dans le pool d'IP dont OpenStack ou le sous-réseau dispose. Cela engendre un problème de taille : Il est impossible de connaître à l'avance les adresses sur lesquelles les machines seront accessibles. Pour le moment, cela ne pose pas de soucis, mais imaginez si vous souhaitez automatiser un déploiement de logiciel, avec Ansible ou Puppet : Il devient alors impossible de connaître à l'avance le pool d'adresses sur lesquelles se connecter! C'est pour cela que nous avons besoin du DNS.

Etape 5 - DNS Everywhere

Le DNS va nous permettre, comme nous venons de le dire, d'abstraire l'adresse IP, et donc de ne plus se soucier du changement de cette dernière. La première chose a faire est de créer une Zone DNS sur OpenStack, pour pouvoir ensuite y ajouter des enregistrements.

Créons donc un fichier zone_dns.tf :

# DNS Zone 
resource "openstack_dns_zone_v2" "base" {
  name        = "tuto.openstack.local."
  email       = "localhost@local.fr"
  description = "DNS zone"
  ttl         = 300
  type        = "PRIMARY"
}

Ici, nous avons créé une zone DNS, nommée "tuto.openstack.local." (le "." final est une convention dans les zones DNS). Cette zone à un TTL (Time To Live) de 300ms, et c'est un enregistrement de type primaire. Ensuite, nous pouvons créer des enregistrements DNS sur cette zone fraichement créée. Editons le fichier dns_recordsets.tf :

resource "openstack_dns_recordset_v2" "hello_dns" {
  zone_id     = "${openstack_dns_zone_v2.base.id}"
  name        = "${openstack_compute_instance_v2.hello_world.name}.${openstack_dns_zone_v2.base.name}"
  description = "Machine 1"
  ttl         = 30
  type        = "A"
  records     = ["${openstack_compute_instance_v2.hello_world.network.0.fixed_ip_v4}"]
}

Ici, nous créons un enregistrement sur la zone DNS précédemment créée. Cet enregistrement porte le nom de la machine précédemment créee + le nom de la zone (ici donc, hello.tuto.openstack.local.). On donne sa description, son TTL et son type (standard pour n'importe quel DNS), et nous lui fournissons l'IP à utiliser, à savoir l'adresse IPv4 de la machine. Il est à noter ici que nous parlons de l'IP de la machine sur le réseau, et non d'une IP flottante. Pour les IPs flottantes, on procédera comme suit :

resource "openstack_dns_recordset_v2" "hello_dns" {
  zone_id     = "${openstack_dns_zone_v2.base.id}"
  name        = "${openstack_compute_instance_v2.hello_world.name}.${openstack_dns_zone_v2.base.name}"
  description = "Machine 1"
  ttl         = 30
  type        = "A"
  records     = ["${openstack_networking_floatingip_v2.publication.address}"]
}

Désormais, votre machine sera accessible via une adresse compréhensible! Le dernier élément qu'il nous reste à aborder, mais pas des moindres, concerne le Load-Balancing.

Etape 6 - Load-Balancing

Le Load-Balancing est un système très fortement utilisé en informatique, lorsque nous avons des impératifs et des contraintes de disponibilité ou de support d'une charge importante. En effet, ce dispositif permet de répartir les requêtes arrivant sur une IP vers plusieurs machines, et ce, de manière transparente. C'est par exemple ce que fait Google quand vous allez sur leurs sites. Sur OpenStack, il faut activer le module Octavia. Basiquement, ce sont des instances configurées spécialement pour répartir la tâche. Allons-y, mettons en place un Load-Balancer. Commençons par créer 2 instances : Editons le fichier instances.tf :

resource "openstack_compute_instance_v2" "hello_world" {
  name            = "hello${count.index + 1}"
  image_name      = "Centos 7"
  flavor_name     = "b2-7"
  user_data = templatefile("./conf/data.yml", { id = count.index + 1})

  count = "2"
}

Créons ensuite le fichier loadbalancer.tf, et arrêtons nous sur son contenu :

# LoadBalancer
resource "openstack_lb_loadbalancer_v2" "cluster_test" {
  name          = "lb_test"
  vip_subnet_id = "${openstack_networking_subnet_v2.first_network.id}"
  depends_on    = ["openstack_compute_instance_v2.hello_world"]
}

# Pool
resource "openstack_lb_pool_v2" "cluster_test" {
  name            = "pool_test"
  protocol        = "TCP"
  lb_method       = "ROUND_ROBIN"
  loadbalancer_id = "${openstack_lb_loadbalancer_v2.cluster_test.id}"
}

# Monitor
resource "openstack_lb_monitor_v2" "cluster_test" {
  name        = "monitor_test"
  pool_id     = "${openstack_lb_pool_v2.cluster_test.id}"
  type        = "TCP"
  delay       = 20
  timeout     = 10
  max_retries = 5
}

# Listener
resource "openstack_lb_listener_v2" "cluster_test" {
  name            = "listener_test"
  protocol        = "TCP"
  protocol_port   = "80"
  loadbalancer_id = "${openstack_lb_loadbalancer_v2.cluster_test.id}"
  default_pool_id = "${openstack_lb_pool_v2.cluster_test.id}"
}

#  Member
resource "openstack_lb_member_v2" "cluster_test" {
  name          = "members_test"
  address       = "${openstack_compute_instance_v2.hello_world.*.network.0.fixed_ip_v4[count.index]}"
  protocol_port = "8080"
  pool_id       = "${openstack_lb_pool_v2.cluster_test.id}"
  count         = "2"
}

#  FIP
resource "openstack_networking_floatingip_v2" "cluster_test" {
  pool = "public"
}

#  FIP association
resource "openstack_networking_floatingip_associate_v2" "cluster_test" {
  floating_ip = "${openstack_networking_floatingip_v2.cluster_test.address}"
  port_id     = "${openstack_lb_loadbalancer_v2.cluster_test.vip_port_id}"
}

OK, prenons un café et analysons tout ça, car on à un joli paquet de trucs à voir!

  • La première partie (#LoadBalancer) permet de créer notre Load-Balancer à proprement parler : On le créé sur le réseau "first_network" créé plus haut dans l'article, et on lui demande simplement d'attendre que nos instances hello_world aient été créées.
  • Puis, dans la partie #Pool, nous allons créer le pool de machines qui vont composer ce Load-Balancer. Nous allons lui définir les propriétés importantes : Le type de protocole à balancer, et surtout, la méthode dont il doit balancer le traffic. Il existe plusieurs méthodes :
    • ROUND_ROBIN : Répartis la charge par le biais d'un "jeton" : La première requête sera effectuée sur l'instance A, le second sur l'instance B, le troisième sur l'instance A, etc
    • LEAST_CONNECTIONS : Répartis la charge en assignant la requête à l'instance qui à le moins de connexions actives.
    • SOURCE_IP : Assigne l'IP d'un client à une instance et transmet toujours les requêtes à la même instances.
  • Dans la partie Monitor, nous allons définir les conditions pour qu'une machine reste dans le pool du Load-Balancer. Ici, elle doit répondre à un ping TCP une fois toute les 20 secondes, dans un délai maximal de 10 secondes. Au bout de 5 tentatives infructueuses, on supprme l'instance du LB (Note : OpenStack continuera à pinger la machine et la réinsérera dans le Load Balancer une fois son état revenu à la normale.).
  • Dans la partie Listener, nous ajoutons les informations de ce que le LB va écouter et distribuer : Ici, les connexions entrantes sur le port 80 en TCP.
  • Dans la partie Member, nous définissons les machines qui composeront notre pool : Ici, nous avons mis l'ensemble des machines hello_world précédemment créées. Le Load-Balancer, va donc, quand il va recevoir une requête sur le port 80 en TCP, rediriger en Round-Robin la requête sur le port 8080 d'une de ces instances.
  • FIP et FIP Association permettent d'assigner une IP Flottante à notre Load-Balancer, comme vu plus haut.

Dernier détail, nous devons indiquer à Terraform que nous utilisons Octavia, le module d'OpenStack chargé du Load Balancing. Créons donc un fichier provider.tf :

provider "openstack" {
  use_octavia = true
}

Notre Load-Balancer est maintenant fonctionnel! On peut y ajouter un enregistrement DNS, etc, comme vu précédemment. Si vous contactez à plusieurs reprises le Load Balancer, sur la page "hello.html", vous devriez voir que le numéro affiché change : C'est la preuve que la charge est répartie entre nos instances.

A la fin de cet article, vous êtes donc capables de créer des instances, des volumes, des réseaux, des load-balancer, des enregistrements DNS. C'est un bon début, et vous devriez continuer à essayer dans votre coin de bidouiller un peu! Dans le prochain article, nous aborderons des concepts survolés jusqu'ici, ainsi que diverses problématiques, et les bonnes pratiques à suivre pour ne pas se planter en production. A bientôt !

Article précédent Article suivant