Aller au contenu principal

Création d'une Infrastructure avec Terraform, Ansible et Packer (la suite)

La suite du guide sur la création d'une infrastructure sur le cloud Outscale.

Configuration de l'inventaire dynamique Ansible

Maintenant que les bases de notre infrastructure sont en place, je vais vous présenter comment utiliser Ansible pour automatiser la configuration des instances dans notre environnement cloud. L'aspect le plus intéressant et puissant ici est l'utilisation d'un inventaire dynamique, qui permet à Ansible de s'adapter automatiquement aux changements dans notre infrastructure cloud.

L'inventaire dynamique est une fonctionnalité d'Ansible qui permet de générer automatiquement un inventaire des instances à partir de sources comme AWS, Azure et dans notre cas, Outscale.

Pour configurer l'inventaire dynamique, nous utilisons le plugin AWS, Outscale étant compatible avec l'API Outscale cela va fonctionner. C'est ce plugin qui se chargera d'obtenir les informations sur les instances.

Créer un nouveau répertoire nommé playbooks pour y déposer vos fichiers Ansible.

.
├── playbooks
└── terraform
   ├── bastion
   └── infrastructure

Créer un fichier yaml dont le nom doit se terminer par .aws_ec2.yml. Ici, je vais l'appeler demo.aws_ec2.yml :

plugin: amazon.aws.aws_ec2
aws_profile: outscale
regions:
  - eu-west-2
keyed_groups:
- key: tags.Application
  separator: ''
- key: tags.Group
  separator: ''
- key: tags.Env
  separator: ''
hostnames:
  - ip-address
  - private-ip-address

Quelques explications :

  • plugin: amazon.aws.aws_ec2 : Indique qu'Ansible utilise le plugin aws_ec2 pour interagir avec AWS EC2.
  • aws_profile: outscale : Spécifie le profil AWS à utiliser pour l'authentification. Cela correspond à un profil défini précédemment.
  • regions: - eu-west-2 : Définit la région AWS où le plugin doit chercher les instances.
  • keyed_groups: Permet de créer des groupes dynamiques basés sur des tags spécifiques. Dans votre cas, il crée des groupes basés sur les tags Application, Group et Env sans séparateur (separator: '').
  • hostnames: - ip-address - private-ip-address : Définit les attributs à utiliser comme noms d'hôtes dans l'inventaire, ici les adresses IP publiques et privées des instances.

Pour que cela fonctionne, il faut créer un fichier de configuration contenant ces lignes :

[defaults]
inventory = demo.aws_ec2.yml
remote_user = outscale
callbacks_enabled = timer, profile_tasks, profile_roles
vault_password_file = .vault_pass

[inventory]
enable_plugins = aws_ec2

Cela permet de définir l'inventaire à utiliser, ainsi que l'utilisateur distant qui sera chargé d'appliquer les taches Ansible.

Testons-le :

@all:
  |--@ungrouped:
  |--@aws_ec2:
  |  |--80.247.5.193
  |--@robert:
  |  |--80.247.5.193
  |--@bastion:
  |  |--80.247.5.193
  |--@bastions:
  |  |--80.247.5.193

Cela fonctionne !

Voici le playbook que je vais lancer dessus qui va configurer une certain nombre de choses :

---
- name: Test Ansible
  hosts: aws_ec2
  gather_facts: true
  roles:
    - role: stephrobert.motd
      vars:
        motd_banner: " {{ tags.Name }} "
  pre_tasks:
    - name: Upgrade packages
      ansible.builtin.apt:
        upgrade: true
        update_cache: true
      become: true
    - name: Change Hostname
      ansible.builtin.hostname:
        name: "{{ hostvars[inventory_hostname].tags.Name }}.{{ hostvars[inventory_hostname].tags.Env }}.local"
      tags: name
      become: true
    - name: Add IP address of all hosts to all hosts | {{ inventory_hostname }}
      ansible.builtin.lineinfile:
        dest: /etc/hosts
        regexp: "^.*{{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local"
        line: "{{ (hostvars[item].ansible_all_ipv4_addresses | select('match', '192.168.') | list)[0] }} {{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local {{ hostvars[item].tags.Name }}"
        state: present
      when: hostvars[item].ansible_nodename is defined
      with_items: "{{ groups.aws_ec2 }}"
      become: true
    - name: Add IP address of all hosts to all hosts | {{ inventory_hostname }}
      vars:
        ansible_python_interpreter: /usr/bin/python3
      ansible.builtin.lineinfile:
        dest: /etc/hosts
        regexp: "^.*{{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local"
        line: "{{ (hostvars[item].ansible_all_ipv4_addresses | select('match', '192.168.') | list)[0] }} {{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local {{ hostvars[item].tags.Name }}"
        state: present
      when: hostvars[item].ansible_nodename is defined
      with_items: "{{ groups.all }}"
      delegate_to: localhost
      run_once: true
      become: true

