Guillaume Azerad

Ingénieur DevOps senior avec expertise Docker/Ansible/Gitlab et capacité de développeur full stack (Go, PHP, Javascript, Python)

Guillaume Azerad
Guillaume Azerad

Ingénieur DevOps senior avec expertise Docker/Ansible/Gitlab et capacité de développeur full stack (Go, PHP, Javascript, Python)

Mettre à l'échelle automatiquement une application avec un cloud hybride Azure - partie 2 : scalabilité avec une fonction Azure


Publié le: 2024-10-06
Temps de lecture: 25 min
Modifié le: 2024-10-12
Existe aussi en: Anglais

Scalabilité horizontale

Introduction

Dans le premier article, nous avons décrit l’infrastructure du cloud hybride destinée à assurer la mise à l’échelle d’une application déployée initialement sur une VM d’un réseau local.

Nous abordons maintenant l’implémentation de cette mise à l’échelle (ou scalabilité) qui sera donc horizontale, c’est à dire qui se fera en ajoutant de nouvelles instances afin d’assurer la même tâche.

Scalabilité horizontale vs verticale

L’enjeu détaillé dans cet article consiste à offrir un moyen simple et automatisé d’adapter l’infrastructure en l’étendant sur le cloud public Azure afin de répondre à des pics de charge sur l’infrastructure locale initiale.

Création du mécanisme de mise à l’échelle sur Azure

Présentation de la solution

Rappelons l’objectif de mise à l’échelle automatique de notre application : créer de une à deux VM au maximum sur le cloud Azure lorsqu’on atteint un pic de charge sur la VM locale, et répartir les connexions entrantes entre elles. Puis, lorsque la charge redevient “normale”, les ressources créées sur Azure doivent être supprimées et les connexions redirigées vers la VM locale uniquement.

Azure Functions est un service cloud serverless qui permet de mettre en place de façon immédiate une API exposant une application dont nous avons déployé le code. En bref, il s’agit d’un moyen direct d’exécuter du code par le biais d’une API, sans se soucier de l’infrastrucure sous-jacente, avec de plus un accès au réseau cloud privé que nous avons créé.

Autoscaling avec Azure Functions

Comme nous le verrons dans le prochain article, la stack de supervision utilise Alertmanager qui est configuré pour appeler une URL lorsque des alertes sont générées par Prometheus. La fonction Azure va donc nous permettre d’assurer la chaîne de mise à l’échelle automatique de l’infrastructure cloud en réponse à des alertes de supervision puisqu’elle proposera une API permettant de créer/supprimer des VM Azure sur laquelle pointeront les webhooks d’Alertmanager.

Explication du choix d’Azure Functions

Azure propose des outils de mise à l’échelle automatiquement gérée qui auraient pu correspondre à notre besoin, et plus particulièrement Virtual Machine Scale Set (VMSS). Cette fonctionnalité permet en effet de définir un groupe de VM équipé d’un load balancer dont on peut faire varier le nombre d’unités en fonction d’évènements de supervision.

De façon très simple comme détaillé dans ce tutoriel, nous pouvons définir un nombre minimum et maximum de VM propre au groupe ainsi que des paramètres de supervision générant la création ou la suppression de VM.

Nous nous situons donc apparemment dans notre cas d’utilisation sauf que nous souhaitons superviser uniquement la VM du réseau local. En l’absence d’une configuration Azure Arc qui permette de gérer indentiquement une VM locale et une VM Azure, il est impossible de définir un VMSS incluant la VM principale pour laquelle des évènements de supervision déclencheraient la mise à l’échelle horizontale sur Azure.

Par ailleurs, Azure Functions présente certains avantages correspondant à notre besoin :

  • API prête à l’emploi : Azure Functions permet de créer une API déclenchable (via HTTP, webhook, etc.) sans avoir à développer ni héberger notre propre API.
  • Automatisation basée sur des événements : une fonction Azure peut répondre automatiquement aux événements (comme des alertes de charge) et exécuter les actions nécessaires (création/suppression de VM) sans interaction manuelle.
  • Scalabilité automatique : de par sa nature serverless Azure Functions s’adapte à la charge sans avoir à gérer d’infrastructure, contrairement à un script CLI qui nécessiterait un serveur.
  • Facturation à l’usage : on paie uniquement pour le temps d’exécution, ce qui est plus rentable que maintenir un processus CLI actif en permanence.
  • Intégration simplifiée : une fonction Azure s’intègre facilement à d’autres services Azure (monitoring, Event Grid, etc.) pour créer des workflows plus complexes et robustes.

