Cloud & Déploiement angularforall.com

- Azure App Service : déployer une API Node.js

Azure App-Service Node-Js Ci-Cd Github-Actions Deployment-Slots Application-Insights Key-Vault Managed-Identity Autoscale Azure-Cli Express Paas Devops
Azure App Service : déployer une API Node.js

Déployez votre API Node.js Express sur Azure App Service : provisioning CLI, slots, Application Insights, Key Vault, scaling et CI/CD GitHub Actions.

Architecture Azure App Service

Azure App Service est un PaaS managé qui exécute des applications web (Node.js, Python, .NET, Java, PHP, Ruby) et des conteneurs Docker, sans gérer ni VM ni système d'exploitation. Vous fournissez le code, Azure provisionne le runtime, le load balancer, le SSL, le scaling et le monitoring. C'est l'équivalent direct d'AWS Elastic Beanstalk et de Google Cloud App Engine.

Pour une API Node.js Express, App Service propose le runtime Linux NODE:20-lts (ou 22-lts) avec un reverse proxy Kestrel exposant le port process.env.PORT. Le code est stocké sur un partage Azure Files monté dans /home, partagé entre toutes les instances en cas de scale-out.

Composants clés du service

Ressource Rôle Granularité
App Service Plan Pool de ressources de calcul (CPU, RAM) facturé à l'heure 1 plan = N WebApps
Web App L'application elle-même : code, runtime, configuration 1 WebApp = N slots
Deployment Slot Environnement parallèle (staging, dev) avec son propre URL Standard+ : 5 slots / Premium : 20
Application Insights Télémétrie : requêtes, exceptions, dépendances, métriques live 1 instance par WebApp
Key Vault Coffre-fort pour secrets, certificats, clés cryptographiques Référencé via Managed Identity

Comparaison Azure vs AWS vs GCP pour Node.js managé

Critère Azure App Service AWS Elastic Beanstalk GCP App Engine (Standard)
Tier gratuit F1 : 60 min CPU/j, 1 Go RAM partagée Aucun (EC2 t2.micro 12 mois) F1 : 28 h gratuites/jour
Coût production minimal B1 ~13 $/mois (1 cœur, 1.75 Go) ~12 $/mois (t3.small + ELB) Pay-per-instance ~25 $/mois
Slots / blue-green Natifs dès Standard (gratuits) Environments cloning manuel Versions traffic split natif
SSL custom domain Certificat managé gratuit ACM gratuit + ALB requis Certificat managé gratuit
Monitoring inclus Application Insights (1 Go/mois) CloudWatch (limité free) Cloud Trace + Logging
CI/CD natif GitHub Actions, Azure DevOps CodePipeline, GitHub Actions Cloud Build, GitHub Actions
Cas d'usage API .NET/Node simples à moyennes Apps multi-instance custom Stateless très scalable
Quand choisir App Service ? Pour une API Node.js stateless, un back-office Express ou un BFF Angular, App Service est imbattable en simplicité. Pour des workloads à très haut trafic ou nécessitant Kubernetes, regardez Azure Container Apps ou AKS.

Setup Azure CLI et création de la WebApp

Toutes les opérations de cet article passent par Azure CLI, l'outil officiel multi-plateforme. Il s'installe sur Windows (MSI), macOS (brew install azure-cli) et Linux (curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash).

Authentification et sélection de l'abonnement

# Authentification interactive (ouvre le navigateur)
az login

# Lister les abonnements disponibles
az account list --output table

# Sélectionner l'abonnement actif (par nom ou GUID)
az account set --subscription "Visual Studio Enterprise"

# Vérifier l'identité courante et l'abonnement
az account show --query "{name:name, id:id, user:user.name}" -o json

Création du Resource Group et de l'App Service Plan

Toutes les ressources Azure vivent dans un Resource Group — un conteneur logique facilitant la facturation et la suppression groupée. L'App Service Plan définit la taille (SKU) des workers.

