DevSecOps & Déploiement

 

Master Expert Technologie de l'information EPITECH 2020.

Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)

Formation PSPO-1 Agile Scrum 2022.

Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.

Valentin MONTAGNE

1

Mettre en place un déploiement automatique d'application

3

Maitriser la Stratégie DevOps et évoluer vers le DevSecOps

2

Les enjeux de la sécurité pour tous les environnements

Déroulement du cours

4

Implémenter une chaine de surveillance orientée Cybersécurité

Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.

1

Théorie

Après la théorie, nous verrons alors la pratique en réalisant des exercices sur un repository gitlab.

2

Pratique

Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.

3

Correction

Déroulement des journées

Connaissez-vous l'IaC / Terraform ?

Découverte de Terraform

1.

Qu'est-ce que l'Infrastructure as Code ?

Définir toute son infrastructure via des fichiers de configuration.

Automatiser la création, l'édition et la suppression des ressources de l'infrastructure dans le cloud.

Suivre les différentes versions de l'infrastructure en fonction de la solution.

Qu'est-ce que Terraform ?

Automatise le déploiement sur tous les grands Clouds.

Est capable de gérer des grands cluster avec Kubernetes.

S'intègre facilement dans les pipelines CI / CD.

Pourquoi utiliser Terraform ?

Est devenu une référence comme outil d'IaC.

Des fonctionnalités internes pour gérer des situations complexes.

Plusieurs outils CI / CD ont déjà des intégrations.

Configuration as Code vs IaC

CaC et IaC sont deux moyens de gérer les ressources de l'infrastructure, mais ils se concentrent sur des choses différentes :

Le CaC gère la configuration, les logiciels et les paramètres au sein des serveurs, comme les paramètres des utilisateurs et les configurations des applications. Ansible et Puppet sont des exemples d'outils CaC.

 

Conclusion : alors que l'IaC met en place l'environnement, le CaC s'assure que le logiciel au sein de cet environnement fonctionne correctement.

Alternative à Terraform - OpenTofu

Terraform a été mis en open-source en 2014 sous la Mozilla Public License (v2.0). Puis, le 10 août 2023, avec peu ou pas de préavis, HashiCorp a changé la licence pour Terraform de la MPL à la Business Source License (v1.1), une licence non open source.

 

OpenTofu est un fork de la version Open source de Terraform et est géré par la fondation Linux. C'est donc une bonne alternative à Terraform aujourd'hui.

La migration à OpenTofu est extrêmement simple car il n'y a pas de différence de fonctionnement avec Terraform.

Découverte de HCL

HCL, ou HashiCorp Configuration Language, est un langage lisible par l'homme pour les outils DevOps. Il est utilisé pour coder la gestion de l'infrastructure et l'orchestration des services de manière claire et gérable.

HCL est conçu pour trouver un équilibre entre un langage de configuration générique comme JSON ou YAML et un langage de script de haut niveau.

Plusieurs produits HashiCorp, dont Terraform, utilisent HCL comme langage de configuration principal. Sa syntaxe et sa structure claires permettent de créer des modules de ressources et des configurations.

La syntaxe de HCL

La syntaxe de base du langage de configuration HCL comprend la définition de blocs, d'attributs et d'expressions.

Les blocs sont des unités fondamentales telles que la ressource, le module et le provider, identifiées par des mots-clés et placées entre accolades.

Les attributs sont des paires clé-valeur à l'intérieur des blocs, où les clés sont des chaînes et les valeurs peuvent être des chaînes, des nombres ou d'autres types de données.

Les expressions permettent d'intégrer des variables, des fonctions et des références à d'autres ressources, ce qui permet des configurations dynamiques.

Exemple de la syntaxe de HCL

resource "aws_instance" "ec2" {     # <-- Bloc ressource : type "aws_instance", nom "ec2"
  ami           = data.aws_ami.amazon_linux_2.id  # <-- Expression : référence à une autre ressource
  instance_type = var.instance_type               # <-- Expression : variable d'entrée
  key_name      = "staging"                       # <-- Attribut string

  user_data = <<-EOF                # <-- Bloc d'attribut multi-lignes (heredoc)
              #!/bin/bash
              yum update -y
              yum install -y awscli jq docker
              service docker start

              export GITLAB_PASSWORD=$(echo $(aws secretsmanager get-secret-value --secret-id GITLAB_CONTAINER_REGISTRY --query SecretString --output text --region eu-west-3) | jq -r '.password')
              echo $(aws secretsmanager get-secret-value --secret-id workspace/staging --query SecretString --output text --region eu-west-3) | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" > .env

              docker login registry.gitlab.com -u vm-singularity -p $GITLAB_PASSWORD
              docker run --env-file .env -d -p 80:80 registry.gitlab.com/marvelab/workspace:${var.hash}
           EOF

  iam_instance_profile   = aws_iam_instance_profile.ec2_profile.name  # <-- Référence à une autre ressource
  vpc_security_group_ids = [aws_security_group.ec2.id]                # <-- Liste avec référence

  # Bloc d'attributs imbriqué pour les tags
  tags = {
    Name = "${var.namespace}_EC2_${var.environment}"  # <-- Interpolation de variables
  }

  user_data_replace_on_change = true  # <-- Attribut booléen
}

Providers

Qu'est-ce qu'un Provider ?

Les Providers sont des plugins qui permettent d'interagir avec diverses API externes. Ils gèrent le cycle de vie des ressources en définissant les types de ressources et les sources de données.

Chaque Provider nécessite une configuration, qui comprend généralement des détails d'authentification et des URLs.

Les Providers sont spécifiés dans le bloc provider, et plusieurs Providers peuvent être utilisés dans un seul projet Terraform pour gérer les ressources sur différentes plateformes.

Terraform Registry

Permet de découvrir, de partager et d'utiliser les modules et Providers Terraform.

 

Lien vers le Registry

Configurer un provider

La configuration est réalisée dans le bloc provider de vos fichiers de configuration Terraform. Ce bloc comprend des paramètres tels que les identifiants d'authentification, la région et d'autres paramètres spécifiques au provider.

Les providers doivent être initialisés à l'aide de terraform init pour télécharger et installer les plugins nécessaires.

Des configurations multiples peuvent être gérées en créant des alias de provider, ce qui permet de gérer les ressources dans différents environnements ou comptes au sein d'un même provider.

Version d'un provider

La spécification des versions des Providers dans Terraform garantit un comportement cohérent et prévisible dans différents environnements. La version doit être définie dans le bloc required_providers.

Cette approche permet d'éviter les changements inattendus ou les problèmes de compatibilité dus aux mises à jour des providers, améliorant ainsi la stabilité et la fiabilité de la gestion de l'infrastructure.

Exemple de configuration

# Bloc principal de configuration de Terraform
terraform {
  # Déclaration des providers nécessaires pour ce projet
  required_providers {
    aws = {
      # Le provider AWS est développé par HashiCorp (source officielle)
      source  = "hashicorp/aws"

      # Spécifie la version du provider AWS compatible
      version = "~> 4.16"
    }
  }

  # Version minimale de Terraform requise pour exécuter ce projet
  required_version = ">= 1.2.0"
}

# Configuration du provider AWS
# Ce bloc précise comment Terraform va interagir avec AWS
provider "aws" {
  # Région AWS cible : ici, eu-west-3 correspond à la région "Paris"
  region = "eu-west-3"

  # Optionnel : vous pouvez ajouter un profil si vous utilisez ~/.aws/credentials
  # profile = "mon-profil"

  # Terraform utilisera automatiquement vos identifiants AWS si :
  # - vous les avez définis via les variables d’environnement AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY
  # - ou vous avez un fichier ~/.aws/credentials correctement configuré
}

Resources

Qu'est-ce qu'une Resource ?

Représente des composants de l'infra comme les VMs, les buckets, les bases de données ou les VPS. 

Chaque déclaration génère l'élément côté Provider après execution de Terraform.

Chaque resource est configurable en fonction du Provider.

Comportement d'une Resource

Lorsque Terraform crée un nouvel objet d'infrastructure représenté par un bloc de ressources, l'identifiant de cet objet réel est enregistré dans le State de Terraform, ce qui permet de le mettre à jour et de le détruire en réponse à des changements futurs.

Pour les blocs de ressources qui ont déjà un objet d'infrastructure associé dans le State, Terraform compare la configuration réelle de l'objet avec les arguments donnés dans la configuration et, si nécessaire, met à jour l'objet pour qu'il corresponde à la configuration.

Comportement d'une Resource

L'application d'une configuration Terraform va :

  1. Créer des ressources qui existent dans la configuration mais qui ne sont pas associées à un objet d'infrastructure réel dans l'état.
  2. Détruire les ressources qui existent dans l'état mais qui n'existent plus dans la configuration.
  3. Mettre à jour les ressources en place dont les arguments ont changé.
  4. Détruire et recréer les ressources dont les arguments ont changé mais qui ne peuvent pas être mises à jour sur place en raison des limitations de l'API distante.

Accès à une Resource et Data sources

Pour accéder à une ressource dans un fichier de configuration, on utilise une Expression qui a la syntaxe suivante : <RESOURCE TYPE>.<NAME>.<ATTRIBUTE>

On peut accéder à une ressource pour ses attributs en lecture seule obtenues à partir de l'API distante ; il s'agit souvent d'éléments qui ne peuvent être connus avant la création de la ressource, comme l'id de la ressource.

De nombreux providers incluent également des Data sources, qui sont un type spécial de ressources utilisées uniquement pour rechercher des informations.

Exemple d'accès et de Data source

## Get most recent AMI for an ECS-optimized Amazon Linux 2 instance
data "aws_ami" "amazon_linux_2" {
  most_recent = true

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"]
  }

  owners = ["amazon"]
}

