Infrastructure LAMP

Ce lab poursuit uniquement objectif pédagogique, intellectuel et démonstratif qui démarre d’une “solution d’infrastructure” parmi beaucoup d’autres possiblités. Aussi, une mise en production exigerait une autre approche probablement orientée sur l’application et les services qu’elle rendra aux clients.

Source originale : https://github.com/geerlingguy/ansible-for-devops/tree/master/lamp-infrastructure.

Livres de jeu adaptés : https://github.com/goffinet/ansible-for-devops/, dans le dossier lamp-infrastructure.

1. Architecture de l’infrastructure

Voici l’architecture de départ constituée de six serveurs Centos 7 :

  • Un serveur Cache/Reverse Proxy/Load Balancer “Frontend” Varnish.
  • Deux serveurs Web “Backend” Apache.
  • Un serveur de cache SQL Memcached.
  • Deux serveurs de base de données MySQL.

Infrastructure LAMP sur AWS EC2

On propose dans ce lab de provisionner et de configurer avec Ansible cette infrastructure LAMP uniquement basée sur le service Amazon AWS EC2.

Cette architecture offre plusieurs niveaux de mise en cache et une haute disponibilité/redondance à presque tous les niveaux, bien que pour simplifier les choses, il existe des points de défaillance uniques. Toutes les données persistantes stockées dans la base de données sont stockées dans un serveur esclave, et l’une des parties les plus lentes et les plus contraignantes de la pile (les serveurs web, dans ce cas, exécutant une application PHP via Apache) est facile à dimensionner horizontalement, derrière Varnish, qui agit comme couche cache (reverse proxy) et load balancer.

Pour les besoins de la démonstration, la mise en cache de Varnish est complètement désactivée, vous pouvez donc rafraîchir et voir les deux serveurs Apache (avec la mise en cache activée, Varnish mettra en cache la première réponse puis continuera à la servir sans toucher le reste de la pile). Vous pouvez voir la configuration de mise en cache et d’équilibrage de charge dans playbooks/varnish/templates/default.vcl).

Aussi, la page de l’application expose les adresses IP des serveurs de base données (à des fins de démonstration).

On se posera immédiatement la question de la sécurité quant à l’accès aux adresses IPv4 publiques ou privées, sur l’accès aux ports utilisés, du support de l’HTTPS. Certaines de ces questions pourraient trouver dans ce document des pistes de réfléxion de réponses avec de nombreuses alternatives en remplacement ou en complément de ce qui est proposé ici.

Notons enfin que l’approvisionnement et la configuration de l’architecture sécurisée avec son application pourrait être représentée en seul modèle CloudFormation.

2. Gestion

Il est préférable de disposer d’une station de gestion avec Ansible installé, de préférence au plus proche des cibles chez AWS.

Environnement Ansible pour AWS

Par exemple, ce script Bash pourrait vous aider à démarrer. Attention, les clés montrées ici sont factices, veuillez remplacer la valeur des variables AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY.

#!/bin/bash
export DEBIAN_FRONTEND="noninteractive"
sudo apt update
sudo apt-get upgrade --yes --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
sudo apt -y install python-pip
pip install boto boto3 ansible --upgrade --user
export AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="nqkh7zp7X/JSVfM3DFKQRNbw5x5D1Qzq0xeLKl9F"
export AWS_DEFAULT_REGION="eu-west-3"
export ANSIBLE_HOST_KEY_CHECKING=False
echo "export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX" >> ~/.profile
echo "export AWS_SECRET_ACCESS_KEY=nqkh7zp7X/JSVfM3DFKQRNbw5x5D1Qzq0xeLKl9F" >> ~/.profile
echo "export AWS_DEFAULT_REGION=eu-west-3" >> ~/.profile
echo "export ANSIBLE_HOST_KEY_CHECKING=False" >> ~/.profile
ssh-keygen -b 4096 -t rsa -f ~/.ssh/id_rsa -q -N ""

Récupération du livre de jeu

Il est préférable de récupérer le livre de jeu de ce lab :

git clone https://github.com/goffinet/ansible-for-devops
cd ansible-for-devops/lamp-infrastructure

