+ - 0:00:00
Notes for current slide

Raccourci clavier : P -> permet d'afficher le plan Raccourci clavier : C -> permet d'ouvrir une autre fenêtre avec les slides

Bonjour à tous. Merci d'être venu si nombreux.

Notes for next slide

Je m'appelle François Dume.

Retour d'expérience ARTE GEIE :

Développement API

1 / 55

Raccourci clavier : P -> permet d'afficher le plan Raccourci clavier : C -> permet d'ouvrir une autre fenêtre avec les slides

Bonjour à tous. Merci d'être venu si nombreux.

Ich heiße François Dume

2 / 55

Je m'appelle François Dume.

ARTE France Allemagne

3 / 55
  • Dominique Chapatte ne travaille pas chez Arte. Vous pouvez le retrouver sur M6.

-->

4 / 55

Au niveau numérique, ARTE édite un certain nombre de sites internet : arte.tv

5 / 55

Future, plateforme dédiée à la science

6 / 55

Creative, plateforme dédiée aux arts visuels et numériques

7 / 55

Concert, anciennement Arte Live Web

8 / 55

Cinéma, qui permet de retrouver les films diffusés à l'antenne

9 / 55

Tracks, exemple de site d'une émission


Orange Bouygues SFR ...

10 / 55

On développe une solution de CatchUP (ARTE+7). Cette solution est packagée dans la plupart des box des opérateurs. On développe également des applications pour les TV connectées (hbbtv).

11 / 55

On édite également des applications mobiles, notamment pour android et Apple.

We need (to be) API !

12 / 55

Pour pouvoir développer ces magnifiques applications, nous avons besoin d'API.

Mise à disposition :

  • Des métadonnées des programmes diffusés à l'antenne (titre, description, photo, producteur, casting, horaires de diffusion)
  • Des URLs des streams (mp4, hls)
  • Des statistiques de consultation de nos contenus
13 / 55

plage de droits, heures de diffusion

Exemple :

