Hyperledger Fabric sur AKS : tracer la chaîne d'approvisionnement aéronautique avec Bevel et GitOps
Un constructeur et son fournisseur échangent des certificats de conformité sur un registre immuable. Voici la modélisation complète, du MSP au chaincode, automatisée par Hyperledger Bevel 1.3.0 sur des clusters AKS managés.
La traçabilité aéronautique pose un problème que les bases de données classiques résolvent mal. Quand un constructeur reçoit une pièce critique d'un fournisseur, le certificat de conformité qui l'accompagne doit rester vérifiable pendant trente ans — et aucune des deux parties ne doit pouvoir le modifier après coup.
Un registre distribué répond à cette exigence. Reste à le déployer sans transformer le projet en cauchemar opérationnel, car Hyperledger Fabric cumule deux difficultés : une topologie réseau complexe et une gestion cryptographique intransigeante.
L'architecture qui suit repose sur deux impératifs. L'immuabilité de l'infrastructure, obtenue par GitOps : aucun composant ne se configure à la main. Et la rigueur du Membership Service Provider, car une clé privée mal gérée compromet l'ensemble du réseau.
Le cas d'usage et sa modélisation
Deux organisations partagent un canal.
Org1 désigne le constructeur. Il émet des demandes de conformité et valide les certificats reçus. Org2 désigne le fournisseur. Il produit les pièces et émet les certificats correspondants.
Chaque organisation exploite son propre cluster AKS, dans son propre abonnement Azure. Aucune ne délègue sa cryptographie à l'autre — condition non négociable pour que le registre conserve sa valeur probante.
| Composant | Org1 — Constructeur | Org2 — Fournisseur |
|---|---|---|
| Autorité de certification | ca.constructeur.aero | ca.fournisseur.aero |
| Pairs | 2 (endorsement + requête) | 2 |
| Base d'état | CouchDB | CouchDB |
| Service d'ordonnancement | 3 nœuds Raft, partagés | — |
| Cluster | AKS aks-constructeur-weu | AKS aks-fournisseur-neu |
Le service d'ordonnancement en Raft mérite une précision. Trois nœuds constituent le minimum pour tolérer une panne. Les placer tous chez une seule organisation crée un déséquilibre de gouvernance : celle-ci pourrait censurer des transactions. Dans notre modèle, Org1 en héberge deux et Org2 un seul — compromis acceptable quand le constructeur porte la responsabilité réglementaire.
Le MSP, cœur du sujet
Un MSP définit qui appartient à quelle organisation et avec quel rôle. Concrètement, il s'agit d'une arborescence de certificats X.509 dont la structure ne tolère aucune approximation.
crypto-config/
└── peerOrganizations/
└── constructeur.aero/
├── ca/ # clé privée de l'AC — jamais dans Git
├── msp/
│ ├── cacerts/ # certificat racine
│ ├── tlscacerts/ # racine TLS, distincte de la racine d'identité
│ ├── config.yaml # définition des unités organisationnelles
│ └── admincerts/ # obsolète depuis Fabric 2.x
├── peers/
│ └── peer0.constructeur.aero/
│ ├── msp/ # identité du pair
│ └── tls/ # certificat serveur TLS
└── users/
└── Admin@constructeur.aero/Le fichier config.yaml porte la logique de rôles. Depuis Fabric 2.x, l'appartenance se détermine par unité organisationnelle plutôt que par liste de certificats administrateurs.
NodeOUs:
Enable: true
ClientOUIdentifier:
Certificate: cacerts/ca-constructeur-aero.pem
OrganizationalUnitIdentifier: client
PeerOUIdentifier:
Certificate: cacerts/ca-constructeur-aero.pem
OrganizationalUnitIdentifier: peer
AdminOUIdentifier:
Certificate: cacerts/ca-constructeur-aero.pem
OrganizationalUnitIdentifier: admin
OrdererOUIdentifier:
Certificate: cacerts/ca-constructeur-aero.pem
OrganizationalUnitIdentifier: ordererUn certificat portant OU=admin confère les droits d'administration. Révoquer un administrateur revient donc à révoquer son certificat auprès de l'autorité, puis à publier la liste de révocation — opération que beaucoup d'équipes découvrent le jour où elles en ont besoin.
Ne versionnez jamais les clés privées. Bevel génère la cryptographie puis la dépose dans HashiCorp Vault. Le dépôt Git ne contient que des références à des chemins Vault. Une clé d'autorité de certification exposée dans un historique Git impose la reconstruction complète du réseau.
L'automatisation par Bevel
Hyperledger Bevel fournit des playbooks Ansible et des chartes Helm qui déploient un réseau complet. La version 1.3.0 s'appuie sur le modèle d'opérateur et sur ArgoCD pour la réconciliation.
Le fichier de configuration réseau décrit l'intégralité de la topologie.
# network-aero.yaml — configuration Bevel 1.3.0
network:
type: fabric
version: "2.5.9"
frontend: disabled
env:
type: "production"
proxy: ambassador
ambassadorPorts: { portRange: { from: 15010, to: 15043 } }
retry_count: 40
external_dns: enabled
docker:
url: "registry.aero.io/bevel"
username: "lecteur-bevel"
password: "{{ vault_registry_password }}"
# Configuration du canal partagé
channels:
- channel_name: canal-conformite
consortium: ConsortiumAeronautique
channel_status: new
chaincode:
name: "certificat-conformite"
version: "1.0"
lang: "golang"
repository:
username: "ci-bevel"
password: "{{ vault_git_token }}"
url: "gitlab.aero.io/chaincode/certificat-conformite.git"
branch: main
path: "chaincode/"
arguments: '"init"'
endorsements: "AND('ConstructeurMSP.peer','FournisseurMSP.peer')"
participants:
- organization: constructeur
type: creator
org_status: new
peers:
- peer: peer0
gossipAddress: peer0.constructeur-net:7051
peerAddress: peer0.constructeur.aero:8443
- organization: fournisseur
type: joiner
org_status: new
organizations:
- organization:
name: constructeur
type: peer
external_url_suffix: constructeur.aero
cloud_provider: azure
aws: { access_key: "", secret_key: "" }
k8s:
region: westeurope
context: "aks-constructeur-weu"
config_file: "/opt/bevel/kubeconfig/constructeur.yaml"
vault:
url: "https://vault.constructeur.aero"
root_token: "{{ vault_root_constructeur }}"
secret_path: "secretsv2"
gitops:
git_protocol: "https"
git_url: "https://gitlab.aero.io/gitops/reseau-conformite.git"
branch: main
release_dir: "platforms/hyperledger-fabric/releases/prod"
chart_source: "platforms/hyperledger-fabric/charts"
git_repo: "gitlab.aero.io/gitops/reseau-conformite.git"
username: "bot-gitops"
password: "{{ vault_git_token }}"
email: "gitops@constructeur.aero"
services:
ca:
name: ca
subject: "/C=FR/ST=Ile-de-France/L=Versailles/O=Constructeur/CN=ca.constructeur.aero"
type: ca
grpc: { port: 7054 }
peers:
- peer:
name: peer0
type: anchor
gossippeeraddress: peer0.constructeur-net:7051
peerAddress: peer0.constructeur.aero:8443
certificate: "/opt/bevel/certs/peer0.crt"
grpc: { port: 7051 }
couchdb: { port: 5984 }La clé d'endorsements — AND('ConstructeurMSP.peer','FournisseurMSP.peer') — impose que toute transaction soit approuvée par les deux organisations. C'est elle qui donne sa valeur au registre : aucune partie ne peut inscrire un certificat unilatéralement.
Le chaincode, écrit pour durer
Le contrat gère trois opérations : émettre un certificat, le consulter, et tracer son historique. Voici le cœur, en Go avec l'API de contrat.
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// CertificatConformite représente l'attestation liée à une pièce livrée.
type CertificatConformite struct {
Reference string `json:"reference"`
NumeroSerie string `json:"numeroSerie"`
Fournisseur string `json:"fournisseur"`
NormeAppliquee string `json:"normeAppliquee"` // EN 9100, NADCAP…
DateEmission time.Time `json:"dateEmission"`
EmpreinteDoc string `json:"empreinteDoc"` // SHA-256 du PDF signé
Statut string `json:"statut"` // emis | valide | conteste
}
type Contrat struct {
contractapi.Contract
}
// Emettre inscrit un certificat. Seul le fournisseur y est habilité.
func (c *Contrat) Emettre(ctx contractapi.TransactionContextInterface,
reference, numeroSerie, norme, empreinte string) error {
// Contrôle du MSP appelant : la logique métier ne se délègue pas au réseau.
mspID, err := ctx.GetClientIdentity().GetMSPID()
if err != nil {
return fmt.Errorf("identité illisible : %w", err)
}
if mspID != "FournisseurMSP" {
return fmt.Errorf("seul le fournisseur émet un certificat, MSP reçu : %s", mspID)
}
existant, err := ctx.GetStub().GetState(reference)
if err != nil {
return fmt.Errorf("lecture du registre impossible : %w", err)
}
if existant != nil {
return fmt.Errorf("le certificat %s existe déjà", reference)
}
// L'horodatage provient de la transaction, jamais de l'horloge du pair :
// deux pairs doivent produire un résultat identique.
horodatage, err := ctx.GetStub().GetTxTimestamp()
if err != nil {
return fmt.Errorf("horodatage indisponible : %w", err)
}
cert := CertificatConformite{
Reference: reference,
NumeroSerie: numeroSerie,
Fournisseur: mspID,
NormeAppliquee: norme,
DateEmission: time.Unix(horodatage.Seconds, 0).UTC(),
EmpreinteDoc: empreinte,
Statut: "emis",
}
donnees, err := json.Marshal(cert)
if err != nil {
return fmt.Errorf("sérialisation impossible : %w", err)
}
// Événement consommable par les systèmes amont du constructeur.
if err := ctx.GetStub().SetEvent("CertificatEmis", donnees); err != nil {
return fmt.Errorf("émission de l'événement impossible : %w", err)
}
return ctx.GetStub().PutState(reference, donnees)
}
// Valider bascule le statut. Réservé au constructeur.
func (c *Contrat) Valider(ctx contractapi.TransactionContextInterface, reference string) error {
mspID, _ := ctx.GetClientIdentity().GetMSPID()
if mspID != "ConstructeurMSP" {
return fmt.Errorf("seul le constructeur valide, MSP reçu : %s", mspID)
}
donnees, err := ctx.GetStub().GetState(reference)
if err != nil || donnees == nil {
return fmt.Errorf("certificat %s introuvable", reference)
}
var cert CertificatConformite
if err := json.Unmarshal(donnees, &cert); err != nil {
return err
}
if cert.Statut != "emis" {
return fmt.Errorf("statut incompatible : %s", cert.Statut)
}
cert.Statut = "valide"
maj, _ := json.Marshal(cert)
return ctx.GetStub().PutState(reference, maj)
}Deux points appellent l'attention.
L'horodatage provient de GetTxTimestamp() et non de time.Now(). Chaque pair exécute le chaincode indépendamment ; si deux pairs produisent des horodatages différents, leurs jeux de lecture-écriture divergent et la transaction échoue à la validation.
Le contrôle du MSP appelant s'effectue dans le code. La politique d'endorsement garantit qu'un nombre suffisant d'organisations approuve, pas que la bonne organisation initie.
L'exposition réseau entre clusters
Deux clusters AKS dans deux abonnements distincts doivent communiquer en gRPC avec TLS mutuel. Bevel s'appuie sur Ambassador, qui expose chaque composant sur un port dédié avec passage TLS transparent.
apiVersion: getambassador.io/v3alpha1
kind: TCPMapping
metadata:
name: peer0-constructeur
namespace: constructeur-net
spec:
port: 8443
service: peer0.constructeur-net:7051
---
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: peer0-constructeur
namespace: constructeur-net
spec:
hostname: peer0.constructeur.aero
acmeProvider: { authority: none } # certificats issus du MSP Fabric
tlsSecret: { name: peer0-tls }
requestPolicy:
insecure: { action: Reject }Le point sensible tient au DNS. Les pairs se joignent par nom, et ce nom doit se résoudre identiquement depuis les deux clusters. Une zone DNS publique gérée par external-dns résout le problème proprement ; un fichier hosts bricolé le reporte au premier incident.
La réconciliation GitOps
Bevel écrit les manifestes Helm dans le dépôt GitOps. ArgoCD les applique. Le déploiement devient ainsi reproductible et auditable.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: fabric-constructeur
namespace: argocd
spec:
project: reseau-conformite
source:
repoURL: https://gitlab.aero.io/gitops/reseau-conformite.git
targetRevision: main
path: platforms/hyperledger-fabric/releases/prod/constructeur
destination:
server: https://aks-constructeur-weu.hcp.westeurope.azmk8s.io:443
namespace: constructeur-net
syncPolicy:
automated: { prune: false, selfHeal: true } # prune désactivé : un pair supprimé perd son registre
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
retry:
limit: 5
backoff: { duration: 30s, factor: 2, maxDuration: 10m }Notez prune: false. Sur un réseau Fabric, la suppression automatique d'une ressource devenue absente du dépôt détruirait le volume persistant d'un pair — et son exemplaire du registre avec. La synchronisation reste automatique, la suppression reste manuelle.
Les écueils rencontrés
La rotation des certificats TLS. Les certificats émis par l'autorité Fabric expirent, souvent à un an. Sans procédure de renouvellement automatisée, le réseau s'arrête du jour au lendemain. Prévoyez un CronJob qui surveille les échéances et alerte trente jours avant.
La croissance de CouchDB. La base d'état conserve la dernière valeur de chaque clé, mais les index se multiplient. Sur un réseau actif, prévoyez de l'espace disque et une politique de compaction.
Le versionnement du chaincode. Depuis Fabric 2.x, le cycle de vie exige l'approbation de chaque organisation avant validation. Une mise à jour non coordonnée bloque le canal. Traitez chaque version de chaincode comme un changement d'interface de programmation : contrat de compatibilité, période de recouvrement, plan de retour arrière.
L'ordonnancement en Raft et la latence. Les nœuds d'ordonnancement échangent en permanence. Répartis entre deux régions Azure distantes, la latence dégrade le débit de transactions. Mesurez avant de décider de la répartition géographique.
Ce que cette architecture apporte
Un certificat émis par le fournisseur, validé par le constructeur, inscrit dans un registre que ni l'un ni l'autre ne peut réécrire. Chaque modification laisse une trace horodatée et signée. En cas de litige sur une pièce, l'historique complet se reconstitue par un simple appel GetHistoryForKey.
Le tout déployé par GitOps, donc reproductible : reconstruire le réseau sur un nouvel abonnement Azure se résume à provisionner les clusters et laisser ArgoCD converger.