3. Le livre de jeu LAMP Infrastructure

Le livre de jeu “Infrastructure LAMP” peut être déployé avec :

  • Vagrant et VirtualBox,
  • Digital Ocean,
  • AWS EC2, ce qui est pour le choix retenu.

3.1. Le livre de jeu principal

Pour faire fonctionner ce lab sur AWS EC2, le livre de jeu ansible-for-devops/lamp-infrastructure/provision.yml a été adapté par rapport à son original, déjà très fonctionnel pour que les tâches d’approvisionnement sur AWS soient activées.

---
# Uncomment the provisioner you would like to use. For for Vagrant provisioning,
# use `vagrant up` instead of this playbook.

#- provisioners/digitalocean.yml
- provisioners/aws.yml

# Configure provisioned servers.
- configure.yml

3.2. Approvisionnement AWS

Ensuite, il a été nécessaire de paraméter le livre de jeu ansible-for-devops/lamp-infrastructure/provisioners/aws.yml comme ceci :

---
- hosts: localhost
  connection: local
  gather_facts: false

  vars:
    ssh_key: "a4d-key"
    region: "eu-west-1" # Dublin
    image: "ami-0ff760d16d9497662" #Centos7
    instances:
      - name: a4d.lamp.varnish
        group: "lamp_varnish"
        security_group: ["default", "a4d_lamp_http"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"
      - name: a4d.lamp.www.1
        group: "lamp_www"
        security_group: ["default", "a4d_lamp_http"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"
      - name: a4d.lamp.www.2
        group: "lamp_www"
        security_group: ["default", "a4d_lamp_http"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"
#      - name: a4d.lamp.www.3
#        group: "lamp_www"
#        security_group: ["default", "a4d_lamp_http"]
#        type: "t2.micro"
#        image: "{{ image }}"
#        region: "{{ region }}"
      - name: a4d.lamp.db.1
        group: "lamp_db"
        security_group: ["default", "a4d_lamp_db"]
        type: t2.micro
        image: "{{ image }}"
        region: "{{ region }}"
      - name: a4d.lamp.db.2
        group: "lamp_db"
        security_group: ["default", "a4d_lamp_db"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"
      - name: a4d.lamp.memcached
        group: "lamp_memcached"
        security_group: ["default", "a4d_lamp_memcached"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"

    security_groups:
      - name: a4d_lamp_http
        rules:
          - proto: tcp
            from_port: 80
            to_port: 80
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: 0.0.0.0/0
        rules_egress: []

      - name: a4d_lamp_db
        rules:
          - proto: tcp
            from_port: 3306
            to_port: 3306
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: 0.0.0.0/0
        rules_egress: []

      - name: a4d_lamp_memcached
        rules:
          - proto: tcp
            from_port: 11211
            to_port: 11211
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: 0.0.0.0/0
        rules_egress: []
  pre_tasks:
    - name: "Ensure key pair is present"
      ec2_key:
        name: "{{ ssh_key | default('lamp_aws') }}"
        key_material: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
        region: "{{ region | default('eu-west-1') }}" # Dublin
  tasks:
    - name: Configure EC2 Security Groups.
      ec2_group:
        name: "{{ item.name }}"
        description: Example EC2 security group for A4D.
        region: "{{ item.region | default('eu-west-1') }}" # Dublin
        state: present
        rules: "{{ item.rules }}"
        rules_egress: "{{ item.rules_egress }}"
      with_items: "{{ security_groups }}"

    - name: Provision EC2 instances.
      ec2:
        key_name: "{{ ssh_key | default('lamp_aws') }}"
        instance_tags:
          inventory_group: "{{ item.group | default('') }}"
          inventory_host: "{{ item.name | default('') }}"
        group: "{{ item.security_group | default('') }}"
        instance_type: "{{ item.type | default('t2.micro')}}" # Free with agreement
        image: "{{ item.image | default('ami-0ff760d16d9497662') }}" # CentOS 7
        region: "{{ item.region | default('eu-west-1') }}" # Dublin
        wait: yes
        wait_timeout: 500
        exact_count: 1
        count_tag:
          inventory_group: "{{ item.group | default('') }}"
          inventory_host: "{{ item.name | default('') }}"
      register: created_instances
      with_items: "{{ instances }}"

    - name: Add EC2 instances to inventory groups.
      add_host:
        name: "{{ item.1.tagged_instances.0.public_ip }}"
        #name: "{{ item.1.tagged_instances.0.private_ip }}"
        groups: "aws,{{ item.1.item.group }},{{ item.1.item.name }}"
        # You can dynamically add inventory variables per-host.
        ansible_ssh_user: centos
        host_key_checking: false
        mysql_replication_role: >
          {{ 'master' if (item.1.item.name == 'a4d.lamp.db.1')
          else 'slave' }}
        mysql_server_id: "{{ item.0 }}"
      when: item.1.instances is defined
      with_indexed_items: "{{ created_instances.results }}"

# Run some general configuration on all AWS hosts.
- hosts: aws
  gather_facts: false

  tasks:
    - name: Wait for port 22 to become available.
      local_action: "wait_for port=22 host={{ inventory_hostname }}"

    - name: Set selinux into 'permissive' mode.
      selinux: policy=targeted state=permissive
      become: yes

Il est adapté de la manière suivante :

  • ansible_ssh_user: centos est adapté à la tâche “Add EC2 instances to inventory groups”.
  • Une clé pre_tasks s’assure que la clé nommée existe “lamp_aws”, dans le cas contraire votre clé publique par défaut sera importée sous ce nom.
  • Les variables instances.[].type, instances.[].image, instances.[].region pour une image dans la région “eu-west-3” (Paris) voir https://wiki.centos.org/Cloud/AWS.
  • En commentaire, on trouvera un exemple pour ajouter un serveur Web ou adapter la configuration en adresses IPv4 privées.

On sera attentif à la manière dont l’inventaire est nourri dynamiquement :

    - name: Add EC2 instances to inventory groups.
      add_host:
        name: "{{ item.1.tagged_instances.0.public_ip }}"
        groups: "aws,{{ item.1.item.group }},{{ item.1.item.name }}"
        # You can dynamically add inventory variables per-host.
        ansible_ssh_user: centos
        host_key_checking: false
        mysql_replication_role: >
          {{ 'master' if (item.1.item.name == 'a4d.lamp.db.1')
          else 'slave' }}
        mysql_server_id: "{{ item.0 }}"
      when: item.1.instances is defined
      with_indexed_items: "{{ created_instances.results }}"

Enfin afin de rendre ce lab unique, il sera aussi nécessaire d’adapter le terme a4d qui identifie le projet dans le livre de jeu. Voici la commande qui permet de remplacer cette occurence a4d par lab0 à partir du dossier courant ansible-for-devops/lamp-infrastructure :

find ./ -type f -print0 | xargs -0 sed -i 's/a4d/lab0/g'

3.3. Lancement du livre de jeu

Il est nécessaire d’importer tous les rôles du livre de jeu à partir du fichier requirements.yml :

ansible-galaxy install -r requirements.yml

Et puis on peut lancer le livre de jeu avec ansible-playbook.

ansible-playbook provision.yml

3.4. Inventaire statique

Il est nécessaire d’être en mesure de désigner ses éléments d’infrastructure (fichier inventories/aws/inventory) :

# Varnish
[lamp_varnish]
[a4d.lamp.varnish]

[lamp_varnish:children]
#a4d.lamp.varnish

# Apache
[lamp_www]
[a4d.lamp.www.1]
[a4d.lamp.www.2]

[lamp_www:children]
a4d.lamp.www.1
a4d.lamp.www.2

# MySQL
[lamp_db]
[a4d.lamp.db.1]
[a4d.lamp.db.2]

[lamp_db:children]
#a4d.lamp.db.1
#a4d.lamp.db.2

# Memcached
[lamp_memcached]
[a4d.lamp.memcached]

[lamp_memcached:children]
#a4d.lamp.memcached

Livres de jeu

On est invité à lire les livres de jeux suivants :

  • playbooks/varnish/main.yml, playbooks/varnish/vars.yml et playbooks/varnish/templates/default.vcl.j2
  • playbooks/www/main.yml, playbooks/www/vars.yml et playbooks/www/templates/index.php.j2
  • playbooks/db/main.yml et playbooks/db/vars.yml
  • playbooks/memcached/main.yml et playbooks/memcached/vars.yml

Il est essentiel de porter attention :

  • à la portée de chaque jeu (clé hosts:)
  • aux paramètres de pare-feu définis dans les fichiers vars.yml
  • aux mondèles Jinja2 riches d’enseignements
  • à la page “index.php” qui nous donnera un diagnostic
  • à l’usage des rôles dans les différents livres de jeu et leur paramètres (voyez leur documentation)

4. Test du déploiement

4.1. Inventaire dynamique

Les machines au moment de leur création sont identifiées par leurs “tags” qui correspondent à des groupes d’inventaire ou des hôtes d’inventaire dynamique. Si on fait commencer les livres de jeu ultérieurs par un jeu - provisioners/aws.yml, il récupérera les balises des machines pour en faire un inventaire dynamique.

Pour des tâches ultérieures sur l’infrastructure, il serait probablement plus judicieux d’utiliser le script d’inventaire ec2.py inclus :

inventories/aws/ec2.py --list

C’est le moment d’exploiter le programme ansible-inventory avec le script d’inventaire ec2.py :

ansible-inventory --inventory-file inventories/aws/ec2.py --graph
ansible-inventory -i inventories/aws/ec2.py --list --yaml
ansible -m ping -i inventories/aws/ec2.py -u centos tag_inventory_group_lamp_db
inventories/aws/ec2.py --list | jq .tag_inventory_host_a4d_lamp_varnish

4.2. Test de la solution

Mise en variable de l’URL du serveur Varnish :

varnish="http://$(inventories/aws/ec2.py --list | jq -r '.tag_inventory_host_a4d_lamp_varnish[]')"
curl $varnish
<!DOCTYPE html>
<html>
<head>
  <title>Host 52.215.204.61</title>
  <style>* { font-family: Helvetica, Arial, sans-serif }</style>
</head>
<body>
  <h1>Host 52.215.204.61</h1>
      <p>MySQL Connection (34.249.244.102):
    <span style="color: green;">PASS</span> (slave)</p>
      <p>MySQL Connection (34.252.226.132):
    <span style="color: green;">PASS</span> (slave)</p>
    <p>Memcached Connection: <span style="color: green;">PASS</span></p>
</body>
</html>
while true ; do curl -s $varnish | grep 'h1' ; done
  <h1>Host 52.215.204.61</h1>
  <h1>Host 52.215.204.61</h1>
  <h1>Host 34.253.203.217</h1>
  <h1>Host 52.215.204.61</h1>
  <h1>Host 34.253.203.217</h1>
  <h1>Host 52.215.204.61</h1>
  <h1>Host 52.215.204.61</h1>
  <h1>Host 52.215.204.61</h1>

5. Scaling horizontal

5.1. Scaling du groupe a4d_lamp_http

Comment faire évoluer horizontalement des serveurs Web ou des serveurs de base de données ?

On ajoutant un serveur dans la liste de la variable instances, par exemple :

      - name: a4d.lamp.www.3
        group: "lamp_www"
        security_group: ["default", "a4d_lamp_http"]
        type: "t2.micro"
        image: "{{ image }}"
        region: "{{ region }}"

Il sera aussi nécessaire d’adapter le fichier de variables lamp-infrastructure/playbooks/memcached/vars.yml du livre de jeu memcached qui dispose de règles iptables vers chaque membre défini du groupe lamp_www, de cette manière :

---
firewall_allowed_tcp_ports:
  - "22"
firewall_additional_rules:
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 11211 -s {{ groups['lamp_www'][0] }} -j ACCEPT"
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 11211 -s {{ groups['lamp_www'][1] }} -j ACCEPT"
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 11211 -s {{ groups['lamp_www'][2] }} -j ACCEPT"

memcached_listen_ip: "{{ ansible_default_ipv4.address }}"

5.2. Scaling du groupe a4d_lamp_db

6. Infrastructure privée et sécurisée

6.1. Gestion avec des adresse IPv4 privées

Les adresses IP publiques sont utilisées pour toutes les communications inter-instances (par exemple, communication PHP vers MySQL/Memcached, réplication maître/esclave MySQL). Pour une meilleure sécurité et potentiellement une petite amélioration des performances, vous pouvez utiliser la variable private_ip d’instances pour cette communication inter-instances au lieu de public_ip dans le livre de jeu provisioners/aws.yml.

Il alors sera nécessaire de placer le contrôlleur dans le même VPC que les hôtes cibles à créer et à configurer.1

sed -i 's/public_ip/private_ip/' provisioners/aws.yml

6.2. Règles de trafic entrant sur les groupes de sécurité

Il pourrait être judicieux d’adapter la liste des règles de trafic entrant sur les hôtes autorisés uniquement (la clé cidr_ip:) dans les variables de définition des groupes de sécurité. Plus encore, on peut aussi proposer de n’autoriser l’accès SSH aux serveurs uniquement à partir de la station de gestion. A cet égard, une installation du logiciel faib2ban pourrait trouver son utilité ici …

6.3. Règles de pare-feu local

Alors une adaptation des règles de pare-feu sur base de l’origine du trafic en autorisation sur les bons ports serait-elle une solution acceptable ?

Flux de trafic TCP

Outre le trafic de gestion SSH (TCP22), on trouve quatre flux entrants à controler dans l’infrastructure :

  • Flux 1 : Venant de l’Internet public sur le port TCP80 du load balancer HTTP Varnish.
  • Flux 2 : Venant du load balancer HTTP Varnish vers les serveurs Web (Apache) sur leur port TCP80.
  • Flux 3 : Venant des serveurs Web vers le cache de base de donnée Memcached sur son port TCP 11211.
  • Flux 4 : Venant du cache de base de donnée Memcached vers les serveurs de base de données (MySQL) sur leurs ports TCP3306.

Il s’agit d’adapter les règles de pare-feu iptables.

6.4. Filtrage des flux 4 et 3 : des serveurs HTTP vers les bases de données

Pour le group lamp_db, on autorise le trafic TCP 3306 venant des groupes d’hôtes lamp_www et lamp_memcached dans le fichier lamp-infrastructure/playbooks/db/vars.yml:

firewall_allowed_tcp_ports:
  - "22"
#  - "3306"
firewall_additional_rules:
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 3306 -s {{ groups['lamp_memcached'][0] }} -j ACCEPT"
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 3306 -s {{ groups['lamp_www'][0] }} -j ACCEPT"
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 3306 -s {{ groups['lamp_www'][1] }} -j ACCEPT"
#  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 3306 -s {{ groups['lamp_www'][2] }} -j ACCEPT"

6.5. Filtrage du flux 2 : du cache HTTP vers les serveurs HTTP

Pour le groupe lamp_www, on autorise le trafic TCP 80 venant du groupe d’hôtes lamp_varnish dans le fichier lamp-infrastructure/playbooks/www/vars.yml :

firewall_allowed_tcp_ports:
  - "22"
#  - "3306"
firewall_additional_rules:
  - "iptables -A INPUT -p tcp -d {{ ansible_default_ipv4.address }} --dport 80 -s {{ groups['lamp_varnish'][0] }} -j ACCEPT"

6.6. Filtrage du flux 1 : du l’internet vers le cache HTTP

Le serveur cache / load balancer HTTP Varnish accepte pour l’instant uniquement le trafic sur son port TCP 80 venant de partout.

7. Reverse Proxy HTTPS

Acteurs d’un scénario Reverse Proxy HTTPS :

  • Serveur DNS du domaine
  • Générateur de certificat
  • Reverse Proxy HTTPS
  • Service CDN

Comment Placer un Proxy qui sert les requêtes en HTTPS en HTTP vers les serveurs de Backend ?

8. Mise en place d’une application PHP


  1. Il s’agirait probablement d’adapter la configuration du module ec2 avec l’argument assign_public_ip: false mais un VPC spécifique sera nécessaire avec une solution d’infrastructure réseau propre à AWS. [return]