eadl:bloc4:fm2:td3

TD3 - 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 déjà provisionné les conteneurs. Vous utilisez Ansible pour configurer.

Un rôle permet d’organiser :

  • tâches
  • variables
  • handlers
  • templates

Quel critère doit guider le découpage en rôles : le service (nginx, flask…) ou l'environnement (production, staging…) ? Justifiez.

roles/
  nginx/
    tasks/main.yml
    handlers/main.yml
    templates/

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 \
      sudo \
      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 ..

Créez 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 à Ansible quels sont les hôtes à gérer 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 :

Installez au préalable sshpass sur votre machine :

sudo apt-get install -y sshpass
puis :
  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 }}"
    state: present
- name: Attribution des privilègees
  become: yes
  become_user: postgres
  postgresql_privs:
    database: "{{ db_name }}"
    role: "{{ db_user }}"
    privs: ALL
    type: database
    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 declenché ?
  • Quelle est la différence entre notify et un appel direct à service ?

Créez 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
  shell: nohup python3 /opt/flask/app.py > /var/log/flask.log 2>&1 &
  args :
    chdir: /opt/flask

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

---
- name: restart flask
  shell: pkill -f "python3 /opt/flask/app.py"; nohup python3 /opt/flask/app.py > /var/log/flask.log 2>&1 &
  args :
    chdir: /opt/flask

Créez 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 plutôt 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:8090/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 ?
  • eadl/bloc4/fm2/td3.txt
  • Dernière modification : il y a 7 semaines
  • de jcheron