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 |
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
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
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
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).
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
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);
});
- 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 Userassigné - 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
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.
- 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.
- Toujours écouter
process.env.PORTdans Express - Activer
Always Ondè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-onlyetmin-tls-version 1.2sur toute WebApp publique