{
"videos": [
{
"id": "055075-000-A_SHOW_ALW_FR_fr",
"programId": "055075-000-A",
"channel": "FR",
"language": "fr",
"kind": "SHOW",
"platform": "ALW",
"title": "Angus & Julia Stone \u00e0 la Maroquinerie",
"originalTitle": "Angus & Julia Stone \u00e0 la Maroquinerie",
"durationSeconds": 3101,
"shortDescription": "Depuis Down The Way en 2010, Angus et Julia s'\u00e9taient \u00e9chapp\u00e9s chacun de leur c\u00f4t\u00e9 chantant l'un sans l'autre pendant un temps. La s\u00e9paration ne f\u00fbt heureusement pas d\u00e9finitive puisque les fr\u00e8res et soeurs retrouvent aujourd'hui le chemin vers de nouvelles sc\u00e8nes. Les retrouvailles se scellent \u00e9galement dans un troisi\u00e8me album o\u00f9 les puret\u00e9s folk et les m\u00e9lodies fredonn\u00e9es c\u00f4toient les ballades cotonneuses et les mots doux. ",
"producer": "ARTE FRANCE",
"videoRightsBegin": "2014-08-25T17:00:00Z",
"videoRightsEnd": "2015-02-25T22:59:00Z",
"mainImage": {
"name": "055075-000_1392179_32_202.jpg",
"extension": "jpg",
"caption": "Angus & Julia Stone",
"url": "http:\/\/www.arte.tv\/prog_img\/IMG_APIOS\/055000\/055000\/055075-000_1392179_32_202.jpg"
},
"programmingId": 1781191,
"mainReassembly": true,
"reassembly": "A",
"reassemblyRef": "A",
14 / 55

Développement d'une nouvelle API

15 / 55

Nous disposons déjà d'une API qui permet de mettre à disposition le contenu antenne.

Objectifs

  • volonté de mettre à disposition tout le contenu ARTE
  • adresser de manière unifiée tous les workflows
    • broadcast (ARTE+7)
    • web (Concert, Future, Creative, ...)
  • ouverture (Open Data ?)
  • authentification oAuth
  • suivi de l'usage (throttling)
16 / 55

Pas seulement le contenu broadcast mais également les contenus développés pour le web. On a de plus en plus de contenu créé uniquement pour le web qui dispose d'un workflow différent de publication.

Architecture technique

  • socle Symfony2/MongoDB
  • synchronisation des données via messages asynchrones (RabbitMQ)
  • utilisation du standard {json:api}
  • découplage en composants autonomes (microservices ?) :
    • authentification
    • Open Program API (OPA)
    • générateur de player ARTE (iframe/oEmbed)
    • statistiques
17 / 55

ça marche

18 / 55

Mis en production pour 24h Jerusalem en avril 2014. Début du développement décembre. Utilisé par Tracks.

Sécurisation des API et throttling

19 / 55
  • API sécurisée par authentification oAuth 2.0
  • Mise en place d'un reverse proxy authentifiant (Openresty, distribution nginx avec support de Lua)
  • Développement de scripts Lua chargés dans la configuration nginx permettant de valider le token oAuth
  • Toutes les API "sécurisées" sont protégées par ce reverse proxy
  • Le reverse proxy authentifiant est également en charge du throttling (exemple : 1000 requêtes/heure)
20 / 55
  • Ce reverse proxy est utilisable avec tout type de serveur applicatif (Symfony2, Ruby, Java).
  • Le serveur applicatif n'a pas connaissance des utilisateurs. Il ne possède que la notion de rôles.

Exemple d'un process de requête à nos API

21 / 55

1. Interrogation du serveur oAuth pour récupérer un token oAuth

curl https://.../oauth/token?client_id=...
&client_secret=...&grant_type=credentials

Réponse :

{
"access_token":"MDBjYzMzNTRjNTQxM...",
"expires_in":3600,
"token_type":"bearer",
"scope":"user",
"refresh_token":"ZTJjZTEyOWFiNjQ1YTkw...",
"roles":["USER"],
"rate_limit":1000
}
22 / 55

2. Utilisation du token

curl -I https://.../api1/resource?access_token=MDBjY...
HTTP/1.1 200 OK
Server: openresty
Date: Thu, 23 Oct 2014 19:51:39 GMT
Content-Type: application/vnd.api+json
Vary: X-ARTE-Roles
X-Rate-Limit-Limit: 1000
X-Rate-Limit-Remaining: 999
X-Rate-Limit-Reset: 1412887899
...
23 / 55

3. Après 1000 requêtes...

curl -I https://.../api1/resource?access_token=MDBjY...
HTTP/1.1 429 Too Many Requests
Server: openresty
24 / 55

Schéma d'architecture globale

schéma d'architecture

25 / 55

Un Reverse Proxy qui protège toutes nos applications. Une application oauth : Symfony2, FOSOauthServerBundle, fournisseur d'identités Plusieurs API (api1, api2). Une base de données clé-valeurs associée au serveur nginx. Cette base de donnée sert de cache et de stockage du suivi de l'usage (Redis, mémoire partagée, memcache).

(1) L'utilisateur fait une requête à une de nos API avec un token

curl -I https://.../api1/resource?access_token=MDBjY...
26 / 55

Il passe en paramètre le access_token oAuth.

(2) nginx va essayer de valider le token oAuth en effectuant une sous-requête au fournisseur d'identité (oAuth 2.0) :

local token = ngx.var.arg_access_token
local subrequest = ngx.location.capture(
'/oauth/verifToken?access_token=' .. token
)
if subrequest.status == ngx.HTTP_OK then
local content = subrequest.body
...
end
27 / 55

Cette sous-requête ne sera exécutée que lorsque nginx ne connait pas le token.

(3) Si le token est valide, le serveur oAuth va retourner au serveur nginx des informations liées au token :

HTTP/1.1 200 OK
Server: openresty
...
{
"client": "Tracks",
"rateLimit": 1000,
"expires": 3600,
"roles" : "USER"
...
}
28 / 55

(4) Ces informations seront stockées par nginx dans une base de données clé-valeur, le compteur de requêtes est décrémenté.

local succ, err, forcible = cache:set(
key, content, contentLife
)
...
cache:incr('throttle_' .. key, 1)

N.B : À la requête suivante, le Reverse Proxy n'interrogera plus le serveur oAuth, il lira son cache et décrémentera le compteur de requêtes (remaining).

29 / 55

(5) Si le token est valide, le Reverse Proxy accepte de rediriger la requête au backend et ajoute des entêtes à la requête (rôles) :

GET http://api1.local/api1/resource
X-ARTE-Roles: USER

La réponse du backend contient bien un entête pour faire varier le cache :

Vary: X-ARTE-Roles
30 / 55

(6) Le backend traite la réponse. En renvoyant la réponse, le Reverse Proxy ajoute les entêtes de suivi d'usage :

ngx.header["X-Rate-Limit-Limit"] = userRateLimit
ngx.header["X-Rate-Limit-Remaining"] = remaining
ngx.header["X-Rate-Limit-Reset"] = expiresAt
31 / 55

(7) \o/ L'utilisateur reçoit la réponse

HTTP/1.1 200 OK
Server: openresty
Content-Type: application/vnd.api+json
Cache-Control: max-age=60, public, s-maxage=60
Vary: X-ARTE-Roles
X-Rate-Limit-Limit: 5000
X-Rate-Limit-Remaining: 4997
X-Rate-Limit-Reset: 1413659922
{
"content" : ...
}
32 / 55

Configuration nginx

location /api1 {
# lua_code_cache off; # Dev : disable cache
set $roles ''; # This variable is set by lua script
access_by_lua_file /dir/oauth-throttle.lua;
header_filter_by_lua_file /dir/header-filter.lua;
proxy_pass http://api1.local;
proxy_set_header X-Roles $roles;
}
# api2 n'est pas protégé par oAuth
location /api2 {
proxy_pass http://api2.local;
}

Pour aller plus loin, documentation du module Lua pour nginx

33 / 55

Description des directives

  • init_by_lua : script lua exécuté au démarrage du processus nginx principal (inclusion de librairie)
  • init_worker_by_lua : script lua exécuté au démarrage d'un worker nginx
  • content_by_lua : script de génération de contenu (~= php)
  • log_by_lua : est appelé à chaque écriture de log
  • rewrite_by_lua : script exécuté après un traitement de réécriture d'URL
  • access_by_lua : script intervenant lors de l'accès à une ressource (permet de protéger une URL, par exemple)
  • header_filter_by_lua : permet d'ajouter des headers dans la réponse

It works !

  • 1500 requêtes/minute
  • temps de réponse moyen : <50ms
  • 2 VM load balancés

Prévision :

  • au moins 10x cette charge
34 / 55

Limites de la solution

  • la première requête est plus lente (sous-requête vers le serveur oAuth)
    • on pourrait modifier le script lua pour lire la BDD du serveur oAuth
  • tests de la solution
    • pas de framework de tests unitaires
    • mise en place de tests fonctionnels (casper-js, frisby.js)
  • peu de documentation (bientôt un article sur le blog d'Arte et/ou sur le blog de Jolicode - ping )
35 / 55

Solutions alternatives (throttling)

  • Implémentation au niveau du backend : il y a un bundle Symfony2 pour ça (est-ce que vous voulez vraiment recevoir une requête sur votre applicatif pour gérer le throttling ?) :
  • Implémentation Varnish
  • Autres solutions ? (Vos retours m'intéressent !)
36 / 55

Configuration mutualisée sur toute la plate-forme, difficilement modifiable

Standard {json:api}

37 / 55

S'appuyer sur un standard pour construire toutes nos API. Il n'a rien de très révolutionnaire. Ce standard décrit certains mécanismes qui sont des standards de facto. Il détaille à la fois le format de la réponse JSON mais également la manière de requêter l'API.

{json:api} est un standard pour construire une API

38 / 55

L'objectif de JSON API est conçu pour limiter le nombre de requêtes et la taille des requêtes à réaliser entre le client et le serveur.

A JSON object MUST be at the root of every JSON API document. This object defines a document's "top level".

A document's top level SHOULD contain a representation of the resource or collection of resources primarily targeted by a request (i.e. the "primary resource(s)").

The primary resource(s) SHOULD be keyed either by their resource type or the generic key "data".

A document's top level MAY also have the following members:

  • "meta": meta-information about a resource, such as pagination.
  • "links": URL templates to be used for expanding resources' relationships URLs.
  • "linked": a collection of resource objects, grouped by type, that are linked to the primary resource(s) and/or each other (i.e. "linked resource(s)").

No other members should be present at the top level of a document.

Exemple de réponse

GET /posts?limit=1
{
"links": {
"posts.author": {
"href": "http://example.com/people/{posts.author}",
"type": "people"
},
"posts.comments": {
"href": "http://example.com/comments/{posts.comments}",
"type": "comments"
}
},
"posts": [{
"id": "1",
"href" : "http://example.com/posts/1"
"title": "Rails is Omakase",
"links": {
"author": "9",
"comments": [ "5", "12", "17", "20" ]
}
}]
}
39 / 55

{json:api} décrit la manière d'inclure des sous-documents (réduction du nombre de requêtes)

GET /users?limit=1&include=groups
{
...
"users": [
{
"id": "gaston",
"username": "Gaston",
"href": "https://server/users/gaston",
"links": {
"groups": {"href": "https://server/groups?user=gaston"}
}
}
}
],
"linked" : {
"groups": [
{
"id": "group1",
"name" : "group1",
"href": "https://server/groups/group-1",
},
{
"id": "group-2",
"name" : "group2",
"href": "https://server/groups/group-2",
},
]
}
}
40 / 55

{json:api} décrit comment limiter les attributs retournés :

GET /users?limit=1&fields=id
{
"users": [
{
"id": "gaston",
"href": "https://server/users/gaston",
"links": {
"groups": {"href": "https://server/groups?user=gaston"}
}
}
}
]
}
GET /users?limit=1&fields=id,name
{
"users": [
{
"id": "gaston",
"name": "Gaston",
"href": "https://server/users/gaston",
"links": {
"groups": {"href": "https://server/groups?user=gaston"}
}
}
}
]
}
41 / 55

Création de requête complexe avec {json:api}

GET /users?enable=true&tags=marsupilami,spirou&fields=name
&include=groups&sort=-id

Retourne la liste des utilisateurs actifs triés par id possédant les tags marsupilami et spirou en incluant les groupes associés à ces utilisateurs. On ne retourne que le champ name.

42 / 55

Solutions mises en oeuvre

pour l'implémentation de

{json:api}

dans projet Symfony2

43 / 55

Surcharge de BazingaHateoasBundle :

utilisation d'annotations pour ajouter les links à la volée ('serializer.post_serialize')

use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation("programs",
* href = @Hateoas\Route("arte_api_v2_programs",
* parameters = {
* "programId" = "expr(object.getProgramId())",
* "language" = "expr(object.getLanguage())",
* "kind" = "expr(object.getKind())"
* },
* absolute = true
* )
* )

va générer : https://server/api1/programs?programId=0123456-FZD&language=fr&kind=SHOW

44 / 55

Gestion des inclusions :

utilisation d'un 'serializer.post_serialize'

  • dans le cas de l'inclusion d'une seule ressource, utilisation d'une sous-requête Symfony2 (pull-request sur jms-serializer en attente)
  • dans le cas d'une inclusion multiple, utilisation de multi-curl (cache varnish)
45 / 55

Limitation des attributs retournés :

mise en place d'une classe ExclusionStrategy (ping )

namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
...
/**
* {@inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
46 / 55

Problèmes rencontrés

dans le cadre du développement

des nouvelles API

(Symfony2)

47 / 55

Performance du JMSSerializer :

  • création d'un bundle permettant de mettre en cache les "visitors"
  • le bundle devrait bientôt être libéré
  • ping
48 / 55

Performance de BazingaHateoasBundle :

49 / 55

Next ?

  • monitoring de l'usage : script Lua pour envoyer des métriques à StatsD ?
  • mise à disposition de SDK pour faciliter l'utilisation de l'API par des partenaires externes :
    • Work In Progress : Module Drupal
  • ouvrir le code du serveur de l'API (cf. The Guardian)
  • ouvrir l'API à des développeurs externes (Open Data ?)
  • intégration d'une stratégie d'invalidation du cache varnish (FOSHttpCache)
  • HHVM ?
50 / 55

Annexes

51 / 55

Stack technique

Drupal Ruby Go Symfony2 MongoDB Redis Newrelic JIRA Github docker Vagrant Java

52 / 55

Juste pour information, voici notre stack technique. Historiquement, nous faisions beaucoup de Java. Nous avons de plus en plus de Drupal. On a un peu de Go, de Ruby. On a bien sûr du Symfony2. Et puisque c'est la mode, on fait aussi un peu de docker ;-)

Merci

(ainsi qu'à )

54 / 55

Des questions ?

55 / 55

Ich heiße François Dume

2 / 55

Je m'appelle François Dume.

Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
b Toggle blackout mode
f Toggle fullscreen mode
c Clone slideshow
p Toggle presenter mode
w Pause/Resume the presentation
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow