Infras éphémères pour les tests avec Terraform et Azure DevOps

CI/CD azure-devops terraform
Par Walid AMMAR il y a 3 ans 21 minutes

Le Cloud est devenu aujourd'hui un élément incontournable pour accompagner la croissance des entreprises et leur permettre de répondre rapidement à leurs besoins d'agilité et de scalabilité.

Grace au Cloud, il est devenu facile pour nous de créer des infra complètes en deux clics trois mouvements, cependant, si nous ne restons pas vigilants à ce que nous faisons, nous risquons vite d’exploser le compteur et de nous retrouver avec le contrôle de gestion sur le dos nous demandant des explications sur des factures poivrées de consommation Cloud. Ceux d'entre vous qui sont déjà passés par là se souviennent sans doute de la solitude qu’on ressent à ces moments-là :)

Comment faire alors pour optimiser ses coûts sans déroger un instant à la qualité de nos livrables ? Grande question mais totalement à notre portée !

Je vais vous illustrer dans cet article comment nous pouvons réduire considérablement le coût de nos environnements de test en mettant en œuvre le principe d'Infrastructures éphémères.

1. Stack Techno

Pour cette illustration, nous allons être sur une Stack Microsoft (ma préférée :) et nous allons mettre en œuvre un pipe de déploiement et de test auto en employant la stack suivante :

  • Cloud : Microsoft Azure
  • InfraAsCode : Terraform 0.15.3 / azurerm provider 2.31.1
  • Factory : Azure DevOps & YAML
  • Test Framework : xUnit
  • Appli : AspNet Core Web API / C#
  • IDE : VSCode

2. Principe

L’idée est simple ; Partant du principe qu’un environnement des tests automatisés n’est sollicité qu’au moment d’un merge de code pour jouer les tests de non-régression (voire d’autres types de tests en fonction des besoins de chaque équipe), nous allons tenter de le créer uniquement pendant ces exécutions (directement depuis la pipe d’intégration) et de le détruire une fois le travail terminé.

Cela réduit la consommation Cloud à la seule durée des tests, on sera donc sur des ratio imbattables.

Imaginez une Feature Team standard (4/5 peoples) qui gère une ou plusieurs applications aux architectures non-complexes (Appli de gestion, Micros Service…), cette équipe merge son code 3 fois par jour et lance une batterie de tests automatisés à chaque intégration, imaginons que chaque exécution des scénarios de tests dure 20min

Notre solution va réduire la durée de vie des environnements de tests à (3 x 20min = 1h) contre 24 heures sur des environnements dédiés.

Le calcul est simple : Vous allez diviser votre facture de conso sur vos environnements de tests par 24 et cela fera sans doute des heureux !

3. Use Case

Pour les besoins de cet article, nous allons opter pour un exemple d’API très simple, cela nous permettra de nous concentrer sur le cœur du sujet à savoir, le provisionning éphémère de nos environnements de test.

Partons sur l’exemple fourni par défaut par Microsoft pour la création de tout nouveau projet API ; il s’agit d’une API Météo assez simple incluant une seule route GET /weatherforecast et qui retourne une réponse JSON indiquant la météo des 5 prochains jours.

4. Mise en œuvre

4.1 Bootstrappez le projet ASP.NET WebAPI

D’abord nous allons commencer par créer cette API, pour cela, nous allons initialiser un nouveau projet ASP.NET Webapi en utilisant VSCode.

Pour les besoins de l’exercice, créez sur votre disque l'arborescence suivante :

image

  • Le dossier app contiendra le code applicatif de l’API Weather Forecast
  • Le dossier infra contiendra le code de déploiement Terraform
  • Le dossier tests contiendra le projet de test xUnit

Nous allons placer le code applicatif de l’API dans le dossier app.

Positionnez-vous dans le dossier app, tapez la commande :

dotnet new webapi .

(Si vous voulez le faire directement depuis VSCode, vous devrez afficher la console Terminal. Pour cela, ouvrez le dossier app dans VSCode et tapez les touches CRTL + SHIFT + ù)

image

Le CLI dotnet core va ainsi créer un nouveau projet Web API en y intégrant l’exemple de code mentionné plus haut. L'arborescence créée sous le dossier app devra ressembler à cela :

image

Si nous regardons dans le fichier WeatherForecastController.cs, nous distinguerons la route GET en question qui n’est rien de plus qu’un bouchon qui retourne un choix randomisé sur un tableau statique.

