Projet 1 – Pipeline CI/CD

Déploiement d'un site HTML statique via Jenkins, SonarCloud, Docker, Ansible & Terraform sur AWS.

CI/CD

Pipeline Jenkins déclenchée à chaque changement de code : build, tests, analyse, déploiement.

Cloud AWS

Hébergement sur EC2 avec sécurité réseau (Security Groups) et rôles IAM.

IaC

Infrastructure décrite en code avec Terraform : reproductible, traçable, versionnée.

Qualité & Sécurité

Analyse continue sous SonarCloud (bugs, vulnérabilités, code smells).

Objectif

Mettre en place une chaîne de bout en bout : analyses qualité/sécurité, build d'image Docker, provisionnement de l'infrastructure, puis déploiement automatisé de l'application sur AWS.


Architecture globale

Architecture du Projet 1 (CI/CD → AWS EC2)
Chaîne complète : GitHub → Jenkins → SonarCloud → Docker Hub → Terraform/Ansible → EC2

Stack & Outils

  • CI/CD : Jenkins (pipeline déclarative)
  • Qualité : SonarCloud (bugs, vulnérabilités, couverture)
  • IaC : Terraform (VPC/SecGroup/EC2/IAM)
  • Config Mgmt : Ansible (rôles, idempotence)
  • Containers : Docker (build multi-étapes, tags)
  • Cloud : AWS (EC2, IAM, Security Groups)

Fonctionnalités clés

  • Build & tests automatisés
  • Analyse qualité/sécurité continue
  • Build & push image Docker vers Docker Hub
  • Provisioning infra AWS avec Terraform
  • Déploiement applicatif via Ansible
  • Rollbacks simplifiés via tags d'images

1) Provisionnement AWS avec Terraform

L'infrastructure est décrite en HCL et versionnée sur GitHub. Un terraform apply crée une instance EC2, configure les Security Groups (HTTP/HTTPS/SSH) et applique les rôles IAM nécessaires.

