flexway
FRENParler à un expert

Configurations cibles des prochaines versions

GuideExploitationDepuis la version 1.6.0

Markdown

Sur cette page

Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : guide produit ; l'appeler : guide développeur.

Cette page montre, pour chaque cas d'usage prévu, la forme cible d'un flexway.yaml complet et l'extrait de contrat qui va avec, et la version qui doit le livrer : 1.7.0 (appareils et industrie) et 1.8.0 (besoins des entreprises). Rien de ce qui suit n'est construit : un détail peut encore bouger d'ici la version, et cette page suivra. Là où la forme d'une clé n'est pas encore fixée, l'exemple le dit au lieu de l'inventer. Chaque exemple deviendra un fichier de examples/, validé comme les autres, avec sa version.

Déjà livré en 1.6.0, et donc décrit ailleurs : le proxy Kafka natif (Clients Kafka natifs : clusters virtuels par tenant), le chiffrement par champ (Chiffrement par champ), les charges Protobuf, Avro et CBOR sous schéma et le registre de schémas (Charges Avro sous schéma, registre de schémas), NATS et AMQP 1.0 (Exemples : Kafka, RabbitMQ, MQTT, Avro, NATS, AMQP 1.0).

Ce que fait la passerelle d'aujourd'hui de ces formes (vérifié avec flexway validate) :

L'exemple déclare Aujourd'hui
Une clé nouvelle de flexway.yaml : listeners.mqtt, plans, webhooks, server.ip_deny, server.proxy_protocol, ai.budget.per_subject, ai.use_cases, ai.aliases, plan_claim, use_case_claim, trusted_clients, auth.mtls.clients[].plan Refusée au chargement (field ... not found) : toute la configuration est refusée
Une liaison x-flexway-bind: "cert:<champ>", un schéma de webhook lu dans un autre en-tête qu'Authorization Refusé au chargement
Un type de politique à venir : compress, ip-filter Accepté, jamais appliqué : flexway validate et le démarrage l'avertissent (a later version - NOT applied)
Une extension de contrat à venir : x-flexway-device, x-flexway-sparkplug, x-flexway-use-case, x-flexway-webhook, x-flexway-coalesce Refusée au chargement (not served yet - planned for <version>) : tout le contrat est refusé, comme toute extension x-flexway-* inconnue (unknown extension, avec la plus proche si elle est évidente) ou écrite là où elle n'est pas lue (not read here) - Pas encore construit

Prévu en 1.7

Appareils MQTT : X.509 ou jeton, topic lié au tenant et au certificat

Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (listeners.mqtt, x-flexway-bind: "cert:cn", x-flexway-device).

La passerelle est un proxy MQTT devant le courtier du client : une connexion amont par appareil, chaque paquet jugé par la même décision qu'une requête HTTP. Sessions, retenus, testaments et QoS 2 restent au courtier.

# flexway.yaml - l'écouteur ; aucun topic ici, ils viennent du contrat
version: 1
contracts:
  asyncapi: api/asyncapi.yaml
listeners:
  mqtt:
    address: ":8883"
    tls: { cert_file: /tls/tls.crt, key_file: /tls/tls.key, client_ca_file: /tls/ca.crt }   # X.509 des appareils
    websocket: { path: /mqtt }
    max_packet_bytes: 262144
    connect_timeout: 5s                # un CONNECT lent est fermé
upstreams:
  plant-broker:
    protocol: mqtt
    brokers: ["mqtts://broker.plant.internal:8883"]
    protocol_version: 5
auth:
  issuers:                             # pour un appareil sans certificat : un jeton en mot de passe
    - name: devices
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: plant-devices
      tenant_claim: site
# api/asyncapi.yaml - la convention des écouteurs, celle du proxy Kafka natif (1.6.0) :
# l'extension sur l'opération, send = l'appareil publie, receive = il s'abonne
channels:
  telemetry:
    address: "plant/{site}/line/{line}/{device}/telemetry"
    parameters:
      site:   { x-flexway-bind: tenant }      # le tenant vérifié, jamais celui du topic
      device: { x-flexway-bind: "cert:cn" }   # le nom commun du certificat de l'appareil
    messages:
      reading:
        contentType: application/x-protobuf
        payload: { schemaFormat: "application/vnd.google.protobuf;version=3", schema: { $ref: "./reading.binpb#Reading" } }
