eadl:bloc4:fm2:td3

Ceci est une ancienne révision du document !


TD - Automatisation d'une stack web avec Ansible

A l'issue de ce TD, vous serez capable de :

  • Comprendre la séparation entre provisionnement (Terraform) et configuration (Ansible)
  • Configurer une stack web multi-services avec Ansible
  • Ecrire des templates Jinja2 pour générer des fichiers de configuration dynamiques
  • Utiliser les handlers pour gérer les redémarrages conditionnels
  • Structurer un projet Ansible en rôles réutilisables
  • Gérer les dépendances entre services
  • Utiliser des variables par environnement

Vous intégrez l'équipe DevOps d'une plateforme e-commerce en forte croissance. L'infrastructure tourne sur AWS en production, mais l'equipe travaille en local avec Docker pour les environnements de developpement.

Votre mission : automatiser la configuration d'une stack composee de quatre serveurs :

  • un reverse proxy Nginx
  • une application Flask
  • une base de donnees PostgreSQL
  • un cache Redis

Terraform a deja ete utilise dans le TD precedent pour provisionner les conteneurs. Vous prenez le relai avec Ansible pour configurer chaque service.

Creez la structure suivante :

  td-ansible/
    docker/
      Dockerfile
    terraform/
      main.tf
    ansible/
      inventory/
        hosts.yml
      group_vars/
        all.yml
        frontend.yml
        backend.yml
        db.yml
        cache.yml
      roles/
        nginx/
          tasks/
            main.yml
          templates/
            nginx.conf.j2
          handlers/
            main.yml
        flask/
          tasks/
            main.yml
          handlers/
            main.yml
        postgresql/
          tasks/
            main.yml
          handlers/
            main.yml
        redis/
          tasks/
            main.yml
          handlers/
            main.yml
      playbook.yml

Construisez l'image manuellement depuis le dossier docker.

Fichier : docker/Dockerfile

FROM ubuntu:22.04

RUN apt-get update -qq && \
    apt-get install -y -qq \
      openssh-server \
      python3 \
      python3-pip \
      curl \
    && mkdir -p /run/sshd \
    && echo 'root:root' | chpasswd \
    && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]

cd docker
docker build -t ansible-node:local .
cd ..

Creez le fichier suivant, puis appliquez la configuration.

Fichier : terraform/main.tf

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "base" {
  name         = "ansible-node:local"
  keep_locally = true
}

resource "docker_network" "stack_network" {
  name = "stack_network"
}

resource "docker_container" "nginx" {
  name  = "nginx"
  image = docker_image.base.image_id
  networks_advanced {
    name = docker_network.stack_network.name
  }
  ports {
    internal = 22
    external = 2221
  }
  ports {
    internal = 80
    external = 8090
  }
}

resource "docker_container" "flask" {
  name  = "flask"
  image = docker_image.base.image_id
  networks_advanced {
    name = docker_network.stack_network.name
  }
  ports {
    internal = 22
    external = 2222
  }
}

resource "docker_container" "postgresql" {
  name  = "postgresql"
  image = docker_image.base.image_id
  networks_advanced {
    name = docker_network.stack_network.name
  }
  ports {
    internal = 22
    external = 2223
  }
}

resource "docker_container" "redis" {
  name  = "redis"
  image = docker_image.base.image_id
  networks_advanced {
    name = docker_network.stack_network.name
  }
  ports {
    internal = 22
    external = 2224
  }
}

cd terraform
terraform init
terraform apply

Si vous obtenez une erreur indiquant que le réseau ou des conteneurs existent déjà, supprimez-les avant de relancer :

docker rm -f nginx flask postgresql redis
docker network rm stack_network
terraform apply

Questions :

  • Que fait terraform init ?
  • Pourquoi expose-t-on le port 22 de chaque conteneur sur un port différent de la machine hôte ?
  • Que se passe-t-il si deux conteneurs exposent le même port ?

L'inventaire indique a Ansible quels sont les hotes a gerer et comment s'y connecter.

Creez le fichier suivant.

Fichier : ansible/inventory/hosts.yml

