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.
Qu'est-ce qu'une CI / CD ?
Bonus - Mise en place d'une CI / CD avec Github Actions
Mise en place d'une CI / CD avec Gitlab
Mise en place du CD avec Terraform
Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.
Après la théorie, nous verrons alors la pratique en réalisant des exercices sur un repository gitlab.
Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.
Pour faciliter le rendu final des TPs et exercices, vous allez créer un dépôt sur Github avec le nom cicd en publique et le cloner sur votre machine.
Si vous préférez le garder en privé, vous devez m'ajouter avec les droits pour voir votre dépôt avec le nom d'utilisateur : ValentinMontagne
Intégration fréquente des changements au code source pour éviter les régressions.
Automatiser pour éviter les erreurs humaines et la pénibilité de tâche répétitive.
Utilise une pipeline avec différentes étapes pour vérifier la qualité des changements.
La régression est un type de bug, une fonctionnalité déjà présente et fonctionnel dans la solution n'est maintenant plus utilisable.
Un autre type de régression que la régression fonctionnelle existe, on l'appelle la régression de performance. Plus précisément, la fonctionnalité consomme maintenant bien plus de ressources pour fonctionner.
C'est un principe fondamental du développement d'éviter les bugs et les régressions.
Par exemple en Agilité, chaque itération du produit doit développer le produit, jamais diminuer involontairement sa qualité ou ses fonctionnalités.
Itérer rapidement, apprendre rapidement.
Permettre le Lean et l'Agile
Garder un produit fonctionnel car le déploiement est automatisable avec le Cloud as a service.
Lean, Agile, etc.
Beaucoup de méthode de gestion d'entreprise, de projet ou de produit se base sur une itération continue où l'apprentissage est la clé.
Google Design Sprint
Grâce au Sprint Design, Google est capable de tester une nouvelle fonctionnalité en 5 jours ouvrés.
Course à l'itération !
Pour cela, les équipes ont besoin de méthodes pour éviter les régressions et augmenter la rapidité des livraisons.
Scientifiquement prouvé
Corrélation entre les performances et le bien-être au travail avec le CI / CD et le Lean
Computers perform repetitive tasks; People solve problems.
Key Indicators
Protéger l'entreprise d'incidents techniques
Créer un lien de confiance et garder les utilisateurs
Augmenter la productivité et le bien-être des équipes techniques
Réduire le temps et le coût de la validation des changements.
Les tâches pénibles et répétitives sont facilement ignorées.
Éviter les régressions à la source au lieu de les corriger après déploiement.
Créé par Kohsuke Kawaguchi en 2011 avec Oracle car réalisé en Java.
Open source, s'interface avec des systèmes de versions comme Git.
Automatise le build, les tests et le déploiement.
Créé par Gitlab Inc. en 2014, open source avec une version Entreprise.
Solution tout-en-un du CI / CD avec gestion des issues et pipelines liés aux projets.
Permet d'héberger une version de Gitlab directement sur les serveurs de l'entreprise.
Après l'impulsion du rachat par Microsoft, créations de la partie Actions de Github en 2018.
Leader dans le stockage de dépôt de code et développement de projet Open Source, grande communauté.
Plus limité que Gitlab CI sur la partie DevOps / Docker.
Les hooks sont des scripts qui s'exécutent automatiquement à des moments précis du Workflow Git, par exemple lors d'un commit, pull ou push des modifications d'un dépôt. Ces scripts peuvent être utilisés pour effectuer diverses tâches, comme la validation du code, le formatage des fichiers ou même l'envoi de notifications.
Il existe deux types de hooks Git :
Côté client : Ils sont exécutés sur votre machine locale avant d'effectuer des modifications.
Côté serveur : Ils sont exécutés sur le serveur distant lorsque vous transférez des modifications.
Nous allons utiliser cet outil côté client pour éviter qu'un développeur ne commit du code qui n'est pas validé par notre CI.
Le code est déployé après chaque modification.
Au vu de la fréquence, on automatise alors le déploiement.
Plusieurs stratégies de déploiement existent. (Blue / Green, Feature Flag, Canary...)
Gérer correctement les différents environnements de la solution.
Identifier et corriger rapidement les bugs et régressions.
Améliorer la stabilité de la solution et revenir rapidement à la version d'avant.
Externaliser son infrastructure
Automatiser et monitorer le déploiement des applications
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.
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.
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.
Blue / Green
Feature Flag
Canary
1. Comment déployer fréquemment sans interrompre les utilisateurs ?
2. Comment déployer fréquemment sans bugs ou régressions pour les utilisateurs ?
3. Comment tester les changements avec de vrais utilisateurs ?
On crée deux environnements distincts mais identiques. Un environnement (Blue) exécute la version actuelle de l'application et un environnement (Green) exécute la nouvelle version de l'application.
Gère les fonctionnalités dans le code source. Une condition dans le code permet d'activer ou de désactiver une fonctionnalité pendant l'exécution. Voir Unleash.
Redirige le trafic d'une portion des utilisateurs en fonction de critères ou d'un simple pourcentage vers la nouvelle version. Après un certain temps ou d'une condition, l'ensemble du trafic est rédirigé.
– Conrad Anker
Gitlab utilise le fichier à la racine de votre dépôt .gitlab-ci.yml qui contient toutes les instructions pour votre pipeline CI / CD. Lors d'évènement comme un commit ou une pull request (ou merge request), la pipeline s'execute grâce à des runners.
Les runners peuvent être hébergé par l'équipe (personnalisé) ou directement par Gitlab (partagé). Ils s'executent sur des machines physiques, virtuelles ou des conteneurs Docker.
# .gitlab-ci.yml File
# Configure the global image and cache for every stage
image: node:latest
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
# Define all the stages used for the pipeline
stages:
- validate
- test
- build
- release
- deploy
# Add first job using the '.pre' stage defined by Gitlab, could be a defined stage too
# .pre is always run as the first stage
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
– Conrad Anker
# The configuration for the `remote` backend.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.aws_region
}
# AWS EC2 instance
resource "aws_instance" "example" {
ami = var.ec2_ami
instance_type = var.ec2_instance_type
key_name = var.ec2_key_name
tags = {
Name = "ExampleInstance"
}
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
# Pull and run your Docker image here
docker run -d -p 80:80 valentinmontagne/nginx-web-example:${var.docker_image_version}
EOF
}
Gitlab CI
Création du compte Gitlab
Si vous n'avez pas encore de compte Gitlab, je vous invite pour la suite du TP et des exercices à créer un compte avec votre adresse étudiante.
Récupérer le projet de base
Maintenant que vous avez un compte Gitlab, vous pouvez "Fork" directement le projet ou le cloner sur votre machine et créer un nouveau projet vide pour récupérer le contenu du projet de base pour réaliser ce TP :
https://gitlab.com/vm-marvelab/ci-cd
(Tous les fichiers sont à mettre à la racine de votre dépôt.)
Le projet de base est une API très simple en NodeJS, elle utilise Express pour lancer un serveur qui écoute les requêtes sur des routes définies.
N'hésitez pas à lire le README pour comprendre comment fonctionne la route /auth.
Utilisez la commande pour installer les dépendances du projet :
npm install
Ensuite utilisez la commande pour lancer l'API :
npm start
Maintenant que notre projet de base fonctionne, nous allons commencer à intégrer notre pipeline CI.
Pour cela, créez à la racine du projet le fichier .gitlab-ci.yml
N'hésitez pas à installer une extension sur votre IDE pour vous aider avec le format du fichier.
Passons à la prochaine étape.
Nous devons spécifier avec quel image notre Runner doit s'executer pour lancer notre pipeline CI, étant une API en nodejs, nous allons utiliser une image nodejs.
Nous allons ajouter des informations sur le cache pour éviter qu'à chaque Job de chaque Stage que nous soyons obliger de réinstaller les dépendances.
# .gitlab-ci.yml file
image: node:latest
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
Comme vu ensemble, un Job est une étape spécifique du processus, comme exécuter des tests, compiler du code, ou déployer une application. Un job peut être configuré avec des scripts, des environnements d'exécution, et des conditions spécifiques (par exemple, quand il doit être exécuté). Plusieurs jobs peuvent s'exécuter en parallèle ou en séquence, en fonction des dépendances et de la structure du pipeline.
Un Stage est un groupe de Job et permet d'organiser la pipeline, ils s'exécutent dans l'ordre dont ils sont définis.
Définissons nos Stages et réalisons notre premier Job qui sera l'installation des dépendances à la suite du fichier :
stages:
- validate
- test
- build
- release
- deploy
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
Ici le stage .pre est un Stage déjà créé par Gitlab, il est obligatoirement le premier à être lancé.
Nos Stages sont les étapes habituelles d'une pipeline de CI / CD.
Une fois ajouté, réaliser un git commit et un git push pour envoyer les changements à votre dépôt sur Gitlab.
Une fois fait, allez dans la section Build dans la barre à gauche du dépôt sur gitlab.com et cliquez sur Pipelines, il n'y a pas encore de Pipeline lancée, c'est parce que nous n'avons pas encore de Job hors installation.
Passons à l'étape suivante pour réaliser notre Stage validate.
Ajoutons maintenant le Stage de validation, pour cela nous allons ajouter les outils Eslint pour vérifier la qualité de notre code NodeJS :
npm init @eslint/config@latest
Choisissez les options "Style et problems", puis "CommonJS", "None of these", "No" pour Typescript et cochez "Node".
Un nouveau fichier eslint.config.mjs est apparu, passons à l'étape suivante.
Changez la configuration du fichier pour ignorer les fichiers de tests et ajouter des règles comme ici ne pas avoir de variables inutilisés, de variable non défini ou de console.log dans le code.
// eslint.config.mjs
import globals from "globals";
import pluginJs from "@eslint/js";
export default [
{
ignores: ["**/*.test.js"],
files: ["**/*.js"],
languageOptions: { sourceType: "commonjs" },
},
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
{
rules: {
"no-unused-vars": "error",
"no-undef": "error",
"no-console": "error",
},
},
];
Ajoutons maintenant le script "lint" au package.json pour pouvoir lancer la commande :
npm run lint
// package.json
// ...
"scripts": {
"start": "node src/index.js",
"lint": "eslint src --max-warnings=0"
},
// ...
Ajoutons maintenant un console.log que l'on a "oublié" dans une route de notre API dans le fichier index.js qui affiche la variable secret et qui ne devrait pas être dans nos logs !
const express = require("express");
const auth = require("./modules/authentication");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/auth/:secret", (req, res) => {
const { secret } = req.params;
const response = auth(secret);
console.log(secret);
res.status(response.status).send(response.message);
});
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`Example app listening on http://localhost:${port}`);
});
Lancez la commande npm run lint
vous devriez maintenant avoir une erreur.
Ajoutons maintenant un Job dans notre pipeline CI pour lancer cette commande automatiquement. A la suite du fichier .gitlab-ci.yml, ajoutez le Job lint dans le Stage validate.
// .gitlab-ci.yml
// ...
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
lint:
stage: validate
script:
- npm run lint
Faites un nouveau commit et faites un push sur la branch main à nouveau et retournez voir les Pipelines de votre dépôt pour voir le résultat.
La même erreur est détecté automatiquement par votre CI maintenant. Enlevez le console.log et faites de nouveau un push pour corriger le problème.
Mais cela serait plus fiable si l'on détectait ce genre de problème avant qu'il n'arrive sur notre pipeline non ?
Mettons en place notre Git hooks à l'étape suivante.
Maintenant nous allons installer Husky à notre projet, qui permet de mettre en place les Git hooks pour nous permettre d'executer notre validation ou nos tests avant chaque commit. Cela rentre totalement dans l'intégration continue pour éviter de commit du code qui n'est pas fonctionnel ou de mauvaise qualité.
Lancez la commande :
npm install --save-dev husky && npx husky init
Husky génère alors un dossier .husky, ouvrez le fichier pre-commit dans ce dossier et changez npm test
par npm run lint
.
Essayez de commit, vous allez maintenant avoir npm run lint
qui s'execute automatiquement. Essayez d'oublier à nouveau un console.log dans le code, lors de la tentative de commit vous devriez être bloqué par l'echec de la commande npm run lint
.
Maintenant ajoutons les tests à notre pipeline à l'étape suivante.
Pour réaliser l'étape des tests, nous allons installer Vitest :
npm install -D vitest
Ensuite, ajoutons dans package.json la commande "test".
// package.json
// ...
"scripts": {
"start": "node src/index.js",
"lint": "eslint src --max-warnings=0",
"prepare": "husky",
"test": "vitest run"
},
"devDependencies": {
// ...
Vérifiez que tout est fonctionnel avec la commande :
npm test
Ajoutez ensuite cette commande au Git hooks.
Ensuite, ajoutons notre Job unit-test qui sera dans notre Stage test.
// .gitlab-ci.yml
// ...
install:
stage: .pre
script:
# define cache dir & use it npm!
- npm ci --cache .npm --prefer-offline
lint:
stage: validate
script:
- npm run lint
unit-test:
stage: test
script:
- npm test
Faites un push à nouveau et vérifiez que le job apparait bien dans votre pipeline et qu'il lance les tests.
Nous allons maintenant permettre de release une version de notre API via notre Pipeline. Ce Job doit alors être manuel pour nous laisser la possibilité d'activer la release au bon moment.
Par sécurité nous allons ajouter des Rules pour éviter que le Job Release se lance sur d'autres branches que main.
Pour créer une nouvelle version nous allons utiliser Release-it :
npm init release-it
Choisissez les réponses "Yes" et "package.json" à l'installation.
Ajoutez dans le package.json dans le champ "release-it" les informations "git" suivantes :
Voir à l'étape suivante l'ajout au .gitlab-ci.yml du Stage Release.
// package.json
// ...
"dependencies": {
"express": "^4.18.2"
},
"release-it": {
"$schema": "https://unpkg.com/release-it/schema/release-it.json",
"gitlab": {
"release": true
},
"git": {
"commitMessage": "chore: release v${version}"
}
}
// ...
// .gitlab-ci.yml
// ...
unit-test:
stage: test
script:
- npm test
release:
stage: release
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"
&& $CI_COMMIT_TAG == null
&& $CI_COMMIT_TITLE !~ /^chore: release/'
before_script:
- git config user.email $GITLAB_USER_EMAIL
- git config user.name $GITLAB_USER_NAME
- git remote set-url origin
"https://gitlab-ci-token:$GITLAB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git"
- git checkout $CI_COMMIT_BRANCH
- git pull origin $CI_COMMIT_BRANCH --rebase
script:
- npx --yes release-it --ci
// Permet de spécifier que ce Job sera activé manuellement.
when: manual
// Permet d'ajouter des rules au Job pour l'afficher ou non grâce au if.
// Ici le commit doit être sur la branche main, cela ne doit pas être un tag
// et le title du commit doit être différent de ce format.
rules:
- if: '$CI_COMMIT_BRANCH == "main"
&& $CI_COMMIT_TAG == null
&& $CI_COMMIT_TITLE !~ /^chore: release/'
Quelques explications :
Ensuite, pour fonctionner, Release-it a besoin d'une variable d'environnement GITLAB_TOKEN. Pour cela nous allons devoir l'ajouter manuellement, mettez votre souris sur Settings et cliquez sur CI / CD, ensuite ouvrez le menu Variables. Ouvrez sur une autre page vos Access Tokens en mettant votre souris sur votre profil en haut à gauche, puis en cliquant sur Préférences, là vous devez cliquer sur Access Tokens et générer un nouveau Token avec les droits api.
Ajoutez ce token dans une nouvelle variable CI / CD de votre projet GITLAB_TOKEN.
Faites un push et lancez la release.
Appelez-moi quand vous avez terminé pour que l'on valide ensemble le TP.
Nous avons un problème, ici après la release, deux pipelines se lancent, cela est inutile.
Vous devez retirer les étapes validate et test en ajoutant des rules aux Jobs pour qu'ils ne se lancent pas lorsqu'un tag est créé ou que le commit est le commit de release "chore: release".
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Préparons un nouveau Job e2e-test dans le Stage test, pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello E2E !"
Il doit être visible uniquement dans les Merge Requests (Pull Requests).
Appelez-moi pour que l'on puisse valider ensemble.
Préparons un nouveau Job only-canary dans le Stage validate, qui ne doit se lancer que quand l'on lance la pipeline pour l'environnement Canary, pour cela, nous allons créer une variable d'environnement "ENV_TARGET" qui doit être égale à "canary" pour que le Job se lance.
Pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello Only Canary !"
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Préparons un nouveau Job integration-test dans le Stage test, qui ne doit se lancer que quand le Job unit-test réussi, de même pour e2e-test qui ne doit maintenant se lancer que quand integration-test réussi.
Pour l'instant il ne doit juste réaliser la commande suivante :
echo "Hello Integration !"
Appelez-moi pour que l'on puisse valider ensemble.
Documentation :
Durant les derniers exercices, nous avons souvent appliqué le même comportement à plusieurs Jobs. Pour éviter la duplication de code et que le fichier soit plus maintenable, utilisons les Anchors en YAML pour pouvoir faire hériter à nos Jobs une configuration.
Appelez-moi pour que l'on puisse valider ensemble.
Maintenant que vous avez terminé l'intégration continue, nous allons maintenant voir le côté déploiement continu.
Pour cela, nous allons rajouter les étapes de builds pour l'image Docker et du déploiement de celle-ci.
Vous devez réaliser un fichier Dockerfile pour build le projet.
Ajouter l'étape suivante build-image :
# If your language needs to build you can create another build job before
# Add docker job to build the image
# Only for testing purpose, you can add a docker run + command to check health
# Gitlab has a lot of variables to use in the pipeline
# Here we are using $CI_REGISTRY_IMAGE and $CI_COMMIT_SHORT_SHA
build-image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- docker image build -t $IMAGE_TAG .
Ici, nous utilisons un service spécial docker:20.10.16-dind pour permettre l'utilisation de Docker dans un container Docker.
On pourrait ici build plusieurs images si besoin durant cette étape.
Vérifiez que cette étape fonctionne en réalisant un push sur votre branche.
Ajoutez ensuite l'étape deploy-image :
deploy-image:
needs:
- release
image: docker:20.10.16
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null && $CI_COMMIT_TITLE !~ /^chore: release/'
services:
- docker:20.10.16-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker image build --platform=linux/amd64 -t $IMAGE_TAG -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE --all-tags
environment: production
Vérifiez après avoir push à nouveau les changements que votre image est bien ajoutée dans votre Container Registry en allant dans le menu Deploy sur votre dépôt Gitlab. C'est terminé !
Voici un exemple pour déployer sur un serveur distant, ceci n'est pas la meilleure méthode mais elle est très simple.
deploy:
needs:
- deploy-image
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG == null && $CI_COMMIT_TITLE !~ /^chore\(release\): publish/ && $CI_COMMIT_TITLE !~ /^chore\(pre-release\): publish/'
before_script:
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
script:
- ssh-add <(echo "$SSH_PRIVATE")
- ssh -o StrictHostKeyChecking=no "$SSH_SERVER" 'which docker || (sudo yum update -y && sudo yum install -y docker && sudo service docker start)'
- <SAME> "sudo docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY"
- <SAME> "(sudo docker rm -f example || true) && sudo docker run -p 80:3000 -d --name example registry.gitlab.com/vm-marvelab/docker-cicd:$CI_COMMIT_SHORT_SHA"
environment: production
– Conrad Anker
Découvrons ensemble Github Actions, pour cela nous allons recommencer ce TP en utilisant uniquement Github.
Je vous invite à créer un compte Github si ce n'est pas déjà le cas pour la suite du TP.
name: Node.js CI
on:
push:
branches: ['main']
pull_request:
branches: ['main']
jobs:
build:
runs-on:
ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install
run: npm ci --cache .npm --prefer-offline
- name: Validate
run: npm run lint
- name: Test
run: npm test
- name: Build for production
run: npm run build
- uses: actions/upload-artifact@master
with:
name: web
path: ./dist
Github Actions
Maintenant que vous êtes plus à l'aise avec l'intégration continue, vous serez en autonomie, suivez la documentation https://docs.github.com/fr/actions pour réaliser le même TP qu'avec Gitlab CI, vous devez retrouver via la documentation de Github Actions comment refaire les étapes qu'on a réalisé avec Gitlab.
N'hésitez pas à m'appeler en cas de problème.
– Conrad Anker
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.
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.
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.
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.
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.
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 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.
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
}
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.
Permet de découvrir, de partager et d'utiliser les modules et Providers Terraform.
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.
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.
# 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é
}
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.
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.
L'application d'une configuration Terraform va :
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.
## 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
...
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 :
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
.
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.
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.
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.
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.
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.
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.
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.
# 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 {}
}), {})
}))
}
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>
.
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
}
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" }'
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-\"."
}
}
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.
# 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
}
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.
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
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.
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.
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.
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.
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é.
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.
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 :
Et la slide suivante pour voir le job permettant le déploiement.
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.
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 :
Je recommande d'utiliser la méthode 2 qui est pour moi plus sécurisé.
# 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 :
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.
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
}
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"
}
}
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"
}
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 :
terraform fmt
terraform validate
terraform plan
terraform apply
Vérifiez que les changements, que remarquez-vous ?
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 ?
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 !'"
}
}
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.
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
}
}
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.
Bravo, vous avez terminé ce premier TP !
Je vous invite maintenant à supprimer l'infrastructure actuelle et de passer aux exercices.
Ajoutez des variables Terraform à la place des valeurs codées en dur suivantes :
Le nom de l’image Docker (nginx:latest
)
Le nom du conteneur
Le port externe exposé
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.
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.
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.
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 :
Créer un réseau Docker dédié.
Connecter les deux conteneurs à ce réseau.
Faire en sorte que le conteneur client
utilise curl
pour interroger http://nginx:80 et sleep
ensuite.
Vérifier que la communication fonctionne dans le conteneur client
.
Appelez-moi pour que l'on puisse valider ensemble.
Reprenez l'exercice précédent, puis :
Modifiez le conteneur client
pour qu’il soit déployé en plusieurs exemplaires (ex. 3) à l’aide de count
.
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.
Le nombre de clients doit être paramétrable via une variable.
Appelez-moi pour que l'on puisse valider ensemble.
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.
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.
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.
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
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"
}
}
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")
}
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.
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 }
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"
}
}
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"
}
}
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}"
}
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.
Ajoutez des variables Terraform à la place des valeurs codées en dur suivantes :
Le type d'instance EC2
Le nom de l'instance EC2
Le nom du bucket S3
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.
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.
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 :
Créer une nouvelle instance EC2
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.
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. (N'hésitez pas à consulter AWS details)
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.
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.
Récupérez un projet que vous avez déjà réalisé et réalisez une image Docker de ce projet.
Déployez l'image Docker et les images nécessaires pour votre projet en utilisant Terraform sur le compte AWS fournit pour cet atelier.
Appelez-moi pour que l'on puisse valider ensemble.