resource "aws_instance" "ec2" {
  ami                    = data.aws_ami.amazon_linux_2.id # <-- Expression d'accès
  instance_type          = var.instance_type
  ...

Meta arguments

Les méta-arguments dans les ressources Terraform fournissent un contrôle supplémentaire sur la façon dont les ressources sont gérées et interagissent dans la configuration :

  • count
  • for_each
  • depends_on
  • provider
  • lifecycle

Count

Permet de spécifier le nombre d'instances d'une ressource particulière à créer. Terraform génère dynamiquement plusieurs instances de la ressource, indexées de 0 à count-1.

Cette fonction est utile pour gérer des infrastructures qui nécessitent plusieurs ressources identiques ou similaires, telles que la création de plusieurs machines virtuelles ou de plusieurs buckets. Vous pouvez créer des ressources de manière conditionnelle en définissant la valeur en fonction de variables ou d'expressions.

Chaque instance de la ressource peut être référencée de manière unique à l'aide de la valeur count.index.

Depends_on

Déclare explicitement les dépendances entre les ressources, en s'assurant qu'une ou plusieurs ressources sont créées ou détruites seulement après que les ressources dépendantes spécifiées ont été appliquées avec succès.

Ceci est crucial pour gérer les dépendances de ressources qui ne sont pas automatiquement détectées par l'analyse implicite des dépendances de Terraform.

For_each

Permet de créer plusieurs instances d'une ressource en fonction d'un ensemble ou d'une map. Contrairement à count, qui utilise un simple entier, for_each permet une création de ressources plus granulaire et dynamique, puisque chaque instance est associée à une paire clé-valeur spécifique d'un objet ou d'une map.

Ce méta-argument est particulièrement utile pour créer des ressources avec des configurations uniques dérivées d'une map obtenu par un Data source par exemple.

Provider

Spécifie la configuration du provider à utiliser pour une ressource, en remplaçant la sélection du provider par défaut basée sur le nom du type de ressource.

Ceci est utile dans les scénarios où plusieurs configurations du même provider sont nécessaires, comme la gestion des ressources dans différentes régions ou environnements. En définissant l'argument provider, vous pouvez vous assurer que la ressource utilise la configuration spécifiée du fournisseur, identifiée par son alias.

Lifecycle

Personnalise le comportement des ressources lors de leur création, de leur mise à jour et de leur suppression. Il comprend des paramètres tels que create_before_destroy, qui garantit qu'une nouvelle ressource est créée avant que l'ancienne ne soit détruite, ce qui évite les temps d'arrêt.

prevent_destroy protège les ressources contre les suppressions accidentelles, et ignore_changes spécifie les attributs à ignorer lors des mises à jour, ce qui permet d'apporter des modifications externes sans déclencher de changements dans Terraform.

Variables

Qu'est-ce qu'une Variable ?

Terraform utilise des variables pour rendre les configurations plus flexibles et réutilisables. Les variables peuvent être déclarées dans des fichiers .tf et se voir attribuer des valeurs par différentes méthodes, notamment des valeurs par défaut, des flags de ligne de commande, des variables d'environnement ou des fichiers .tfvars distincts. Elles prennent en charge plusieurs types de données tels que les chaînes de caractères, les nombres, les bools, les listes et les maps. Les variables peuvent être référencées dans toute la configuration à l'aide du préfixe var.<myVariable>.

Ce système permet à l'infrastructure en tant que code d'être plus dynamique et de s'adapter à différents environnements ou cas d'utilisation.

Input Variable

Les variables d'entrée Terraform sont des paramètres pour les modules, déclarés à l'aide de blocs de variables. Elles prennent en charge plusieurs types de données, des valeurs par défaut et des descriptions. Les utilisateurs fournissent des valeurs lorsqu'ils invoquent des modules ou exécutent Terraform.

Elles peuvent être marquées comme sensibles pour des raisons de sécurité et sont généralement définies dans un fichier variables.tf.

 

Exemple : Définir lors l'exécution que les instances EC2 seront de type T3.micro.

Contraintes de type

Les contraintes de type de variable Terraform spécifient les types de données autorisés pour les variables d'entrée. Elles incluent les types primitifs (string, number, bool), les types complexes (list, set, map, object), et any pour les types non spécifiés.

Les contraintes peuvent imposer des structures spécifiques, des types imbriqués ou des plages de valeurs. Elles sont définies dans l'argument de type du bloc de variables, ce qui permet de détecter rapidement les erreurs et de garantir une utilisation correcte des variables dans toutes les configurations.

Contraintes de type - Exemple

# Déclaration d'une variable nommée "buckets"
variable "buckets" {
  # On impose une contrainte de type stricte à cette variable :
  # il s'agit d'une LISTE d'OBJETS, chacun représentant un bucket à créer
  type = list(object({

    # Chaque objet de la liste doit obligatoirement avoir une clé "name"
    # de type chaîne de caractères (string)
    name = string

    # Clé facultative : "enabled"
    # de type booléen, qui indique si le bucket est actif ou non
    # Valeur par défaut : true (si non précisée dans l’appel)
    enabled = optional(bool, true)

    # Clé facultative : "website", qui elle-même est un objet complexe
    website = optional(object({
      index_document = optional(string, "index.html")
      error_document = optional(string, "error.html")
      routing_rules = optional(string)
    # Valeur par défaut pour l’objet website : objet vide {}
    }), {})

  }))
}

Local Values

Les valeurs locales peuvent être considérées comme un nom attribué à toute expression afin de pouvoir l'utiliser plusieurs fois directement par le nom dans votre module terraform. Les valeurs locales sont appelées locals et peuvent être déclarées à l'aide du bloc locals. Les valeurs locales peuvent être des constantes littérales, des attributs de ressources, des variables ou d'autres valeurs locales. Les valeurs locales sont utiles pour définir des expressions ou des valeurs que vous devez utiliser plusieurs fois dans le module, car elles permettent d'actualiser facilement la valeur en mettant simplement à jour la valeur locale. On peut accéder à une valeur locale en utilisant l'argument local comme local.<nom_de_la_valeur>.

Local Values - Exemple

locals {
  # Ids for multiple sets of EC2 instances, merged together
  instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}

locals {
  # Common tags to be assigned to all resources
  common_tags = {
    Service = local.service_name
    Owner   = local.owner
  }
}

# Utilisation :
resource "aws_instance" "blue" {
  # ...

  tags = local.common_tags
}

Variables d'environnement

Les variables d'environnement peuvent être utilisées pour modifier le comportement par défaut de terraform, par exemple augmenter la verbosité, mettre à jour le chemin du fichier journal, définir l'espace de travail, etc.

TF_VAR_name permet de donner une valeur à certaines variables comme par exemple : 

export TF_VAR_region=us-west-1

export TF_VAR_ami=ami-049d8641

export TF_VAR_alist='[1,2,3]'

export TF_VAR_amap='{ foo = "bar", baz = "qux" }'

Validation Rules

Les règles de validation peuvent être utilisées pour spécifier des validations personnalisées pour une variable. L'ajout de règles de validation a pour but de rendre la variable conforme aux règles. Les règles de validation peuvent être ajoutées à l'aide d'un bloc de validation dans un block variable.

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    condition     = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

Outputs

Qu'est-ce qu'un Output ?

Un output expose les valeurs sélectionnées d'une configuration ou d'un module, les rendant accessibles aux utilisateurs ou à d'autres modules.

Définies dans des blocs de sortie, généralement dans un fichier outputs.tf, ils peuvent faire référence à des attributs de ressources ou à d'autres valeurs calculées.

Les sorties sont affichées après les opérations d'application, peuvent être interrogées à l'aide de commande Terraform et sont essentielles pour transmettre des informations entre les modules ou à des systèmes externes.

Syntaxe d'un Output

# Déclaration d'une sortie (output) dans Terraform
# Les outputs permettent d'exposer certaines valeurs après l'exécution.
output "name" {
  # La valeur que Terraform affichera en sortie
  # Il peut s’agir d’un nom de ressource, d’un attribut, d’une variable, etc.
  # Exemple concret : aws_s3_bucket.mon_bucket.bucket
  value = expression

  # (Facultatif) Une description textuelle de la sortie
  # Utile pour la documentation et la compréhension dans les grands projets
  description = "Optional description"

  # (Facultatif) Marque cette sortie comme sensible
  # Cela empêche Terraform d'afficher la valeur dans la console ou les logs
  # Exemple d'utilisation : mots de passe, clés d’API, tokens
  sensitive = bool
}

Sensitive Output

L'attribut sensitive est une fonctionnalité utilisée pour protéger les informations sensibles dans les configurations Terraform. Lorsqu'une sortie est marquée comme sensible, Terraform obscurcit sa valeur dans la sortie de la console, en l'affichant sous la forme <sensitive> au lieu de la valeur réelle.

 

Cette fonction est essentielle pour protéger les données sensibles telles que les mots de passe ou les clés d'API.

Le CLI Terraform

Les commandes principales

Terraform possède un CLI permettant d'exécuter des commandes.

Voici les commandes principales à connaître pour vérifier, planifier, lancer et détruire votre infrastructure :

  • terraform fmt
  • terraform validate
  • terraform plan
  • terraform apply
  • terraform destroy

Format

Formate automatiquement les fichiers de configuration dans un style cohérent. Elle ajuste l'indentation, aligne les arguments et trie les blocs et les arguments par ordre alphabétique. La commande réécrit les fichiers de configuration Terraform (.tf et .tfvars) dans le répertoire courant et ses sous-répertoires.

Elle est utilisée pour maintenir un style cohérent entre les projets et les équipes, améliorant la lisibilité et réduisant les conflits de fusion.

 

Bonne pratique : utiliser un git hook pour automatiser le lancement de la commande avant chaque commit.

Validate

Permet de vous assurer que votre code Terraform est syntaxiquement correct avant de le déployer.

Vous évitez ainsi les erreurs de configuration dues à des attributs manquants ou à des dépendances incorrectes, ce qui vous permet de gagner du temps, d'améliorer l'efficacité et de réduire les coûts.

 

Bonne pratique : utiliser un git hook pour automatiser le lancement de la commande avant chaque commit.

Plan

Crée un plan d'exécution, montrant les changements que Terraform va apporter à votre infrastructure.

Il compare l'état actuel avec l'état souhaité défini dans les fichiers de configuration et produit une liste détaillée des ressources à créer, modifier ou supprimer. Il est important de noter qu'il n'apporte aucun changement réel à l'infrastructure, mais qu'il aide à identifier les problèmes potentiels avant d'appliquer les changements. Le plan peut être enregistré dans un fichier en vue d'une exécution ou d'une révision ultérieure.

Bonne pratique : Lancer la commande dans un job de votre CI pour valider les modifications d'infrastructure proposées.

Apply

Mets en œuvre les changements définis dans vos fichiers de configuration Terraform. Elle crée, met à jour ou supprime les ressources d'infrastructure spécifiées afin qu'elles correspondent à l'état souhaité.

Avant d'effectuer les changements, elle affiche un plan similaire à terraform plan et demande une confirmation, sauf si l'option -auto-approve est utilisée.

Apply met à jour le fichier d'état pour refléter l'état actuel de l'infrastructure, ce qui permet à Terraform de suivre et de gérer les ressources au fil du temps. Il gère les dépendances entre les ressources, en les créant dans le bon ordre.

Destroy

Supprime toutes les ressources gérées par une configuration Terraform. Elle crée un plan de suppression de toutes les ressources et demande une confirmation avant l'exécution. Cette commande est utile pour nettoyer des environnements temporaires ou mettre hors service des infrastructures entières.

Elle supprime les ressources dans l'ordre inverse de leurs dépendances pour garantir un démantèlement correct.

Après la destruction, Terraform met à jour le fichier state pour refléter les changements, mais il est important de gérer ou de supprimer ce fichier si le projet est complètement déclassé.

Qu'est-ce qu'un State ?

Permet de suivre l'état actuel de votre infrastructure gérée. Il est généralement stocké dans un fichier nommé terraform.tfstate, qui associe les ressources du monde réel à votre configuration.

Cet état permet à Terraform de déterminer quels changements sont nécessaires pour obtenir la configuration souhaitée.

Il contient des informations sensibles et doit être stocké de manière sécurisée, souvent dans des backends distants comme S3 ou Terraform Cloud.

Les bonnes pratiques

  • Stockez les fichiers d'état à distance dans des backends cryptés et contrôlés par version, tels que S3 ou Terraform Cloud, pour permettre l'accès de l'équipe et renforcer la sécurité.
  • Mettez en œuvre le verrouillage des états pour empêcher les modifications simultanées. Utiliser des espaces de travail ou des fichiers d'état distincts pour différents environnements.
  • Sauvegardez régulièrement les fichiers d'état et activez le contrôle de version pour les capacités de retour en arrière.
  • Évitez de stocker des données sensibles directement dans l'état ; utilisez plutôt des outils de gestion des secrets.
  • Conservez les fichiers d'état séparément de votre configuration Terraform dans le contrôle de version.

Intégration à une CI / CD

Terraform est un très bon outil pour réaliser du CI / CD dans un projet. Il permet d'automatiser le déploiement de l'infrastructure à chaque nouvelle version de votre application.

Pour cela, je vous propose de consulter un exemple simple d'un déploiement d'une image Docker sur AWS via Github Actions :

Lien vers le projet exemple

 

Et la slide suivante pour voir le job permettant le déploiement.

Déployer l'image Docker avec Terraform sur un Cloud Provider

deploy-image:
    runs-on: ubuntu-latest
    needs: build-image
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: eu-west-3
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
      - name: Terraform Init
        run: terraform init
      - name: Terraform Format
        run: terraform fmt -check
      - name: Terraform Plan
        run: terraform plan -input=false
      - name: Terraform Apply
        run: terraform apply -auto-approve -input=false
        # Add condition here to avoid to deploy when checking a PR.

TP - Prise en main de Terraform en local

Installation de Terraform

En premier lieu, nous allons maintenant installer Terraform sur votre machine : https://developer.hashicorp.com/terraform/install

Vérifiez que vous Terraform est bien installé avec la commande suivante : terraform -v

Ensuite, créez le dossier "TP-local" et créez à l'intérieur le "main.tf" qui sera le fichier principal de votre infrastructure :

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}