# Variables d'environnement (PowerShell ou bash)
RESOURCE_GROUP="rg-api-nodejs-prod"
LOCATION="francecentral"
PLAN_NAME="plan-api-nodejs"
APP_NAME="api-nodejs-af-2026"   # nom global unique → URL .azurewebsites.net

# 1. Resource Group
az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION

# 2. App Service Plan Linux B1 (Basic — 13 $/mois)
# SKU disponibles : F1 (free), B1/B2/B3 (basic), S1/S2/S3 (standard),
# P1V3/P2V3/P3V3 (premium v3), I1V2/I2V2/I3V2 (isolated v2)
az appservice plan create \
  --name $PLAN_NAME \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --is-linux \
  --sku B1

# 3. Web App Node.js 20 LTS
az webapp create \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --plan $PLAN_NAME \
  --runtime "NODE:20-lts"

# 4. Vérifier l'URL générée
az webapp show \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "{name:name, url:defaultHostName, state:state}" -o table

Configurer le port et HTTPS-only

# Forcer HTTPS (rejeter les connexions HTTP)
az webapp update \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --https-only true

# Définir la version min de TLS
az webapp config set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --min-tls-version "1.2"

# Activer "Always On" (instance en mémoire 24/7)
# IMPORTANT : indispensable en B1+ pour éviter les cold starts
az webapp config set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --always-on true
Always On n'est pas disponible sur le tier Free F1. Sans cette option, App Service éteint l'application après 20 minutes d'inactivité, provoquant un cold start de 5 à 30 secondes au prochain hit. C'est rédhibitoire pour une API en production.

Déployer une API Node.js Express

App Service exécute votre serveur Node directement — pas besoin de Dockerfile ni de Nginx en frontal. Le runtime expose la variable process.env.PORT qu'il faut écouter (Azure utilise un port dynamique entre 8080 et 8081).

Application Express minimale

// server.js
const express = require('express');
const app = express();

// IMPORTANT : utiliser process.env.PORT fourni par Azure
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Endpoint de healthcheck consommé par Azure pour le warm-up
app.get('/health', (_req, res) => {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    node: process.version,
    env: process.env.NODE_ENV || 'development',
  });
});

// Endpoint métier exemple
app.get('/api/products', (_req, res) => {
  res.json([
    { id: 1, name: 'Angular Pro', price: 299 },
    { id: 2, name: 'Cloud Bundle', price: 499 },
  ]);
});

app.listen(PORT, () => {
  console.log(`API listening on port ${PORT}`);
});

package.json — script start obligatoire

{
  "name": "api-nodejs-azure",
  "version": "1.0.0",
  "engines": {
    "node": ">=20.0.0"
  },
  "scripts": {
    "start": "node server.js",
    "dev":   "nodemon server.js",
    "test":  "jest --coverage"
  },
  "dependencies": {
    "express": "^4.19.2",
    "applicationinsights": "^2.9.5",
    "@azure/identity": "^4.0.1",
    "@azure/keyvault-secrets": "^4.7.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "jest": "^29.7.0"
  }
}

Azure lance automatiquement npm install --production puis npm start. Si votre script de démarrage diffère, surchargez-le via la commande de démarrage personnalisée :

# Définir une startup command custom
az webapp config set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --startup-file "node dist/server.js"

# Définir la version Node.js exacte (override engines.node)
az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --settings WEBSITE_NODE_DEFAULT_VERSION="~20"

Déploiement via ZIP push

# Préparer un ZIP en excluant node_modules et fichiers inutiles
zip -r app.zip . \
  -x "node_modules/*" "*.git*" "tests/*" "*.env*" "README*"

# Déployer en mode "zip deploy" (upload + npm install distant)
az webapp deploy \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --src-path app.zip \
  --type zip \
  --async false

# Vérifier le statut du dernier déploiement
az webapp deployment list \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[0].{id:id, status:status, time:received_time}" -o table
Build remote vs local : Par défaut, App Service Linux fait npm install sur le serveur (lent, mais reproductible). Pour accélérer le déploiement, ajoutez le setting SCM_DO_BUILD_DURING_DEPLOYMENT=true et zippez le code sans node_modules. Pour des builds personnalisés, fournissez un Dockerfile via App Service for Containers.

