Chapitre 1018 minIntermédiaire
Déployer une application : Azure App Service et Container Apps
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- déployer une application Node.js sur App Service ;
- passer la configuration et les secrets par des paramètres d'application ;
- lancer un conteneur sur Azure Container Apps ;
- choisir entre VM, App Service, Container Apps et Kubernetes.
4 exercices corrigés
Azure App Service est le PaaS d'hébergement d'applications web de Microsoft : tu lui confies le code d'une application Node.js, PHP, Python, .NET ou Java, et il fournit les serveurs, l'environnement d'exécution, le HTTPS et la mise à l'échelle. Azure Container Apps fait la même chose pour une image de conteneur, avec la possibilité de descendre à zéro instance quand personne n'utilise l'application. Ce chapitre déploie une petite API Node.js qui lit la base parc du chapitre 9, d'abord sur App Service, puis sous forme de conteneur sur Container Apps, et t'aide à choisir où déployer.
App Service : un plan et des applications
App Service repose sur deux ressources :
- le plan App Service, c'est-à-dire les machines que tu loues : leur système (Linux ou Windows), leur taille et leur nombre. C'est lui qui est facturé ;
- les applications web, hébergées sur un plan. Plusieurs applications peuvent partager un plan.
| Niveau du plan | Exemples | Ce qu'il apporte |
|---|---|---|
| Free | F1 | Gratuit, 1 heure de calcul par jour, pas de domaine personnalisé : pour apprendre |
| Basic | B1, B2, B3 | Domaine personnalisé, application toujours démarrée (Always On), intégration à un réseau virtuel, engagement de disponibilité de 99,95 % |
| Standard et Premium | S1, P0v3, P1v3… | En plus : mise à l'échelle automatique, emplacements de déploiement (slots) pour basculer une nouvelle version sans coupure |
Chaque application reçoit une adresse en azurewebsites.net avec un certificat HTTPS fourni par Microsoft. Les environnements d'exécution disponibles se listent avec az webapp list-runtimes --os linux ; en novembre 2026, Node.js 24 LTS s'écrit NODE:24-lts.
Lab 1 : l'API du parc sur App Service
L'application tient en deux fichiers. Elle répond sur /sante (une sonde pour vérifier qu'elle tourne) et sur /api/postes (la liste des postes de la base parc, avec leur utilisateur). Elle lit sa configuration dans des variables d'environnement, jamais dans le code (cours Node.js).
{
"name": "app-parc",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"engines": {
"node": ">=24"
},
"dependencies": {
"express": "^5.1.0",
"mysql2": "^3.14.0"
}
}
// server.js
import express from 'express';
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME ?? 'parc',
ssl: { rejectUnauthorized: true }, // Azure impose TLS : on vérifie le certificat du serveur
connectionLimit: 5,
});
const app = express();
// Sonde de santé : la plateforme l'appelle pour savoir si l'application répond.
app.get('/sante', (req, res) => {
res.json({ statut: 'ok' });
});
app.get('/api/postes', async (req, res) => {
try {
const [lignes] = await pool.query(
`SELECT p.num_inventaire, p.type_poste, p.marque, p.modele,
CONCAT(e.prenom, ' ', e.nom) AS utilisateur
FROM poste p
LEFT JOIN employe e ON e.id_employe = p.id_employe
ORDER BY p.num_inventaire`,
);
res.json(lignes);
} catch (err) {
console.error('Base injoignable :', err.code ?? err.message);
res.status(503).json({ erreur: 'base de données indisponible' });
}
});
const port = process.env.PORT ?? 3000; // App Service fournit le port dans PORT
app.listen(port, () => console.log(`app-parc écoute sur le port ${port}`));
Teste-la d'abord sur ton poste : npm install, puis npm start. Sans base joignable, /sante répond {"statut":"ok"} et /api/postes renvoie une erreur 503 propre, sans planter le serveur.
1. Crée le plan et l'application. Le serveur MySQL mysql-parc-jc42 du chapitre 9 doit exister dans le même groupe de ressources, avec la base parc et le compte app_parc : si tu as supprimé le groupe à la fin du chapitre 9, refais ses étapes 1 à 3.
APPLI=app-parc-jc42 # unique dans tout Azure : <nom>.azurewebsites.net
az appservice plan create --name asp-parc-lab --resource-group rg-cloud-lab --sku F1 --is-linux
az webapp create \
--name "$APPLI" \
--resource-group rg-cloud-lab \
--plan asp-parc-lab \
--runtime "NODE:24-lts"
az webapp update --name "$APPLI" --resource-group rg-cloud-lab --https-only true
2. Passe la configuration par des paramètres d'application. Ils deviennent des variables d'environnement dans l'application. SCM_DO_BUILD_DURING_DEPLOYMENT=true demande à App Service de lancer npm install après le dépôt de l'archive.
read -rs -p "Mot de passe de app_parc : " MDP_APP; echo
az webapp config appsettings set --name "$APPLI" --resource-group rg-cloud-lab --settings \
DB_HOST=mysql-parc-jc42.mysql.database.azure.com \
DB_USER=app_parc \
DB_NAME=parc \
DB_PASSWORD="$MDP_APP" \
SCM_DO_BUILD_DURING_DEPLOYMENT=true
3. Ouvre le pare-feu de la base aux adresses sortantes de l'application, et à elles seules.
for ip in $(az webapp show --name "$APPLI" --resource-group rg-cloud-lab \
--query possibleOutboundIpAddresses --output tsv | tr ',' ' '); do
az mysql flexible-server firewall-rule create \
--resource-group rg-cloud-lab \
--name mysql-parc-jc42 \
--rule-name "app-${ip//./-}" \
--start-ip-address "$ip" \
--output none
done
Le nom d'une règle n'accepte ni point ni espace : ${ip//./-} remplace les points de l'adresse par des tirets.
4. Déploie l'archive, sans node_modules : App Service installe les dépendances lui-même.
zip -r app.zip server.js package.json package-lock.json
az webapp deploy --resource-group rg-cloud-lab --name "$APPLI" --src-path app.zip --type zip
HOTE=$(az webapp show --name "$APPLI" --resource-group rg-cloud-lab --query defaultHostName --output tsv)
curl "https://$HOTE/sante"
curl "https://$HOTE/api/postes"
[{"num_inventaire":"PC-0001","type_poste":"fixe","marque":"Dell","modele":"OptiPlex 7010","utilisateur":"Julie Lefèvre"},
{"num_inventaire":"PC-0002","type_poste":"portable","marque":"Lenovo","modele":"ThinkPad T14","utilisateur":"Hélène Garnier"},
...
{"num_inventaire":"PC-0013","type_poste":"fixe","marque":"Dell","modele":"OptiPlex 7010","utilisateur":null},
...]
Le poste PC-0013 est en stock : la jointure externe le garde, avec un utilisateur null (cours SQL).
5. Lis les journaux si quelque chose ne va pas.
az webapp log config --name "$APPLI" --resource-group rg-cloud-lab --docker-container-logging filesystem
az webapp log tail --name "$APPLI" --resource-group rg-cloud-lab
Azure Container Apps : des conteneurs sans serveur à gérer
Container Apps exécute des conteneurs sur une infrastructure Kubernetes que Microsoft gère entièrement : tu ne vois ni nœuds ni cluster.
| Notion | Ce que c'est |
|---|---|
| Environnement | La frontière commune à plusieurs applications : réseau, journaux (un espace de travail Log Analytics) |
| Application | Ton conteneur, son image, ses variables, ses secrets |
| Révision | Une version figée de l'application ; chaque changement d'image ou de configuration en crée une, et l'on peut répartir le trafic entre deux révisions |
| Réplique | Une instance en fonctionnement ; la plateforme en ajoute selon le nombre de requêtes, et peut descendre à zéro |
| Entrée (ingress) | L'exposition HTTPS de l'application : externe (Internet) ou interne (dans l'environnement) |
À zéro réplique, l'application ne consomme rien ; la première requête attend quelques secondes le démarrage d'un conteneur (un démarrage « à froid »). Le forfait de Container Apps comprend chaque mois 180 000 vCPU-secondes, 360 000 Gio-secondes et 2 millions de requêtes gratuits.
Lab 2 : l'API du parc en conteneur sur Container Apps
1. Prépare l'abonnement, une seule fois.
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
2. Déploie d'abord l'image de démonstration de Microsoft, pour vérifier que tout fonctionne. az containerapp up crée l'environnement, l'espace de journaux et l'application.
az containerapp up \
--name ca-parc \
--resource-group rg-cloud-lab \
--location francecentral \
--environment cae-cloud-lab \
--image mcr.microsoft.com/k8se/quickstart:latest \
--target-port 80 \
--ingress external \
--query properties.configuration.ingress.fqdn
La commande affiche l'adresse de l'application : ouvre-la en HTTPS dans ton navigateur.
3. Passe à ton image. Le Dockerfile de l'API suit les bonnes pratiques du cours Docker : image de base légère, dépendances de production seulement, utilisateur non privilégié.
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Construis l'image pour l'architecture d'Azure et publie-la dans un registre, par exemple GitHub Container Registry (cours Intégration continue). La connexion au registre demande un jeton d'accès personnel GitHub (classic) avec le droit write:packages.
# Construire pour l'architecture d'Azure (obligatoire sur un Mac à puce Apple)
docker build --platform linux/amd64 -t ghcr.io/<ton-compte>/app-parc:1.0.0 .
# Publier sur GitHub Container Registry
read -rs -p "Jeton GitHub : " JETON_GITHUB; echo
echo "$JETON_GITHUB" | docker login ghcr.io -u <ton-compte> --password-stdin
docker push ghcr.io/<ton-compte>/app-parc:1.0.0
Une image publiée sur GitHub Container Registry est privée au départ : pour le lab, rends-la publique dans les paramètres du paquet sur GitHub, sinon Container Apps ne pourra pas la télécharger.
Ouvre ensuite la base aux services d'Azure, puis mets à jour l'application : nouvelle image, nouveau port, secret et variables.
az mysql flexible-server firewall-rule create --resource-group rg-cloud-lab --name mysql-parc-jc42 \
--rule-name autoriser-services-azure --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0
az containerapp secret set --name ca-parc --resource-group rg-cloud-lab \
--secrets db-password="$MDP_APP"
az containerapp update --name ca-parc --resource-group rg-cloud-lab \
--image ghcr.io/<ton-compte>/app-parc:1.0.0 \
--set-env-vars DB_HOST=mysql-parc-jc42.mysql.database.azure.com DB_USER=app_parc \
DB_NAME=parc DB_PASSWORD=secretref:db-password \
--min-replicas 0 --max-replicas 3
az containerapp ingress update --name ca-parc --resource-group rg-cloud-lab --target-port 3000
secretref:db-password injecte le secret dans la variable sans que sa valeur apparaisse dans la configuration. La règle de pare-feu 0.0.0.0 laisse l'environnement Container Apps joindre la base, car elle ouvre celle-ci à tous les services d'Azure : c'est acceptable pour un lab, avec la réserve du chapitre 9 ; en production, on place l'environnement et la base dans le même réseau virtuel, la base en accès privé.
Sans registre, az containerapp up --source . construit l'image dans Azure à partir du Dockerfile du dossier courant, en créant un registre Azure Container Registry.
4. Observe, puis supprime.
az containerapp revision list --name ca-parc --resource-group rg-cloud-lab --output table
az containerapp logs show --name ca-parc --resource-group rg-cloud-lab --follow
az group delete --name rg-cloud-lab --yes --no-wait
Choisir où déployer
| Solution | Ce que tu gères | Descend à zéro | Pour |
|---|---|---|---|
| VM (IaaS) | Tout le système | Non | Un logiciel qui impose son système, un besoin de contrôle total (TP VPS ou PaaS) |
| App Service (PaaS) | Le code et sa configuration | Non | Une application web classique, sans conteneur |
| Container Apps (CaaS serverless) | L'image et sa configuration | Oui | Une application livrée en conteneurs, des microservices, une charge irrégulière |
| AKS (Kubernetes managé) | Les déploiements Kubernetes, les nœuds | Non (les nœuds tournent) | Une grande plateforme, une équipe qui maîtrise Kubernetes |
| Azure Functions (FaaS) | Des fonctions | Oui | Des traitements courts déclenchés par des événements |
| Static Web Apps | Les fichiers d'un site statique ou d'un front React | Sans objet | Un portfolio, un front qui appelle une API (TP portfolio sur Vercel : le même principe chez un autre fournisseur) |
Exercices
Exercice 1 sur 4 L'application ne démarre pas Réussi À revoir
Une étudiante déploie une API Express sur App Service Linux. La page affiche « Application Error ». Dans les journaux, on lit que l'application « écoute sur le port 3000 », puis que le conteneur ne répond pas sur le port attendu et qu'il est redémarré. Son code contient app.listen(3000). Explique et corrige.
Voir la réponseMasquer la réponse de l'exercice 1
App Service indique à l'application, dans la variable d'environnement PORT, le port sur lequel il lui transmet les requêtes. L'application écoute sur 3000, en dur : la plateforme ne la joint pas, conclut qu'elle est en panne et la redémarre en boucle. Correction : const port = process.env.PORT ?? 3000; puis app.listen(port). En local, le port 3000 reste utilisé ; sur App Service, c'est celui de la plateforme.
Tu avais trouvé ?
Exercice 2 sur 4 Le mot de passe dans le dépôt Réussi À revoir
Un développeur a versionné un fichier .env contenant DB_PASSWORD=... dans un dépôt GitHub public, il y a trois jours. Il propose de supprimer le fichier dans un nouveau commit. Est-ce suffisant ? Que faut-il faire ?
Voir la réponseMasquer la réponse de l'exercice 2
Non : le fichier reste dans l'historique Git, et un dépôt public est parcouru en permanence par des robots qui cherchent des secrets. Le mot de passe est donc compromis :
- changer le mot de passe du compte MySQL immédiatement, et mettre à jour le paramètre d'application ;
- vérifier dans les journaux de la base s'il y a eu des connexions inhabituelles depuis trois jours, et traiter l'incident si des données ont été lues ;
- ajouter
.envau.gitignore, et purger l'historique si besoin ; - pour l'avenir : les secrets dans les paramètres d'application ou dans Key Vault, l'analyse des secrets activée sur le dépôt (cours Intégration continue), et si possible une identité managée à la place du mot de passe.
Tu avais trouvé ?
Exercice 3 sur 4 Où déployer ? Réussi À revoir
Choisis une solution d'hébergement Azure pour chaque projet, et justifie.
- Le portfolio d'un étudiant, construit avec React et sans serveur d'API.
- L'API PHP de calcul des frais de livraison, appelée en continu de 8 h à 19 h par le site de Sorbier.
- Un outil interne livré par un prestataire sous forme d'image Docker, utilisé une fois par semaine.
- Le logiciel de gestion d'entrepôt qui exige Windows Server 2022.
Voir la réponseMasquer la réponse de l'exercice 3
- Static Web Apps (ou une plateforme comme Vercel) : des fichiers statiques, distribués avec HTTPS, presque gratuitement.
- App Service : une application web classique, sollicitée en continu ; un plan Basic ou Premium la garde démarrée.
- Container Apps : l'image est fournie telle quelle, et l'application peut descendre à zéro réplique les six autres jours ; on ne paie presque rien entre deux utilisations.
- Une VM Windows Server : le logiciel impose son système.
Tu avais trouvé ?
Exercice 4 sur 4 La première requête est lente Réussi À revoir
Le service client se plaint : chaque matin, la première consultation de l'outil hébergé sur Container Apps met huit secondes à répondre, puis tout va bien. Explique et propose deux solutions, avec leur conséquence sur le coût.
Voir la réponseMasquer la réponse de l'exercice 4
L'application est réglée avec --min-replicas 0 : la nuit, sans requête, elle descend à zéro réplique. La première requête du matin doit attendre le démarrage à froid d'un conteneur.
- Passer à une réplique minimale (
--min-replicas 1) : plus de démarrage à froid, mais une réplique est facturée en permanence. - Réduire le temps de démarrage : une image plus légère, moins de travail au lancement. Ou déclencher une requête automatique peu avant 8 h. Le coût reste quasi nul la nuit.
Le choix dépend de l'enjeu : pour un outil interne, huit secondes une fois par jour sont souvent acceptables.
Tu avais trouvé ?