provider "local" {}

resource "local_file" "test_file" {
  content  = "Hello depuis Terraform !"
  filename = "${path.module}/hello.txt"
}

Initialisation du projet

Pour l'instant, nous allons utiliser le provider "local" qui permet de réaliser des changements en local sur votre machine.

Dans le fichier main.tf, la resource local_file permet de générer un nouveau fichier.

Initialisez le projet avec la commande :

terraform init

Lancez les commandes suivantes :

  1. terraform fmt
  2. terraform validate
  3. terraform plan
  4. terraform apply

 

Vérifiez que les changements, que remarquez-vous ?

Modification du projet

Nous allons maintenant modifier notre "infrastructure", changer le contenu de votre fichier hello.txt qui a pour nom "test_file" dans votre configuration.

Relancez les différentes commandes précédentes.

 

Que remarquez-vous ?

Utilisation de commande

Nous allons maintenant ajouter une commande qui va s'exécuter automatiquement.

Ajoutez dans votre main.tf le code suivant :

 

 

 

Ajoutez les dépendances nécessaire avec la commande :

terraform init -upgrade

 

Exécutez à nouveau la configuration, qu'observez-vous ?

resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "echo 'Commande exécutée !'"
  }
}

Détruire l'infrastructure

Maintenant que nous avons terminé avec cette configuration, vous allez pouvoir la détruire, pour cela exécutez la commande :

terraform destroy

 

Qu'observez-vous ?

 

Oui, le fichier terraform.tfstate est toujours présent avec sa sauvegarde, nous allons les supprimer car nous n'allons plus utiliser cette configuration.

Utilisation de Docker

Nous allons maintenant créer une infrastructure en utilisant Docker pour simuler ce que l'on va pouvoir réaliser avec un Cloud Provider. Remplacez le contenu de main.tf par celui-ci :

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.5.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = true
}

resource "docker_container" "nginx" {
  name  = "nginx-terraform"
  image = docker_image.nginx.image_id
  ports {
    internal = 80
    external = 8080
  }
}

Utilisation de Docker

Attention, pour MacOS et Windows, cela peut ne pas fonctionner sans plus de configuration.

Pour Windows, vous devez allez via General > Expose Daemon on tcp://localhost:2375 et ajouter dans le block provider "docker" la ligne :

host = "tcp://localhost:2375".

Pour MacOS, vous devez mettre le bon chemin :

host = "unix://${pathexpand("~/.docker/run/docker.sock")}"

Exécutez cette configuration.

 

Que remarquez-vous ?

Appelez-moi pour que valider ensemble.

Clean up

Bravo, vous avez terminé ce premier TP !

Je vous invite maintenant à supprimer l'infrastructure actuelle et de passer aux exercices.

Exercice 1 : Variables

Ajoutez des variables Terraform à la place des valeurs codées en dur suivantes :

  1. Le nom de l’image Docker (nginx:latest)

  2. Le nom du conteneur

  3. Le port externe exposé

  4. Le port interne du conteneur

Vous devez :

  • Déclarer chaque variable dans le fichier variables.tf

  • Fournir des valeurs par défaut

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 2 : Output

Ajoutez un output nommé nginx_container_id à votre configuration Terraform.

Celui-ci doit afficher l’identifiant (id) du conteneur nginx créé avec la ressource docker_container.nginx.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 3 : Créer une commande

Ajoutez une commande à votre configuration Terraform qui permet de tester automatiquement que le serveur Nginx déployé sur le port spécifié répond avec une page d’index.

La commande doit tester l’URL :

http://localhost:<external_port> 

Et vérifier que la réponse contient le mot Welcome (présent dans l’index par défaut de Nginx).

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 4 : Créer un autre container Docker

Ajoutez un deuxième conteneur Docker nommé client, basé sur l’image appropriate/curl, qui exécutera une commande pour appeler le serveur nginx (déjà déployé).

Pour cela, vous devez :

  1. Créer un réseau Docker dédié.

  2. Connecter les deux conteneurs à ce réseau.

  3. Faire en sorte que le conteneur client utilise curl pour interroger http://nginx:80 et sleep ensuite.

  4. Vérifier que la communication fonctionne dans le conteneur client.

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 5 : Utilisation de count

Reprenez l'exercice précédent, puis :

  1. Modifiez le conteneur client pour qu’il soit déployé en plusieurs exemplaires (ex. 3) à l’aide de count.

  2. Assurez-vous que chaque conteneur :

    • ait un nom unique (client-0, client-1, etc.),

    • soit connecté au même réseau Docker que nginx,

    • exécute un curl http://nginx suivi d’un sleep de 30 secondes.

  3. Le nombre de clients doit être paramétrable via une variable.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 6 : Utilisation de for_each

Reprenez l'exercice précédent, transformez le count par un for_each qui doit boucler sur une liste de nom pour vos serveurs.

 

Chaque serveur doit posséder le bon nom :

server-<nom>

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 7 : Utilisation des types de variables

Vous êtes en train de créer un module Terraform qui déploie des machines virtuelles. Chaque machine doit être définie avec : un nom, un nombre de vCPU (min. 2, max. 64), une taille de disque (en Go, min. 20), une région (parmi : "eu-west-1", "us-east-1", "ap-southeast-1").

Créez une variable machines de type liste d'objets contenant les 4 attributs cités.

Ajoutez une validation personnalisée sur : les vcpu (entre 2 et 64),

le disk_size (>= 20), la region.

 

Appelez-moi pour que l'on puisse valider ensemble.

Déployer avec Terraform sur un Cloud Provider

2.

Configurer le Provider AWS

Pour utiliser un Provider comme AWS, il faut le configurer pour qu'il puisse se connecter à notre compte AWS.

Pour cela, la documentation du Provider indique plusieurs manières de faire :

  1. Paramètres dans la configuration du fournisseur
  2. Variables d'environnement
  3. Fichiers d'identification partagés
  4. Fichiers de configuration partagés
  5. Informations d'identification du conteneur
  6. Informations d'identification du profil d'instance et région

 

Je recommande d'utiliser la méthode 2 qui est pour moi plus sécurisé.

Utiliser les variables d'environnement

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}

# Create a VPC
resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

# terminal
% export AWS_ACCESS_KEY_ID="anaccesskey"
% export AWS_SECRET_ACCESS_KEY="asecretkey"
% terraform plan

Exemple de configuration :

Les ressources avec AWS

Le Provider AWS possède plus de 1499 ressources, c'est pour cela que nous n'allons voir que les plus simples durant cet atelier.

Débutons par la ressource de base, l'instance AWS qui est une instance EC2 :

Lien vers la documentation complète

(Le panel de gauche permet de rechercher à l'intérieur)

 

Un exemple est disponible à la slide suivante.

Instance EC2 - Exemple

resource "aws_instance" "ec2" {
  # ID de l'AMI Amazon Linux 2, récupéré dynamiquement via un data source
  ami = data.aws_ami.amazon_linux_2.id
  # Type d'instance défini via une variable (ex: t2.micro, t3.medium, etc.)
  instance_type = var.instance_type
  # Nom de la clé SSH permettant d'accéder à l'instance
  key_name = "staging"

  # Script shell exécuté au démarrage de l'instance (cloud-init)
  user_data = <<-EOF
              #!/bin/bash
              yum update -y                           # Mise à jour des paquets
              yum install -y awscli jq docker         # Installation de l’AWS CLI, jq (JSON parser) et Docker
              service docker start                    # Démarrage du service Docker
           EOF

  # Profil IAM attaché à l’instance pour lui donner des permissions AWS (ex: accès à S3)
  iam_instance_profile = aws_iam_instance_profile.ec2_profile.name

  # Liste des IDs de groupes de sécurité associés à l’instance
  vpc_security_group_ids = [aws_security_group.ec2.id]

  # Étiquettes (tags) appliquées à l’instance pour l'organisation et le suivi
  tags = {
    Name = "${var.namespace}_EC2_${var.environment}"  # Exemple : "dev_EC2_staging"
  }

  # Forcer le remplacement de l'instance si le user_data change
  user_data_replace_on_change = true
}

Les clés SSH avec AWS

Pour nous connecter à nos instances, nous allons avoir besoin de clés SSH. Voici comment générer et configurer une paire de clé :

ssh-keygen -t rsa -b 4096 -f ~/.ssh/my-ec2-key

Ne jamais partager la clé privée.

Exemple de configuration avec AWS :

provider "aws" {
  region = "eu-west-3"
}

resource "aws_key_pair" "my_key" {
  key_name   = "my-ec2-key"
  public_key = file("~/.ssh/my-ec2-key.pub")
}

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0" # à adapter à la région
  instance_type = "t2.micro"
  key_name      = aws_key_pair.my_key.key_name

  tags = {
    Name = "Terraform-Instance"
  }
}