all:
  vars:
    ansible_user: root
    ansible_password: root
    ansible_ssh_common_args: '-o StrictHostKeyChecking=no'

  children:
    frontend:
      hosts:
        nginx:
          ansible_host: 127.0.0.1
          ansible_port: 2221

    backend:
      hosts:
        flask:
          ansible_host: 127.0.0.1
          ansible_port: 2222

    db:
      hosts:
        postgresql:
          ansible_host: 127.0.0.1
          ansible_port: 2223

    cache:
      hosts:
        redis:
          ansible_host: 127.0.0.1
          ansible_port: 2224

Testez la connexion aux conteneurs :

cd ansible/
ansible all -i inventory/hosts.yml -m ping

Questions :

  • Que signifie le parametre StrictHostKeyChecking=no ?
  • Pourquoi est-ce acceptable en developpement mais pas en production ?
  • Que retourne la commande ping d'Ansible ?

Les variables permettent de centraliser la configuration et de la rendre reutilisable.

Creez les fichiers suivants.

Fichier : ansible/group_vars/all.yml

app_name: ecommerce
app_env: development
app_domain: localhost

Fichier : ansible/group_vars/db.yml

db_name: ecommerce_db
db_user: app_user
db_password: changeme
db_port: 5432

Fichier : ansible/group_vars/cache.yml

redis_port: 6379
redis_maxmemory: 256mb

Fichier : ansible/group_vars/backend.yml

flask_port: 5000
flask_debug: true
db_host: postgresql
cache_host: redis

Fichier : ansible/group_vars/frontend.yml

nginx_port: 80
backend_host: flask
backend_port: 5000

On commence par la base de donnees car les autres services en dependent.

Creez le fichier suivant.

Fichier : ansible/roles/postgresql/tasks/main.yml

---
- name: Installation de PostgreSQL
  apt:
    name:
      - postgresql
      - postgresql-contrib
      - python3-psycopg2
    state: present
    update_cache: yes

- name: Demarrage de PostgreSQL
  service:
    name: postgresql
    state: started
    enabled: yes

- name: Creation de la base de donnees
  become: yes
  become_user: postgres
  postgresql_db:
    name: "{{ db_name }}"
    state: present

- name: Creation de l'utilisateur applicatif
  become: yes
  become_user: postgres
  postgresql_user:
    name: "{{ db_user }}"
    password: "{{ db_password }}"
    priv: "{{ db_name }}.*:ALL"
    state: present

Creez le fichier suivant.

Fichier : ansible/roles/redis/tasks/main.yml

---
- name: Installation de Redis
  apt:
    name: redis-server
    state: present
    update_cache: yes

- name: Configuration de Redis
  lineinfile:
    path: /etc/redis/redis.conf
    regexp: '^maxmemory '
    line: "maxmemory {{ redis_maxmemory }}"
  notify: restart redis

- name: Demarrage de Redis
  service:
    name: redis-server
    state: started
    enabled: yes

Fichier : ansible/roles/redis/handlers/main.yml

---
- name: restart redis
  service:
    name: redis-server
    state: restarted

Questions :

  • Qu'est-ce qu'un handler dans Ansible ?
  • Quand est-il declenche ?
  • Quelle est la difference entre notify et un appel direct a service ?

Creez le fichier suivant.

Fichier : ansible/roles/flask/tasks/main.yml

---
- name: Installation des dependances Python
  apt:
    name:
      - python3
      - python3-pip
      - python3-venv
    state: present
    update_cache: yes

- name: Creation du repertoire applicatif
  file:
    path: /opt/flask
    state: directory
    mode: '0755'

- name: Creation de l'application Flask
  copy:
    dest: /opt/flask/app.py
    content: |
      from flask import Flask, jsonify
      import os
      import redis
      import psycopg2

      app = Flask(__name__)

      @app.route('/health')
      def health():
          return jsonify(status='ok', env=os.environ.get('APP_ENV', 'unknown'))

      if __name__ == '__main__':
          app.run(host='0.0.0.0', port={{ flask_port }}, debug={{ flask_debug | lower }})
  notify: restart flask