devops@DevOps:~/Projet1/terraform — terraform apply
devops@DevOps:~/Projet1/terraform$ terraform apply

          Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
          following symbols:
            + create

          Terraform will perform the following actions:

            # aws_eip.web_ip will be created
            + resource "aws_eip" "web_ip" {
                + allocation_id        = (known after apply)
                + arn                  = (known after apply)
                + association_id       = (known after apply)
                + carrier_ip           = (known after apply)
                + customer_owned_ip    = (known after apply)
                + domain               = "vpc"
                + id                   = (known after apply)
                + instance             = (known after apply)
                + ipam_pool_id         = (known after apply)
                + network_border_group = (known after apply)
                + network_interface    = (known after apply)
                + private_dns          = (known after apply)
                + private_ip           = (known after apply)
                + ptr_record           = (known after apply)
                + public_dns           = (known after apply)
                + public_ip            = (known after apply)
                + public_ipv4_pool     = (known after apply)
                + region               = "eu-west-3"
                + tags                 = {
                    + "Name" = "projet1-staging-web-eip"
                  }
                + tags_all             = {
                    + "Name" = "projet1-staging-web-eip"
                  }
              }

            # aws_instance.web will be created
            + resource "aws_instance" "web" {
                + ami                                  = "ami-04ec97dc75ac850b1"
                + arn                                  = (known after apply)
                + associate_public_ip_address          = false
                + availability_zone                    = (known after apply)
                + disable_api_stop                     = (known after apply)
                + disable_api_termination              = (known after apply)
                + ebs_optimized                        = (known after apply)
                + enable_primary_ipv6                  = (known after apply)
                + force_destroy                        = false
                + get_password_data                    = false
                + host_id                              = (known after apply)
                + host_resource_group_arn              = (known after apply)
                + iam_instance_profile                 = (known after apply)
                + id                                   = (known after apply)
                + instance_initiated_shutdown_behavior = (known after apply)
                + instance_lifecycle                   = (known after apply)
                + instance_state                       = (known after apply)
                + instance_type                        = "t2.micro"
                + ipv6_address_count                   = (known after apply)
                + ipv6_addresses                       = (known after apply)
                + key_name                             = "sshsenan"
                + monitoring                           = (known after apply)
                + outpost_arn                          = (known after apply)
                + password_data                        = (known after apply)
                + placement_group                      = (known after apply)
                + placement_partition_number           = (known after apply)
                + primary_network_interface_id         = (known after apply)
                + private_dns                          = (known after apply)
                + private_ip                           = (known after apply)
                + public_dns                           = (known after apply)
                + public_ip                            = (known after apply)
                + region                               = "eu-west-3"
                + secondary_private_ips                = (known after apply)
                + security_groups                      = (known after apply)
                + source_dest_check                    = true
                + spot_instance_request_id             = (known after apply)
                + subnet_id                            = (known after apply)
                + tags                                 = {
                    + "Name" = "projet1-staging-web-server"
                  }
                + tags_all                             = {
                    + "Name" = "projet1-staging-web-server"
                  }
                + tenancy                              = (known after apply)
                + user_data_base64                     = (known after apply)
                + user_data_replace_on_change          = false
                + vpc_security_group_ids               = (known after apply)

                + capacity_reservation_specification (known after apply)

                + cpu_options (known after apply)

                + ebs_block_device (known after apply)

                + enclave_options (known after apply)

                + ephemeral_block_device (known after apply)

                + instance_market_options (known after apply)

                + maintenance_options (known after apply)

                + metadata_options (known after apply)

                + network_interface (known after apply)

                + primary_network_interface (known after apply)

                + private_dns_name_options (known after apply)

                + root_block_device (known after apply)
              }

            # aws_internet_gateway.igw will be created
            + resource "aws_internet_gateway" "igw" {
                + arn      = (known after apply)
                + id       = (known after apply)
                + owner_id = (known after apply)
                + region   = "eu-west-3"
                + tags     = {
                    + "Name" = "staging-igw"
                  }
                + tags_all = {
                    + "Name" = "staging-igw"
                  }
                + vpc_id   = (known after apply)
              }

            # aws_route_table.public will be created
            + resource "aws_route_table" "public" {
                + arn              = (known after apply)
                + id               = (known after apply)
                + owner_id         = (known after apply)
                + propagating_vgws = (known after apply)
                + region           = "eu-west-3"
                + route            = [
                    + {
                        + cidr_block                 = "0.0.0.0/0"
                        + gateway_id                 = (known after apply)
                          # (11 unchanged attributes hidden)
                      },
                  ]
                + tags             = {
                    + "Name" = "staging-public-rt"
                  }
                + tags_all         = {
                    + "Name" = "staging-public-rt"
                  }
                + vpc_id           = (known after apply)
              }

            # aws_route_table_association.public_a will be created
            + resource "aws_route_table_association" "public_a" {
                + id             = (known after apply)
                + region         = "eu-west-3"
                + route_table_id = (known after apply)
                + subnet_id      = (known after apply)
              }

            # aws_security_group.projet1_web_sg will be created
            + resource "aws_security_group" "projet1_web_sg" {
                + arn                    = (known after apply)
                + description            = "Allow HTTP and SSH access"
                + egress                 = [
                    + {
                        + cidr_blocks      = [
                            + "0.0.0.0/0",
                          ]
                        + from_port        = 0
                        + ipv6_cidr_blocks = []
                        + prefix_list_ids  = []
                        + protocol         = "-1"
                        + security_groups  = []
                        + self             = false
                        + to_port          = 0
                          # (1 unchanged attribute hidden)
                      },
                  ]
                + id                     = (known after apply)
                + ingress                = [
                    + {
                        + cidr_blocks      = [
                            + "0.0.0.0/0",
                          ]
                        + from_port        = 22
                        + ipv6_cidr_blocks = []
                        + prefix_list_ids  = []
                        + protocol         = "tcp"
                        + security_groups  = []
                        + self             = false
                        + to_port          = 22
                          # (1 unchanged attribute hidden)
                      },
                    + {
                        + cidr_blocks      = [
                            + "0.0.0.0/0",
                          ]
                        + from_port        = 443
                        + ipv6_cidr_blocks = []
                        + prefix_list_ids  = []
                        + protocol         = "tcp"
                        + security_groups  = []
                        + self             = false
                        + to_port          = 443
                          # (1 unchanged attribute hidden)
                      },
                    + {
                        + cidr_blocks      = [
                            + "0.0.0.0/0",
                          ]
                        + from_port        = 80
                        + ipv6_cidr_blocks = []
                        + prefix_list_ids  = []
                        + protocol         = "tcp"
                        + security_groups  = []
                        + self             = false
                        + to_port          = 80
                          # (1 unchanged attribute hidden)
                      },
                  ]
                + name                   = "projet1_web_sg"
                + name_prefix            = (known after apply)
                + owner_id               = (known after apply)
                + region                 = "eu-west-3"
                + revoke_rules_on_delete = false
                + tags_all               = (known after apply)
                + vpc_id                 = (known after apply)
              }

            # aws_subnet.public_a will be created
            + resource "aws_subnet" "public_a" {
                + arn                                            = (known after apply)
                + assign_ipv6_address_on_creation                = false
                + availability_zone                              = "eu-west-3a"
                + availability_zone_id                           = (known after apply)
                + cidr_block                                     = "10.0.1.0/24"
                + enable_dns64                                   = false
                + enable_resource_name_dns_a_record_on_launch    = false
                + enable_resource_name_dns_aaaa_record_on_launch = false
                + id                                             = (known after apply)
                + ipv6_cidr_block_association_id                 = (known after apply)
                + ipv6_native                                    = false
                + map_public_ip_on_launch                        = true
                + owner_id                                       = (known after apply)
                + private_dns_hostname_type_on_launch            = (known after apply)
                + region                                         = "eu-west-3"
                + tags                                           = {
                    + "Name" = "staging-public-a"
                  }
                + tags_all                                       = {
                    + "Name" = "staging-public-a"
                  }
                + vpc_id                                         = (known after apply)
              }

            # aws_vpc.main will be created
            + resource "aws_vpc" "main" {
                + arn                                  = (known after apply)
                + cidr_block                           = "10.0.0.0/16"
                + default_network_acl_id               = (known after apply)
                + default_route_table_id               = (known after apply)
                + default_security_group_id            = (known after apply)
                + dhcp_options_id                      = (known after apply)
                + enable_dns_hostnames                 = true
                + enable_dns_support                   = true
                + enable_network_address_usage_metrics = (known after apply)
                + id                                   = (known after apply)
                + instance_tenancy                     = "default"
                + ipv6_association_id                  = (known after apply)
                + ipv6_cidr_block                      = (known after apply)
                + ipv6_cidr_block_network_border_group = (known after apply)
                + main_route_table_id                  = (known after apply)
                + owner_id                             = (known after apply)
                + region                               = "eu-west-3"
                + tags                                 = {
                    + "Name" = "staging-main"
                  }
                + tags_all                             = {
                    + "Name" = "staging-main"
                  }
              }