Comme notre application est conteneurisée, il aurait été pertinent d’utiliser le service Azure Container Instances, solution serverless permettant d’assurer la mise à l’échelle par la création d’instances de conteneurs. Par soucis didactique nous choisissons de rester sur une scalabilité par VM.

Création des ressources Azure

Nous allons maintenant nous occuper de créer les ressources Azure qui nous permettront d’assurer la scalabilité sur le cloud de notre application.

Nous avons tout d’abord besoin de définir la configuration propre aux VM qui se caractérise par les éléments suivants :

  • une règle autorisant le trafic HTTP (port 80) pour l’application web déployée sur les VM
  • un compte de stockage pour la sortie du diagnostic de démarrage de la VM
RESOURCE_GROUP="TpCloudHybrideVPN"
LOCATION="francecentral"
VM_BOOT_STORAGE_ACCOUNT="vmbootdiagstoragevpn"

# Créer un groupe de sécurité réseau
az network nsg create --resource-group $RESOURCE_GROUP --name myNSG

# Créer une règle de groupe de sécurité réseau
az network nsg rule create \
  --resource-group $RESOURCE_GROUP --nsg-name myNSG --name myNSGRuleHTTP \
  --protocol '*' --direction inbound \
  --source-address-prefix '*' --source-port-range '*' \
  --destination-address-prefix '*' --destination-port-range 80 \
  --access allow --priority 200

# Créer un compte de stockage pour la sortie du diagnostic de démarrage
az storage account create --name $VM_BOOT_STORAGE_ACCOUNT \
  --location $LOCATION --resource-group $RESOURCE_GROUP\
  --sku Standard_LRS # --allow-blob-public-access false

Nous allons également créer la fonction Azure et les ressources suivantes qui lui sont associées :

Nous remarquons dans les commandes CLI que la fonction est hébergée dans la région germanywestcentral puisque la région francecentral ne proposait pas une telle fonctionnalité (au moment où le TP a été réalisé).

FUNCTION_LOCATION="germanywestcentral"
FUNCTION_NAME="tp-cloud-autoscale-vpn"
FUNCTION_STORAGE_ACCOUNT="tpcloudfuncvpnstorage"

# Créer compte de stockage pour la fonction Azure
az storage account create --name $FUNCTION_STORAGE_ACCOUNT \
  --location $FUNCTION_LOCATION --resource-group $RESOURCE_GROUP\
  --sku Standard_LRS # --allow-blob-public-access false

# Créer la fonction (dans la région germanywestcentral car impossible dans $LOCATION)
az functionapp create --resource-group $RESOURCE_GROUP \
  --consumption-plan-location $FUNCTION_LOCATION --runtime powershell \
  --functions-version 4 --name $FUNCTION_NAME --storage-account $FUNCTION_STORAGE_ACCOUNT

# Créer un conteneur de stockage pour le script d'initialisation des VM
az storage container create \
    --account-name $FUNCTION_STORAGE_ACCOUNT \
    --name init-script-vm

# Ajouter le fichier init.sh que l'on suppose présent ici
az storage blob upload \
    --account-name $FUNCTION_STORAGE_ACCOUNT \
    --container-name init-script-vm \
    --name init.sh \
    --file init.sh

Et voilà, la fonction Azure sera maintenant visible sur le portail Azure parmi les ressources de la souscription associée.

Liste des ressources

Script d’initialisation des VM

Assurer la création et suppression des VM sur Azure est au coeur du mécanisme de scalabilité automatique dont nous avons parlé, mais n’est pas suffisant : il faut en effet configurer les VM pour l’application que nous souhaitons faire fonctionner et évidemment lancer cette dernière.

Azure propose une fonctionnalité très intéressante pour cela avec la possiblité d’utiliser cloud-init pour lancer un script bash sur une VM.

cloud-init permet de définir de façon déclarative en yaml les instructions que l’on souhaite exécuter sur les serveurs cibles lors de leur création, un peu à l’exemple d’Ansible pour la gestion de configuration.

Avec Azure CLI, il est alors possible d’exécuter un script issu d’une définition cloud-init lors de la création d’une VM :

az vm create \
  --resource-group myResourceGroup \
  --name vmName \
  --image imageCIURN \
  --custom-data config.yaml \
  --generate-ssh-keys

Malheureusement, cette fonctionnalité n’est pas disponible avec le module Az de Powershell que nous utilisons pour le script de la fonction Azure.

Ce n’est cependant pas un problème puisque nous allons définir les instructions que nous souhaitons voir exécuter à la création de la VM par un script bash que nous appellerons init.sh. Nous aurons besoin à ce titre de :

  • installer Docker puisque notre application est conteneurisée
  • initier Docker Compose avec un service comprenant notre application
  • lancer l’application en démarrant Docker Compose

init.sh

#!/bin/bash

# Installation de Docker
sudo apt-get -y update
sudo apt-get -y install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get -y update
sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker

# Démarrage de l'application avec Docker Compose
mkdir -p /home/azureuser/myapp
cd /home/azureuser/myapp

cat >docker-compose.yml <<EOL
services:
  application:
    image: crccheck/hello-world
    container_name: application
    ports:
      - "80:8000"
EOL

sudo docker compose up -d

Nous pouvons remarquer que nous utilisons l’user azureuser qui est défini par défaut sur les VM crées sur Azure. Nous disposons donc maintenant de la configuration à appliquer aux VM créées automatiquement grâce à la fonction Azure dont nous allons maintenant détailler le contenu.

On peut même aller plus loin et créer une image personnalisée de VM en utilisant Azure CLI. De la sorte, nous n’aurions plus besoin du script d’initialisation puisque les VM seraient directement créées à partir de cette image.

Définition et test de la fonction Azure

Choix du langage et préparation de l’environnement de développement

Tout d’abord, nous devons choisir le langage de programmation de la fonction parmi ceux disponibles.

Notre choix se porte sur Powershell pour sa simplicité de mise en place (pas de packages supplémentaires à déployer) et une intégration native avec Azure grâce au module Az en particulier.

A titre de comparaison, la gestion des dépendances dans un autre langage nécessite de stocker les bibliothèques tierces soit dans le répertoire lui-même du projet, soit en montant un partage de fichiers sur Azure comme indiqué en Python par exemple.

Pour développer le script Powershell appelé par la fonction Azure, nous allons utiliser Visual Studio Code qui propose un environnement de développement dédié aux fonctions Azure.

Nous pourrons donc accéder à la fonction Azure tp-cloud-autoscale-vpn définie précédemment et y déployer ce que nous aurons développé. Une fois Visual Studio Code configuré avec les consignes du lien précédent, nous disposerons d’un modèle de projet basé sur un déclencheur HTTP (notre cas) qui créera localement une arborescence de ce type :

├── AutoscaleFunction
    ├── autoscale
    │   ├── function.json
    │   ├── run.ps1
    │   └── sample.dat
    ├── autoscale-vpn
    │   ├── function.json
    │   ├── run.ps1
    │   └── sample.dat
    ├── host.json
    ├── local.settings.json
    ├── profile.ps1
    └── requirements.psd1

Nous avons donc nommé notre projet AutoscaleFunction et nous avons défini dans celui-ci deux applications autoscale et autoscale-vpn (celle que nous allons déployer). Celles-ci se basent sur la même configuration à la racine du projet se composant des fichiers suivants, initiés par Visual Studio Code :

  • host.json : fichier de metadonnées contenant des paramètres de configuration appliqués à toutes les fonctions Azure du projet. Dans notre cas, nous ne le modifions pas.

  • local.settings.json : permet de conserver la définition de paramètres pour pouvoir exécuter localement les fonctions. Dans notre cas, il s’agit essentiellement de variables d’environnement utilisées par le script. Il n’a pas vocation à être déployé dans la fonction Azure finale.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME_VERSION": "7.2",
    "FUNCTIONS_WORKER_RUNTIME": "powershell",
    "MAX_NB_VM": 2,
    "RESOURCE_GROUP": "TpCloudHybrideVPN",
    "LOCATION": "francecentral",
    "VNET_NAME": "VNetVPN",
    "SUBNET_NAME": "Frontend",
    "NETWORK_SG_NAME": "FrontendNSG",
    "STORAGE_ACCOUNT_NAME": "tpcloudfuncvpnstorage",
    "BOOTDIAG_STORAGE_ACCOUNT_NAME": "vmbootdiagstoragevpn",
    "SSH_PUBLIC_KEY": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMaFCF87N0L+YZkPuBpu6ikx68q+wFAA10/mSnrqnyS",
    "PASSWORD": "XXXXXXXXXXXXXXXXXXXX",
    "INIT_SCRIPT_CONTAINER": "init-script-vm",
    "INIT_SCRIPT_NAME": "init.sh",
  }
}
  • profile.ps1 : propre à Powershell, définit des commandes exécutées avant le lancement des fonctions Azure du projet (en particulier l’authentification). Ici encore, nous le laissons tel quel.
  • requirements.psd1 : ce fichier rassemble les dépendances en modules Powershell du script appelé par la fonction Azure. Dans notre cas, nous indiquons la version 5 du module Az.
# This file enables modules to be automatically managed by the Functions service.
# See https://aka.ms/functionsmanageddependency for additional information.
#
@{
    'Az'='5.*' 
}

Enfin, lors de la création d’une fonction de déclenchement HTTP avec Visual Studio Code, un sous-répertoire est créé avec les fichiers suivants :

  • function.json : contient les paramètres de déclenchement http de la fonction Azure, pour les requêtes entrantes et les réponses fournies par la fonction.
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "Request",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "Response"
    }
  ]
}
  • run.ps1 : le gros morceau, le script Powershell assurant la création et suppression des VM que nous détaillerons juste après.
  • sample.dat : fichier d’exemple à des fins de test, non-utilisé dans notre cas.

Script Powershell d’autoscaling

Nous allons détailler dans cette partie le contenu du script run.ps1, lancé par la fonction Azure, qui sera chargé de créer/supprimer des VM selon les requêtes HTTP reçues sur les endpoints respectifs.

C’est ce que nous appellerons l’application associée à la fonction Azure précédemment créée. Elle sera disponible en tant qu’API à une URL générée spécifiquement pour la fonction Azure.

Rappelons les specifications que le script doit respecter :

  • le nombre maximum de VM est de deux
  • les VM sont de classe Standard_B1s (1 vCpu, 1 GB RAM) et leur OS est Debian 12, comme la VM du réseau local
  • l’adresse IP des VM est déterminée : 10.1.0.100 et 10.1.0.101
  • un script d’initialisation init.sh doit être lancé au démarrage

Nous allons logiquement offrir 2 endpoints qui seront accessibles à partir de l’URL de la fonction Azure https://<nom fonction Azure>.azurewebsites.net/api/<nom application> :

  • create : création d’une VM avec les caractéristiques attendues s’il en existe moins de 2
  • delete : suppression d’une VM précédemment créée par la fonction

Par exemple, pour créer une VM, il faut donc appeler le endpoint https://tp-cloud-autoscale-vpn.azurewebsites.net/api/autoscale-vpn?action=create dans notre cas (fonction tp-cloud-autoscale et application autoscale-vpn). De la sorte, nous disposons donc d’une API serverless pouvant être sollicitée par les outils de supervision afin de réaliser la mise à l’échelle automatique de notre application.

Voici maintenant le script run.ps1 composant l’application autoscale-vpn :

using namespace System.Net

# Récupération du contenu de la requête, par GET ou POST
param($Request)

$action = $Request.Query.action
if (-not $action) {
    $action = $Request.Body.action
}

Write-Output "INFO - Action is : $action"

# Utilisation des variables d'environnement pour les autres paramètres
$maxNbVm = $env:MAX_NB_VM
$resourceGroupName = $env:RESOURCE_GROUP
$location = $env:LOCATION
$vNetName = $env:VNET_NAME
$subnetName = $env:SUBNET_NAME
$nsgName = $env:NETWORK_SG_NAME
$storageAccountName = $env:STORAGE_ACCOUNT_NAME
$bootdiagStorageAccountName = $env:BOOTDIAG_STORAGE_ACCOUNT_NAME
$sshPublicKey = $env:SSH_PUBLIC_KEY
$password = $env:PASSWORD
$initScriptContainer = $env:INIT_SCRIPT_CONTAINER
$initScriptName = $env:INIT_SCRIPT_NAME

Write-Output "INFO - Resource group is : $resourceGroupName"

# Connexion au compte Azure
Connect-AzAccount -Identity

#############
# Fonctions #
#############