[ApiController] 
[Route("[controller]")] 
public class WeatherForecastController : ControllerBase 
{ 
	private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 
	private readonly ILogger<WeatherForecastController> _logger; 
	
	public WeatherForecastController(ILogger<WeatherForecastController> logger) 
	{ 
		_logger = logger; 
	} 
	
	[HttpGet] 
	public IEnumerable<WeatherForecast> Get() 
	{ 
		var rng = new Random(); 
		return Enumerable.Range(1, 5).Select(index => new WeatherForecast 
			{ 
				Date = DateTime.Now.AddDays(index), 
				TemperatureC = rng.Next(-20, 55), 
				Summary = Summaries[rng.Next(Summaries.Length)] 
			}).ToArray(); 
	} 
}

Pour tester cette API, il faudra exécuter la commande suivante : dotnet run

Le cli dotnet core va alors démarrer un listener http sur les ports 5000 (http) et 5001 (https) comme illustré ci-après :

image

Testez simplement le fonctionnement de l’API en naviguant à l’adresse https://localhost:5001/weatherforecast, vous obtiendrez alors une réponse JSON avec 5 records.

4.2. Créez votre projet de test

Une fois l’API créée, nous allons passer à la création du projet de test. Pour cela, redémarrez l’opération précédente dans le dossier tests mais changez cette fois le template de projet à créer >> dotnet new xUnit

image

Comme pour la création d’API, dotnet cli va vous créer un nouveau projet dans le dossier tests et va vous proposer un premier Test vide.

image

Nous allons implémenter un test tout simple qui vérifie que le nombre d’éléments retournés par l’API est toujours égal à 5.

Pour cela, nous devons d’abord rajouter quelques packages nuGet nécessaires au fonctionnement du projet :

  • Extensions.Configuration.* : Packages servant au chargement des variables d’environnement et de la configuration du projet de test
  • NFluent : Une de mes librairies préférées pour faire de l’assertion en .NET (produit purement français 😊 Merci @Cycril pour la qualité du travail fourni depuis des années sur ce projet).

Également, nous devons déclarer nos fichiers settings pour les embarquer dans le build.

Voici le résultat final du csproj :

image

Le code du test est simple et répond au formalisme basic AAA (Arrange, Act, Assert).

Bien entendu, vous pouvez le faire en Gherkin ou plus simplement en ShouldWhen ou n’importe quel autre formalisme, tout fonctionnera de la même manière 😉

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using NFluent;
using Xunit;

namespace tests
{
    public class UnitTest1
    {
        [Fact]
        public async Task Test1()
        {
            // Arrange
            var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

            var config = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile("appsettings.{environmentName}.json", optional: true, reloadOnChange: true)
				.AddEnvironmentVariables()
				.Build(); 
			
			var section = config.GetSection("Targets")["ApiUrl"]; 
			var targetUri = new Uri(section); 
			var httpClient = new HttpClient(); 
			
			// Act 
			var httpResult = await httpClient.GetAsync(new Uri(targetUri, "/weatherforecast")); 
			
			// Arrange 
			Check.That(httpResult.IsSuccessStatusCode).IsTrue(); 
			var records = JsonConvert.DeserializeObject<IEnumerable<object>>(await httpResult.Content.ReadAsStringAsync()); 
			Check.That(records).HasSize(5); 
		} 
	} 
}

Il est temps maintenant de tester tout cela ;

  1. D’abord, démarrez l’API sur une console app à part en vous positionnant sur le dossier app et en exécutant la commande dotnet run.
  2. Ensuite, exécutez la commande suivante sur VSCode
Set "ASPNETCORE_ENVIRONMENT=Development" dotnet test -v n

La première partie de la commande sert à définir la variable d’environnement ASPNETCORE_ENVIRONMENT afin de charger le bon fichier de configuration.

Il s’agit du formalisme Windows Shell, si vous êtes sur Linux ou Mac, vous pouvez simplement l’écrire ainsi :

ASPNETCORE_ENVIRONMENT=Production dotnet test

Voilà le résultat de l’exécution :

image

Tout va bien, notre API fonctionne et le test est bien vert. Nous allons passer maintenant à la partie Infra.

4.3. Créez votre projet de déploiement

Pour l’infra, nous allons imaginer une architecture très simple composée de deux PaaS Azure ; App Service Plan & Web App.

  • L’App service plan représente la capacité de « compute » sur laquelle s’exécutera l’application (que nous pouvons de manière très approximative assimiler à une VM).

  • La Web App représente le site web qui hébergera l’API (en gros, c’est le site IIS dans lequel on déploiera les binaires).

  • Pour commencer, nous allons créer un projet Terraform dans le dossier infra. Vous devez donc installer plusieurs choses sur votre poste ;