Ce playbook modifie le message d'accueil, fais la mise à jour des packages, change le nom de la machine et créé des entrées dans le fichier /etc/hosts des machines distantes et locales (je n'ai pas de serveur DNS). Testons son exécution :

ansible-playbook provision.yml --limit bastion

PLAY [Test Ansible] ***

TASK [Gathering Facts] ***
mercredi 24 janvier 2024  08:48:52 +0100 (0:00:00.007)       0:00:00.008 ******
mercredi 24 janvier 2024  08:48:52 +0100 (0:00:00.007)       0:00:00.007 ******
ok: [80.247.5.193]

Playbook run took 0 days, 0 hours, 0 minutes, 38 seconds
mercredi 24 janvier 2024  08:49:31 +0100 (0:00:00.814)       0:00:38.331 ******
==============================================================================
Upgrade packages ------------------------------------------------------- 30.58s
Gathering Facts --------------------------------------------------------- 1.72s
stephrobert.motd : Install Figlet --------------------------------------- 1.12s
stephrobert.motd : Disable Default motd --------------------------------- 0.93s
stephrobert.motd : Copy config to Disable Motd in sshd ------------------ 0.87s
Change Hostname --------------------------------------------------------- 0.85s
stephrobert.motd : Configure New Dynamic Motd in PAM -------------------- 0.82s
stephrobert.motd : Copy new dynamic motd -------------------------------- 0.73s
Add IP address of all hosts to all hosts | 80.247.5.193 ----------------- 0.52s
Add IP address of all hosts to all hosts | 80.247.5.193 ----------------- 0.19s
mercredi 24 janvier 2024  08:49:31 +0100 (0:00:00.815)       0:00:38.330 ******
===============================================================================
ansible.builtin.apt ---------------------------------------------------- 30.58s
stephrobert.motd -------------------------------------------------------- 4.46s
gather_facts ------------------------------------------------------------ 1.72s
ansible.builtin.hostname ------------------------------------------------ 0.85s
ansible.builtin.lineinfile ---------------------------------------------- 0.71s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total ------------------------------------------------------------------ 38.32s

On se connecte à notre bastion :