- name: Installation de Flask et des dependances
  pip:
    name:
      - flask
      - redis
      - psycopg2-binary
    executable: pip3

- name: Creation du service systemd Flask
  copy:
    dest: /etc/systemd/system/flask.service
    content: |
      [Unit]
      Description=Flask Application
      After=network.target

      [Service]
      Environment=APP_ENV={{ app_env }}
      Environment=DB_HOST={{ db_host }}
      Environment=CACHE_HOST={{ cache_host }}
      ExecStart=/usr/bin/python3 /opt/flask/app.py
      Restart=always

      [Install]
      WantedBy=multi-user.target
  notify: restart flask

- name: Demarrage de Flask
  systemd:
    name: flask
    state: started
    enabled: yes
    daemon_reload: yes

Fichier : ansible/roles/flask/handlers/main.yml

---
- name: restart flask
  systemd:
    name: flask
    state: restarted
    daemon_reload: yes

Creez les fichiers suivants.

Fichier : ansible/roles/nginx/tasks/main.yml

---
- name: Installation de Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes

- name: Configuration de Nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/default
    mode: '0644'
  notify: restart nginx

- name: Demarrage de Nginx
  service:
    name: nginx
    state: started
    enabled: yes

Fichier : ansible/roles/nginx/templates/nginx.conf.j2

upstream backend {
    server {{ backend_host }}:{{ backend_port }};
}

server {
    listen {{ nginx_port }};
    server_name {{ app_domain }};

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /health {
        proxy_pass http://backend/health;
    }
}

Fichier : ansible/roles/nginx/handlers/main.yml

---
- name: restart nginx
  service:
    name: nginx
    state: restarted

Questions :

  • Quelle est la difference entre copy et template dans Ansible ?
  • Que fait la directive proxy_pass ?
  • Pourquoi utilise-t-on le nom du conteneur plutot que son adresse IP ?

Creez le fichier suivant.

Fichier : ansible/playbook.yml

---
- name: Configuration de la base de donnees
  hosts: db
  become: yes
  roles:
    - postgresql

- name: Configuration du cache
  hosts: cache
  become: yes
  roles:
    - redis

- name: Configuration de l'application
  hosts: backend
  become: yes
  roles:
    - flask

- name: Configuration du reverse proxy
  hosts: frontend
  become: yes
  roles:
    - nginx

Questions :

  • Pourquoi l'ordre des plays dans le playbook est-il important ?
  • Que se passe-t-il si on configure Nginx avant Flask ?
  • A quoi sert le parametre become ?

Executez le playbook :

cd ansible/
ansible-playbook -i inventory/hosts.yml playbook.yml

Verifiez que l'application repond :

curl http://localhost:8080/health

Modifiez le fichier group_vars/frontend.yml et changez backend_host :

backend_host: wronghost

Relancez le playbook, puis testez :

curl http://localhost:8080/health

Questions :

  • Que se passe-t-il et pourquoi ?
  • Comment Ansible vous aide-t-il a identifier le probleme ?
  • Ou dans les logs trouvez-vous l'erreur ?
  • Comment corriger ?

Corrigez la valeur de backend_host et relancez le playbook. Verifiez que l'application repond correctement.

Vous devez ajouter un nouveau service de monitoring.

Sans aide, creez un role Ansible pour installer et configurer Netdata sur le conteneur flask.

Contraintes :

  • Le service doit demarrer automatiquement
  • Le port d'ecoute doit etre defini dans une variable
  • Un handler doit gerer le redemarrage
  • Le conteneur flask doit exposer le port correspondant dans main.tf

Verifiez que Netdata est accessible depuis votre navigateur.

  • Ajoutez un tag Ansible sur chaque play pour pouvoir executer uniquement un role specifique
  • Creez un fichier ansible.cfg pour eviter de specifier l'inventaire a chaque commande
  • Chiffrez le fichier group_vars/db.yml avec ansible-vault pour proteger le mot de passe
  • eadl/bloc4/fm2/td3.1779749609.txt.gz
  • Dernière modification : il y a 8 heures
  • de jcheron