4.3.1 D’abord Terraform en lui-même

Terraform est un middleware servant comme couche d’abstraction des différents langages Infra As Code des principaux Cloud Providers du marché. Il fournit un langage commun (ou du moins presque) pour déployer ses ressources Saas, PaaS ou Iaas sur n’importe quelle souscription Cloud (qu’elle soit Azure, Amazon, GCP ou autre…).

Terraform est un outil puissant car il combine une approche déclarative (l’essence même de l’Infra As Code) avec une panoplie de macros totalement intégrées permettant d’effectuer toutes les acrobaties dont vous aurez besoin pour déclarer vos infras. (Ceux qui ont longuement souffert des JSON ARM à rallonge adoreront ce langage).

Pour plus d’information sur cette techno, je vous invite à visionner leur vidéo d’introduction qui n’est pas mal : https://www.terraform.io/intro/index.html

Pour l’installer sur Windows, vous devez d’abord le télécharger : https://www.terraform.io/downloads.html

Ensuite, le déposer quelque part sur votre disque et y faire référence à deux endroits :

  • Dans vos variables d’environnement Windows type Utilisateur

image image image image

  • Puis, dans vos variables d’environnement Windows type Système

image image

A partir de là, la commande terraform vous sera directement accessible dans vos shell windows.

4.3.2 Ensuite, son extension dans VSCode

L’extension Terraform dans VSCode est très utile, elle vous permettra d’obtenir une « Intellisense » et une mise en forme très pratique pour vos séances de scripting. Vous la trouverez dans la marketplace VSCode comme suit :

image

Pour plus d’informations, vous pouvez visiter la page suivante : https://docs.microsoft.com/fr-fr/azure/developer/terraform/configure-vs-code-extension-for-terraform

4.3.3 Et enfin, son script de déploiement

Passons maintenant aux choses sérieuses, nous allons scripter notre Infra sur Terraform.

Commencez par le fichier main.tf dans le dossier infra et copiez-y le code suivant :

provider "azurerm" {
  features {}
}

variable "location" {
  description = "Azure data center"
  type        = string
  default     = "westeurope"
}

locals {
    project_name        = "weather-forecast"
    environment_name    = "test"
}

resource "azurerm_resource_group" "rg" {
    name        = "${local.project_name}-${local.environment_name}-rg"
    location    = var.location
}

resource "azurerm_app_service_plan" "plan" {
  name                  = "${local.project_name}-${local.environment_name}-plan"
  location              = var.location
  resource_group_name   = azurerm_resource_group.rg.name
  kind                  = "Windows"
  per_site_scaling      = true
  sku {
    tier     = "PremiumV2"
    size     = "P1v2"
    capacity = 1
  }
}

resource "azurerm_app_service" "app" {
  name                    = "${local.project_name}-${local.environment_name}-web"
  location                = var.location
  resource_group_name     = azurerm_resource_group.rg.name
  app_service_plan_id     = azurerm_app_service_plan.plan.id
  enabled                 = true
  https_only              = true
  site_config {
    always_on                 = true
    use_32_bit_worker_process = false
  }
}

output "resource_group_name" {
    value = azurerm_resource_group.rg.name
}

output "app_name" {
    value = azurerm_app_service.app.name
}

output "hostname" {
    value = "https://${azurerm_app_service.app.name}.azurewebsites.net"
}

Globalement, le script se compose de 3 parties ;

  • Une partie input pour prendre en entrée les variables que nous passera la factory au moment du build.

  • Une partie main pour déclarer les ressources à créer.

  • Une partie output pour pousser les variables en sortie (et qui nous sera très utile car nous récupérerons l’url de la web app créée et nous nous en servirons pour configurer le projet de test).

  • Pour plus d’informations quant à la Syntaxe Terraform pour Azure, je vous invite à consulter leur documentation officielle : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

Maintenant pour initialiser notre projet, vous devez exécuter la commande suivante depuis VSCode :

terraform init

Vous devrez alors obtenir le résultat ci-dessous et voir apparaitre un dossier .terraform dans votre dossier infra, ce dernier contient tous les fichiers internes à TF incluant le contexte et les variables pour vous permettre d’exécuter vos scripts.

image