# Fonction pour créer une VM
function New-VM {
    # Génération automatique des noms de VM et NIC
    $randValue = Get-Random -Minimum 1000 -Maximum 9999
    $vmName = "AutoScaledVM" + $randValue
    $nicName = "AutoScaledNIC" + $randValue

    # Vérifier le nombre de VM existantes avec le tag "AutoScaled"
    $vms = Get-AzVM -ResourceGroupName $resourceGroupName | Where-Object { $_.Tags["AutoScaled"] -eq "true" }
    if ($vms.Count -ge $maxNbVm) {
        $returnObj = "" | Select-Object -Property body,status,httpStatus
        $returnObj.body = "Maximum number of auto-scaled VMs reached"
        $returnObj.status = "OK"
        $returnObj.httpStatus = "OK"
        return $returnObj
    }
    Write-Output "INFO - There are currently $($vms.Count) autoscaled VM running over a maximum number of $maxNbVm."

    # Placer le réseau virtuel dans une variable
    $net = @{
        Name              = $vNetName
        ResourceGroupName = $resourceGroupName
    }
    $vnet = Get-AzVirtualNetwork @net

    # Récupérer le sous-réseau dans lequel on veut créer la VM
    $subnet = Get-AzVirtualNetworkSubnetConfig -Name $subnetName -VirtualNetwork $vnet

    # Placer le groupe de sécurité réseau dans une variable
    $ns = @{
        Name              = $nsgName
        ResourceGroupName = $resourceGroupName
    }
    $nsg = Get-AzNetworkSecurityGroup @ns

    # Créer l'interface réseau avec une adresse IP privée déterminée
    # à partir de la plage du sous-réseau et du nombre de VM créées
    $subnetIpRange = [System.Net.IPAddress]::Parse($subnet.AddressPrefix.Split('/')[0])
    $newIp = $subnetIpRange.GetAddressBytes()
    # Incrémenter l'octet de fin pour chaque nouvelle VM à partir de 100
    $newIp[3] = $newIp[3] + 100 + $vms.Count
    $calculatedIp = [System.Net.IPAddress]::new($newIp)
    Write-Output "INFO - New VM static IP adress will be : $calculatedIp"

    $ipConfig = New-AzNetworkInterfaceIpConfig -Name "IpConfig$vmName" -Subnet $Subnet -PrivateIpAddress $calculatedIp

    $nic = @{
        Name                 = $nicName
        ResourceGroupName    = $resourceGroupName
        Location             = $location
        NetworkSecurityGroup = $nsg
        IpConfiguration      = $ipConfig
    }
    $nicVM = New-AzNetworkInterface @nic

    # Configurer le type de VM
    $vmsz = @{
        VMName = $vmName
        VMSize = 'Standard_B1s'  
    }

    # Définition des credentials (azureuser / $password)
    $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
    $cred = New-Object System.Management.Automation.PSCredential ("azureuser", $securePassword)

    # Définition d'OS SSH pour Linux
    $vmos = @{
        ComputerName                  = $vmName
        Credential                    = $cred
        DisablePasswordAuthentication = $true
    }

    $vmimage = @{
        PublisherName = 'Debian'
        Offer         = 'debian-12'
        Skus          = '12'
        Version       = 'latest'    
    }

    # Boucle d'attente que l'interface réseau soit bien créée
    $nicExists = $false
    $maxRetries = 3
    $retryCount = 0
    while (-not $nicExists -and $retryCount -lt $maxRetries) {
        try {
            $nic = Get-AzNetworkInterface -ResourceGroupName $resourceGroupName -Name $nicName -ErrorAction Stop
            $nicExists = $true
        } catch {
            Write-Host "Network interface $nicName not yet created. Waiting during 2 seconds..."
            Start-Sleep -Seconds 2
            $retryCount++
        }
    }

    $vmConfig = New-AzVMConfig @vmsz
    $vmConfig = Set-AzVMOperatingSystem -VM $vmConfig @vmos -Linux
    $vmConfig = Set-AzVMSourceImage -VM $vmConfig @vmimage
    $vmConfig = Add-AzVMNetworkInterface -VM $vmConfig -Id $nicVM.Id
    # Définition du compte de stockage pour le diagnostic de démarrage des VM
    $vmConfig = Set-AzVMBootDiagnostic -VM $vmConfig -Enable -ResourceGroupName $resourceGroupName -StorageAccountName $bootdiagStorageAccountName

    # Ajouter la clef SSH si Linux
    $vmssh = @{
        VM = $vmConfig
        KeyData = $sshPublicKey
        Path = "/home/azureuser/.ssh/authorized_keys"
    }
    Add-AzVMSshPublicKey @vmssh

    ## Créer la VM à partir de la configuration précédemment définie ##
    $tags = @{AutoScaled="true"}

    $vm = @{
        ResourceGroupName = $resourceGroupName
        Location          = $location
        Zone              = 1
        Tag               = $tags
        VM                = $vmConfig
    }

    try {
        $vmCreated = New-AzVM @vm
        if ($vmCreated) {
            # Si la VM est créée, on lance le script qui va installer Docker
            # Et lancer l'application avec docker compose
            Write-Output "INFO - New VM $vmName successfully created, now executing the init script."

            $storageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -AccountName $storageAccountName)[0].Value
            $storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
            $blobUri = (Get-AzStorageBlob -Context $storageContext -Container $initScriptContainer -Blob $initScriptName).ICloudBlob.Uri.AbsoluteUri

            $Params = @{
                ResourceGroupName  = $resourceGroupName
                VMName             = $vmName
                Location           = $location
                Name               = "VMInitScript"
                Publisher          = 'Microsoft.Azure.Extensions'
                ExtensionType      = 'CustomScript'
                TypeHandlerVersion = '2.1'
                Settings           = @{fileUris = @($blobUri); commandToExecute = "./$initScriptName"}
                ProtectedSettings  = @{storageAccountName = $storageAccountName; storageAccountKey = $storageAccountKey}
            }

            try {
                Set-AzVMExtension @Params
                Write-Output "INFO - Init script correctly executed"
                $body = "VM $($vmName) successfully created and init script correctly executed"
                $status = "OK"
                $httpStatus = "OK"
            } catch {
                Write-Output "ERROR - An error occured during init script execution"
                Write-Output $_
                $body = "VM $($vmName) successfully created but init script failed. Please execute it directly on the VM."
                $status = "WARNING"
                $httpStatus = "OK"
            }
        } else {
            $body = "ERROR : NIC $($nicVM.Name) created but VM $($vmName) failed to be created"
            $status = "KO"
            $httpStatus = "InternalServerError"
        }
    } catch {
        $body = "ERROR : NIC $($nicVM.Name) created but VM $($vmName) failed to be created"
        $status = "KO"
        $httpStatus = "InternalServerError"
    }

    $returnObj = "" | Select-Object -Property body,status,httpStatus
    $returnObj.body = $body
    $returnObj.status = $status
    $returnObj.httpStatus = $httpStatus
    return $returnObj
}

