flexway
FRENParler à un expert

Exemples : TLS, remplacement, quotas partagés, gRPC, cache, corps, appel externe

ExempleExploitationDepuis la version 1.0.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.

mTLS vers un amont

Hors maillage seulement : dans un maillage, le sidecar le fait déjà. Les fichiers doivent exister au chargement - c'est pourquoi ce cas n'a pas de fichier d'exemple validé en CI.

upstreams:
  payments:
    url: https://payments.partner.example:8443   # TLS exige https://
    tls:
      ca_file: /etc/flexway/tls/partner-ca.pem
      cert_file: /etc/flexway/tls/client.crt  # relu à sa rotation, sans redémarrage
      key_file: /etc/flexway/tls/client.key
      server_name: payments.partner.example

Écouteur TLS de la passerelle

Sans edge qui termine le TLS : la passerelle vérifie elle-même le certificat client, sur FLEXWAY_TLS_ADDR (:8443 par défaut). Les clients se déclarent comme en 12.15 ; les fichiers doivent exister au chargement, d'où l'absence de fichier d'exemple validé en CI.

server:
  tls:
    cert_file: /etc/flexway/tls/tls.crt              # relu à sa rotation, sans redémarrage
    key_file: /etc/flexway/tls/tls.key
    client_ca_file: /etc/flexway/tls/clients-ca.pem  # autorité des certificats clients, relue aussi
auth:
  mtls:
    clients:
      - { id: bank-settler, uri: "spiffe://bank.example/settler", tenant: bank, operations: [settle] }

Remplacer une passerelle en place

Venir d'une passerelle existante : l'import de sa configuration écrit le point de départ - amonts, émetteurs, débits, coupe-circuits, extensions du contrat - et le rapport de ce qui ne passe pas (Reprendre une passerelle existante). Ce qui suit est ce qu'on y ajoute le premier jour.

# What a team replacing its gateway usually needs on day one (1.0.0): anonymous
# users behind a BFF counted apart, a bucket per operation as most gateways count, errors typed under
# its own portal with field codes in its own words, the body left to the service, tokens of an old
# and a new audience accepted while the audience is renamed.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://shop.internal:8080
hosts:
  api.example.com: { default_upstream: default, validation: params }   # the services validate their bodies
server:
  trusted_proxies: [local]                       # the mesh proxy: the pod's own addresses (not the loopback)
  peer_identity_header: l5d-client-id            # the mTLS identity that proxy verified
errors:
  type_base: https://docs.example.com/errors/    # type = https://docs.example.com/errors/<code>
  field_codes: { minLength: too_short, maxLength: too_long, minimum: out_of_range, maximum: out_of_range }
auth:
  issuers:
    - name: customers
      discovery: https://login.example.com/.well-known/openid-configuration
      audiences: [shop-api, api]                 # the old name and the new, during the rename
policies:
  definitions:
    per-user:
      type: rate-limit
      config:
        key: header                              # anonymous: X-Client-Key, if the BFF set it
        header: X-Client-Key
        trusted: [web-bff.web.serviceaccount.identity.linkerd.cluster.local]
        scope: operation                         # a bucket per operation and user, not one per class
        classes:
          read:  { rps: 20, burst: 40 }
          write: { rps: 5,  burst: 10 }
  chains:
    global: [per-user]
# extrait de examples/replace-gateway/openapi.yaml
  /orders:
    post:
      operationId: createOrder
      x-flexway-validation: full                 # this one is checked here too, body included

Débit partagé, forfaits, délestage

# Several replicas sharing one rate, a plan per tenant, and load shed when the gateway saturates
# (1.0.0).
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://reports.internal:8080
server:
  counters: { store: "${COUNTERS_URL}", sync: 1s }   # Redis, Dragonfly or Valkey, shared by the replicas
  shedding: { target: 20ms }                         # p99 scheduling latency above which load is shed
auth:
  issuers:
    - name: customers
      discovery: https://login.example.com/.well-known/openid-configuration
      audience: api
      tenant_claim: org                              # what a quota counts by
policies:
  definitions:
    per-caller:
      type: rate-limit
      config:
        classes:
          read:  { rps: 50, burst: 100 }
          write: { rps: 10, burst: 20 }
    plans:
      type: quota
      config: { limits: { day: 100000, month: 2000000 } }
  chains:
    global: [per-caller, plans]
# extrait de examples/shared-quotas/openapi.yaml
  /reports:
    post:
      operationId: buildReport
      x-flexway-priority: sheddable              # shed first when the gateway saturates
  /payments/{id}:
    get:
      operationId: getPayment
      x-flexway-priority: critical               # never shed