Une fois le projet initialisé, nous allons vérifier qu’il n’y a aucune erreur dans notre script, pour cela vous devez exécuter la commande suivante :

terraform validate

Si vous avez bien suivi mes instructions, vous devrez alors obtenir un joli success message comme suit :

image

4.4. Créez votre Storage Azure pour héberger vos states

A la différence des providers InfraAsCode traditionnels réputés StateLess tel qu’Azure Resource Manager ou AWS CloudFormation, Terraform lui est un provider Statefull, cela veut dire qu’il stocke l’état des déploiements qu’il opère dans une base de données portable, à savoir, les fameux fichiers tfstate.

Ces fichiers contiennent une image de l’infra Azure créée par Terraform au dernier déploiement. Cela est pratique pour bien des raisons, comme l’optimisation des temps de déploiement ou encore les snapshots pour un éventuel DR, etc.

Pour plus d’informations sur ces fichiers, vous pouvez visiter la documentation officielle ici : https://www.terraform.io/docs/language/state/index.html

Nous allons donc dans cette section préparer le stockage Azure qui hébergera ces fichiers.

Allez sur le portail d’administration d’Azure et cliquez sur « Create a resource ».

image

Chercher l’option storage account et sélectionnez-là dans la liste.

image

Nommez et configurez votre storage puis validez la création et attendez quelques secondes.

Une fois la création terminée, vous devez configurer un container pour y stocker les states Terraform. Pour cela, allez sur la page d’administration de votre storage et cliquez sur « Containers » puis procédez à la création.

image

Nous pouvons à présent attaquer la dernière partie de notre préparation.

4.5. Configurez vos pipelines d’intégration

Notre code est prêt à tous les niveaux, notre container Terraform est créé, nous pouvons maintenant configurer nos pipes d’intégration.

Pour ce faire, nous devrons dans un premier temps pousser notre code sur GIT. Je vous ai préparé un « .gitignore » sur la racine du dossier blog qui peut s’avérer utile pour garder la propreté de nos repos. (Vous trouverez le lien vers le repo contenant l’ensemble du projet à la fin de l’article).

Nous allons faire tourner nos build sur la Factory Azure DevOps. Pour cela, nous utiliserons le langage YAML pour déclarer nos pipes de build et de release.

Créez un fichier YAML sur la racine du dossier blog et appelez le build.yaml. Voici le code source :

trigger:
  - master
  
pool:
  vmImage: 'windows-latest'

name: $(Year:yy).$(DayOfYear)$(Rev:.r)

variables:
- name: azure_subscription
  value: 'TO_REPLACE_BY_AZURE_DEVOPS_VAR' 
- name: azure_tf_storage_rg
  value: 'TO_REPLACE_BY_AZURE_DEVOPS_VAR' 
- name: azure_tf_storage_name
  value: 'TO_REPLACE_BY_AZURE_DEVOPS_VAR' 
- name: azure_tf_storage_container
  value: 'TO_REPLACE_BY_AZURE_DEVOPS_VAR' 
- name: azure_tf_storage_statefile_name
  value: 'TO_REPLACE_BY_AZURE_DEVOPS_VAR' 
- name: azure_dc
  value: 'westeurope' 

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet build'
  inputs:
    projects: app/app.csproj
    arguments: '--configuration Release'

- task: DotNetCoreCLI@2
  displayName: 'dotnet publish'
  inputs:
    command: publish
    arguments: '--configuration Release'


- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
  displayName: 'install terraform 0.15.3'
  inputs:
    terraformVersion: 0.15.3

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
  displayName: 'terraform init'
  inputs:
    workingDirectory: infra
    backendServiceArm: '$(azure_subscription)'
    backendAzureRmResourceGroupName: '$(azure_tf_storage_rg)'
    backendAzureRmStorageAccountName: '$(azure_tf_storage_name)'
    backendAzureRmContainerName: '$(azure_tf_storage_container)'
    backendAzureRmKey: '$(azure_tf_storage_statefile_name)'

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
  displayName: 'terraform validate'
  inputs:
    command: validate
    workingDirectory: infra

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
  displayName: 'terraform plan'
  inputs:
    command: plan
    workingDirectory: infra
    backendServiceArm: '$(azure_subscription)'

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
  displayName: 'terraform apply'
  inputs:
    command: apply
    workingDirectory: infra
    backendServiceArm: '$(azure_subscription)'
    commandOptions: '-var "location=$(azure_dc)"'