# Fonction pour supprimer une VM
function Remove-VM {
    # Supprimer une VM auto-scalée aléatoire
    $vms = Get-AzVM -ResourceGroupName $resourceGroupName | Where-Object { $_.Tags["AutoScaled"] -eq "true" }
    if ($vms.Count -gt 0) {
        $vmToDelete = $vms | Get-Random
        $nicToDelete = $vmToDelete.NetworkProfile.NetworkInterfaces.Id.Split('/')[-1]
        $diskToDelete = $vmToDelete.StorageProfile.OsDisk.Name
        
        # Supprimer la VM
        Remove-AzVM -ResourceGroupName $resourceGroupName -Name $vmToDelete.Name -Force

        # Supprimer l'interface réseau
        Remove-AzNetworkInterface -ResourceGroupName $resourceGroupName -Name $nicToDelete -Force

        # Supprimer le disque
        Remove-AzDisk -ResourceGroupName $resourceGroupName -DiskName $diskToDelete -Force

        $body = "VM $($vmToDelete.Name), NIC $($nicToDelete) and disk $($diskToDelete) deleted"
        $status = "OK"
        $httpStatus = "OK"
    } else {
        $body = "No auto-scaled VMs to delete"
        $status = "OK"
        $httpStatus = "OK"
    }

    $returnObj = "" | Select-Object -Property body,status,httpStatus
    $returnObj.body = $body
    $returnObj.status = $status
    $returnObj.httpStatus = $httpStatus
    return $returnObj
}

########
# Main #
########

# Creation ou suppression de VM selon la valeur
# du champ "action" foruni dans la requête entrante
if ($action -eq "create") {
    Write-Output "INFO - Creating a new VM"
    $returnObj = New-VM
} elseif ($action -eq "delete") {
    Write-Output "INFO - Removing an existing VM"
    $returnObj = Remove-VM
} else {
    Write-Output "ERROR - Invalid parameters in the request"
    $returnObj = "" | Select-Object -Property body,status,httpcode
    $returnObj.body = "Invalid request"
    $returnObj.status = "KO"
    $returnObj.httpStatus = BadRequest
}

# Envoi de la réponse
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 
    StatusCode = $returnObj.httpStatus
    Headers = @{
        "Content-type" = "application/json"
    }
    # Garde les données ordonnées comme indiqué dans la sortie json
    Body = [Ordered]@{
        "Status" = $returnObj.status
        "Msg" = $returnObj.body
    } | ConvertTo-Json
})

Quelques remarques sur le contenu du script :

  • Le script accepte des requêtes HTTP GET ou POST, et renvoie le statut de la tâche demandée dans le corps de la réponse.
  • Une création de VM consiste en 3 ressources Azure : la VM elle-même, la carte réseau (NIC, Network Interface Card) et le disque associé.
  • Les VM créées se voient attribuer le groupe de sécurité réseau FrontendNSG dans la configuration de leur NIC.
  • Le tag Autoscaled positionné à la valeur true permet d’identifier les VM créées pour assurer la mise à l’échelle de l’application (dont le nombre maximum est de 2)
  • L’attribution d’une adresse IP statique aux VM (10.1.0.100 ou 10.1.0.101) oblige à créer un objet de configuration d’interface IP avec la commande suivante, qui sera ensuite appliqué à la carte réseau.
$ipConfig = New-AzNetworkInterfaceIpConfig -Name "IpConfig$vmName" -Subnet $Subnet -PrivateIpAddress $calculatedIp
  • La création de la carte réseau (NIC) pouvant prendre quelques secondes, il a été nécessaire de créer une boucle d’attente avant de lancer la création de la VM.
  • Le script d’initialisation init.sh est récupéré du conteneur de stockage blob Azure associé au compte de stockage fourni, puis lancé par l’appel de la fonction Powershell Set-AZVMExtension avec les paramètres suivants :