Tester le déploiement

# Tester l'endpoint healthcheck
curl -i https://$APP_NAME.azurewebsites.net/health

# Réponse attendue :
# HTTP/2 200
# content-type: application/json; charset=utf-8
# {"status":"ok","uptime":42.5,"node":"v20.11.1","env":"production"}

# Suivre les logs en temps réel
az webapp log tail \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP

# Activer la rétention des logs (filesystem 35 Mo)
az webapp log config \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --application-logging filesystem \
  --level information \
  --web-server-logging filesystem

Deployment slots et swap zero-downtime

Les deployment slots sont l'arme la plus efficace d'App Service contre les downtimes. Chaque slot est une copie de la WebApp avec son propre URL, ses settings et son historique de déploiement. Le swap bascule atomiquement le trafic entre slots.

Créer un slot staging

# Les slots sont disponibles à partir du tier Standard S1
# Upgrader le plan au tier S1 (~73 $/mois) ou P1V3 (~84 $/mois)
az appservice plan update \
  --name $PLAN_NAME \
  --resource-group $RESOURCE_GROUP \
  --sku P1V3

# Créer un slot "staging" cloné depuis production
az webapp deployment slot create \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --configuration-source $APP_NAME

# URL du slot : https://api-nodejs-af-2026-staging.azurewebsites.net
# (ajout du suffixe -staging au hostname)

Déployer la nouvelle version sur staging

# Build de la nouvelle version
zip -r app-v2.zip . -x "node_modules/*" ".git*"

# Déploiement ciblé sur le slot staging
az webapp deploy \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --src-path app-v2.zip \
  --type zip

# Smoke test sur le slot staging avant le swap
curl -f https://${APP_NAME}-staging.azurewebsites.net/health \
  || { echo "Smoke test KO — abandon swap"; exit 1; }

Swap atomique staging → production

# Étape 1 : "swap with preview" — réchauffe staging avec les settings de prod
az webapp deployment slot swap \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --target-slot production \
  --action preview

# Étape 2 : valider manuellement (tester staging avec config prod)
curl https://${APP_NAME}-staging.azurewebsites.net/health

# Étape 3 : finaliser le swap (bascule effective du trafic)
az webapp deployment slot swap \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --target-slot production \
  --action swap

# En cas de problème en production : rollback immédiat
# (re-swap pour revenir à l'ancienne version désormais en staging)
az webapp deployment slot swap \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --target-slot production

Sticky settings — attacher des paramètres au slot

Par défaut, les App Settings sont swappées avec le slot. Pour qu'un setting reste attaché à son slot (ex. : la chaîne de connexion à la base de données staging ne doit JAMAIS partir en production), marquez-le comme slot setting.

# Définir DATABASE_URL différent en staging et en production
# et marquer ce setting comme "sticky"
az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot staging \
  --settings DATABASE_URL="postgres://staging.db.internal:5432/api"

az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --settings DATABASE_URL="postgres://prod.db.internal:5432/api"

# Marquer DATABASE_URL comme sticky (ne suit pas le swap)
az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --slot-settings DATABASE_URL
Le swap n'est pas instantané au sens HTTP : Azure lance un warm-up (HTTP GET /) sur les workers du slot cible avant de basculer le routage. Ce warm-up dure 30 à 90 secondes mais reste invisible pour les clients (l'ancien slot continue de servir le trafic pendant ce temps).
Auto-swap : Vous pouvez configurer un auto-swap pour qu'un push sur staging déclenche automatiquement le swap après warm-up : az webapp deployment slot auto-swap --slot staging --auto-swap-slot production. Combiné à GitHub Actions, vous obtenez un déploiement continu zero-downtime.

Observabilité avec Application Insights

Application Insights est la solution APM d'Azure : trace distribuée, métriques en temps réel, exceptions, dépendances HTTP/SQL. Pour Node.js, l'intégration est automatique via la variable APPLICATIONINSIGHTS_CONNECTION_STRING.

