← tous les articles

Azure

Comment j'ai containerisé mon API et fait le deploy sur Azure

Un Dockerfile multi-stage, Docker Compose avec un SQL Server avec health check, et la Sports Betting API qui tourne sur Azure Container Apps avec Azure SQL derrière.

Comment j'ai containerisé mon API et fait le deploy sur Azure

La Sports Betting API est le premier projet que j'ai amené du début, en tournant sur ma machine, jusqu'à être vraiment accessible sur internet. Docker pour le packaging, Azure Container Apps pour l'hosting, Azure SQL Database derrière.

Le Dockerfile a deux stages, pas un seul

La version naïve d'un Dockerfile .NET utilise l'image du SDK et s'arrête là. Ça marche, et ça produit une image avec tout le SDK .NET dedans, ce qui est bien plus gros que ce qu'il faut juste pour faire tourner la chose.

Donc le build se passe dans un stage et le résultat est copié dans un plus petit :

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# ... restore et publish ...
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "SportsBetting.API.dll"]

L'image finale est basée sur le runtime ASP.NET et ne contient que l'output publié. Le stage du SDK fait son travail et est jeté.

Copier les fichiers de projet avant le source

C'est le détail qui n'est pas évident et que j'ai raté la première fois. Chaque .csproj est copié en premier, puis dotnet restore tourne, et c'est seulement après que le reste du source est copié :

COPY SportsBetting.sln ./
COPY src/Backend/SportsBetting.API/SportsBetting.API.csproj src/Backend/SportsBetting.API/
COPY src/Backend/SportsBetting.Application/SportsBetting.Application.csproj src/Backend/SportsBetting.Application/
# ... le reste des projets ...

RUN dotnet restore
COPY . .

Docker met en cache chaque layer et le réutilise si rien dont ce layer dépend n'a changé. Comme le restore ne dépend que des fichiers de projet, modifier un fichier C# ne l'invalide pas, et les packages ne sont pas retéléchargés. Tout copier d'abord et faire le restore après fait que chaque changement de source retélécharge chaque package.

C'est un petit réordonnancement qui transforme la plupart des rebuilds de lents en quasi instantanés.

Compose, pour que le database démarre avec l'app

En local, l'API a besoin de SQL Server, et les démarrer séparément dans le bon ordre lasse vite. Compose gère les deux, et la partie utile, c'est de faire attendre l'API jusqu'à ce que le database soit vraiment prêt et pas juste démarré :

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    healthcheck:
      test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "..." -Q "SELECT 1" -C
      interval: 10s
      retries: 5
      start_period: 30s

  api:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      sqlserver:
        condition: service_healthy

depends_on tout seul attend juste que le container démarre, et un container SQL Server accepte des connexions bien après ça. L'associer à un health check qui exécute vraiment une query, c'est ce qui fait la différence entre l'API qui démarre proprement et qui plante sur une connexion faite trop tôt.

Azure Container Apps

Pour le déploiement, j'ai choisi Azure Container Apps plutôt qu'une VM ou App Service. Il exécute directement une image container, ce qui est exactement ce que j'avais déjà, et il scale à zéro quand rien ne le sollicite.

Scaler à zéro a une conséquence visible : le premier request après une période d'inactivité prend quelques secondes le temps qu'un container démarre. Pour un projet d'apprentissage, c'est un bon compromis, puisque ça ne coûte rien pendant l'inactivité. Pour quelque chose avec de vrais utilisateurs, je garderais plutôt au moins une réplique chaude.

Le database est Azure SQL Database, l'option managed, donc je ne maintiens pas moi-même une instance SQL Server. La configuration et la connection string viennent des environment variables du Container App, le même mécanisme que Compose utilise en local, ce qui veut dire que l'image elle-même est identique aux deux endroits.

Ce qui a vraiment fait tilt

La chose que je n'ai comprise qu'après l'avoir fait : le container est l'unité déployable, et l'environnement n'est que de la configuration autour de lui. La même image que je construis et exécute en local est celle qui tourne sur Azure, avec des environment variables différentes pointant vers une infrastructure différente. Pas de build séparé pour la production, pas de code qui se comporte différemment selon l'endroit où il tourne.

C'est la partie qui a fait que toute l'idée de Docker a fait tilt pour moi. Pas « ça empaquette l'app », mais que la chose que j'ai testée en local est littéralement la chose qui tourne dans le cloud.

Ce que je ferais ensuite

Un GitHub Actions workflow qui build l'image et la déploie à chaque push sur main, pour que déployer ne soit pas quelque chose que je fais à la main. C'est l'écart évident entre ça et comment ça fonctionnerait dans une vraie équipe.