operations:
  publishTelemetry:
    action: send                       # l'appareil publie : écrit dans le canal
    channel: { $ref: "#/channels/telemetry" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]
Paquet Décision
CONNECT Certificat vérifié, sinon mot de passe (jeton vérifié ou clé), sinon refus 0x87 ; client id réécrit, lié à l'identité - un appareil n'en évince jamais un autre ; testament jugé comme un PUBLISH
PUBLISH Topic -> canal déclaré, paramètres liés égaux aux valeurs vérifiées (site = tenant, device = CN), portée, quota, schéma si validate: full ; refus PUBACK 0x87, 0x97 (quota), 0x99 (format) en MQTT 5, fermeture en 3.1.1
SUBSCRIBE Chaque filtre, jokers compris, doit tenir dans un canal déclaré une fois les paramètres liés posés ; sinon SUBACK 0x87
exp du jeton DISCONNECT 0xA0 (MQTT 5) ; l'appareil revient avec un jeton neuf

Prévu en 1.7

Sparkplug B

Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (listeners.mqtt, x-flexway-bind: "cert:cn", x-flexway-sparkplug).

flexway.yaml est celui de 12.29.6. Le canal Sparkplug B 3.0 se déclare d'un mot : l'espace de noms spBv1.0/... et le descripteur sparkplug_b.proto sont embarqués.

# api/asyncapi.yaml - extrait
channels:
  sparkplug:
    address: "spBv1.0/{group}/{type}/{edge_node}"
    x-flexway-sparkplug: true
    parameters:
      group:     { x-flexway-bind: tenant }     # le groupe Sparkplug est le tenant
      edge_node: { x-flexway-bind: "cert:cn" }  # un nœud ne publie que sous son nom
Élément Traitement
Naissance et mort NBIRTH/DBIRTH -> en ligne, NDEATH (testament)/DDEATH -> hors ligne, par tenant ; bdSeq vérifié ; vus dans la console
Séquences seq 0-255 vérifié par nœud, écart compté (flexway.sparkplug.sequence.gaps), jamais bloquant
Commandes NCMD/DCMD publiés par une application (porte HTTP x-flexway-publish ou MQTT) sous une portée d'écriture déclarée

Prévu en 1.7

OPC UA PubSub JSON sur MQTT

Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui (listeners.mqtt, x-flexway-device).