TP - Déployer en local avec AWS

Installation de Localstack

Pour éviter d'utiliser directement un vrai compte AWS qui pourrait être coûteux ou laborieux quand vous devez réaliser des tests, nous allons utiliser un outil très puissant : Localstack.

Installez Localstack en suivant les instructions dans le Readme du projet.

Vérifiez l'installation de Localstack avec la commande :

localstack -v

Puis démarrez l'environnement local :

localstack start

 

Nous allons maintenant installer le CLI d'AWS.

Installation de AWS CLI

Pour MacOS :

brew install awscli

Pour Windows :

https://awscli.amazonaws.com/AWSCLIV2.msi

Pour Linux :

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

 

Vérifiez la bonne installation du CLI :

aws --version

Configurer le provider AWS

Nous allons devoir ajouter des configurations non nécessaire normalement vu que nous allons être sur un faux environnement d'AWS dans le main.tf :

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  access_key                  = "test"
  secret_key                  = "test"
  region                      = "us-east-1"
  
  # LocalStack endpoint configuration
  s3_use_path_style           = true
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  
  endpoints {
    s3       = "http://localhost:4566"
    ec2 	 = "http://localhost:4566"
  }
}

Créer un bucket

Nous allons créer une ressource emblématique d'AWS, un bucket S3 pour pouvoir stocker des fichiers.

Pour cela, nous allons créer le fichier s3.tf :

# Create an S3 bucket
resource "aws_s3_bucket" "demo_bucket" {
  bucket = "my-bucket"
}

# Enable versioning for the bucket
resource "aws_s3_bucket_versioning" "demo_bucket_versioning" {
  bucket = aws_s3_bucket.demo_bucket.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Upload a file to the bucket
resource "aws_s3_object" "demo_object" {
  bucket = aws_s3_bucket.demo_bucket.id
  key    = "hello-world.txt"
  source = "./test-file.txt"
  etag   = filemd5("./test-file.txt")
}

Créer un bucket - 2

Dans la slide précédente, on peut voir que l'on upload un fichier sur notre Bucket, nous allons donc créer le fichier test-file.txt :

 

Lancez l'initialisation du projet Terraform, planifiez et appliquez les changements. Vérifiez vos changements avec les commandes suivantes pour Linux et MacOS :

AWS_ACCESS_KEY_ID="test" AWS_SECRET_ACCESS_KEY="test" AWS_DEFAULT_REGION="us-east-1" aws --endpoint-url=http://localhost:4566 s3 ls

AWS_ACCESS_KEY_ID="test" AWS_SECRET_ACCESS_KEY="test" AWS_DEFAULT_REGION="us-east-1" aws --endpoint-url=http://localhost:4566 s3 ls s3://my-bucket

Hello, World! This is a test file for my Terraform and LocalStack demo.

Créer un bucket - 3

Pour Windows :

& { $env:AWS_ACCESS_KEY_ID = "test"; $env:AWS_SECRET_ACCESS_KEY = "test"; $env:AWS_DEFAULT_REGION = "us-east-1"; aws --endpoint-url=http://localhost:4566 s3 ls }

& { $env:AWS_ACCESS_KEY_ID = "test"; $env:AWS_SECRET_ACCESS_KEY = "test"; $env:AWS_DEFAULT_REGION = "us-east-1"; aws --endpoint-url=http://localhost:4566 s3 ls s3://my-bucket }

Créer une instance EC2

Maintenant que notre bucket est fonctionnel, nous allons créer une instance EC2 qui lancera un Nginx, créez le fichier ec2.tf :

# Generate SSH key
resource "tls_private_key" "key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

# Create key pair
resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = tls_private_key.key.public_key_openssh
}

# Store private key locally
resource "local_file" "private_key" {
  content         = tls_private_key.key.private_key_pem
  filename        = "${path.module}/deployer-key.pem"
  file_permission = "0600"
}

# Create EC2 instance with Nginx
resource "aws_instance" "web" {
  ami             = "ami-12345678"
  instance_type   = "t2.micro"
  security_groups = [aws_security_group.web.name]
  key_name        = aws_key_pair.deployer.key_name

  user_data = <<-EOF
              #!/bin/bash
              # Install and configure Nginx
              yum update -y
              amazon-linux-extras install -y nginx1
              systemctl start nginx
              systemctl enable nginx
              
              # Create a simple webpage
              echo "<h1>Hello from Terraform and LocalStack!</h1>" > /usr/share/nginx/html/index.html
              EOF

  tags = {
    Name = "nginx-server"
  }
}

Créer un groupe de sécurité

Avec AWS, la connexion à une instance doit être géré par des ressources réseau comme un groupe de sécurité, créez sg.tf :

# Create a security group for the EC2 instance
resource "aws_security_group" "web" {
  name        = "nginx-sg"
  description = "Allow web and SSH traffic"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTP traffic"
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow SSH traffic"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound traffic"
  }

  tags = {
    Name = "nginx-sg"
  }
}

Créer les Outputs

Pour vérifier que notre instance EC2 fonctionne correctement, nous allons récupérer des informations, malheureusement localstack ne va pas jusqu'à simuler une vraie VM capable d'exécuter nginx, nous allons juste vérifier si elle est bien lancé.

Créez outputs.tf :

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}

output "instance_public_ip" {
  description = "Public IP of the EC2 instance"
  value       = aws_instance.web.public_ip
}

output "ssh_command" {
  description = "SSH command to connect to the instance"
  value       = "ssh -i deployer-key.pem ec2-user@${aws_instance.web.public_ip}"
}

Modification et vérification

Lancez l'application de la nouvelle configuration et vérifiez que l'instance EC2 est bien présente :

AWS_ACCESS_KEY_ID="test" AWS_SECRET_ACCESS_KEY="test" AWS_DEFAULT_REGION="us-east-1" aws --endpoint-url=http://localhost:4566 ec2 describe-instances --instance-ids <Instance ID>

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 1 - Variables

Ajoutez des variables Terraform à la place des valeurs codées en dur suivantes :

  1. Le type d'instance EC2

  2. Le nom de l'instance EC2

  3. Le nom du bucket S3

  4. Le port par défaut pour le groupe de sécurité

Vous devez :

  • Déclarer chaque variable dans le fichier variables.tf

  • Fournir des valeurs par défaut

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 2 - Output

Ajoutez un output nommé bucket_id à votre configuration Terraform.

Celui-ci doit afficher l’identifiant (id) du bucket créé avec la ressource aws_s3_bucket.demo_bucket.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 3 - Création d'une nouvelle instance EC2

Grâce aux exercices précédents, une application web statique est déployée sur une instance EC2 avec Nginx. Nous allons maintenant déployer une nouvelle instance EC2 qui contiendra une base de donnée.

Vous devez :

  1. Créer une nouvelle instance EC2

  2. Changer la configuration pour qu'on puisse facilement l'identifier comme le serveur de la base de donnée. 

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 4 - Déployer sur AWS

Maintenant que vous avez validé plusieurs configurations grâce à Localstack, le but est de mettre en place sur un vrai compte AWS la dernière configuration et de vérifier son bon fonctionnement.

Arrêtez et retirer la configuration en lien avec Localstack et les buckets et déployez directement sur le compte AWS fournit pour l'atelier.

Créez une paire de clé et liez les à votre instance EC2 pour vous permettre de vous connecter via SSH à l'instance.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 5 - Déployer un bucket

Ajoutez un S3 bucket à votre infrastructure.

Créez un fichier en local "test-file.txt" avec comme contenu "Hello World" et ajoutez dans la configuration l'upload du fichier sur le bucket S3.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 6 - Rendre le bucket public

Maintenant que vous avez un bucket S3, ajoutez les permissions pour permettre la lecture de tous les fichiers à l'intérieur par n'importe qui.

 

On doit pouvoir accéder à l'objet S3 via son URL.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 7 - Déploiement d'une application

Récupérez un projet que vous avez déjà réalisé et réalisez une image Docker pour chaque partie de ce projet.

Déployez les images Docker de votre projet en utilisant Terraform sur le compte AWS fournit pour cet atelier sur plusieurs instances EC2.

Vous pouvez utiliser l'IP publique généré pour chaque EC2 par AWS pour les faire communiquer.

 

Appelez-moi pour que l'on puisse valider ensemble.

Configurer avec Ansible

3.

Qu'est-ce qu'Ansible ?

Logiciel open-source pour automatiser le déploiement, la configuration et la gestion d’infrastructures.

Contrairement à d’autres outils, n’utilise pas d'agent mais SSH pour se connecter et exécuter les tâches.

Les tâches à exécuter sont décrites de manière déclarative dans des fichiers au format YAML.

Pourquoi utiliser Ansible avec Terraform ?

  1. Complémentarité entre provisionnement et configuration : Terraform est idéal pour créer l’infrastructure, tandis qu’Ansible excelle dans la configuration des machines (installation de paquets, déploiement d’applications).

  2. Séparation des responsabilités : Utiliser Terraform pour gérer l’état de l’infrastructure et Ansible pour appliquer des configurations cela sépare "ce qui est" (l’infra) de "ce qu’elle fait" (les services qu’elle héberge).

  3. Flexibilité post-provisionnement : Ansible peut être relancé facilement et indépendamment pour appliquer des mises à jour, corriger une configuration ou réagir à un changement, sans toucher à l’infrastructure sous-jacente.

Les inventaires

L’inventaire (ou inventory) est un fichier ou une source dynamique qui liste tous les hôtes (machines) que Ansible peut gérer.


Il permet aussi de regrouper ces hôtes, de leur affecter des variables, et de cibler précisément les machines sur lesquelles exécuter des tâches.

 

Par défaut, l’inventaire est un fichier texte situé dans /etc/ansible/hosts ou défini via -i dans la ligne de commande.

Format d'un inventaire

[webservers]
web1.example.com ansible_host=192.168.1.100
web2.example.com ansible_host=192.168.1.101

[dbservers]
db1.example.com ansible_host=192.168.1.200 mysql_port=3306
db2.example.com ansible_host=192.168.1.201 mysql_port=3306

# Production Environment Group using another group
[production:children]
webservers
dbservers

# Specific Server Variables
[webservers:vars]
http_port=80
https_port=443

# Group with Connection Variables
[remote_servers]
remote1.example.com ansible_host=203.0.113.10 ansible_user=admin ansible_ssh_private_key_file=/path/to/private/key