ssh bastion
  _               _   _
 | |__   __ _ ___| |_(_) ___  _ __
 | '_ \ / _` / __| __| |/ _ \| '_ \
 | |_) | (_| \__ \ |_| | (_) | | | |
 |_.__/ \__,_|___/\__|_|\___/|_| |_|

System info:
  Hostname····: bastion.robert.local
  Distro······: Ubuntu 22.04.3 LTS
  Kernel······: Linux 5.15.0-91-generic
  Uptime······: up 24 minutes
  Load········: 0.10 (1m), 0.09 (5m), 0.03 (15m)
  Processes···: 101 (root), 7 (user), 108 (total)
  CPU·········: Intel Xeon Processor (Skylake) (2 vCPU)
  Memory······: 149Mi used, 3.4Gi avail, 3.8Gi total

Disk usage:
  /                               7% used out of  42G
  [===···············································]
  /boot/efi                       6% used out of 110M
  [===···············································]

Last login: Wed Jan 24 07:49:30 2024 from 90.110.185.174
outscale@bastion:~$

Nous jouerons ce playbook à chaque fois que nous créerons une nouvelle instance !

Création d'une instance dans le réseau privé

Après avoir configuré le bastion, je me concentre maintenant sur le sous-réseau privé. C'est ici que notre service web sera hébergé, isolé du trafic Internet direct pour une sécurité accrue. Cette section est essentielle pour assurer que notre service web fonctionne de manière optimale tout en étant protégé.

On crée un nouveau dossier terraform appelé test :

├── playbooks
└── terraform
   ├── bastion
   ├── infrastructure
   └── test

On procède comme précédemment. Éditez le fichier providers.tf et changer la clé du backend :

terraform {
  backend "s3" {
    profile = "outscale"
    bucket  = "lab-terraform"
    key     = "service/terraform.state"
    region  = "eu-west-2"
    endpoint = "https://oos.eu-west-2.outscale.com"
    skip_credentials_validation = true
    skip_region_validation      = true
  }
  required_providers {
    outscale = {
      source  = "outscale/outscale"
      version = "0.10.0"
    }
    aws = {
    }
  }
}

On crée notre instance test dans le réseau privé avec ce code :

data "outscale_images" "images" {
    filter {
        name   = "image_names"
        values = ["ubuntu_2204_steph"]
    }
}

data "outscale_subnet" "private_subnet" {
  filter {
    name = "tag_values"
    values = ["private_subnet"]
  }
}

data "outscale_security_group" "sg-all-all" {
  security_group_name = "seg-all-all"
}

resource "outscale_keypair" "keypair01" {
    keypair_name = "test"
    public_key   = file("~/.ssh/id_ed25519.pub")
}

resource "outscale_vm" "test" {
    image_id                 = data.outscale_images.images.images[0].image_id
    vm_type                  = "tinav5.c1r1p2"
    keypair_name             = outscale_keypair.keypair01.keypair_name
    security_group_ids       = [data.outscale_security_group.sg-all-all.id]
    subnet_id                = data.outscale_subnet.private_subnet.id
    state = "running"
    tags {
      key   = "Name"
      value = "test"
    }
    tags {
      key = "Application"
      value = "test"
    }
    tags {
      key = "Group"
      value = "test"
    }
    tags {
      key = "Env"
      value = "robert"
    }
}

On peut appliquer :

terraform apply
...

Changes to Outputs:
  + public_name = (known after apply)
outscale_keypair.keypair01: Creating...
outscale_keypair.keypair01: Creation complete after 1s [id=test]
outscale_vm.test: Creating...
outscale_vm.test: Still creating... [10s elapsed]
outscale_vm.test: Still creating... [20s elapsed]
outscale_vm.test: Creation complete after 28s [id=i-04ed2427]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

On applique notre playbook Ansible :

cd ../../playbooks

ansible-playbook provision.yml --limit test

On teste la connexion :

ssh 192.168.3.200

  _            _
 | |_ ___  ___| |_
 | __/ _ \/ __| __|
 | ||  __/\__ \ |_
  \__\___||___/\__|

System info:
  Hostname····: test.robert.local
  Distro······: Ubuntu 22.04.3 LTS
  Kernel······: Linux 5.15.0-91-generic
  Uptime······: up 11 minutes
  Load········: 0.09 (1m), 0.19 (5m), 0.10 (15m)
  Processes···: 86 (root), 7 (user), 93 (total)
  CPU·········: Intel Xeon Processor (Skylake) (1 vCPU)
  Memory······: 167Mi used, 634Mi avail, 957Mi total

Disk usage:
  /                               7% used out of  42G
  [===···············································]
  /boot/efi                       6% used out of 110M
  [===···············································]

Last login: Wed Jan 24 08:32:07 2024 from 192.168.2.113

On installe nginx :

sudo apt install -y nginx

On teste que le port 80 est bien démarré :

ss -tlnp
State             Recv-Q             Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                  511                                  0.0.0.0:80                                0.0.0.0:*
LISTEN            0                  4096                           127.0.0.53%lo:53                                0.0.0.0:*
LISTEN            0                  128                                  0.0.0.0:22                                0.0.0.0:*
LISTEN            0                  511                                     [::]:80                                   [::]:*
LISTEN            0                  128                                     [::]:22                                   [::]:*

Création d'un load balancer

Un load balancer distribue le trafic réseau entrant entre plusieurs machines virtuelles (VM) du Cloud public ou d’un réseau. Les VM enregistrées auprès d’un load balancer sont appelées des VM back-ends. Vous pouvez enregistrer autant de VM back-ends que nécessaire auprès d’un load balancer et vous pouvez enregistrer ou retirer des VM back-ends à tout moment selon vos besoins.

En fonction de ses security groups, une VM back-end peut recevoir soit le trafic venant uniquement du load balancer soit le trafic venant à la fois du load balancer et d’une autre source (par exemple un autre load balancer, internet, ou autres).

Le load balancer vérifie la santé des VM back-ends pour déterminer les VM saines vers lesquelles il peut distribuer du trafic.

Ici, je vais utiliser un load balancer de type internet. Dans le fichier main.tf du répertoire test ajoutez ces lignes :

data "outscale_subnet" "public_subnet" {
  filter {
    name = "tag_values"
    values = ["public_subnet"]
  }
}

resource "outscale_load_balancer" "load_balancer" {
  load_balancer_name = "test-balancer"
  listeners {
    backend_port           = 80
    backend_protocol       = "TCP"
    load_balancer_protocol = "TCP"
    load_balancer_port     = 80
  }
  # subregion_names    = ["eu-west-2a"]
  security_groups = [data.outscale_security_group.sg-all-all.security_group_id]
  load_balancer_type = "internet-facing"
  subnets            = [data.outscale_subnet.public_subnet.subnet_id]
  tags {
    key   = "name"
    value = "test"
  }
}

resource "outscale_load_balancer_vms" "outscale_load_balancer_test" {
  load_balancer_name = outscale_load_balancer.load_balancer.load_balancer_name
  backend_vm_ids     = [outscale_vm.test.id]
}

Dans le fichier output.tf ajoutez ces lignes :

output "public_name" {
  value = outscale_load_balancer.load_balancer.dns_name
}

Vous remarquez que je déploie le load balancer dans le sous-réseau public et je le lie à l'instance test qui se trouve dans le sous réseau privé !

On le déploie :

terraform apply -auto-approve
...

public_name = "test-balancer-964931616.eu-west-2.lbu.outscale.com"

On peut vérifier l'état du load balancer avec la CLI d'AWS :

aws elb describe-instance-health --load-balancer-name test-balancer --instance i-04ed2427 --endpoint https://lbu.eu-west-2.outscale.com

{
    "InstanceStates": [
        {
            "InstanceId": "i-04ed2427",
            "State": "InService",
            "ReasonCode": "N/A",
            "Description": "N/A"
        }
    ]
}

Il est bien en service. Si ce n'est pas le cas vérifier que depuis le réseau public, vous pouvez atteindre le port 80 de l'instance test avec la commande netcat ou un simple.

On teste depuis l'accès depuis internet :

curl http://test-balancer-964931616.eu-west-2.lbu.outscale.com

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Cela fonctionne 👍

Faire le ménage

Dans chaque répertoire Terraform, en commençant par l'instance test, puis l'instance bastion et pour finir l'infrastructure :

terraform destroy -auto-approve

Conclusion

En conclusion, la création d'une infrastructure réseau sur le cloud Outscale en utilisant Terraform, Ansible et Packer est une démarche complexe, mais enrichissante. Nous avons parcouru les étapes de configuration de l'environnement, création du VPC et des sous-réseaux, mise en place du sous-réseau public et privé, intégration du Load Balancer et l'automatisation avec Ansible. Nous avons également abordé les bonnes pratiques et les techniques de dépannage et de restauration.

Ce projet illustre l'importance d'une approche structurée et réfléchie dans la gestion d'infrastructures cloud. Les compétences et outils que nous avons utilisés sont essentiels pour tout professionnel DevOps souhaitant évoluer dans l'environnement cloud moderne.

Je vous encourage à explorer davantage ces outils et à les adapter à vos propres besoins en infrastructure. Le potentiel d'apprentissage et d'optimisation est immense dans le domaine du cloud computing.

important

Ce lab n'est pas une solution exploitable en l'état, cela demande encore pas mal de travail d'industrialisation de l'ensemble. On peut imaginer la création de modules Terraform pour éviter de dupliquer le code, d'une collection Ansible pour prendre en charge les days-2, à créer des images optimisées avec Packer, à améliorer la sécurité, à utiliser d'autres objets cloud fournis par Outscale, ...