Provisionner Application Insights

# 1. Créer un workspace Log Analytics (requis par AI v2)
az monitor log-analytics workspace create \
  --workspace-name "law-api-nodejs" \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION

# 2. Créer la ressource Application Insights
az monitor app-insights component create \
  --app "ai-api-nodejs" \
  --location $LOCATION \
  --resource-group $RESOURCE_GROUP \
  --workspace "law-api-nodejs" \
  --kind web

# 3. Récupérer la connection string
AI_CONNECTION=$(az monitor app-insights component show \
  --app "ai-api-nodejs" \
  --resource-group $RESOURCE_GROUP \
  --query connectionString -o tsv)

# 4. L'injecter dans la WebApp
az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --settings \
    APPLICATIONINSIGHTS_CONNECTION_STRING="$AI_CONNECTION" \
    ApplicationInsightsAgent_EXTENSION_VERSION="~3"

Instrumenter le code Node.js

// telemetry.js — à charger AVANT tout autre import (server.js)
const appInsights = require('applicationinsights');

if (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) {
  appInsights
    .setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING)
    .setAutoCollectRequests(true)        // toutes les requêtes HTTP entrantes
    .setAutoCollectPerformance(true)     // CPU, mémoire, GC
    .setAutoCollectExceptions(true)      // exceptions non rattrapées
    .setAutoCollectDependencies(true)    // SQL, HTTP sortants, Redis
    .setAutoCollectConsole(true, true)   // console.log + console.error
    .setUseDiskRetryCaching(true)        // bufferise si AI down
    .setSendLiveMetrics(true)            // dashboard temps réel
    .start();

  // Tag custom : version de l'app dans toutes les traces
  const client = appInsights.defaultClient;
  client.context.tags[client.context.keys.cloudRole] = 'api-products';
  client.context.tags['ai.application.ver'] = process.env.APP_VERSION || 'unknown';
}

module.exports = appInsights;
// server.js — charger telemetry EN PREMIER
require('./telemetry'); // doit précéder express et autres imports

const express = require('express');
const appInsights = require('applicationinsights');
const client = appInsights.defaultClient;

const app = express();

app.post('/api/orders', async (req, res) => {
  const start = Date.now();
  try {
    // Logique métier...
    const order = await createOrder(req.body);

    // Custom event (analytics produit)
    client?.trackEvent({
      name: 'OrderCreated',
      properties: { orderId: order.id, total: order.total },
    });

    // Custom metric (KPI métier)
    client?.trackMetric({ name: 'OrderValue', value: order.total });

    res.status(201).json(order);
  } catch (err) {
    // Exception capturée explicitement avec contexte
    client?.trackException({
      exception: err,
      properties: { route: '/api/orders', userId: req.user?.id },
    });
    res.status(500).json({ error: 'Internal error' });
  } finally {
    client?.trackMetric({
      name: 'OrderEndpointDurationMs',
      value: Date.now() - start,
    });
  }
});

Requêtes Kusto (KQL) utiles

// Top 10 endpoints les plus lents (P95 sur 24 h)
requests
| where timestamp > ago(24h)
| summarize p95 = percentile(duration, 95), count = count() by name
| order by p95 desc
| take 10

// Taux d'erreur 5xx par endpoint
requests
| where timestamp > ago(1h)
| extend isError = resultCode startswith "5"
| summarize total = count(), errors = countif(isError) by name
| extend errorRate = round(100.0 * errors / total, 2)
| where errorRate > 0
| order by errorRate desc

// Exceptions groupées par type
exceptions
| where timestamp > ago(7d)
| summarize count = count() by type, outerMessage
| order by count desc
| take 20
Coût AI : Le tier free inclut 1 Go d'ingestion et 90 jours de rétention. Au-delà, c'est environ 2.30 €/Go ingéré. Pour limiter la facture, configurez le sampling adaptatif côté SDK : setAutoCollectIncomingRequestsAtRoot(true).setSendingHistoricalData(false).setMaxBatchSize(250).