# Global SSH Connection Defaults
[all:vars]
ansible_connection=ssh
ansible_user=ansible
ansible_become=yes
ansible_become_method=sudo

Les Playbooks

Un Playbook est un fichier YAML qui décrit une ou plusieurs séquences de tâches à exécuter sur un ou plusieurs hôtes.
Il permet d’automatiser des configurations, des déploiements, ou de l’orchestration de services de manière déclarative.

 

On parle de scénario, contenant des tasks, exécutées sur des hosts.

Le format d'un Playbook

---
- hosts: webservers # <-- Means the tasks will run on all servers in the webservers group
  tasks:
    - name: Ensure nginx is installed
      apt: # <-- Use APT to install packages on Debian / Ubuntu
        name: nginx
        state: present
    
    - name: Configure web server
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify:
        - Restart Nginx # <-- Call handler Restart Nginx

  handlers:
    - name: Restart Nginx
      service: # <-- Use Debian / Ubuntu's service system
        name: nginx
        state: restarted

Exemple d'un Playbook

---
- hosts: webservers
  become: yes  # Use sudo for elevated privileges
  vars:
    # Centralized variables for easy management
    web_package: nginx
    web_port: 80
    application_name: myapp
    app_root_directory: "/var/www/{{ application_name }}"
    
  # Pre-tasks run before roles and tasks
  pre_tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600  # Only update if older than 1 hour
      when: ansible_os_family == 'Debian'

  # Roles allow for modular, reusable configuration
  roles:
    - role: security
      tags: 
        - security
    
    - role: webserver
      tags:
        - web

  # Individual tasks for specific configurations
  tasks:
    - name: Ensure web server is installed
      apt:
        name: "{{ web_package }}"
        state: present
      tags:
        - packages

    - name: Create application directory
      file:
        path: "{{ app_root_directory }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
      tags:
        - app-setup

    - name: Deploy application code
      git:
        repo: 'https://github.com/yourorg/yourapp.git'
        dest: "{{ app_root_directory }}"
        version: main  # Specify branch/tag
      notify: 
        - Restart Nginx
      tags:
        - deployment

    - name: Configure Nginx virtual host
      template:
        src: templates/nginx-site.conf.j2
        dest: "/etc/nginx/sites-available/{{ application_name }}"
      notify: 
        - Validate Nginx Configuration
        - Restart Nginx
      tags:
        - configuration

    - name: Enable Nginx site
      file:
        src: "/etc/nginx/sites-available/{{ application_name }}"
        dest: "/etc/nginx/sites-enabled/{{ application_name }}"
        state: link
      notify: 
        - Restart Nginx
      tags:
        - configuration

    - name: Set up application environment
      copy:
        content: |
          DB_HOST={{ database_host }}
          DB_USER={{ database_user }}
          SECRET_KEY={{ secret_key }}
        dest: "{{ app_root_directory }}/.env"
        owner: www-data
        group: www-data
        mode: '0600'
      no_log: true  # Prevents logging sensitive information
      tags:
        - secrets

  # Handlers for service management
  handlers:
    - name: Validate Nginx Configuration
      command: nginx -t
      register: nginx_test
      failed_when: nginx_test.rc != 0
      changed_when: false

    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

  # Post-tasks run after roles and tasks
  post_tasks:
    - name: Verify application is responding
      uri:
        url: "http://localhost:{{ web_port }}"
        status_code: 200
      register: webapp_response
      retries: 3
      delay: 5
      until: webapp_response.status == 200
      tags:
        - validation

# Example of a separate play for database servers
- hosts: dbservers
  become: yes
  roles:
    - role: database
      tags:
        - database

# Conditional play for monitoring servers
- hosts: monitoring
  become: yes
  tasks:
    - name: Install monitoring tools
      apt:
        name: 
          - prometheus
          - grafana
        state: present
      when: monitoring_enabled | default(false)

TP - Configurer des serveurs

Installation de Ansible et multipass

Installez Ansible avec la commande suivante :

pipx install --include-deps ansible

 

Puis, installez Multipass, qui nous permettra de simuler des serveurs.

 

Vérifiez que tout est bien installé :

multipass -v

ansible -v

Créer les serveurs

Créez les serveurs nécessaires pour la suite du TPs :

 

 

Ensuite, générez la paire de clé SSH qui permettra à Ansible de se connecter à nos serveurs :

multipass launch -n web-server && multipass launch -n db-server
# Generate SSH key if you don't have one
ssh-keygen -t rsa -b 4096 -f ./ansible_test_key

# Add SSH key to instances
multipass exec web-server -- mkdir -p /home/ubuntu/.ssh
multipass exec db-server -- mkdir -p /home/ubuntu/.ssh

# Copy public key to instances
multipass exec web-server -- sh -c "echo '$(cat ./ansible_test_key.pub)' >> /home/ubuntu/.ssh/authorized_keys"
multipass exec db-server -- sh -c "echo '$(cat ./ansible_test_key.pub)' >> /home/ubuntu/.ssh/authorized_keys"

Créer l'inventaire

Créez ensuite le fichier inventory avec les informations des serveurs :

[webservers]
web-server ansible_host=<web-server IP> # <-- multipass info web-server | grep IPv4

[dbservers]
db-server ansible_host=<db-server IP> # <-- multipass info db-server | grep IPv4

