OCPI — Manuel de déploiement

Ce runbook décrit le déploiement complet de la couche OCPI sur un environnement neuf (Dev, Staging ou Production). Il couvre :

  1. La génération et l'application de la migration EF Core.
  2. Le déploiement des binaires Chargemsi.OcpiGateway et Chargemsi.Worker.
  3. La configuration des secrets (connection string, URLs Gireve).
  4. Le CSR Gireve et le handshake OCPI initial.
  5. La vérification post-déploiement.
  6. La procédure de rollback.

Hypothèse : la base SQL Server existe déjà (les migrations OCPP historiques ont été appliquées). Si ce n'est pas le cas, suivre d'abord la procédure de déploiement OCPP.


Pré-requis

Outil Version Installation
.NET SDK 8.0+ (10.0 pour le gateway) https://dotnet.microsoft.com/download
EF Core CLI 6+ dotnet tool install -g dotnet-ef
Accès SQL Server login avec db_ddladmin au moins le temps de la migration DBA
Identifiant certificat Gireve un par environnement Portail / support Gireve
Certificat client .crt + cle .key Gireve un par environnement Email CTM Gireve apres traitement du CSR
Whitelist firewall Gireve inbound + outbound Equipe reseau ChargeMSI

Reseau — IPs Gireve a autoriser

Gireve communique sa liste d'IPs sortantes (par environnement) en reponse au CSR. Pour la Pre-Production (PP-IOP), les IPs communiquees le 2026-05-19 sont :

34.76.32.171
35.240.33.86
195.21.21.104

Regles a poser :

Sens Source -> Destination Port Notes
Inbound 34.76.32.171, 35.240.33.86, 195.21.21.104 -> ocpi.chargemsi.com (ou ocpi-preprod.chargemsi.com) TCP/443 Gireve appelle nos endpoints /ocpi/versions, /credentials, push tokens & commands.
Outbound Gateway + Worker -> 34.76.32.171, 35.240.33.86, 195.21.21.104 TCP/443 ChargeMSI pousse locations, sessions, cdrs vers Gireve.

En Production reelle (ocpi.gireve.com), redemander la liste a jour a l'equipe CTM Gireve : elle differe de la liste PP-IOP.

Certificat client mTLS Gireve