Key Vault et sécurité des secrets

Les App Settings sont chiffrées au repos, mais elles restent visibles dans le portail Azure et les logs CLI. Pour les vrais secrets (clés API tierces, JWT secrets, mots de passe DB), utilisez Azure Key Vault et Managed Identity — App Service obtient un token JWT auprès d'Azure AD sans aucun secret stocké côté code.

Provisionner Key Vault et Managed Identity

# 1. Créer le Key Vault (nom global unique)
KV_NAME="kv-api-nodejs-2026"

az keyvault create \
  --name $KV_NAME \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --enable-rbac-authorization true \
  --sku standard

# 2. Y stocker un secret (ex. : JWT signing key)
az keyvault secret set \
  --vault-name $KV_NAME \
  --name "JwtSecret" \
  --value "$(openssl rand -base64 32)"

az keyvault secret set \
  --vault-name $KV_NAME \
  --name "StripeApiKey" \
  --value "sk_live_xxxxxxxxxxxxxxxx"

# 3. Activer la System-assigned Managed Identity sur la WebApp
az webapp identity assign \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP

# Récupérer le principalId généré
PRINCIPAL_ID=$(az webapp identity show \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query principalId -o tsv)

# 4. Donner le rôle "Key Vault Secrets User" à l'identity
KV_ID=$(az keyvault show --name $KV_NAME --query id -o tsv)

az role assignment create \
  --assignee $PRINCIPAL_ID \
  --role "Key Vault Secrets User" \
  --scope $KV_ID

Référencer un secret depuis App Service (sans code)

Azure propose une syntaxe spéciale dans les App Settings : @Microsoft.KeyVault(SecretUri=...). App Service résout le secret au démarrage et le rend disponible comme une variable d'environnement standard. Le code Node.js lit simplement process.env.JWT_SECRET sans appeler Key Vault explicitement.

# Récupérer l'URI du secret
SECRET_URI=$(az keyvault secret show \
  --vault-name $KV_NAME \
  --name JwtSecret \
  --query id -o tsv)

# Référencer le secret dans la WebApp
az webapp config appsettings set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --settings JWT_SECRET="@Microsoft.KeyVault(SecretUri=$SECRET_URI)"

# Vérifier la résolution (status doit être "Resolved")
az webapp config appsettings list \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[?name=='JWT_SECRET']" -o table

Lecture programmatique avec @azure/keyvault-secrets

Pour des secrets lus dynamiquement (rotation à chaud, secrets multi-tenant), utilisez le SDK Azure avec DefaultAzureCredential. Aucune clé n'est stockée côté code — l'authentification passe par Managed Identity en prod et par Azure CLI en local.

// secrets.js
const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient }           = require('@azure/keyvault-secrets');

const KV_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

// DefaultAzureCredential teste dans l'ordre :
// 1. Variables d'environnement (CI/CD)
// 2. Managed Identity (App Service)
// 3. Azure CLI (dev local)
const credential = new DefaultAzureCredential();
const client     = new SecretClient(KV_URL, credential);

// Cache simple en mémoire (TTL 5 min)
const cache = new Map();

async function getSecret(name) {
  const cached = cache.get(name);
  if (cached && Date.now() - cached.at < 300000) {
    return cached.value;
  }

  const { value } = await client.getSecret(name);
  cache.set(name, { value, at: Date.now() });
  return value;
}

module.exports = { getSecret };
// Usage dans une route
const { getSecret } = require('./secrets');

app.post('/api/payments', async (req, res) => {
  // La clé Stripe est récupérée à l'exécution, jamais stockée en clair
  const stripeKey = await getSecret('StripeApiKey');
  const stripe    = require('stripe')(stripeKey);

  const charge = await stripe.charges.create({
    amount: req.body.amount,
    currency: 'eur',
    source: req.body.token,
  });
  res.json(charge);
});
Checklist sécurité avant production :
  • HTTPS-only activé (--https-only true)
  • TLS minimum 1.2 (idéalement 1.3)
  • Aucun secret en clair dans App Settings — tout passe par Key Vault
  • Managed Identity activée et rôle Key Vault Secrets User assigné
  • App Service Authentication configurée si endpoints privés (Easy Auth)
  • Soft-delete + purge protection activés sur le Key Vault
  • Diagnostic settings envoyés vers Log Analytics (audit trail)
  • IP restrictions ou Private Endpoint pour les APIs internes