Amont gRPC en gRPC, gRPC-Web et REST

# A gRPC upstream served three ways from its own .proto (1.1.0): to gRPC clients (h2c on the
# traffic listener), to browsers in gRPC-Web, and in REST/JSON at the routes its google.api.http
# annotations declare. One operation per method - shop.v1.Orders/GetOrder - whatever the protocol:
# the same admission, rate limits, quotas and audit.
#
# orders.binpb is the descriptor set built from orders.proto (`buf build -o orders.binpb`): Flexway
# reads it, never the .proto, and needs no generated code. Rebuild it when the .proto changes.
version: 1
contracts:
  protobuf: [./orders.binpb]
upstreams:
  # Named by the methods' option (upstream: "orders"), as x-flexway-upstream names one.
  orders:
    # Inside a mesh the sidecar does mTLS, so the gateway speaks HTTP/2 in clear (h2c).
    url: http://orders.internal:9000
    protocol: grpc
auth:
  issuers:
    - { name: staff, discovery: "https://id.example.com/realms/staff/.well-known/openid-configuration", audience: orders-api }
// extrait de examples/grpc-upstream/orders.proto
service Orders {
  rpc GetOrder(GetOrderRequest) returns (Order) {
    option (google.api.http) = { get: "/v1/shop/orders/{id}" };          // the REST route
    option (flexway.v1.operation) = {                                    // what x-flexway-* says in OpenAPI
      upstream: "orders"
      security: [ { scopes: ["orders:read"] } ]
      timeout: "2s"
    };
  }
  rpc CreateOrder(CreateOrderRequest) returns (Order) {
    option (google.api.http) = { post: "/v1/shop/orders" body: "order" };
    option (flexway.v1.operation) = { upstream: "orders" security: [ { anonymous: true } ] };
  }
  rpc WatchOrder(GetOrderRequest) returns (stream Order) {               // an event stream in REST
    option (google.api.http) = { get: "/v1/shop/orders/{id}/watch" };
    option (flexway.v1.operation) = { upstream: "orders" security: [ { anonymous: true } ] };
  }
}

Cache de réponses

# Responses kept by the gateway (1.2.0): opt-in, per operation in the contract
# (x-flexway-cache) or by a cache policy over routes - the operation's own wins. Public by default: an
# identified caller is never served from, nor stored into, a public entry; per tenant and subject only
# when the operation declares it. The upstream's Cache-Control only shortens the ttl; no-store,
# private, Set-Cookie and streams are never kept.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
server:
  cache:
    max_bytes: 64MiB                             # per replica; past it, the least recently used goes
    max_entry_bytes: 512KiB                      # a larger response is relayed, never kept
auth:
  issuers:
    - name: customers
      discovery: https://login.example.com/.well-known/openid-configuration
      audience: api
      tenant_claim: org                          # a cache per subject keys by tenant and subject
policies:
  definitions:
    catalog-cache:
      type: cache
      config: { ttl: 5m }                        # public, always: a policy never caches per subject
  chains:
    routes:
      "/products/**": [catalog-cache]            # the reads under it; the writes are left alone
# extrait de examples/response-cache/openapi.yaml
  /products:
    get:
      operationId: listProducts
      x-flexway-cache: { ttl: 60s }              # its own, over the route's policy
  /me/cart:
    get:
      operationId: getCart
      security: [ { customers: [] } ]
      x-flexway-cache: { ttl: 30s, scope: subject }   # each subject of each tenant, its own entry

Corps et statut de réponse

# Response bodies shaped by the gateway (1.2.0): a closed list of fields served, personal
# data masked, a legacy upstream's statuses turned into semantic ones in RFC 9457. Only the operations
# that declare one of these policies are read whole - every other keeps the direct relay. A stream, a
# response that is not JSON, or a projection that drops a field the contract requires refuses the
# start.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
server:
  body:
    max_bytes: 2MiB                              # past it: 502 response_too_large, never a cut body
    validate_sample: 0.05                        # one served body in twenty checked against its schema
policies:
  definitions:
    public-order:
      type: transform-body
      config: { project: [id, status, total, items, customer], on_missing: fail }
    no-pii:
      type: redact
      config: { fields: [body.customer.email, body.customer.phone], with: "***" }
    legacy-errors:
      type: map-status
      config:
        - { from: 200, when: "has(body.error)", to: 422, code: legacy_rejected }
        - { from: 520, to: 502 }                 # a CDN's private status: the upstream failing
  chains:
    routes:
      "/orders/**": [legacy-errors]              # every order read; /health keeps the direct relay