[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=./ansible_test_key
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

Créer le Playbook

Créez ensuite le fichier playbook.yml avec les tâches à réaliser pour Ansible :

---
- hosts: all
  become: yes
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install basic packages
      apt:
        name:
          - htop
          - vim
          - curl
        state: present

- hosts: webservers
  become: yes
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present

- hosts: dbservers
  become: yes
  tasks:
    - name: Install MySQL
      apt:
        name: 
          - mysql-server
          - python3-pymysql
        state: present

Appliquer la configuration

Vérifiez que Nginx et Mysql ne sont pas déjà présent sur les serveurs :

 

 

 

Vous devriez avoir une erreur pour les deux commandes.

Maintenant, lancez Ansible pour configurer les serveurs :

 

 

Réessayez les commandes pour vérifier que Nginx et Mysql sont bien présents.

 

Appelez-moi pour que l'on puisse valider ensemble.

# Run the playbook
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i inventory playbook.yml
# Connectez-vous à web-server pour lancer la commande dans son terminal
curl localhost

# Connectez-vous à db-server pour lancer la commande dans son terminal
mysql -v

Exercice 1 - Configurer Docker

Ajoutez dans le Playbook Ansible tout ce qui est nécessaire pour ajouter au web-server le service Docker.

Retirez Nginx et lancez l'image traefik/whoami via Ansible.

Vérifiez que votre web-server affiche quelque chose sur le port 80.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 2 - Ansible et Terraform

Déployez avec Terraform, 3 instances EC2 et créez automatiquement un inventory avec toutes les informations nécessaires pour que Ansible puisse configurer ces instances.

(IP, Clés SSH, etc)

Appliquez sur les 3 instances une configuration via un Playbook qui installe et lance sur toutes les instances Nginx.

Vérifiez sur vos instances EC2 que Nginx réagit au port 80.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 3 - AWS et Docker

Déployez avec Terraform une instance EC2 "webserver" et une instance EC2 "dbserver" et créez automatiquement un inventory avec toutes les informations nécessaires pour que Ansible puisse configurer ces instances.

(IP, Clés SSH, etc)

Appliquez sur l'instance "webserver" une configuration via un Playbook qui installe et lance l'image Docker Nginx et sur l'instance "dbserver" une base de donnée MySQL.

Vérifiez sur vos instances EC2 que Nginx et MySQL soient bien présents.

 

Appelez-moi pour que l'on puisse valider ensemble.

Bonus - Exercice 4 - Configurer un Docker Swarm

Ajoutez dans le Playbook Ansible tout ce qui est nécessaire pour déployer un cluster Docker Swarm sur vos instances.

Créer un manager et deux workers, il faut sauvegarder via Ansible les informations du manager pour permettre aux workers de rejoindre le cluster.

Utilisez l'image traefik/whoami pour le cluster via Ansible.

 

Vérifiez que votre cluster fonctionne correctement.

 

Appelez-moi pour que l'on puisse valider ensemble.

Bonus - Exercice 5 - Déployer et configurer une application

Déployez avec Terraform une infrastructure pour mettre en ligne une application que vous avez déjà réalisé précédemment.

Créez les images Docker de votre projet pour pouvoir ensuite utiliser Ansible pour configurer l'ajout et l'utilisation des images Docker et d'autres images nécessaires pour votre projet sur vos instances déployées par Terraform.

 

Appelez-moi pour que l'on puisse valider ensemble.

Projet

Déploiement automatisé et sécurisé

Les enjeux de la sécurité pour les environnements

4.

Les bonnes pratiques de sécurité et de qualité

4.1

Qu'est-ce que le Security by Design ?

Approche consistant à intégrer la sécurité dès la conception d’un système, d’une application ou d’une architecture.

Au lieu d’ajouter des mécanismes de sécurité après coup, on réfléchit dès le départ à :

  • Quels sont les risques et menaces ?

  • Quelles mesures techniques et organisationnelles mettre en place ?

Cela réduit les failles structurelles, diminue les coûts de correction et améliore la robustesse globale.

Des exemples de Security by Design

Chiffrement des données prévu dès la phase de conception (et non ajouté après une fuite).

Modélisation des menaces (threat modeling) en phase de design.

Utilisation du principe du moindre privilège dans l’architecture.

Qu'est-ce que le Security by Default ?

Principe qui impose qu’un système soit sécurisé dès son installation et sa configuration initiale, sans action supplémentaire de l’utilisateur :

  • Les paramètres par défaut doivent privilégier la sécurité, quitte à limiter certaines fonctionnalités.

  • Les options moins sûres doivent être explicitement activées par l’utilisateur conscient du risque.

Cela protège les utilisateurs non experts et évite que des failles soient exploitables simplement parce que les réglages de base étaient trop permissifs.

Des exemples de Security by Default

Compte administrateur désactivé par défaut.

Communication chiffrée activée (HTTPS/TLS par défaut).

Mot de passe fort obligatoire à la première connexion.

La différence entre le par défaut et par design

  • By Design = sécurité pensée et intégrée dès la conception (amont).

  • By Default = sécurité appliquée dans la configuration initiale (aval, côté déploiement/utilisateur).

 

Les deux approches sont complémentaires : un système peut avoir une architecture sécurisée (by design), mais rester vulnérable si ses paramètres par défaut sont laxistes (by default).

Qu'est-ce que le Twelve Factor App ?

La Twelve-Factor App est un ensemble de 12 bonnes pratiques proposées par des ingénieurs de Heroku (2011) pour concevoir des applications cloud-native, scalables, portables et faciles à maintenir.


Elle s’applique surtout aux applications SaaS et aux environnements de déploiement modernes (Docker, Kubernetes, PaaS).

Qu'est-ce que le Twelve Factor App ?

1. Codebase : une seule base de code, déployée sur plusieurs environnements.

2. Dépendances : déclarer et isoler explicitement toutes les dépendances.

3. Config : séparer la configuration du code. (.env)

Qu'est-ce que le Twelve Factor App ?

4. Backing services : traiter les services externes comme des ressources interchangeables.

5. Build, release, run : séparer ces étapes.

6. Stateless : l’application s’exécute comme un ou plusieurs processus stateless.

Qu'est-ce que le Twelve Factor App ?

7. Port binding : l’app expose elle-même son service via un port, sans dépendre d’un serveur externe

8. Concurrency : mise à l’échelle par duplication de processus (horizontal scaling).

9. Disposability : Les processus doivent démarrer/arrêter rapidement.

Qu'est-ce que le Twelve Factor App ?

10. Dev/prod parity : garder les environnements de dev, staging et prod similaires.

11. Logs : les logs sont des flux d’événements envoyés vers un agrégateur externe.

12. Admin processes : les tâches admin sont à faire dans l'environnement de l’app.

Pourquoi suivre Twelve Factor App ?

  • Portabilité : pas d’attache forte à une infra spécifique.

  • Scalabilité : facile à déployer sur le cloud, répartir la charge.

  • Résilience : redémarrage rapide, tolérance aux pannes.

  • Maintainabilité : séparation claire du code, de la config et des dépendances.

En résumé, la Twelve-Factor App est une philosophie de développement qui permet de construire des applications cloud-native robustes, adaptées aux microservices et aux déploiements modernes.

Qu'est-ce que Least Privilege ?

Le principe du moindre privilège (en anglais Least Privilege) consiste à donner à un utilisateur, un processus ou un système uniquement les droits strictement nécessaires pour accomplir sa tâche — ni plus, ni moins.

 

C’est un des fondements de la cybersécurité : plus les privilèges sont limités, plus on réduit la surface d’attaque et les conséquences d’une compromission.

Exemples d'application

  • Utilisateurs : un employé n’a accès qu’aux ressources nécessaires à son poste (ex. un comptable n’a pas accès aux serveurs de production).

  • Applications : un service web qui doit uniquement lire une base de données n’a pas de droit d’écriture.

  • Systèmes : les comptes administrateurs ne sont utilisés que pour les tâches critiques, pas pour le quotidien.

  • Réseau : pare-feu configuré pour n’ouvrir que les ports requis.

Pourquoi utiliser le Least Privilege ?

Réduction des dégâts en cas de compromission (l’attaquant est limité dans ses actions).

Limitation des erreurs humaines (moins de possibilités de fausses manipulations).

Conformité avec les standards (ISO 27001, NIST, RGPD, etc.).

Les défis

Bien analyser les besoins réels pour attribuer les bons droits.

Éviter les exceptions trop larges ("accès admin temporaire") qui deviennent permanentes.

Maintenir la règle dans le temps (droits évolutifs selon les missions).

Comment sécuriser une appplication ?

4.2

Le Secure Coding et les bonnes pratiques

Le Secure Coding (ou développement sécurisé) désigne l’ensemble des méthodes et bonnes pratiques de programmation visant à prévenir les failles de sécurité dès la phase de développement.


Le but est d’écrire du code robuste, fiable et résistant aux attaques.

Pourquoi suivre le Secure Coding ?

70 à 80 % des vulnérabilités viennent d’erreurs de développement (OWASP, NIST).

Corriger une faille pendant le développement coûte 10 à 100 fois moins cher qu’en production.

Répond aux exigences de conformité (ISO 27034, RGPD, PCI-DSS).

1. Validation et assainissement des entrées

Par exemple :

  • Ne jamais faire confiance aux données fournies par l’utilisateur.

  • Filtrer, valider et échapper toutes les entrées (input validation).

  • Exemple : protéger contre l’injection SQL, XSS.

2. Gestion sécurisée de l’authentification et des sessions

Par exemple :

  • Stocker les mots de passe avec des algorithmes robustes (bcrypt, Argon2).
  • Utiliser l’authentification multifactorielle (MFA).

  • Générer des jetons de session aléatoires et limités dans le temps.

3. Contrôle d’accès strict (Least Privilege)

Par exemple :

  • Implémenter des vérifications côté serveur (pas seulement côté client).

  • Appliquer le principe du moindre privilège.

4. Gestion sécurisée des erreurs et logs

Par exemple :

  • Ne pas divulguer d’informations sensibles dans les messages d’erreur.

  • Centraliser et protéger les logs (pour détection et forensic).

5. Chiffrement et protection des données

Par exemple :

  • Chiffrer les données en transit (TLS 1.3) et au repos (AES-256).

  • Ne pas réinventer ses propres algorithmes : utiliser des bibliothèques éprouvées.

6. Mises à jour et dépendances sûres

Par exemple :

  • Maintenir les bibliothèques et frameworks à jour.

  • Vérifier les dépendances contre des bases de vulnérabilités (ex. Snyk, Dependabot).

7. Sécurité du code source

Par exemple :

  • Ne jamais stocker de secrets (mots de passe, clés API) en clair dans le code.

  • Utiliser des gestionnaires de secrets (Vault, AWS Secrets Manager, etc.).

8. Tests de sécurité

Par exemple :

  • Revue de code (peer review).

  • Tests automatisés de sécurité (linting, SAST, DAST).

  • Tests de pénétration avant mise en production.

Les références en sécurité

  • OWASP Top 10 - liste des 10 vulnérabilités web les plus critiques.
  • OWASP ASVS : Application Security Verification Standard.
  • ISO/IEC 27034 : normes internationales sur la sécurité applicative.
  • OWASP Checklists : Permet d'auditer facilement la solution ou une fonctionnalité.

La Code Review

Processus collaboratif où des développeurs examinent le code écrit par leurs pairs avant son intégration (merge) afin d’améliorer sa qualité, lisibilité, maintenabilité et sécurité.

 

Le but :

  • Détecter les erreurs logiques ou de style.

  • Identifier des failles potentielles de sécurité.

  • Améliorer la cohérence et le partage de connaissances dans l’équipe.

Les bonnes pratiques

Employer des checklists (qualité, sécurité, performance).

Favoriser des discussions constructives (pas du jugement personnel).

Compléter la review manuelle avec des outils automatisés (linters, SAST).

Le Testing

Ensemble des activités visant à vérifier le bon fonctionnement d’un logiciel selon les spécifications attendues :

  • Unitaires : testent des fonctions ou modules isolés.

  • Intégration : vérifient la coopération entre composants.

  • Système / end-to-end : testent l’application complète dans un contexte proche de la prod.

  • Régression : s’assurent qu’une modification n’a pas introduit de bug.

  • Performance / charge : évaluent robustesse et montée en charge.

Le Security Testing

Sous-domaine du testing qui vise à évaluer la robustesse du logiciel face aux menaces et à identifier des vulnérabilités exploitables.

 

Cela consiste à :

  • Intégrer le security testing dans le cycle DevSecOps (CI/CD).

  • Automatiser les tests simples et récurrents.

  • Réaliser régulièrement des pentests manuels pour compléter.

  • Se baser sur des standards comme l’OWASP Top 10 ou l’ASVS.

Les méthodes du Security Testing

  • SAST (Static Application Security Testing) : Analyse du code source ou bytecode sans exécution (détection d’injections, failles XSS, mauvaise gestion de la mémoire…).
  • DAST (Dynamic Application Security Testing) : Analyse de l’application en cours d’exécution (simulation d’attaques, injections, comportement aux entrées malveillantes).

  • IAST (Interactive Application Security Testing) : Combinaison de SAST + DAST, instrumente l’application pendant les tests pour plus de précision.

  • Pentests (tests d’intrusion) : Attaques manuelles ou automatisées par des experts pour simuler un hacker réel.

Le hardening du système d’exploitation OS

Le hardening d’un OS est l’ensemble des techniques visant à réduire la surface d’attaque d’un système en supprimant, désactivant ou renforçant tous les éléments non nécessaires.

Rendre l’OS plus robuste face aux attaques en éliminant les faiblesses par défaut.

Principes du hardening d'OS - 1

  • Réduction de la surface d’attaque

    • Désinstaller ou désactiver les services inutiles (FTP, Telnet, etc.).

    • Supprimer les comptes utilisateurs par défaut.

  • Gestion des accès et authentification

    • Appliquer le principe du moindre privilège (Least Privilege).

    • Forcer l’usage de mots de passe complexes ou d’authentification multifactorielle.

    • Limiter l’accès SSH (clé publique/privée, interdiction du login root direct).

Principes du hardening d'OS - 2

  • Mises à jour et correctifs

    • Maintenir le système et les paquets à jour.

    • Automatiser l’application de correctifs de sécurité (patch management).

  • Renforcement des configurations système

    • Configurer correctement le pare-feu (iptables, ufw, Windows Firewall).

    • Restreindre les permissions de fichiers et répertoires sensibles.

    • Activer le chiffrement (ex. BitLocker, LUKS).

Principes du hardening d'OS - 3

  • Surveillance et journalisation

    • Activer les logs système et les centraliser.

    • Mettre en place une détection d’intrusion (IDS/IPS).

  • Sécurisation des communications

    • Forcer l’usage de protocoles sécurisés (SSH, HTTPS, TLS 1.2/1.3).

    • Désactiver les versions obsolètes et vulnérables (SSLv3, TLS 1.0).

Un exemple concret

Un serveur web Linux (Ubuntu) fraîchement installé peut être renforcé en :

  • Désactivant SSH root (PermitRootLogin no).

  • Limitant SSH à quelques IP de confiance.

  • Supprimant Apache modules inutiles.

  • Configurant ufw pour n’autoriser que 22 (SSH), 80 (HTTP), 443 (HTTPS).

  • Forçant TLS 1.3 et désactivant les chiffrements faibles.

En résumé

  • Le hardening OS est une étape indispensable pour passer d’un système “par défaut” (souvent trop permissif) à un système durci et résilient.

  • C’est une base de la défense en profondeur (defense in depth).

  • Il doit être appliqué systématiquement sur tout serveur ou poste sensible avant mise en production.

Hardening des services

Le hardening des services consiste à sécuriser les applications et services réseau (serveur web, base de données, serveur mail, SSH, etc.) en réduisant leur surface d’attaque et en configurant uniquement ce qui est nécessaire.
 

C’est le prolongement logique du hardening de l’OS, mais appliqué au niveau applicatif.

En quoi ça consiste ?

  • Réduction de la surface d’attaque

    • Désactiver les fonctionnalités non utilisées (modules inutiles d’Apache/Nginx, extensions PHP, etc.).

    • Supprimer les comptes ou services par défaut.

    • Restreindre l’accès réseau (ex. base de données accessible uniquement en local).

  • Configuration sécurisée

    • Utiliser des versions sécurisées des protocoles (ex. IMAPS/SMTPS au lieu de IMAP/SMTP en clair).

    • Interdire les chiffrements obsolètes (SSLv2, SSLv3, TLS 1.0/1.1) et forcer l’utilisation de certificats valides.

En quoi ça consiste ?

  • Gestion des identités et accès : principe du Least Privilege appliqué aux comptes de service et authentification forte (MFA, clés SSH) avec rotation régulière des mots de passe et clés API.

  • Mises à jour et patching : mettre à jour régulièrement les services et frameworks (Apache, MySQL, Postfix…) et automatiser si possible (apt/yum, WSUS, Ansible) 

  • Surveillance et journalisation : activer la journalisation fine (ex. logs d’accès Apache, logs de requêtes SQL) et détecter les anomalies (tentatives de brute-force, injections, scans).

Exemple de Hardening

  • SSH

    • Désactiver l’accès root (PermitRootLogin no).

    • Restreindre aux utilisateurs autorisés (AllowUsers).

    • Forcer l’authentification par clé SSH.

    • Limiter à certaines IP via firewall.

  • Serveur Web (Apache/Nginx)

    • Désactiver les modules non utilisés.

    • Forcer HTTPS (HSTS, TLS 1.3).

    • Masquer les bannières serveur (ServerTokens Prod).

    • Protéger contre XSS/CSRF via en-têtes HTTP (X-Frame-Options, Content-Security-Policy).

Exemple de Hardening

  • Base de données (MySQL/PostgreSQL)

    • Accès uniquement depuis le serveur applicatif.

    • Comptes utilisateurs avec droits minimaux.

    • Désactiver les fonctionnalités non nécessaires (ex. LOAD DATA LOCAL INFILE).

    • Chiffrement des connexions (ssl=ON).

En résumé

  • Le service hardening sécurise chaque composant applicatif exposé.

  • Il complète l’OS hardening pour une défense en profondeur (defense in depth).

  • Chaque service a ses propres recommandations (guides CIS, NIST, OWASP).

Firewalling

Le pare-feu (firewall) est un dispositif (matériel ou logiciel) qui contrôle le trafic réseau entrant et sortant selon des règles prédéfinies.

C’est la première barrière entre un réseau interne et l’extérieur.

Types de firewalls

  • Filtrage statique (L3/L4) : basé sur adresses IP, ports, protocoles.

  • Pare-feu applicatif (L7 / WAF) : analyse du contenu applicatif (ex. HTTP, SQL).

  • Next-Generation Firewall (NGFW) : intègre IDS/IPS, analyse comportementale, inspection SSL.

Bonnes pratiques

Règles en mode deny by default (tout est bloqué, sauf ce qui est explicitement autorisé).

Journalisation et surveillance en temps réel.

Mise à jour régulière des signatures et règles.

Zero Trust Network (ZTN)

Modèle de sécurité réseau basé sur le principe :
“Never trust, always verify” (ne jamais faire confiance, toujours vérifier).

 

Règles :

  • Pas de zone de confiance implicite (même à l’intérieur du réseau).

  • Authentification et autorisation continues à chaque requête.

  • Application stricte du least privilege.

  • Micro-segmentation des ressources.

Exemple du ZTN

Un employé doit accéder à une application interne :

  1. Il s’authentifie via MFA
  2. Son identité est vérifiée, son terminal est validé.
  3. Il reçoit uniquement les droits nécessaires à cette tâche, même s’il est déjà “dans le réseau”.

DMZ

Zone tampon du réseau, isolée, où sont placés les services exposés à Internet (serveurs web, mail, DNS).
Protéger le réseau interne en cas de compromission d’un service exposé.

Fonctionnement :

  • Les serveurs accessibles depuis Internet (public-facing) sont dans la DMZ.

  • Les échanges avec le réseau interne passent par des contrôles stricts (firewall, proxy).

  • Exemple classique : un site web hébergé en DMZ peut interroger une base de données dans le réseau interne, mais ne doit pas être directement exposé à celui-ci.

Network Segmentation

Diviser un réseau en sous-réseaux isolés pour limiter la propagation d’attaques et contrôler les flux soit :

  • Segmentation physique : réseaux séparés physiquement (switches/routeurs distincts).

  • Segmentation logique : VLAN, ACL, micro-segmentation via SDN.

Avantages :

  • Réduit l’impact d’une compromission (l’attaquant ne peut pas se déplacer latéralement).

  • Applique des politiques de sécurité différenciées selon les zones (ex. VLAN IoT vs VLAN prod).

En résumé

  • Firewalling : barrière de contrôle du trafic.

  • Zero Trust : pas de confiance implicite, vérification continue.

  • DMZ : zone tampon pour protéger le réseau interne.

  • Segmentation : confinement et cloisonnement pour limiter les dégâts.

Qu'est-ce que le scan de repositories ?

C’est l’ensemble des techniques et outils qui analysent les dépôts de code (history + tree + dépendances) pour détecter :

  • Secrets/credentials exposés (clés API, tokens, mots de passe, certificats…),
  • Vulnérabilités dans les dépendances (bibliothèques tierces) et dans le code (patterns vulnérables),
  • Problèmes de supply chain (packages compromis, versions vulnérables),
  • Anomalies dans l’historique Git (fuites passées toujours présentes dans les commits).

Pourquoi utiliser un scan de repositories ?

Un secret publié dans un repo, même supprimé, est souvent exploitable.

Les vulnérabilités dans les dépendances constituent la majorité des incidents.

Détection précoce = réponse rapide (révocation / rotation / patch) = réduction du blast radius.

Les outils

Détection de secrets (static secrets scanning)

  • gitleaks, truffleHog, gitleaks-action (GitHub Action), git-secrets, gitleaks : recherche regex / heuristiques.

Scanning de vulnérabilités de dépendances (SCA - Software Composition Analysis)

  • Snyk, Dependabot (GitHub), OSS Index, npm audit, pip-audit, mvn dependency:check (OWASP Dependency-Check).

Code scanning / Static Analysis (SAST & CodeQL)

  • CodeQL (GitHub Advanced Security), Semgrep, SonarQube (rules sécurité).

Les bonnes pratiques - 1

  • Scanner en pré-commit et côté développeur

    • Pre-commit hooks (pre-commit, git-secrets) pour bloquer les fuites avant push.

    • Extensions IDE ou plugin qui avertissent lors d’insertion de clés.

  • Scanner à chaque push / PR (gated checks)

    • Job CI qui exécute : secrets scanning (gitleaks), SCA (Snyk/Dependabot), SAST (Semgrep/CodeQL).

    • Bloquer le merge si des secrets sont trouvés ou si une vuln critique est détectée (policy).

Les bonnes pratiques - 2

  • Scanner régulièrement l’historique Git

    • Recherche rétrospective (truffleHog / gitleaks --repo) pour trouver secrets committés puis supprimés.

    • Si un secret est découvert, rotation / révocation immédiate + purge de l’historique (git filter-repo / BFG) si nécessaire.

  • Automatiser la remédiation / ticketing

    • Créer automatiquement issues ou tickets pour vulnérabilités détectées.

    • Prioriser par score CVSS / criticité business.

Les bonnes pratiques - 3

  • Politique et gouvernance

    • Policy “deny-by-default” pour secrets en clair.

    • Exiger la rotation des clés et l’usage de vaults (Secrets Manager, HashiCorp Vault).

    • Mesures d’audit et preuve de conformité (logs, SBOM).

Exemple avec Github Actions

name: Repo Security Scan

on: [push, pull_request]

jobs:
  secrets-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run gitleaks
        run: |
          curl -sLo gitleaks https://github.com/zricethezav/gitleaks/releases/latest/download/gitleaks-linux-amd64
          chmod +x gitleaks
          ./gitleaks detect --source . --report-path gitleaks-report.json || true
      - name: Upload gitleaks report
        uses: actions/upload-artifact@v4
        with:
          name: gitleaks-report
          path: gitleaks-report.json

  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Snyk
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        run: |
          npm install -g snyk
          snyk test --json > snyk-report.json || true
      - name: Upload snyk report
        uses: actions/upload-artifact@v4
        with:
          name: snyk-report
          path: snyk-report.json

Les limites du scan

Secrets déjà compromis : supprimer un commit ne suffit pas sans rotation / révocation.

Historique Git : les secrets peuvent rester dans les commits antérieurs ; il faut scrub + réécrire l’historique avec précaution.

Performance & bruit : scans très agressifs peuvent ralentir CI ; adapter granularité et fréquence.

Comment résoudre une faille ?

  1. Identifier & valider (investigation).

  2. Si secret réel : révoquer / rotate immédiatement (cloud IAM, token, clé).

  3. Purger l’historique si nécessaire (BFG / git-filter-repo) et forcer re-clone pour collaborateurs.

  4. Corriger la vulnérabilité (update lib / patch code).

  5. Documenter & créer ticket post-mortem si incident.

En résumé

Le scan des repositories est une pratique indispensable dans une stratégie DevSecOps moderne : il combine détection de secrets, SCA et SAST, intégré en local (pre-commit) et dans la CI (PR checks).

 

L’importance réelle n’est pas seulement de détecter mais d’avoir un workflow clair de remédiation (révocation, patch, purge historique) et des politiques pour éviter la récurrence.

IAM : Identity and Access Management

La gestion des identités et des accès (IAM) regroupe l’ensemble des méthodes, outils et processus qui garantissent que :

  1. Authentification (AuthN) : vérifier qui est l’utilisateur ou le système.

  2. Autorisation (AuthZ) : déterminer à quoi il a le droit d’accéder, et avec quels privilèges.

C’est un pilier fondamental de la sécurité des systèmes modernes.

AWS l'utilise actuellement.

Authentification (AuthN)

Processus qui permet de prouver son identité.

  • Facteurs d’authentification :

    • Ce que l’on sait (mot de passe, PIN).

    • Ce que l’on a (token, carte à puce, smartphone).

    • Ce que l’on est (biométrie : empreinte, visage).

  • Bonnes pratiques :

    • MFA (Multi-Factor Authentication) = combinaison de plusieurs facteurs.

    • Gestion sécurisée des mots de passe (hash + sel avec bcrypt/Argon2).

    • Rotation et révocation rapide en cas de compromission.

Credentials

  • Mots de passe, certificats, clés SSH, API keys.
  • Problème : difficiles à gérer, souvent exposés par erreur (dans le code ou les repos Git).

Tokens

  • Jetons temporaires délivrés après authentification.

  • Exemples :

    • JWT (JSON Web Token) : très utilisé dans les API / microservices.

    • OAuth 2.0 : délégation d’accès (ex. connexion avec Google/Facebook).

    • OIDC (OpenID Connect) : extension d’OAuth pour l’authentification.

  • Avantages : durée de vie limitée, révocables, scopes précis.

Vault

Un vault est une solution dédiée à la gestion sécurisée des secrets (mots de passe, clés API, certificats). Il répond au besoin suivant : ne jamais stocker de secrets en clair dans le code ou les repos. Il existe de nombreux outils :

  • HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager.

Fonctionnalités

  • Stockage chiffré des secrets.

  • Accès contrôlé par IAM et audit logs.

  • Rotation automatique des clés et mots de passe.

  • Distribution sécurisée aux applications.

En résumé

  • Authentification (AuthN) : prouver qui je suis.

  • Autorisation (AuthZ) : prouver ce que j’ai le droit de faire.

  • Credentials : identifiants classiques (souvent sensibles).

  • Tokens : jetons temporaires, adaptés aux environnements distribués (APIs, microservices).

  • Vault : coffre-fort sécurisé pour gérer et distribuer les secrets.

La culture de la sécurité pour les collaborateurs

4.3

Le paradigme de « l’utilisateur et la sécurité »

Ce paradigme met en lumière le rôle central de l’utilisateur dans la sécurité des systèmes d’information.

Un système peut être techniquement très sécurisé (pare-feu, chiffrement, IAM…), mais si l’utilisateur final adopte de mauvais comportements (mots de passe faibles, clic sur un lien de phishing, contournement des règles), la sécurité globale est compromise.

 

En d’autres termes : « L’humain est souvent le maillon faible de la cybersécurité ».

Les défis - Comportement utilisateur

Utilisation de mots de passe faibles ou réutilisés.

Clic sur des liens malveillants (phishing, spear-phishing).

Contournement volontaire des politiques de sécurité jugées contraignantes (shadow IT).

Les défis - Erreurs involontaires

Envoi d’un mail sensible au mauvais destinataire.

Mauvaise gestion des droits d’accès.

Stockage de données sensibles dans des env' non sécurisés (clé USB, cloud personnel).

Les défis - Facteurs psychologiques/ergonomiques

Si les mesures de sécurité sont trop complexes : rejet / contournement par l’utilisateur.

L’équilibre sécurité vs. facilité d’usage est crucial.

Comprendre le pourquoi.

Bonnes pratiques pour intégrer l’utilisateur dans la sécurité - 1

  • Sensibilisation et formation

    • Campagnes régulières de sensibilisation (phishing simulation, ateliers sécurité).

    • Formation adaptée au niveau de chaque profil (direction, technique, utilisateur lambda).

  • Conception centrée utilisateur (Security by Design)

    • Intégrer la sécurité sans nuire à l’expérience utilisateur.

    • Exemples : authentification SSO + MFA ergonomique, gestion simplifiée des mots de passe.

Bonnes pratiques pour intégrer l’utilisateur dans la sécurité - 2

  • Politiques de sécurité réalistes

    • Règles compréhensibles et applicables (pas des mots de passe impossibles à mémoriser → privilégier un gestionnaire de mots de passe).

    • Clarifier les responsabilités de chaque acteur.

  • Contrôles techniques complémentaires

    • Protection renforcée même si l’utilisateur se trompe :

      • Filtres anti-phishing.

      • DLP (Data Loss Prevention).

      • Contrôles d’accès contextuels (Zero Trust).

Exemple de situation

  • Une entreprise impose un mot de passe complexe changé tous les 30 jours.

  • Résultat : les utilisateurs notent leurs mots de passe sur des post-it, perte de sécurité effective.

  • Solution : adoption de MFA + gestionnaire de mots de passe, sécurité renforcée ET meilleure ergonomie.

En résumé

  • Le paradigme de « l’utilisateur et la sécurité » rappelle que la sécurité n’est pas qu’une question technique.

  • L’utilisateur doit être acteur de la sécurité à travers la sensibilisation, l’accompagnement et des outils adaptés.

  • L’enjeu est de réconcilier sécurité et ergonomie, pour réduire les risques liés aux erreurs humaines et aux comportements à risque.

Les dangers du Social Engineering - Le cas Uber

Le 15 septembre 2022, Uber a annoncé qu’elle avait subi une intrusion majeure dans certains de ses systèmes internes : l’attaquant a publié des captures d’écran montrant un accès aux comptes Slack internes, aux environnements AWS, etc.

 

Mais comment s'infiltrer dans une grande entreprise tech qui possède de très bons process de sécurité ?

Les dangers du Social Engineering - Le cas Uber

Selon les analyses, l’attaquant a ciblé un employé ou un sous-traitant via des techniques d’ingénierie sociale :

  • Il a utilisé ce qu’on appelle le MFA fatigue (ou push bombing) : envoyer de nombreuses demandes de validation de connexion MFA à la victime jusqu’à ce qu’elle accepte par exaspération.

  • Il se serait fait passer pour un membre de l’équipe IT pour persuader la victime de confirmer la requête d’authentification.

  • L'attaquant a pu naviguer dans le réseau interne et trouver des scripts PowerShell stockés sur un partage réseau, certains incluant des identifiants d’administration (PAM, etc.).

Comprendre les différents types d’hébergements

4.4

Définition de l'IaaS

L’IaaS (Infrastructure as a Service) est un modèle de cloud computing dans lequel un fournisseur met à disposition, via Internet, des ressources informatiques virtualisées — serveurs, stockage, réseaux et systèmes d’exploitation — que l’entreprise peut configurer et gérer à la demande.

 

Responsabilités Fournisseur :

Virstualisation, Serveurs, Réseau, Stockage physique.

Responsabilités Client :

Applications, Données, Middlewares, OS

 

Avantages de l'IaaS

Flexible : ressources ajustables à la demande et paiement à l’usage, modèle pay-as-you-go.

Virtualisation : rapidité de déploiement et aucune gestion du matériel physique.

I18n : haute disponibilité et scalabilité mondiale.

Limites de l'IaaS

Gestion technique encore nécessaire (OS, patchs, sécurité applicative)

Risque de dépendance fournisseur (vendor lock-in)

Facturation complexe si mauvaise gouvernance du cloud

Exemple d'IaaS

Tous les Cloud Providers :

AWS, Google Cloud, Azure, OVH...

Définition du PaaS

Le PaaS (Platform as a Service) est un modèle de cloud computing qui fournit un environnement complet de développement et de déploiement dans le cloud.
Le fournisseur gère l’infrastructure sous-jacente (serveurs, stockage, réseau, OS, middleware), tandis que le développeur se concentre sur le code et les applications.

 

Responsabilités Fournisseur :

Runtime, Middleware, OS, Infrastructure.

Responsabilités Client :

Applications, Données

 

Avantages du PaaS

Gain de temps : pas d’installation, d’administration système

Intégration facile avec des services tiers

Focus sur la logique métier plutôt que l’infrastructure

Limites du PaaS

Moins de contrôle sur l’environnement système

Coût plus élevé à long terme pour de gros volumes

Risque de vendor lock-in (dépendance à la plateforme)

Exemple de PaaS

Des solutions "clé en main" :

Netlify, Vercel, Heroku...

Définition du CaaS

Le CaaS (Container as a Service) est un modèle de cloud computing qui fournit une plateforme de gestion, de déploiement et d’orchestration de conteneurs.
Il se situe entre l’IaaS et le PaaS : le fournisseur gère l’infrastructure et les outils d’orchestration, tandis que le client gère ses conteneurs, images et applications.

 

Responsabilités Fournisseur :

Orchestrateur, Infrastructure, Réseau, Stockage

Responsabilités Client :

Applications, Conteneurs, Images

 

Définition du SaaS

Le SaaS (Software as a Service) est un modèle de cloud computing dans lequel les applications sont hébergées et gérées par un fournisseur et accessibles à la demande via Internet.
L’utilisateur n’installe rien localement : il consomme le logiciel comme un service.

 

Responsabilités Fournisseur :

Tout

Responsabilités Client :

Utilisation de l'application

 

Implémenter l’observabilité d’une solution applicative

4.5

Qu'est-ce que l'Observabilité ?

L’observabilité est la capacité à comprendre l’état interne d’un système à partir de ses sorties externes.
Elle repose sur trois piliers principaux :

  1. Logs : traces des événements applicatifs et systèmes.

  2. Metrics : mesures chiffrées de performance, de disponibilité, d’usage.

  3. Traces : suivi d’une requête à travers plusieurs services (distributed tracing).

Le but est de détecter, comprendre et résoudre rapidement les incidents fonctionnels et sécuritaires.

Sentry : un outil clé d’observabilité applicative

Sentry est une plateforme open-source (et SaaS) de monitoring applicatif orientée erreurs, exceptions et performance.


Elle se situe à la jonction entre les pratiques DevOps (résilience, fiabilité) et SecOps (détection d’anomalies).

Sentry : fonctions principales

Domaine Fonctionnalités clés
Erreurs & exceptions Collecte et analyse des erreurs applicatives (stacktrace, contexte utilisateur, version du code).
Performance monitoring Suivi des temps de réponse, transactions lentes, requêtes SQL...
Release tracking Corrélation des erreurs à des versions de déploiement (CI/CD).
Alerting & intégrations Alertes Slack, Teams, email ou Webhook. GitHub, GitLab, Jira, etc.
Security observability Détection de comportements anormaux (injections, auth suspectes).

Sentry : son rôle

Phase du cycle Apport de Sentry Bénéfice DevSecOps
Développement Intégration SDK pour détecter les exceptions dès le dev (JavaScript, Python, Go, etc.) Qualité de code, feedback rapide
CI/CD Corrélation des erreurs avec les commits et releases Traçabilité, responsabilité partagée
Production Suivi des incidents et performance Réduction du MTTR (Mean Time To Repair)
Sécurité Identification d’erreurs révélant des vecteurs d’attaque (ex: injections, overflows) Détection proactive de vulnérabilités

Sentry : exemple d'intégration

npm install @sentry/node

// Exemple Node.js
const Sentry = require("@sentry/node");

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: "production",
  tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());
# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy
  - notify

deploy:
  stage: deploy
  script:
    - npm run deploy
    - sentry-cli releases new "$CI_COMMIT_SHA"
    - sentry-cli releases finalize "$CI_COMMIT_SHA"

Sentry : démo

Voyons comment fonctionne Sentry sur une application en production.

 

Le lien suivant n'est accessible que par moi-même, n'hésitez pas à aller voir la Sandbox :

- Lien privé

- Lien Sandbox

Sentry en résumé

Sentry permet de “rendre visible l’invisible” :

  • Connecte le code aux comportements réels en production,

  • Renforce la qualité logicielle par la détection précoce des anomalies,

  • Participe à la boucle d’amélioration continue entre Dev, Sec et Ops.

Soutenance

QCM

30 minutes

4.

N'hésitez pas à me donner votre avis.

Merci!

DevSecOps & Déploiement

By Valentin MONTAGNE

DevSecOps & Déploiement

  • 113