Scaling et CI/CD GitHub Actions

App Service supporte deux types de scaling : vertical (changer le SKU du plan, B1 → P1V3) et horizontal (ajouter des instances, autoscale). Le tier Premium V3 (P1V3, P2V3, P3V3) débloque le scale-out automatique.

Scaling vertical — changer le SKU à la volée

# Comparatif rapide des SKUs Linux pour Node.js
# F1   : 0 $/mois   - 60 min CPU/jour, 1 Go RAM partagée  - tests
# B1   : 13 $/mois  - 1 vCPU, 1.75 Go RAM, 10 Go disque   - dev/staging
# B2   : 26 $/mois  - 2 vCPU, 3.5 Go RAM                  - small prod
# S1   : 73 $/mois  - 1 vCPU, 1.75 Go, slots, autoscale   - prod basique
# P1V3 : 84 $/mois  - 2 vCPU, 8 Go, AVX2, slots, autoscale - prod recommandée
# P2V3 : 168 $/mois - 4 vCPU, 16 Go RAM                   - APIs intensives

# Upgrade B1 → P1V3 (instantané, < 30 s, sans downtime)
az appservice plan update \
  --name $PLAN_NAME \
  --resource-group $RESOURCE_GROUP \
  --sku P1V3

Autoscale horizontal — règles CPU et HTTP queue

# Activer l'autoscale (1 à 5 instances) sur le plan P1V3
az monitor autoscale create \
  --resource-group $RESOURCE_GROUP \
  --resource $PLAN_NAME \
  --resource-type Microsoft.Web/serverfarms \
  --name "autoscale-api-nodejs" \
  --min-count 1 \
  --max-count 5 \
  --count 1

# Règle scale-out : si CPU > 75 % pendant 10 min → +1 instance
az monitor autoscale rule create \
  --resource-group $RESOURCE_GROUP \
  --autoscale-name "autoscale-api-nodejs" \
  --condition "Percentage CPU > 75 avg 10m" \
  --scale out 1 \
  --cooldown 5

# Règle scale-in : si CPU < 30 % pendant 15 min → -1 instance
az monitor autoscale rule create \
  --resource-group $RESOURCE_GROUP \
  --autoscale-name "autoscale-api-nodejs" \
  --condition "Percentage CPU < 30 avg 15m" \
  --scale in 1 \
  --cooldown 10

# Règle complémentaire : HTTP queue length
az monitor autoscale rule create \
  --resource-group $RESOURCE_GROUP \
  --autoscale-name "autoscale-api-nodejs" \
  --condition "HttpQueueLength > 100 avg 5m" \
  --scale out 2 \
  --cooldown 5

Custom domain et SSL gratuit

# 1. Vérifier le domaine via TXT record DNS
# Ajouter dans votre DNS : asuid.api.angularforall.com → 

az webapp config hostname add \
  --webapp-name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --hostname "api.angularforall.com"

# 2. Créer un certificat managé GRATUIT (DigiCert via Azure)
az webapp config ssl create \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --hostname "api.angularforall.com"

# 3. Activer le binding SNI/SSL
THUMBPRINT=$(az webapp config ssl list \
  --resource-group $RESOURCE_GROUP \
  --query "[?subjectName=='api.angularforall.com'].thumbprint" -o tsv)

az webapp config ssl bind \
  --certificate-thumbprint $THUMBPRINT \
  --ssl-type SNI \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP

CI/CD complet avec GitHub Actions

