Problème du dernier post
Je me suis bien pris la tête sur le dernier billet au sujet d’un nouveau problème qui est apparu lorsque j’ai voulu diviser mon site web en 2, une partie staging et une partie prod. Ca pourrait tout aussi bien être « preprod », « recette » etc…
Avant j’avais qu’un seul site web: blazordemo.reactor.fr. Celui ci a sa config Nginx et son service systemd qui fait tourner Kernel via un proxy sur le port 7777. (voir article )
J’en ai créé un nouveau « staging.blazordemo.reactor.fr ».
L’idée est de faire comme les pros, un site web de « staging » et un de « prod ».
- dev : locale, tambouille de developeur, avec une base de données crado, des machins bricolés, des trucs à jours ou pas…
- staging: distante, même environnement que la prod (meme serveur de base de données, même OS, même TOUT, sauf les données qui sont normalement copiés et anonymisés dans le meilleurs des mondes)
- prod: distante, le site qu’on update le vendredi soir 🙂
Et j’ai mis un temps fou à m’en apercevoir. (j’ai été jusqu’a faire un gros delete de mon repertoire www. Nginx continuait à me servir un beau site web… même après un reboot. car le service était le meme…)
Donc il me faut 2 services… Un de staging, l’autre de prod. Le tout sur un port proxy différent bien évidement.
Bien entendu, ce problème n’existe pas si le staging et la prod ne sont pas sur le meme serveur, ce qui est TRES recommandé. Cependant, je trouve l’exercice intéressant car il pousse dans les retranchements et force à de bonnes pratiques.
Mon ancienne technique
Dans le projet Blazor Server, dans Program.cs j’ajoutais ceci
Une ligne de compilation conditionnelle qui me changait le port si on était en release (ce que je considèrais comme mon unique prod).
Je pourrais ajouter une condition STAGING avec un port 7776 et m’en servir pour la version staging. A mon sens une directive de compilation n’a rien a voir avec les environnements, je veux dire c’est liés, mais c’est pas son but de définir du paramétrage de port…
Je pense qu’on peut faire mieux, avec les fichiers de conf appsettings.js. Créeons deux autres fichiers de config, un pour la prod et l’autre pour le staging. Normalement vous avez déjà par défaut un fichier appsettings.Development.json
launchSettings.json
Si vous travaillez avec Visual Studio ou VS Code vous devez savoir que vous avez des options de lancement de votre app qui sont configurable dans le fichier Properties/launchSettings.json
Encore un fichier de config qui sème le doute…
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54778",
"sslPort": 44382
}
},
"profiles": {
"BlazorDemo.Server": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7249;http://localhost:5249",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Comme on peut le voir dans la section « profiles », je peux lancer mon app soit :
- Via IIS Express
- Via la commande dotnet run
La première est historique, je pense qu’on en parlera plus dans qq temps. Avant .net core on était obliger de lancer un server utilisant le .net Framework sous IIS qui est le serveur de Windows. La version Express est la version de dévelopement. Pour moi c’est voué à mourrir, mais certains ont des habitudes et Microsoft les forces pas à les changers.
Depuis .net Core (et donc .net 5 et 6), on peut executer nos app sur n’importe quel OS et serveur web comme apache ou nginx sous linux. On a un serveur web intermédiaire appelé Kestrel qui fait le job. D’ailleurs c’est lui que l’on utilise en dévelopement.
En executant une commande dotnet run sur notre projet serveur, on lance un Kestrel. C’est lui que l’on veut paramétrer pour avoir une config dev, staging, prod.
C’est possible d’éditer en développement ce fichier pour changer de port. Mais c’est uniquement un fichier de conf pour une execution en dev local. C’est pas un fichier de configuration plateforme de prod.
Je place ça là car c’est un fichier qui peut vous semer le doute lors de vos devops et test local.
AppSettings.{ENV}.json
On va modifier le fichier appSettings.Developement.json et lui rajouter ceci
{
"Plateform": "Dev",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:1234"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Perso je rajoute une ligne « Environment » qui me permet de voir dans sur quel type d’environement je suis (et plus tard de l’afficher dans l’app).
Ce qui nous intéresse c’est la section Kestrel. On ajoute un EndPoints et on lui précise un port.
Faites un dotnet run sur votre projet à présent :
On a un warning qui nous dit que l’adresse utilisé va écrasé celle du launchsetting.json. C’est ce qu’on veut. On voit bien la nouvelle adresse http://localhost:1234.
C’était pour la démo, en dev ça sert a rien de faire cela, autant utiliser le launchSettings.json qui est la pour ça. Donc enlever la ligne Kestrel, c’était pour la démo.
Par contre on va mettre ces configs pour appsettings.staging.json
{
"Plateform": "Staging",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:7776"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
et appsettings.prod.json
{
"Plateform": "Prod",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:7777"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
ASPNETCORE_ENVIRONMENT
Encore une chose à savoir. Pour déterminer sur quel environement .net se trouve, il utilise une variable d’environement ASPNETCORE_ENVIRONMENT
Elle est d’ailleurs clairement définie dans le fichier launchSettings.json à « Development ».
Donc en dev, le fichier de config appsettings utilisé sera appsettings.development.json
Pour que ce mécanisme fonctionne en prod, soit on définie la variable ASPNETCORE_ENVIRONMENT
à « Production » soit par défaut, si elle n’existe pas, c’est déjà le cas (attention moi j’ai nommé mon appsettings prod et non production). Le problème c’est que une variable d’environement est définie pour tout le system sur l’OS.
Si on veut mettre un staging sur le même OS on peut pas définir à la fois « Production » et « Staging » pour la même variable. Ce concept a été introduit avec .net Core. J’ai jamais aimé ce truc. Alors je fais tout pour m’en passer.
Si vous avez une idée de comment faire, lachez un pti commentaire svp.
Modification du YAML
J’ai revu mes fichiers de pipeline, je les ai renommé ainsi :
Ils sont quasiment identiques.
Le premier va publier directement en prod le site web avec une config prod, c’est à dire un Kestrel sur localhost:7777
Le second va publier directement en staging le site web avec une config staging, c’est à dire un Kestrel sur localhost:7776
Mais la grosse différence se trouve plus au niveau git. Car la publication prod se base sur la branche master alors que le staging sur la branche develop.
L’idée est d’automatiser la chaine de deploiement lorsque qu’un commit arrive sur la branche develop, la pipeline se lance et deploie une version du site web directement sur le server staging. Et de même, lorsque la branche master reçoit un nouveau commit, le site web en production est automatiquement mis à jour.
azure-pipeline-prod.yml
# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
trigger:
- master
pool:
vmImage: ubuntu-latest
variables:
projectBlazorWasmServer: 'src/BlazorDemo/BlazorDemo/Server/BlazorDemo.Server.csproj'
buildConfiguration: 'Release'
dateToday: 'Will be set dynamically'
revision: $[counter(format('{0:dd}', pipeline.startTime), 1)]
versionString: >
$(dateToday).$(revision)
steps:
#Preparing Build Number
- task: PowerShell@2
displayName: 'Preparing Build Number'
inputs:
targetType: 'inline'
script: |
$currentDate = $(Get-Date)
$year = $currentDate.Year
$month = $currentDate.Month
$day = $currentDate.Day
Write-Host $currentDate
Write-Host $day
Write-Host $env:revision
Write-Host "##vso[task.setvariable variable=dateToday]$year.$month.$day"
#display value
- script: echo $(revision)
displayName: 'revision display'
- script: echo $(dateToday)
displayName: 'dateToday display'
- script: echo $(versionString)
displayName: 'versionString display'
# Publish Blazor Demo Server
# - script: dotnet publish --configuration $(buildConfiguration) src/BlazorDemo/BlazorDemo/Server/BlazorDemo.Server.csproj
# displayName: 'dotnet publish $(buildConfiguration)'
# Publish Blazor Demo Server
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: publish
publishWebProjects: false
projects: '$(projectBlazorWasmServer)'
arguments: '--configuration $(buildConfiguration) --output $(build.artifactstagingdirectory) /p:Version=$(versionString)'
zipAfterPublish: false
modifyOutputPath: false
#Delete unnecessary appsettings.json
- task: DeleteFiles@1
displayName: 'Delete unnecessary appsettings.json'
inputs:
SourceFolder: '$(build.artifactstagingdirectory)'
Contents: |
appsettings.json
appsettings.Development.json
appsettings.Staging.json
#Renaming appsettings.json
- task: PowerShell@2
displayName: 'Rename appsettings.json'
inputs:
targetType: 'inline'
script: Rename-Item -Path "$(build.artifactstagingdirectory)/appsettings.Prod.json" -NewName "appsettings.json"
# Copy files over SSH
- task: CopyFilesOverSSH@0
inputs:
sshEndpoint: 'SSH to Ikoula'
sourceFolder: $(build.artifactstagingdirectory)
#contents: '**'
targetFolder: '/var/www/blazordemo.reactor.fr'
cleanTargetFolder: true
#overwrite: true # Optional
#failOnEmptySource: false # Optional
#flattenFolders: false # Optional
azure-pipeline-staging.yml
# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
trigger:
- develop
pool:
vmImage: ubuntu-latest
variables:
projectBlazorWasmServer: 'src/BlazorDemo/BlazorDemo/Server/BlazorDemo.Server.csproj'
buildConfiguration: 'Release'
dateToday: 'Will be set dynamically'
revision: $[counter(format('{0:dd}', pipeline.startTime), 1)]
versionString: >
$(dateToday).$(revision)
steps:
#Preparing Build Number
- task: PowerShell@2
displayName: 'Preparing Build Number'
inputs:
targetType: 'inline'
script: |
$currentDate = $(Get-Date)
$year = $currentDate.Year
$month = $currentDate.Month
$day = $currentDate.Day
Write-Host $currentDate
Write-Host $day
Write-Host $env:revision
Write-Host "##vso[task.setvariable variable=dateToday]$year.$month.$day"
#display value
- script: echo $(revision)
displayName: 'revision display'
- script: echo $(dateToday)
displayName: 'dateToday display'
- script: echo $(versionString)
displayName: 'versionString display'
# Publish Blazor Demo Server
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: publish
publishWebProjects: false
projects: '$(projectBlazorWasmServer)'
arguments: '--configuration $(buildConfiguration) --output $(build.artifactstagingdirectory) /p:Version=$(versionString)'
zipAfterPublish: false
modifyOutputPath: false
#Delete unnecessary appsettings.json
- task: DeleteFiles@1
displayName: 'Delete unnecessary appsettings.json'
inputs:
SourceFolder: '$(build.artifactstagingdirectory)'
Contents: |
appsettings.json
appsettings.Development.json
appsettings.Prod.json
#Renaming appsettings.json
- task: PowerShell@2
displayName: 'Rename appsettings.json'
inputs:
targetType: 'inline'
script: Rename-Item -Path "$(build.artifactstagingdirectory)/appsettings.Staging.json" -NewName "appsettings.json"
# Copy files over SSH
- task: CopyFilesOverSSH@0
inputs:
sshEndpoint: 'SSH to Ikoula'
sourceFolder: $(build.artifactstagingdirectory)
#contents: '**'
targetFolder: '/var/www/staging.blazordemo.reactor.fr'
cleanTargetFolder: true
#overwrite: true # Optional
#failOnEmptySource: false # Optional
#flattenFolders: false # Optional
Ce qu’il faut retenir ici, c’est que la pipeline ajoute deux étapes :
- on efface (dans la sortie du publish, pas dans le code source) les fichiers appsettings.json inutile pour l’environnement cible. Par exemple pour déployer sur le staging, je n’ai pas besoin de appsettings.json, appsettings.Development.json et appsettings.Prod.json
- on renomme le fichier de settings restant (appsettingsStaging.json) en appsettings.json.
Ainsi je m’assure d’avoir qu’un seul fichier de settings correspondant à la plateforme cible.
Problème de version
Si on lance https://staging.blazordemo.reactor.fr/ et https://blazordemo.reactor.fr/ on s’apperçoit que les numéros de version ne sont pas logiques.
En effet, chacun utilise une révision propre à sa pipeline. Ainsi j’ai une version 2022.3.29.7 en staging et 2022.3.29.3. Le soucis c’est que la révision en prod peut être supérieure à celle du staging, et surtout elle ne se suivent pas du tout.
Ca peut porter à confusion. Je pense que ce chiffre doit être commun à l’ensemble des environnements. C’est à dire que si en staging j’ai une révision 455, en production, si je lance la pipeline de prod ce chiffre sera de 456.
Afficher des infos de appSettings sur Blazor Client
A force d’avoir plusieurs environnements, parfois on s’y perd. Distinguer la dev du staging de la prod, c’est pas facile sans l’url. Et j’ai pris l’habitude d’afficher clairement sur quelle plateforme je me trouve, histoire de me rappeler facilement ou je suis, et surtout que mon fichier de conf est bien le bon !
Rappelez vous, j’ai rajouté une propriété « Plateform » dans mes fichiers appsettings :
{
"Plateform": "Dev",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Si on tente de les ajouter dans le MainLayout à coté de la version comme ceci :
@inherits LayoutComponentBase
@using System
@using System.Reflection
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<div>
@Configuration["Plateform"] - @versionInfo
</div>
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code
{
private string versionInfo = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
}
L’affichage donne ceci :
On voit rien, la raison est simple, le client Blazor (WASM) n’a pas accès aux fichiers de settings du Server Blazor. Et ceci pour des raison de sécurité. On va donc passer par un nouveau controler :
using BlazorDemo.Shared;
using Microsoft.AspNetCore.Mvc;
namespace BlazorDemo.Server.Controllers
{
[ApiController]
[Route("api/plateform")]
public class PlateformController : ControllerBase
{
private readonly ILogger<PlateformController> _logger;
private readonly IConfiguration _configuration;
public PlateformController(ILogger<PlateformController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
[HttpGet]
public string Get()
{
try
{
string env = _configuration["Plateform"];
if (env == null)
return "null";
else
return env;
}
catch (Exception ex)
{
return $"ERROR SERVER : {ex.Message}";
}
}
}
}
maintenant on a une api qui nous donne ceci :
Il nous reste plus qu’à modifier le code de MainLayout.razor
@inherits LayoutComponentBase
@using System
@using System.Reflection
@inject HttpClient Http
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<div>
@environmentInfo - @versionInfo
</div>
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code
{
private string? versionInfo = "";
private string? environmentInfo = "";
protected override async Task OnInitializedAsync()
{
var assInfo = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>();
versionInfo = assInfo?.InformationalVersion;
environmentInfo = await Http.GetStringAsync("api/plateform");
}
}
Et si vous avez suivis l’intérêt de tout ceci, on a plus qu’à commit + push notre code sur la branche develop pour que nos modifs arrivent direct sur https://staging.blazordemo.reactor.fr juste après l’execution automatique de notre pipeline Azure Devops.