# extrait de examples/body-transforms/openapi.yaml
  /orders/{id}:
    get:
      operationId: getOrder
      x-flexway-policies: [public-order, no-pii]   # what is not listed is not served; the PII masked
  /health:
    get:
      operationId: health                          # no body policy: relayed without a buffer

Appel externe

# A decision that belongs to the client's own system (1.2.0): a risk service asked
# before an order reaches its upstream, a loyalty service asked once a profile is answered. The
# service is an upstream declared like any other - its pool, bulkhead and breaker are its own -,
# never a URL in a policy. The timeout and what a failed call does are declared, never defaulted;
# the call consumes the operation's budget.
version: 1
contracts:
  openapi: ./openapi.yaml
auth:
  issuers:
    - name: customers
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: api
      tenant_claim: org
  tenant_header: X-Tenant                        # sent to the risk service when named, never set by it
upstreams:
  default:
    url: http://orders.internal:8080
  risk:
    url: http://risk.internal:8080               # the external service: an upstream like any other
    max_in_flight: 200                           # its bulkhead
    circuit_breaker: { min_requests: 20, failure_ratio: 0.5, cooldown: 10s }
  loyalty:
    url: http://loyalty.internal:8080
policies:
  definitions:
    risk-check:
      type: external-call
      phase: request
      when: "request.method == 'POST'"
      config:
        upstream: risk
        path: /score
        timeout: 150ms                           # required, within the operation's budget
        on_failure: fail-closed                  # an authorization: no answer, no order
        send: { headers: [X-Tenant], body: true }
        deny: { when: "response.status == 403", status: 403, code: risk_refused }
        set_headers: { X-Risk-Score: "response.body.score" }
    loyalty-tier:
      type: external-call
      phase: response
      config:
        upstream: loyalty
        path: /tier
        timeout: 80ms
        on_failure: fail-open                    # an enrichment: no answer, no header
        send: { headers: [X-Tenant] }
        set_headers: { X-Loyalty-Tier: "response.body.tier" }
# extrait de examples/external-call/openapi.yaml
  /orders:
    post:
      operationId: createOrder
      x-flexway-timeout: 2s                        # the budget the risk call consumes
      x-flexway-policies: [risk-check]
  /health:
    get:
      operationId: health                          # no external call: nothing asked of anyone

Pas encore construit

Écrit ici pour que la forme cible soit connue. D'ici sa version, une clé nouvelle de flexway.yaml est refusée au chargement, un type de politique à venir est accepté avec un avertissement et jamais appliqué, et une extension de contrat à venir est refusée au chargement avec la version qui l'apporte - le détail, relevé avec flexway validate, et un exemple complet et commenté par cas d'usage sont en Configurations cibles des prochaines versions. La syntaxe est celle prévue, pas une promesse de détail.

Cas Forme cible Version
Appareils et industrie : écouteur MQTT des appareils (X.509, jeton ou clé en mot de passe), topic lié au tenant et au certificat, Sparkplug B, OPC UA PubSub JSON listeners.mqtt, x-flexway-device, x-flexway-sparkplug, x-flexway-bind: "cert:<champ>" 1.7.0
Plans et budgets : limites par plan (claim vérifiée ou attribut de clé, plan par défaut), budgets d'inférence par utilisateur et par cas d'usage plans, issuers[].plan_claim, ai.budget.per_subject, ai.use_cases, ai.aliases, x-flexway-use-case 1.8.0
Webhooks entrants signés, listes d'adresses : HMAC générique, Standard Webhooks, préréglages de fournisseurs ; listes blanche et noire, PROXY protocol x-flexway-webhook, webhooks.secrets, server.ip_deny, server.proxy_protocol, politique ip-filter 1.8.0
Compression négociée, coalescence des lectures en vol Politique compress (gzip, brotli, zstd), x-flexway-coalesce 1.8.0
Cartes A2A amont vérifiées, carte publiée re-signée agents.a2a.signers, agents.a2a.signing, agents.a2a.cards, x-flexway-a2a.signer 1.9.0
Délégation d'agent à agent, registre Bornes resserrées par agent sur deux sauts A2A, verdicts dans /config 1.9.0
Licence : sans licence valide, Flexway refuse de démarrer ; clé Développeur gratuite hors production (90 jours, en-tête Flexway-License: developer, 100 requêtes par seconde et par réplique), Business et Enterprise avec 30 jours de grâce ; état dans le journal, la métrique flexway.license.state, /config et la console ; jamais d'arrêt d'un processus en service Fichier de licence monté comme un secret, son chemin dans flexway.yaml (nom de clé encore à fixer) ; flexway validate reste utilisable sans licence À l'ouverture publique, à dater

Étapes suivantes