- task: raul-arrieta.terraform-outputs.terraform-outputs.terraform-outputs@0
  displayName: 'terraform outputs'
  inputs:
    workingDirectory: infra/main.tf

- task: AzureRmWebAppDeployment@4
  displayName: 'deploy api'
  inputs:
    azureSubscription: '$(azure_subscription)'
    WebAppName: '$(app_name)'
    packageForLinux: app/bin/Release/netcoreapp3.1/publish.zip

- task: qetza.replacetokens.replacetokens-task.replacetokens@3
  displayName: 'set fresh deployed api url on test project'
  inputs:
    targetFiles: '**/tests/*.json'

- task: DotNetCoreCLI@2
  displayName: 'dotnet test'
  inputs:
    command: test
    projects: tests/tests.csproj

Faites un commit sur votre branche master en local et n’oubliez pas de push tout cela sur votre branche origin.

Ensuite, allez sur Azure DevOps et configurez votre pipe de build ;

  1. Allez à la section Pipelines > Pipelines et cliquez sur le bouton New pipeline.

image

  1. Sélectionnez en premier lieu votre Repo GIT qui contient votre dossier blog et ensuite l’option « Existing Azure Pipelines YAML file » comme suit :

image

  1. Sélectionnez le fichier YAML que vous avez créé et cliquez sur le bouton « Continuer ».

image

  1. ATTENTION: Avant d’exécuter votre build, vous devez d’abord configurer les variables déclarées dans le fichier YAML.

image

  • La variable azure_dc correspond au Data Center Azure dans lequel vous allez déployer votre environnement éphémère.
  • La variable azure_subscription correspond au nom de votre souscription Azure.
  • Les variables azure_tf_storage_rg, azure_tf_storage_rg, azure_tf_storage_rg et azure_tf_storage_statefile_name correspondent aux paramètres d’accès au storage que nous avons créé à l’étape 4 pour stocker les fichiers d’état tfstate.
  • Vous pouvez rajouter la variable debug à true pour avoir plus de logs au cas où.

image

IMPORTANT : Évitez de configurer la propriété azure_subscription en variable secrète car les tâches Terraform ne semblent pas savoir déchiffrer cela.

UN DERNIER POINT POUR LA ROUTE : Vous avez besoin d’autoriser la manipulation de votre souscription Cloud Azure depuis votre nouveau pipeline Azure DevOps, seul un administrateur de la collection peut le faire. Quand vous allez exécuter votre pipeline la première fois, Azure DevOps vous demandera ce consentement :

image

Si vous-même avez les droits, alors il suffit simplement de cliquer sur le bouton View ensuite validez en cliquant sur le bouton « Permit ».

image

Sinon, demandez à un administrateur de le faire pour vous. Cette approbation n’est requise qu’une seule fois à la première exécution.

image

  1. Il n’y a plus qu’à attendre la fin de l’exécution. Job done !

Si vous avez bien suivi mes instructions, vous devriez obtenir un bien joli build en vert avec une infra créée à la volée et détruite à la fin de l’exécution, et en prime, les résultats du test directement associés au build grâce à notre cher dotnet-cli !

image

REMARQUE : Comme vous le voyez dans les captures ci-dessus, le build généré a pris le numéro 21.129.1, cela correspond à la nomenclature suivante : AA.DDD.R

  • AA correspond aux deux derniers chiffres de l’année en cours (21 pour l’année 2021).
  • DDD correspond au jour de l’exécution du build dans l’année (129).
  • R correspond au numéro d’exécution du build dans la même journée. Cela a été configuré dans le YAML à la 7ème ligne du fichier : name: $(Year:yy).$(DayOfYear)$(Rev:.r)

Conclusion

Dans cet article nous avons pu simuler un déploiement d’application sur Azure, la création d’une infra de test éphémère, l’exécution des tests sur cette infra et sa suppression à la fin de l’opération.

Bien entendu, il ne s’agit là que d’un exemple symbolique pour illustrer le principe, vous pouvez vous-même enrichir cela en rajoutant des ressources supplémentaires comme une base de données ou un service de messaging, etc. Vous pouvez également intercaler une étape d’insertion entre le déploiement de l’infra et l’exécution des tests pour lancer une insertion en masse des jeux de données de tests.

Bref, beaucoup de possibilités se présentent à vous, il n’y a plus qu’à vous lancer !

Vous pouvez télécharger tout le code source pour cet exemple (App, Tests et Infra) depuis ce lien : Télécharger

N’hésitez pas à nous laisser des commentaires si vous avez des questions.

A vous de jouer !