Exécution de terraform plan puis apply pour créer l'infrastructure.

Pourquoi Terraform ?

  • Reproductibilité : même code, même infra.
  • Traçabilité : changements audités via Git.
  • Automatisation : intégré dans la pipeline Jenkins.

2) Docker : build & publication sur Docker Hub

L'application statique est packagée dans une image Docker. La pipeline build l'image, la tag (ex. :latest et :${BUILD_NUMBER}) puis la pousse sur Docker Hub.

Dépôt Docker Hub contenant les images du site, prêtes à être déployées.

Dépôt Docker Hub du projet

3) Pipeline CI/CD Jenkins

La pipeline est déclenchée à chaque commit sur la branche principale. Elle orchestre les étapes :

  1. Checkout du code (GitHub)
  2. Quality Gate : analyse SonarCloud
  3. Build de l'image Docker
  4. Push de l'image vers Docker Hub
  5. Infra : terraform init/plan/apply
  6. Déploiement : exécution des playbooks Ansible
Exécution de la pipeline Jenkins
Pipeline Jenkins avec toutes les étapes automatisées.

4) SonarCloud : qualité & sécurité

À chaque build, un scan SonarCloud détecte bugs, vulnérabilités et code smells. Le tableau de bord permet d'identifier rapidement les points à corriger.

Tableau de bord SonarCloud du projet (qualité & sécurité).

Tableau de bord SonarCloud

5) Ansible : configuration & déploiement

Une fois l'EC2 en place, Ansible installe les dépendances manquantes (Docker, paquets système, etc.) et déploie l'application en conteneur. Les playbooks sont idempotents pour des relances sûres.

Exemples de tâches

  • Installation de Docker et activation du service
  • Récupération de la dernière image depuis Docker Hub
  • Lancement (ou redémarrage) du conteneur applicatif

Résultat : site déployé sur AWS

Au final, le site HTML statique est accessible depuis l'instance EC2. Chaque modification poussée sur GitHub déclenche une nouvelle livraison automatisée (CI/CD complet).


Leçons & bonnes pratiques

  • Versionner l'infra (IaC) pour éliminer la dérive de configuration.
  • Scanner la qualité/sécurité à chaque build pour éviter la dette technique.
  • Taguer les images Docker (ex. latest + build) pour des rollbacks immédiats.
  • Automatiser de bout en bout (du code à la prod) pour réduire le lead time.