# .github/workflows/azure-deploy.yml
name: Build and deploy to Azure App Service

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  AZURE_WEBAPP_NAME: api-nodejs-af-2026
  NODE_VERSION: '20.x'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test -- --ci --coverage

      - name: Build (TypeScript / bundling si applicable)
        run: npm run build --if-present

      - name: Package artifact
        run: |
          zip -r release.zip . \
            -x "node_modules/*" "*.git*" "tests/*" "*.env*" "coverage/*"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: node-app
          path: release.zip

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: ${{ steps.deploy.outputs.webapp-url }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: node-app

      - name: Login to Azure (OIDC, sans secret stocké)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to staging slot
        id: deploy
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          slot-name: staging
          package: release.zip

      - name: Smoke test on staging
        run: |
          curl --fail --retry 5 --retry-delay 10 \
            https://${{ env.AZURE_WEBAPP_NAME }}-staging.azurewebsites.net/health

  swap-to-prod:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production           # protection : approbation manuelle
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Swap staging → production
        run: |
          az webapp deployment slot swap \
            --name ${{ env.AZURE_WEBAPP_NAME }} \
            --resource-group rg-api-nodejs-prod \
            --slot staging \
            --target-slot production
OIDC plutôt que Service Principal : Depuis 2023, GitHub Actions supporte l'authentification fédérée OIDC avec Azure AD — fini les secrets AZURE_CREDENTIALS à faire tourner. Configurez un federated credential sur l'App Registration Azure ciblant repo:owner/repo:ref:refs/heads/main, et utilisez seulement client-id + tenant-id + subscription-id dans le workflow.
Checklist mise en production :
  • Plan upgradé en P1V3 minimum (slots + autoscale)
  • Slot staging créé et auto-swap configuré ou approbation manuelle
  • Application Insights branché avec connection string en App Setting
  • Key Vault provisionné, Managed Identity activée, secrets référencés
  • Custom domain validé + certificat SSL managé bindé
  • Autoscale CPU + HTTP queue activé (1 → 5 instances)
  • GitHub Actions avec OIDC + environnement production protégé
  • Alertes Azure Monitor : 5xx > 1 % sur 5 min, RAM > 80 %, response time P95 > 2 s
  • Backup automatique de la WebApp activé (rétention 30 jours)
  • Diagnostic settings vers Log Analytics pour audit GDPR

Conclusion

Azure App Service offre le meilleur rapport simplicité / fonctionnalités du marché PaaS pour héberger une API Node.js. En une dizaine de commandes az, vous obtenez une infrastructure managée avec HTTPS gratuit, deployment slots zero-downtime, observabilité APM, gestion sécurisée des secrets via Key Vault, et autoscale piloté par métriques.

Pour démarrer, le tier Free F1 reste idéal pour tester en CI ou prototyper. En production, le palier B1 (~13 $/mois) couvre les usages de petites APIs internes, tandis que P1V3 (~84 $/mois) débloque les fonctionnalités essentielles : slots, autoscale et SLA 99.95 %. Combinée à GitHub Actions avec authentification OIDC, la pipeline build → staging → swap garantit des déploiements sûrs et auditables.

Comparé à AWS Elastic Beanstalk, App Service réduit la friction côté IAM et VPC ; comparé à GCP App Engine Standard, il offre un meilleur contrôle du runtime Node.js et une intégration native avec l'écosystème Microsoft (Azure AD, Defender, Sentinel). Pour des architectures plus exigeantes (Kubernetes, isolation réseau forte), regardez ensuite Azure Container Apps ou AKS — mais pour 90 % des APIs Node.js, App Service est la cible parfaite.

Récapitulatif des bonnes pratiques :
  • Toujours écouter process.env.PORT dans Express
  • Activer Always On dès le tier B1 pour éliminer les cold starts
  • Utiliser des deployment slots staging + swap pour zero-downtime
  • Référencer les secrets via @Microsoft.KeyVault(SecretUri=...) + Managed Identity
  • Instrumenter avec Application Insights dès le premier déploiement
  • Configurer l'autoscale sur CPU + HTTP queue en P1V3
  • Authentifier GitHub Actions via OIDC (pas de secret stocké)
  • Activer https-only et min-tls-version 1.2 sur toute WebApp publique

Partager