$Params = @{
    ResourceGroupName  = $resourceGroupName
    VMName             = $vmName
    Location           = $location
    Name               = "VMInitScript"
    Publisher          = 'Microsoft.Azure.Extensions'
    ExtensionType      = 'CustomScript'
    TypeHandlerVersion = '2.1'
    Settings           = @{fileUris = @($blobUri); commandToExecute = "./$initScriptName"}
    ProtectedSettings  = @{storageAccountName = $storageAccountName; storageAccountKey = $storageAccountKey}
}

Configuration de la fonction Azure et déploiement de l’application

Afin de configurer la fonction pour qu’elle puisse exécuter l’application que nous allons déployer, il faut se rendre sur le portail Azure et effectuer les tâches suivantes sur la page de la fonction Azure :

  • Ajouter Microsoft comme identity provider dans Settings -> Authentication
  • Créer une identité system assigned (Settings -> Identity) et ajouter un rôle avec des droits Contributor sur le groupe de ressources
  • N’autoriser que la plage d’IP des clients du VPN dans Settings -> Networking (puisque l’URL de la fonction Azure est à priori publique)
  • Créer les variables d’environnements requises par la fonction dans Settings -> Environment variables dont voici la liste dans notre cas avec leurs valeurs respectives :
MAX_NB_VM:                      2
RESOURCE_GROUP:                 TpCloudHybrideVPN
LOCATION:                       francecentral
VNET_NAME:                      VNetVPN
SUBNET_NAME:                    Frontend
NETWORK_SG_NAME:                FrontendNSG
STORAGE_ACCOUNT_NAME:           tpcloudfuncvpnstorage
BOOTDIAG_STORAGE_ACCOUNT_NAME:  vmbootdiagstoragevpn
SSH_PUBLIC_KEY:                 xxxxxxxxxxxx
PASSWORD:                       XXXXXXXXXXX
INIT_SCRIPT_CONTAINER:          init-script-vm
INIT_SCRIPT_NAME:               init.sh

Ensuite, le déploiement de l’application autoscale-vpn pour laquelle nous avons développé le script Powershell précédent s’effectue très simplement grâce à Visual Studio Code.

Test de la fonction Azure

Nous allons tout d’abord appeler plusieurs fois le endpoint de la fonction Azure assurant la création de VM :

curl https://tp-cloud-autoscale-vpn.azurewebsites.net/api/autoscale-vpn?action=create

Les deux premières fois, nous recevons un message indiquant la création d’une VM :

{
  "Status": "OK",
  "Msg": "VM AutoscaledVM1734 successfully created and init script correctly executed"
}

Ensuite, le message de retour indique comme prévu qu’il n’est pas possible de créer de VM supplémentaire puisqu’on en a déjà atteint le nombre maximum.

{
  "Status": "OK",
  "Msg": "Maximum number of auto-scaled VMs reached"
}

Si on se rend sur le portail Azure et que l’on consulte la liste de ressources de la souscription, nous voyons bien apparaître les deux VM ainsi que le disque et la carte réseau qui leur sont associés.

Liste des ressources

Le portail Azure offre aussi la possibilité d’accéder à l’environnement où est exécutée l’application de la fonction Azure, et de consulter la sortie standard afin d’examiner de possibles problèmes. Pour ce faire, il faut se rendre dans l’onglet Monitor disponible dans le menu de la fonction Azure.

Dans notre cas, la sortie de notre application est la suivante lors d’une création de VM :