Aucune extension propre : un éditeur OPC UA PubSub qui publie en JSON sur MQTT est un appareil de Appareils MQTT, sous un contrat ordinaire - un canal, son schéma JSON, ses liaisons. Le préfixe plant/{site}/ (configuré dans l'éditeur) lie le tenant ; la suite reprend la structure de topic recommandée par OPC UA (partie 14).

# api/asyncapi.yaml - extrait
channels:
  opcua-data:
    address: "plant/{site}/opcua/json/data/{publisher}/{writer_group}"
    parameters:
      site:      { x-flexway-bind: tenant }
      publisher: { x-flexway-bind: "cert:cn" }   # l'éditeur publie sous son propre nom
    messages:
      network-message:
        contentType: application/json
        payload: { $ref: "./opcua-network-message.json" }   # le NetworkMessage JSON, validé
operations:
  publishOpcuaData:
    action: send
    channel: { $ref: "#/channels/opcua-data" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]

Prévu en 1.8

Plans lus d'une claim, plan par défaut

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (plans, plan_claim).

Vendre son API par plans : le plan d'un appelant vient d'une claim vérifiée de son jeton, ou de l'attribut plan de sa clé d'API ou de son certificat ; ses limites sont déclarées ici. Un appelant sans plan, ou dont le plan n'est pas déclaré, reçoit le plan par défaut - jamais « illimité ».

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # exigé : refus au chargement sans magasin partagé
auth:
  issuers:                             # la clé existante est auth.issuers
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: orders-api
      tenant_claim: org
      plan_claim: subscription.plan    # chemin pointé d'une claim du jeton vérifié
plans:
  default: free                        # obligatoire dès que plans: existe
  count_per: tenant                    # tenant (défaut) | subject
  definitions:
    free:
      requests: { day: 1000, month: 20000 }
      tokens:   { day: 50000, month: 1000000 }
      eur:      { month: 5 }
    pro:
      requests: { day: 100000, month: 2000000 }
      tokens:   { month: 50000000 }
      eur:      { month: 500 }
# api/openapi.yaml - extrait : rien de propre aux plans, le contrat dit seulement qui entre
paths:
  /orders:
    get:
      operationId: listOrders
      security: [ { corporate: [orders:read] } ]
Règle Comportement
Plan inconnu Le plan default, une ligne de journal plan_unknown et le compteur flexway.plans.unknown - l'appelant est authentifié, l'écart vient de son fournisseur
Avec les quotas du tenant (Poser des quotas) Les deux s'appliquent, le premier atteint refuse : 429 quota_exceeded, membre limit: plan
Clé d'API, certificat Attribut plan de l'entrée du magasin (écrit par flexway api-key) ou de auth.mtls.clients[]

Prévu en 1.8

Budgets d'inférence par utilisateur et par cas d'usage

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (ai.budget.per_subject, ai.use_cases, ai.aliases, use_case_claim, x-flexway-use-case).

En plus du budget du tenant (Servir l'inférence), un budget par utilisateur (le sujet vérifié) et par cas d'usage. Le cas d'usage vient du contrat (x-flexway-use-case), d'un alias de modèle de /v1/*, ou de la claim d'un client de confiance - jamais d'un en-tête de l'appelant. Le budget le plus restrictif gagne.

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://tickets.internal:8080
auth:
  issuers:
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: ai
      tenant_claim: org
      use_case_claim: ai_use_case      # crue seulement pour les clients de trusted_clients
      trusted_clients: [batch-reviewer]
ai:
  security: [ { oidc: { scopes: [inference] } } ]
  default_backend: hosted
  backends:
    hosted:
      url: https://api.provider.example/v1
      api_key: "file:/etc/flexway/secrets/provider-key"
      default_model: gpt-4.1-mini
      external: true
      price: { input_per_mtok: 0.40, output_per_mtok: 1.60 }
  budget:
    store: "${COUNTERS_URL}"
    monthly_eur: 2000                  # tenant, inchangé
    per_subject: { monthly_eur: 20, daily_tokens: 200000 }   # chaque sujet vérifié
  use_cases:
    support-bot: { monthly_eur: 300 }
    code-review: { monthly_eur: 800, daily_tokens: 5000000 }
  aliases:                             # un nom de modèle de /v1/* qui porte un cas d'usage
    support-assistant: { backend: hosted, model: gpt-4.1-mini, use_case: support-bot }
# api/openapi.yaml - extrait : une opération (ou une méthode gRPC mesurée) porte son cas d'usage
paths:
  /tickets/{id}/summary:
    post:
      operationId: summarizeTicket
      x-flexway-use-case: support-bot  # un cas d'usage absent de ai.use_cases : refus au chargement
Règle Comportement
Appel sans sujet Pas de budget utilisateur ; celui du tenant s'applique
Contrat et claim Le contrat l'emporte : un client de confiance ne déplace pas une opération marquée vers un autre budget
Refus 429 budget_exceeded, membre limit : tenant, plan, subject ou use_case (enveloppe OpenAI sur /v1/*)
Plafond souple Celui du tenant seul ; un budget d'utilisateur ou de cas d'usage est un plafond dur
Consommation par utilisateur GET /usage/subjects (administration), sujet haché ; jamais une dimension de métrique

Prévu en 1.8

Webhook entrant Stripe et rotation du secret

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (webhooks, x-flexway-webhook, schéma lu dans Stripe-Signature).

Recevoir les webhooks d'un fournisseur signés : la signature est l'identité de l'appelant, une alternative de security au contrat. Un préréglage suffit pour Stripe, GitHub, Slack, Shopify et SendGrid.

# flexway.yaml - les secrets par référence, jamais en clair
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://billing.internal:8080
webhooks:
  secrets:
    stripe-endpoint:
      current: file:/secrets/stripe/current
      previous: file:/secrets/stripe/previous   # rotation : les deux vérifient
      tenant: acme                     # lié comme une clé d'API
  replay: { max_ids: 100000 }          # identifiants vus, par réplique, bornés
# api/openapi.yaml - extrait
components:
  securitySchemes:
    stripe:
      type: apiKey
      in: header
      name: Stripe-Signature
      x-flexway-webhook: { preset: stripe, secret: stripe-endpoint }
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      security: [{ stripe: [] }]

Rotation du secret, sans perdre un évènement :

Étape Action
1 L'ancien secret passe dans previous, le nouveau dans current (deux clés du Secret monté) ; rechargement à chaud (Changer la configuration sans redémarrer) - les deux vérifient
2 Le fournisseur bascule sur le nouveau secret ; une signature contre l'un ou l'autre suffit
3 Une fois la bascule faite, previous est retiré
Refus Réponse
En-tête absent ou illisible, signature fausse, horodatage hors tolérance, identifiant déjà vu 401 webhook_signature_invalid, sans dire lequel ; la cause va au journal et à flexway.webhook.verifications{outcome}
Corps au-delà de la borne 413, avant toute vérification

Le corps est lu en entier et borné avant la vérification, puis relayé octet pour octet ; les en-têtes de signature restent (l'amont peut revérifier), le secret ne sort jamais.

Prévu en 1.8

Webhook générique HMAC

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (webhooks, x-flexway-webhook, schéma lu dans X-Signature).

Un partenaire sans préréglage : le schéma HMAC se déclare en entier, à gabarit fermé.

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://partners.internal:8080
webhooks:
  secrets:
    partner-hooks:
      current: file:/secrets/partner/current
      tenant: partner-a
  replay: { max_ids: 100000 }
# api/openapi.yaml - extrait
components:
  securitySchemes:
    partner:
      type: apiKey
      in: header
      name: X-Signature
      x-flexway-webhook:
        secret: partner-hooks
        algorithm: sha256              # sha256 | sha512 ; sha1 refusé
        signature: { header: X-Signature, prefix: "sha256=", encoding: hex }
        timestamp: { header: X-Timestamp, format: unix }
        id: { header: X-Delivery-Id }
        signed: "{timestamp}.{body}"   # gabarit fermé : {id}, {timestamp}, {body}
        tolerance: 5m
paths:
  /hooks/partner:
    post:
      operationId: partnerEvents
      security: [{ partner: [] }]

Prévu en 1.8

Standard Webhooks, mémoire de rejeu partagée

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (webhooks, x-flexway-webhook, schéma lu dans webhook-signature).

Un émetteur conforme à Standard Webhooks, le format des webhooks sortants de Flexway (Publier et consommer sur un courtier) : HMAC-SHA256 (v1) ou Ed25519 (v1a), identifiant et horodatage signés. Avec plusieurs répliques, la mémoire des identifiants vus peut être partagée (option déclarée) : sans elle, un rejeu sur une autre réplique dans la fenêtre de tolérance passe.

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://events.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # la mémoire partagée vit dans le magasin des compteurs
webhooks:
  secrets:
    platform-events:
      current: file:/secrets/platform/current   # whsec_<base64>
      previous: file:/secrets/platform/previous
      tenant: acme
  replay: { max_ids: 100000, shared: true }    # délai, et refus si le magasin ne répond pas
# api/openapi.yaml - extrait
components:
  securitySchemes:
    platform:
      type: apiKey
      in: header
      name: webhook-signature
      x-flexway-webhook: { preset: standard-webhooks, secret: platform-events }
paths:
  /hooks/platform:
    post:
      operationId: platformEvents
      security: [{ platform: [] }]
Préréglage Rejeu
standard-webhooks, stripe, slack, sendgrid Horodatage signé : tolérance et identifiants vus
github, shopify Identifiant non signé : protection partielle, dite par /config

Prévu en 1.8

Listes d'adresses : liste noire globale et ip-filter

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (server.ip_deny, server.proxy_protocol). Seule, une politique ip-filter est acceptée avec un avertissement et n'est pas appliquée.

Deux portes : server.ip_deny, liste noire globale jugée avant le routage (un flux de menaces ne coûte ni routage ni cryptographie) ; la politique ip-filter, attachée à un hôte, une route ou une opération, jugée après le routage et avant CORS et l'identité.

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://shop.internal:8080
server:
  trusted_proxies: [10.0.0.0/8]        # X-Forwarded-For cru de ces pairs seulement (existe déjà)
  proxy_protocol: { from: [10.0.5.0/24] }   # v1 et v2, seulement de ces pairs ; lu au démarrage
  ip_deny:
    inline: [203.0.113.0/24]
    files: [/lists/threat-feed.txt]    # une plage par ligne, # commentaire ; relu au rechargement
policies:
  definitions:
    office-only:   { type: ip-filter, config: { allow: [198.51.100.0/24, "2001:db8::/32"] } }
    stripe-ranges: { type: ip-filter, config: { allow_files: [/lists/stripe-ips.txt] } }
  chains:
    hosts: { admin.example.com: [office-only] }
# api/openapi.yaml - extrait : le niveau opération de la chaîne, au contrat comme toute politique
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      x-flexway-policies: [stripe-ranges]   # les plages publiées par le fournisseur
Règle Comportement
Adresse cliente Le pair TCP ; derrière un pair de trusted_proxies, X-Forwarded-For lu de droite à gauche jusqu'au premier saut non fiable ; PROXY protocol d'un autre pair : connexion fermée
ip-filter allow : l'adresse doit y être ; deny : refusée si elle y est ; deny gagne ; la plus précise des chaînes gagne, !office-only la retire
Route inconnue La chaîne de l'hôte est jugée avant 404 ou 405 : un refusé reçoit 403 et ne cartographie pas les routes
Refus 403 address_denied, avant toute cryptographie ; flexway.ip.refused{level}
Fichier illisible au rechargement La configuration précédente reste - jamais une liste vide substituée

Prévu en 1.8

Compression négociée

Cible de la version 1.8.0 - pas encore construit : aujourd'hui, la politique compress est acceptée avec un avertissement (a later version - NOT applied) et n'est pas appliquée.

gzip, brotli et zstd, négociés par Accept-Encoding ; opt-in par une politique - sans elle, rien ne change.

# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
policies:
  definitions:
    compress-json:
      type: compress
      config:
        encodings: [zstd, br, gzip]    # préférence du serveur à qualité égale
        min_bytes: 1024                # défaut fixé au banc M1
        types: [application/json, "application/*+json", "text/*"]   # liste blanche
  chains:
    global: [compress-json]
# api/openapi.yaml - extrait : rien à déclarer ; un flux n'est jamais compressé
paths:
  /products:
    get:
      operationId: listProducts
Règle Comportement
Éligible Longueur connue au moins min_bytes, type de la liste blanche, pas de Content-Encoding de l'amont, méthode autre que HEAD ; un flux (SSE, longueur inconnue, 101) jamais
En-têtes Content-Encoding, Content-Length recalculé, Vary: Accept-Encoding sur toute réponse éligible, ETag fort rendu faible
Niveaux Fixés au banc, une constante par algorithme : aucune clé
Cache (Mettre une lecture en cache) Une entrée par encodage choisi, jamais par valeur brute d'Accept-Encoding
BREACH Une réponse qui mêle un secret et une entrée reflétée : l'exclure par une condition when

Prévu en 1.8

Coalescence des lectures en vol

Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui (x-flexway-coalesce).

Une rafale de lectures identiques pendant qu'une première est en vol : un seul appel amont, la réponse copiée à chaque attendant. Opt-in par opération, au contrat ; la clé est celle du cache - tenant et sujet pour une opération qui exige une identité. Chaque attendant a été admis et compté : la coalescence épargne l'amont, jamais le quota. flexway.yaml n'a rien de nouveau.

# flexway.yaml - rien de propre à la coalescence
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
# api/openapi.yaml - extrait
paths:
  /catalog/products:
    get:
      operationId: listProducts
      x-flexway-cache: { ttl: 30s }    # facultatif : le cache d'abord, la coalescence sur ses absences
      x-flexway-coalesce: { max_waiters: 256, max_bytes: 1MiB }
Règle Comportement
Refus au chargement Méthode autre que GET ou HEAD, flux, WebSocket, méthode gRPC, inférence
Non partageable Set-Cookie, longueur inconnue, au-delà de max_bytes : chaque attendant appelle l'amont
Au-delà de max_waiters L'appel suit son chemin seul (flexway.coalesce.requests{role="overflow"})
Erreur de l'amont Partagée comme une réponse ; un délai dépassé du meneur ne l'est pas

Étapes suivantes