Apres reception du .crt signe par Gireve, l'installer comme decrit dans first-registration.md / Etape 1 bis :

  • empaqueter .key + .crt dans un .p12 (PKCS#12) ;
  • placer le .p12 dans un dossier hote, ex. /etc/chargemsi/gireve/partner_key/preprod/ ;
  • monter ce dossier :ro dans les conteneurs (voir docker-compose.ocpi.yml) ;
  • exposer Ocpi__Gireve__ClientCertificatePath et Ocpi__Gireve__ClientCertificatePassword au gateway et au worker (les deux appellent Gireve en sortie).

Exemple .env pour le compose :

GIREVE_CERT_DIR=/etc/chargemsi/gireve/partner_key/preprod
GIREVE_CERT_FILE=chargemsi.gireve.com.p12
GIREVE_CERT_PASSWORD=<via Ansible Vault, jamais en clair>

Le compose monte ce dossier sous /certs/gireve :

ocpigateway:
  environment:
    Ocpi__Gireve__ClientCertificatePath: /certs/gireve/chargemsi.gireve.com.p12
    Ocpi__Gireve__ClientCertificatePassword: ${GIREVE_CERT_PASSWORD}
  volumes:
    - ${GIREVE_CERT_DIR}:/certs/gireve:ro

Étape 1 — Générer la migration OCPI

À exécuter une seule fois, lors de la première version OCPI déployée (ou à chaque évolution du modèle OCPI).

# Depuis la racine de la solution
cd D:\AWorkspace\OCPPCLAUDE

dotnet ef migrations add Ocpi_Init `
    --project         Chargemsi.Database `
    --startup-project Chargemsi.OcpiGateway `
    --context         OcpiDbContext `
    --output-dir      Ocpi/Migrations

Appliquer la migration depuis le répertoire de la solution :

dotnet ef database update --context OCPPCoreContext --project Chargemsi.Database --startup-project Chargemsi.ManagementPortal

Ce qui doit se passer

  • Création de Chargemsi.Database/Ocpi/Migrations/{timestamp}_Ocpi_Init.cs + son .Designer.cs.
  • Création de Chargemsi.Database/Ocpi/Migrations/OcpiDbContextModelSnapshot.cs.

Vérifier la migration AVANT de l'appliquer

Ouvrez le .cs généré et contrôlez que :

  • Le schéma cible est bien ocpi (migrationBuilder.EnsureSchema(name: "ocpi") en première ligne).
  • Les tables créées sont uniquement : Partners, Credentials, Endpoints, Sessions, Cdrs, CommandLogs, InboundMessages, OutboundMessages.
  • Aucune table métier OCPP n'est touchée (pas de ALTER TABLE [ChargePoint], etc.).
  • L'index unique sur Credentials.TokenValue est présent.

Si le diff inclut des modifications inattendues, NE PAS appliquer. Régénérer avec dotnet ef migrations remove --context OcpiDbContext après avoir corrigé le modèle.

Commit

git add Chargemsi.Database/Ocpi/Migrations/*
git commit -m "ef(ocpi): initial migration Ocpi_Init"

Étape 2 — Appliquer la migration à un environnement

Mode A — Direct (Dev / Staging)

$env:ASPNETCORE_ENVIRONMENT = "Staging"
$env:ConnectionStrings__OcppSqlServerProd = "Server=...;Database=OCPP_STAGING;User ID=...;Password=...;TrustServerCertificate=true;"

dotnet ef database update `
    --project         Chargemsi.Database `
    --startup-project Chargemsi.OcpiGateway `
    --context         OcpiDbContext

Mode B — Script SQL idempotent (Production recommandée)

dotnet ef migrations script `
    --idempotent `
    --project         Chargemsi.Database `
    --startup-project Chargemsi.OcpiGateway `
    --context         OcpiDbContext `
    --output          deploy/ocpi-init.sql

Le DBA exécute ocpi-init.sql dans une fenêtre de maintenance. Le flag --idempotent rend le script rejouable sans risque (il vérifie l'état de __EFMigrationsHistory_Ocpi avant chaque opération).

Vérification post-migration

-- Le schéma a bien été créé
SELECT name FROM sys.schemas WHERE name = 'ocpi';

-- Les 8 tables OCPI sont là
SELECT name FROM sys.tables
WHERE SCHEMA_NAME(schema_id) = 'ocpi'
ORDER BY name;
-- Attendu : Cdrs, CommandLogs, Credentials, Endpoints,
--           InboundMessages, OutboundMessages, Partners, Sessions

-- La table de migration OCPI est isolée
SELECT * FROM ocpi.[__EFMigrationsHistory_Ocpi];

Étape 3 — Déployer les binaires

3.1 Gateway (Chargemsi.OcpiGateway)

dotnet publish Chargemsi.OcpiGateway `
    -c Release `
    -r linux-x64 `
    --self-contained false `
    -o publish/ocpigateway

Image Docker — la Chargemsi.OcpiGateway/Dockerfile est multi-stage et fait les choses suivantes (voir le fichier pour le détail des commentaires) :

  • Stage base : ASP.NET 10, ajoute curl + ca-certificates + tzdata (UTC). Écoute sur 8080 (HTTP). TLS est terminé par le reverse-proxy.
  • Stage restore : ne copie que les .csproj pour profiter du cache Docker tant que les dépendances ne bougent pas.
  • Stage build + publish : --no-restore (gain CI/CD).
  • Stage final : HEALTHCHECK sur /health/live, exécution en utilisateur non-root via USER $APP_UID.

Build depuis la racine du repo :

docker build -t chargemsi/ocpigateway:latest \
  -f Chargemsi.OcpiGateway/Dockerfile .

Run autonome :

docker run --name chargemsi-ocpigateway \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e ConnectionStrings__OcppSqlServerProd="Server=...;Database=...;User ID=...;Password=...;Encrypt=True;TrustServerCertificate=True;" \
  -e Ocpi__PublicBaseUrl="https://ocpi.chargemsi.com" \
  -e Ocpi__Gireve__VersionsUrl="https://ocpi-pp-iop.gireve.com/ocpi/versions" \
  -p 8080:8080 \
  chargemsi/ocpigateway:latest

3.1 bis Déploiement docker-compose (stack OCPI complète)

Un fichier docker-compose.ocpi.yml à la racine du repo monte le stack complet (SQL Server + Gateway + Worker) :

# 1. Copier le template et remplir les secrets
cp .env.ocpi.sample .env
$EDITOR .env   # SA_PASSWORD, OCPP_DB_PASSWORD, OCPI_PUBLIC_BASE_URL, ...

# 2. Lancer le stack
docker compose -f docker-compose.ocpi.yml up -d --build

# 3. Suivre les logs
docker compose -f docker-compose.ocpi.yml logs -f ocpigateway

# 4. Smoke-test
curl -fsS http://localhost:8080/health/live
curl -fsS http://localhost:8080/health/ready

Ce que fait le compose :

  • ocpi-db : SQL Server 2022, volume persistant chargemsi-ocpi-db-data, healthcheck sqlcmd SELECT 1.
  • ocpigateway : démarre après que la DB soit healthy, écoute sur 8080, healthcheck /health/live.
  • ocpiworker : démarre après le gateway, expose Hangfire sur le port 8081 de l'hôte (à ne pas exposer publiquement sans auth).
  • réseau Docker dédié chargemsi-ocpi ; le reverse-proxy NGINX/Traefik se branche dessus en external: true ou rejoint le réseau via son propre compose.

3.2 Worker (Chargemsi.Worker)

Idem. À noter : le Worker est un service à part, pas un pod du gateway. Il a son propre cycle de vie (déployé en Deployment k8s avec replicas: 1 pour éviter les doubles dépilements de l'outbox — ou utiliser Hangfire.Pro pour permettre N réplicas).


Étape 4 — Configurer les secrets

Variables OBLIGATOIRES

Clé Description
ConnectionStrings__OcppSqlServerProd Chaîne SQL Server (mêmes credentials que l'OCPP) Gateway et Worker
ConnectionStrings__HangfireDb Chaîne SQL Server pour Hangfire Worker uniquement
Ocpi__Gireve__VersionsUrl URL /versions de l'environnement Gireve (https://ocpi-pp-iop.gireve.com/ocpi/versions en PP-IOP, https://ocpi.gireve.com/ocpi/versions en prod) Gateway et Worker
Ocpi__Gireve__ClientCertificatePath Chemin du .p12 (cle + cert Gireve) monte dans le conteneur, ex. /certs/gireve/chargemsi.gireve.com.p12 Gateway et Worker
Ocpi__Gireve__ClientCertificatePassword Mot de passe du .p12 (Ansible Vault / Docker secret / Key Vault) Gateway et Worker
Ocpi__PublicBaseUrl URL publique du gateway (utilisée dans les réponses Versions et Credentials) Gateway

Variables utiles au handshake initial

Clé Description
Ocpi__Gireve__VersionsUrl URL /versions de l'environnement Gireve.

Le token temporaire donne a Gireve pour son premier appel entrant n'est pas lu depuis une variable d'environnement par le gateway. Il doit etre insere dans ocpi.Credentials avec Direction = Inbound, comme decrit dans first-registration.md.

En k8s (Helm)

# values.yaml (extrait)
ocpiGateway:
  env:
    ASPNETCORE_ENVIRONMENT: Production
  envFrom:
    - secretRef:
        name: chargemsi-ocpi-secrets   # contient les ConnectionStrings et tokens

Étape 5 — Handshake OCPI initial

Voir le runbook dédié first-registration.md.

Résumé exécutable :

# Une fois le gateway up :
# 1. generer le CSR avec le Common Name fourni par Gireve ;
# 2. generer TOKEN_TEMPORAIRE_CHARGEMSI ;
# 3. inserer ce token en base dans ocpi.Credentials ;
# 4. transmettre a Gireve l'URL /credentials et le token temporaire.
# Voir le runbook first-registration.

À l'issue du handshake, la table ocpi.Credentials doit contenir :

SELECT TokenKind, Direction, IsActive, CreatedDate
FROM ocpi.Credentials
WHERE PartnerId = (SELECT TOP 1 Id FROM ocpi.Partners WHERE PartyId = 'GIR');
-- Attendu :
-- TokenKind = 'C', Direction = 0, IsActive = 1 -- Inbound: Gireve appelle ChargeMSI
-- TokenKind = 'C', Direction = 1, IsActive = 1 -- Outbound: ChargeMSI appelle Gireve

Étape 6 — Vérification post-déploiement

Checks automatiques

# 1) Liveness — process up
Invoke-WebRequest https://ocpi-preprod.chargemsi.com/health/live
# Attendu : 200 OK, "Healthy"

# 2) Readiness — DB OCPP + DB OCPI + Gireve
Invoke-WebRequest https://ocpi-preprod.chargemsi.com/health/ready
# Attendu : 200 OK avec gireve-versions = Healthy

# 3) Endpoint Versions OCPI (auth requise)
Invoke-RestMethod https://ocpi-preprod.chargemsi.com/ocpi/versions `
    -Headers @{ Authorization = "Token $tokenC_inbound" }
# Attendu : enveloppe OCPI status_code=1000

Checks SQL

-- Outbox vide ou en train de se vider
SELECT Status, COUNT(*) FROM ocpi.OutboundMessages GROUP BY Status;

-- Le Worker écrit son audit des locations
SELECT TOP 5 * FROM ocpi.InboundMessages ORDER BY ReceivedUtc DESC;

Smoke côté Gireve

Demandez à Gireve de :

  1. Appeler GET /ocpi/cpo/2.2.1/locations → réponse 200 + enveloppe OCPI.
  2. Pousser un Token test (PUT /cpo/2.2.1/tokens/FR/GIR/{uid}) → 200.

Étape 7 — Rollback

Cas 1 : la migration a tout cassé

# Annuler la migration OCPI sans toucher aux tables OCPP
dotnet ef database update <NomMigrationPrecedente> `
    --project         Chargemsi.Database `
    --startup-project Chargemsi.OcpiGateway `
    --context         OcpiDbContext

Pour la toute première migration Ocpi_Init, le rollback se fait avec --migration 0 (rétablit l'état initial : tables OCPI supprimées, schéma ocpi enlevé).

Cas 2 : le gateway ne démarre pas

  1. Vérifier kubectl logs deployment/ocpi-gateway --tail=200.
  2. Erreurs courantes :
    • Connection string manquante → contrôler les secrets.
    • OcpiDbContext ne trouve pas la table → la migration n'a pas été appliquée.
    • gireve-versions Degraded → normal au démarrage si Gireve n'est pas joignable, n'empêche pas le démarrage.
  3. Rollback : kubectl rollout undo deployment/ocpi-gateway.

Cas 3 : les CDRs partent en DeadLetter en masse

  1. Identifier la cause via :
    SELECT TOP 20 LastError, COUNT(*) AS N
    FROM ocpi.OutboundMessages
    WHERE Status = 4
    GROUP BY LastError ORDER BY N DESC;
    
  2. Corriger le bug (mapper, payload, certificat...).
  3. Réinjecter :
    UPDATE ocpi.OutboundMessages
    SET Status = 0, RetryCount = 0, NextAttemptUtc = NULL, LastError = NULL
    WHERE Status = 4;
    

Checklist de déploiement (à imprimer)

  • [ ] Migration Ocpi_Init générée et reviewée (Étape 1)
  • [ ] Script ocpi-init.sql exécuté par le DBA (Étape 2)
  • [ ] Tables OCPI présentes en base (vérification SQL)
  • [ ] Image Docker du gateway buildée et poussée (Étape 3.1)
  • [ ] Image Docker du worker buildée et poussée (Étape 3.2)
  • [ ] Secrets k8s ou env vars configurés (Étape 4)
  • [ ] IPs Gireve whitelistees inbound + outbound sur le pare-feu (34.76.32.171, 35.240.33.86, 195.21.21.104 en PP-IOP)
  • [ ] Certificat client Gireve (.crt + .key -> .pfx) deploye et Ocpi__Gireve__ClientCertificate__* injectes
  • [ ] Pods up et readinessProbe OK
  • [ ] Handshake OCPI réussi, tokens finaux Inbound/Outbound stockés (Étape 5)
  • [ ] Smoke checks externes (Gireve) validés (Étape 6)
  • [ ] Procédure rollback connue de l'astreinte (Étape 7)