2024-07-18T12:56:25.578 [Information] Executing 'Functions.autoscale-vpn' (Reason='This function was programmatically called via the host APIs.', Id=c67ff4f2-5158-4d67-b0b8-0547861c8238)
2024-07-18T12:56:25.597 [Information] OUTPUT: INFO - Action is : create
2024-07-18T12:56:25.600 [Information] OUTPUT: INFO - Resource group is : TpCloudHybrideVPN
2024-07-18T12:56:26.246 [Information] OUTPUT:
2024-07-18T12:57:19.369 [Information] INFORMATION: Selecting “North Europe” may reduce your costs. The region you’ve selected may cost more for the same services. You can disable this message in the future with the command “Update-AzConfig -DisplayRegionIdentified $false”. Learn more at https://go.microsoft.com/fwlink/?linkid=2225665
2024-07-18T12:57:24.233 [Warning] WARNING: The output of cmdlet Get-AzStorageAccountKey may compromise security by showing the following secrets: Value. Learn more at https://go.microsoft.com/fwlink/?linkid=2258844
2024-07-18T12:57:24.290 [Warning] WARNING: The output of cmdlet New-AzStorageContext may compromise security by showing the following secrets: TableStorageAccount.Credentials.Key, ConnectionString. Learn more at https://go.microsoft.com/fwlink/?linkid=2258844
2024-07-18T12:57:24.536 [Warning] WARNING: The output of cmdlet Get-AzStorageBlob may compromise security by showing the following secrets: Context.TableStorageAccount.Credentials.Key, Context.ConnectionString. Learn more at https://go.microsoft.com/fwlink/?linkid=2258844
2024-07-18T12:58:56.641 [Information] OUTPUT: Subscription name    Tenant
2024-07-18T12:58:56.641 [Information] OUTPUT: -----------------    ------
2024-07-18T12:58:56.641 [Information] OUTPUT: Azure subscription 1 cd075fd5-dafc-45de-9357-b983733f4ca7
2024-07-18T12:58:56.642 [Information] OUTPUT: INFO - Creating a new VM
2024-07-18T12:58:56.642 [Information] OUTPUT:
2024-07-18T12:58:56.643 [Information] Executed 'Functions.autoscale-vpn' (Succeeded, Id=c67ff4f2-5158-4d67-b0b8-0547861c8238, Duration=151064ms)

Nous allons maintenant tester la connexion SSH aux VM en passant par le bastion que nous avons précédemment créé.

Il faut pour cela éditer le fichier ~/.ssh/config de la session Linux en cours. Les deux clefs SSH utilisées (une pour la VM bastion, l’autre pour les VM d’autoscale) sont celles dont la clef publique a été communiquée lors de la création des VM.

Host azure-bastion
        HostName 10.1.1.4
        User azureuser
        IdentityFile ~/.ssh/id_ed25519_azure_bastion

Host vm-autoscale-1
        HostName 10.1.0.100
        ProxyJump azure-bastion
        User azureuser
        IdentityFile ~/.ssh/id_ed25519_azure

Host vm-autoscale-2
        HostName 10.1.0.101
        ProxyJump azure-bastion
        User azureuser
        IdentityFile ~/.ssh/id_ed25519_azure

Le paramètre de configuration ProxyJump des VM d’autoscale indique que la connexion est dirigée vers la VM de bastion qui servira de rebond.

En se connectant à une des VM, on peut constater que le script d’initialisation a bien été exécuté et que le conteneur de l’application est en cours d’exécution.

Accès à la VM d'autoscale

De la sorte, le trafic peut désormais être routé vers les VM d’autoscale comme on le constate sur le tableau de bord du load balancer Traefik.

Dashboard Traefik autoscale

Maintenant, nous allons appeler deux fois l’API de la fonction Azure pour supprimer les VM.

curl https://tp-cloud-autoscale-vpn.azurewebsites.net/api/autoscale-vpn?action=delete

Le meesage de retour est le suivant, pour chacune des VM :

{
  "Status": "OK",
  "Msg": "VM AutoscaledVM1734, NIC AutoscaledNIC1734 and disk AutoscaledVM1734_OsDisk_1_06a9b8c219f9443fb7e00eaabb818b62 deleted"
}

Nous avons le message de log ci-dessous sur la console de la fonction Azure (onglet Monitor du menu de la fonction sur le portail) :

2024-07-18T10:13:12.723 [Information] Executing 'Functions.autoscale-vpn' (Reason='This function was programmatically called via the host APIs.', Id=4d534ed1-7f5d-4de4-b268-f4eca256d978)
2024-07-18T10:13:12.784 [Information] OUTPUT: INFO - Action is : delete
2024-07-18T10:13:12.787 [Information] OUTPUT: INFO - Resource group is : TpCloudHybrideVPN
2024-07-18T10:13:13.311 [Information] OUTPUT:
2024-07-18T10:14:35.736 [Information] OUTPUT: Subscription name    Tenant
2024-07-18T10:14:35.745 [Information] OUTPUT: -----------------    ------
2024-07-18T10:14:35.749 [Information] OUTPUT: Azure subscription 1 cd075fd5-dafc-45de-9357-b983733f4ca7
2024-07-18T10:14:35.750 [Information] OUTPUT: INFO - Removing an existing VM
2024-07-18T10:14:35.750 [Information] OUTPUT:
2024-07-18T10:14:35.751 [Information] Executed 'Functions.autoscale-vpn' (Succeeded, Id=4d534ed1-7f5d-4de4-b268-f4eca256d978, Duration=83030ms)

Et nous constatons le retour à la situation initiale sur le tableau de bord Traefik.

Dashboard Traefik

Sommaire