Le Déploiement en Zéro Downtime grâce à Terraform
Le Déploiement en Zéro Downtime grâce à Terraform
Une des craintes lors du déploiement d'une nouvelle version d'une application est d'avoir un downtime (une indisponibilité temporaire) ou pire un bug dans le code de la nouvelle version. Pour éviter ces situations diverses solutions ont déjà vu le jour.
Aujourd'hui je voudrais vous parler du principe de "Blue-Green" et de comment l'outil Terraform peut nous permettre de le mettre en place, moyennant quelques ajustements.
Blue-Green
D'abord un peut de définitions, de quoi parle-t-on lorsque que l'on évoque le terme "Blue-Green" ?
L'idée est d'appliquer une coloration à son infrastructure. Le "Bleu" représente la version courante de l'infrastructure et la "Verte" la version que l'on souhaite déployer.
Voici une infrastructure classique, une base de données, deux frontaux et un load-balancer. Comme vous pouvez le voir les deux frontaux possèdent une petite pastille bleu qui indique sa version.
Ceux ci contiennent un serveur http, un php-fmp et le code.
Le déploiement "Blue-Green" s'applique en plusieurs phases.
Premièrement on ajoute les machines de l'infrastructure "verte". Ceux-ci contiennent le code à la nouvelle version que l'on souhaite déployer.
Ensuite on réalise les connexions réseau vers la base de données. On pourrait considérer cette environnement comme du "staging", seuls les développeurs peuvent y accéder pour tester les fonctionnalités mais les utilisateurs non.
Puis il faut indiquer au load-balancer l'existence de ces nouveaux serveurs. A partir de ce moment là le traffic commence à arriver sur ces 2 nouvelles machines. Nous avons donc une architecture hybride d'ancien et de nouveau code.
Si l'on considère que le nouveau code est correct on peut débuter le démantèlement de l'infrastructure "bleu". On commence par la retirer du load-balancer et ses connexions aux bases de données.
Finalement on détuit les machines de l'infrastructure "bleu"
On se retrouve avec une infrastructure identique à l'ancienne mais avec un code mis à jour :)
Terraform
Il existe tout un tas de façon de réaliser ces opérations, la moins optimisée étant de tout réaliser à la main c'est faisable dans le contexte de un ou deux frontaux, mais au bout de trois ou quatre serveurs ça commence à être réellement fastidueux.
Heureusement il existe des outil pour automatiser tout cela. L'un deux se nomme Terraform.
Son rôle est de faciliter l'application du concept d' "Infra as code".
Infra As Code
Derrière ces mots se cache toute une philosophie visant à décrire une infrastructure sous la forme de code. Et donc modulaire et réutilsable.
Il est donc ainsi possible de réaliser des recettes de création d'infrastructure, qui pourrait ressembler à " je veux un sous-réseau privé et deux machines de 1Go de RAM et 10 Go tournant sous Ubuntu 21.04 dans ce sous réseau"
Provider
Un des concepts de base de Terraform est la notion de "provider". Un provider responsable des interactions avec les API des IaaS (AWS, OVH, GCP, OpenStack, Digital Ocean, Azure), des PaaS (Heroku, Clever Cloud) et des SaaS (Cloud Flare, Terraform Cloud).
Ces APIs permettant de gérer les ressources.
Les ressources
Au sens de Terraform celles ci peuvent représenter à peu près n'importe quoi: des machines physiques, des VMs, des interfaces réseau, des load-balancers, des conatiners et bien plus encore. Elles constituent les ingrédients de nos recettes.
Installation
La CLI de Terraform est disponible sur tous les systèmes d'exploitations courants sur cette page, il est aussi possible de suivre les tutos spécifiques par OS.
Bon je suis un peu fainéant et étant sur Mac je peux prendre un petit raccourci:
brew install terraform
Créer une infrastructure
Pour les besoins du tuto j'utiliserai le provider Digital Ocean qui est un IaaS très simple d'utilisation.
La documentation complète est disponible ici
Pour créer un projet Terraform il suffit de créer un nouveau dossier et dedans un fichier .tf
. Peut importe son nom. Nous allons l'appeller par exemple main.tf
provider "digitalocean" {
token = "token"
}
resource "digitalocean_droplet" "web1" {
image = "ubuntu-20-04-x64"
name = "web-1"
region = "lon1"
size = "s-1vcpu-1gb"
}
Le token peut être créé à partir de la page d'adminisration de Digital Ocean dans l'onglet API.
Ce fichier est une recette qui créé une machine Ubuntu 20.04 de 1Go et de 1 virtual CPU sur Digital Ocean.
Si vous ne l'avez pas déjà fait installez Terraform. Normalement la commande terraform help
devrait vous afficher
Usage: terraform [-version] [-help] <command> [args]
The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.
...
Si c'est le cas félicitations, Terraform est correctement installé.
Vous pouvez désormais effectuer un terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "digitalocean" (terraform-providers/digitalocean) 1.16.0...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.digitalocean: version = "~> 1.16"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Cette commande à comme vous venez de lire détecter que le provider était Digital Ocean et a automatiquement initialiser le projet en téléchargeant la dernière version du binaire.
La deuxième phase est de planifier une modification d'infrastructure. terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# digitalocean_droplet.web1 will be created
+ resource "digitalocean_droplet" "web1" {
+ backups = false
+ created_at = (known after apply)
+ disk = (known after apply)
+ id = (known after apply)
+ image = "ubuntu-20-04-x64"
+ ipv4_address = (known after apply)
+ ipv4_address_private = (known after apply)
+ ipv6 = false
+ ipv6_address = (known after apply)
+ ipv6_address_private = (known after apply)
+ locked = (known after apply)
+ memory = (known after apply)
+ monitoring = false
+ name = "web-1"
+ price_hourly = (known after apply)
+ price_monthly = (known after apply)
+ private_networking = (known after apply)
+ region = "lon1"
+ resize_disk = true
+ size = "s-1vcpu-1gb"
+ status = (known after apply)
+ urn = (known after apply)
+ vcpus = (known after apply)
+ volume_ids = (known after apply)
+ vpc_uuid = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Comme énoncé Terraform a planifié la création d'une VM (droplet dans le vocabulaire de Digital Ocean). Et vous indique que vous pouvez appliquer ces modification avec terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# digitalocean_droplet.web1 will be created
+ resource "digitalocean_droplet" "web1" {
+ backups = false
+ created_at = (known after apply)
+ disk = (known after apply)
+ id = (known after apply)
+ image = "ubuntu-20-04-x64"
+ ipv4_address = (known after apply)
+ ipv4_address_private = (known after apply)
+ ipv6 = false
+ ipv6_address = (known after apply)
+ ipv6_address_private = (known after apply)
+ locked = (known after apply)
+ memory = (known after apply)
+ monitoring = false
+ name = "web-1"
+ price_hourly = (known after apply)
+ price_monthly = (known after apply)
+ private_networking = (known after apply)
+ region = "lon1"
+ resize_disk = true
+ size = "s-1vcpu-1gb"
+ status = (known after apply)
+ urn = (known after apply)
+ vcpus = (known after apply)
+ volume_ids = (known after apply)
+ vpc_uuid = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
Pour éviter tout malentendu Terraform vous récapitule une dernière fois ce qui va être effectuer sur le provider. Si vous rentrez yes
. Terraform débute l'opération.
Puis:
digitalocean_droplet.web1: Creating...
digitalocean_droplet.web1: Still creating... [10s elapsed]
digitalocean_droplet.web1: Still creating... [20s elapsed]
digitalocean_droplet.web1: Still creating... [30s elapsed]
digitalocean_droplet.web1: Creation complete after 35s [id=190159268]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Tada 🤩. Une VM vient d'apparaître sur Digital Ocean.
Pour l'opération inverse la commande est terraform destroy
Même chose que pour terraform apply
, la commande nous récapitule les opérations qui vont être réalisées et attend un yes
.
digitalocean_droplet.web1: Refreshing state... [id=190159268]
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# digitalocean_droplet.web1 will be destroyed
- resource "digitalocean_droplet" "web1" {
- backups = false -> null
- created_at = "2020-04-27T15:04:05Z" -> null
- disk = 25 -> null
- id = "190159268" -> null
- image = "ubuntu-20-04-x64" -> null
- ipv4_address = "161.35.37.180" -> null
- ipv6 = false -> null
- locked = false -> null
- memory = 1024 -> null
- monitoring = false -> null
- name = "web-1" -> null
- price_hourly = 0.00744 -> null
- price_monthly = 5 -> null
- private_networking = false -> null
- region = "lon1" -> null
- resize_disk = true -> null
- size = "s-1vcpu-1gb" -> null
- status = "active" -> null
- tags = [] -> null
- urn = "do:droplet:190159268" -> null
- vcpus = 1 -> null
- volume_ids = [] -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value:
Une fois celui-ci entré:
digitalocean_droplet.web1: Destroying... [id=190159268]
digitalocean_droplet.web1: Still destroying... [id=190159268, 10s elapsed]
digitalocean_droplet.web1: Still destroying... [id=190159268, 20s elapsed]
digitalocean_droplet.web1: Destruction complete after 22s
Et sur Digital Ocean, a plus la VM 🥺
Contruire notre infrastructure bleu
Pour rappel notre infra ressemble à ça
La recette Terraform pour arriver à un tel résultat pourrait ressembler à ceci:
// définition du provider
provider "digitalocean" {
token = "token"
}
// création d'une base de donnée
resource "digitalocean_database_cluster" "bdd" {
name = "mysql-001"
engine = "mysql"
version = "8"
size = "db-s-1vcpu-1gb"
region = "lon1"
node_count = 1
}
// création d'un firewall vers la base de données pour
// n'accepter que les connexions venant des frontaux
resource "digitalocean_database_firewall" "bdd-fw" {
cluster_id = digitalocean_database_cluster.bdd.id
rule {
type = "tag"
value = "front"
}
}
// création de deux frontaux
resource "digitalocean_droplet" "web" {
image = "ubuntu-20-04-x64"
name = "web-${format("%03d",count.index + 1)}"
region = "lon1"
size = "s-1vcpu-1gb"
count = 2
private_networking = true
tags = ["front"]
}
// création d'un loadbalancer connecté aux frontaux
resource "digitalocean_loadbalancer" "loadbalancer" {
name = "load-001"
region = "lon1"
forwarding_rule {
entry_port = 80
entry_protocol = "http"
target_port = 80
target_protocol = "http"
}
healthcheck {
port = 22
protocol = "tcp"
}
droplet_ids = digitalocean_droplet.web[*].id
}
// création d'un projet pour manager toutes ces ressources
resource "digitalocean_project" "playground" {
name = "playground"
description = "A project to represent development resources."
purpose = "Web Application"
environment = "Development"
resources = concat(
digitalocean_droplet.web[*].urn,
[digitalocean_database_cluster.bdd.urn],
[digitalocean_loadbalancer.loadbalancer.urn]
)
}
Et hop! Deux frontaux, une base de données MySQL, un load-balancer entre les frontaux et le tout dans un projet "playground", le compte est bon!
Spécialiser nos images
Bon c'est magnifique tout ça mais nos frontaux n'ont de frontaux que le nom, il est temps de leur donner une fonction parce que pour le moment c'est juste une Ubuntu. Pour cela nous allons utiliser deux outils: le premier Packer permet d'automatiser la création d'image dans le Cloud et le second Ansible permet d'automatiser la configuration de ces images.
Packer
Packer est un logiciel conçu par la même société que Terraform: Hashicorp. Son rôle est de fabriquer des images de machines virtuelles qui pourront être bootées dans le Cloud par Terrform ( vous voyez il bien une logique ^^ )
Installation
Pour l'installation c'est ici que ça se passe.
Comme pour Terraform je préfère la méthode simple de homebrew:
brew install packer
Construire une image
Packer utilise un simple fichier json comme configuration, nous appelleront le notre config.json
.
La clef builders
représente les providers
de Terraform, pour nous on continue avec Digital Ocean
Pour vérifier que tout est en ordre:
packer validate config.json
Si tout es bon:
Template validated successfully.
Puis pour créer l'image sur Digital Ocean
packer build config.json
digitalocean: output will be in this color.
==> digitalocean: Creating temporary ssh key for droplet...
==> digitalocean: Creating droplet...
==> digitalocean: Waiting for droplet to become active...
==> digitalocean: Using ssh communicator to connect: 167.71.140.5
==> digitalocean: Waiting for SSH to become available...
==> digitalocean: Connected to SSH!
==> digitalocean: Gracefully shutting down droplet...
==> digitalocean: Creating snapshot: base-image-ubuntu-20.04-x64-v1.0
==> digitalocean: Waiting for snapshot to complete...
==> digitalocean: Destroying droplet...
==> digitalocean: Deleting temporary ssh key...
Build 'digitalocean' finished.
==> Builds finished. The artifacts of successful builds are:
--> digitalocean: A snapshot was created: 'base-image-ubuntu-20.04-x64-v1.0' (ID: 62816352) in regions 'lon1'
Félicitartions cette image ne sert absolument à rien :D, je vous conseille de la supprimer car elle coûte de l'argent :p
Pour quelle est une quelconque utilité il faut la "provisionner". Et pour cela nous allons utiliser Ansible.
Ansible
Ansible est un outil qui permet de s'assurer de l'état d'une ou plusieurs machines. Il se charge d'installer les paquets et de configurer les services, créer les utilisateur et leur donner des droits. Il peut aussi vérifier qu'un service est bien en train de tourner et le démarrer le cas échéant.
Installation
Bon comme d'habitude il faut installer la CLI, ici ou
brew install ansible
Créer une configuration Ansible
Ansible utilise un fichier YAML comme configuration:
À titre d'exemple voici à quoi cela peut ressembler un playbook.yml
- name: "Provision image"
hosts: default
become: true
tasks:
- name: install Nginx
package:
name: nginx
state: present
- name: start Nginx
service:
name: nginx
state: started
Utiliser Ansible avec Packer
Il suffit de rajouter une clef provisioners
de type ansible
Le packer build
applique les différentes modifications:
digitalocean: output will be in this color.
==> digitalocean: Creating temporary ssh key for droplet...
==> digitalocean: Creating droplet...
==> digitalocean: Waiting for droplet to become active...
==> digitalocean: Using ssh communicator to connect: 161.35.35.53
==> digitalocean: Waiting for SSH to become available...
==> digitalocean: Connected to SSH!
==> digitalocean: Provisioning with Ansible...
==> digitalocean: Executing Ansible: ansible-playbook --extra-vars packer_build_name=digitalocean packer_builder_type=digitalocean -o IdentitiesOnly=yes -i /var/folders/rz/ccsb98092xj9dhll8wv4jll80000gp/T/packer-provisioner-ansible681128243 /Users/yguern/Documents/lab/terraform/ansible/playbook.yml -e ansible_ssh_private_key_file=/var/folders/rz/ccsb98092xj9dhll8wv4jll80000gp/T/ansible-key770361892
digitalocean:
digitalocean: PLAY [Provision image] *********************************************************
digitalocean:
digitalocean: TASK [Gathering Facts] *********************************************************
digitalocean: ok: [default]
digitalocean:
digitalocean: TASK [install Nginx] ***********************************************************
digitalocean: changed: [default]
digitalocean:
digitalocean: PLAY RECAP *********************************************************************
digitalocean: default : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
digitalocean:
==> digitalocean: Gracefully shutting down droplet...
==> digitalocean: Creating snapshot: base-image-ubuntu-20.04-x64-v1.0
==> digitalocean: Waiting for snapshot to complete...
==> digitalocean: Destroying droplet...
==> digitalocean: Deleting temporary ssh key...
Build 'digitalocean' finished.
==> Builds finished. The artifacts of successful builds are:
--> digitalocean: A snapshot was created: 'base-image-ubuntu-20.04-x64-v1.0' (ID: 62816837) in regions 'lon1'
Bon on se rapproche de quelque chose on a maintenant une image contenant un serveur nginx. Cette image est appelée dans Digital Ocean une snapshot
son nom est : base-image-ubuntu-20.04-x64-v1.0
.
Utiliser cette image comme frontal
Même si elle n'est pas complète on peut s'accorder la liberté de la tester en condition réelle :)
Dans le main.tf
on peut modifier notre schéma de contruction de notre frontal:
resource "digitalocean_droplet" "web" {
image = "base-image-ubuntu-20.04-x64-v1.0" # <--- image venant de Packer
name = "web-${format("%03d",count.index + 1)}"
region = "lon1"
size = "s-1vcpu-1gb"
count = 2
private_networking = true
tags = ["front"]
}
L'image de contruction de la droplet est maintenant celle crée précédemment par Packer.
On terraform apply
ça build. Et finalement:
On récupère l'IP du loadbalancer 159.65.211.24
on le met dans un navigateur et tada!
Ok on a un nginx qui tourne et qui retourne un site statique mais on est pas encore au bout de nos peine. On doit encore configuré nginx et surtout installer un php-fpm parce que je rappelle qu'à terme c'est une application Laravel et donc PHP qui voulu qui est voulue.
On va donc améliorer un peu la structure de notre dossier ansible
.
Rajouter PHP à notre image
Ansible nous permet de rajouter autant de services que l'on peut le désirer. De ce fait nous allons pouvoir rajouter PHP-FPM à notre image et configurer nginx pour être capable de comprendre la cgi PHP.
Installer PHP
Il existe tout un tas de façon d'installer PHP via Ansible, j'ai choisi d'utiliser un rôle tout fait.
Celui donne un moyen clef en main d'installer n'importe quelle version de PHP et ses packages les plus courants.
Ansible est fourni avec un package manager appelé ansible-galaxy
Pour installer le rôle PHP
ansible-galaxy install ansible-galaxy install sys_fs.php_fpm
Finalement on peut utiliser notre rôle:
roles:
- role: sys_fs.php_fpm
vars:
- php_fpm_packages:
- php7.4
- php7.4-cli
- php7.4-common
- php7.4-curl
- php7.4-fpm
- php7.4-gd
- php7.4-json
- php7.4-mbstring
- php7.4-mysql
- php7.4-opcache
- php7.4-zip
- php_fpm_pools:
- name: web
type: unix
pm: static
pm_max_children: 100
php_admin_values:
- name: memory_limit
value: '512M'
Configurer Nginx en Fast-CGI
Lors de la précédente partie nous avions simplement installé Nginx sans le configurer. Il est temps de rétablir cela.
Cette fois ci on se passera de ansible-galaxy
et nous allons créer de toute pièce notre rôle nginx.
Voici une structure possible pour notre rôle nginx
roles
|_ nginx
|
|-tasks
| |
| |- main.yml # tâches d'installation et de configuration
|
|- templates`
|
|- ufw.j2 # configuration du firewall
|
|- default.conf.j2 # configuration du vhost
En plus d'installer nginx, nous configurons le firewall pour ouvrir le port 80.
---
# main.yml
---
- name: Install Nginx
package:
name: nginx
state: present
- name: Add ufw config file
template:
dest: /etc/ufw/applications.d/nginx.ini
src: ufw.j2
- name: Open firewall for Nginx
ufw:
rule: allow
name: Nginx HTTP
- name: Add vhost
template:
dest: /etc/nginx/sites-enabled/default
src: default.conf.j2
owner: www-data
group: www-data
mode: '0644'
La configuration du vhost
# default.conf.j2
server {
listen 80;
server_name _;
root /srv/web;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# permet de se connecter la pool PHP-FPM installé précédemment
fastcgi_pass unix:/run/php/web-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
La configuration du firewall
[Nginx HTTP]
Web Server
NGINX HTTP traffic
Enable 80/tcp
[Nginx HTTPS] \
HTTPS) \
Web Server (NGINX HTTPS traffic
Enable 443/tcp
[Nginx Full]
HTTP,HTTPS)
Web Server (NGINX HTTP and HTTPS traffic
Enable 80,443/tcp
Le playbook complet ressemble à:
---
- name: Install and configure a webserver Nginx & PHP-FPM
hosts: default
become: true
roles:
- role: nginx
- role: sys_fs.php_fpm
vars:
Ce travail est sous licence CC BY-NC-SA 4.0.