#Retour d'expérience ARTE GEIE :
??? 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.
.twitter[@\_franek\_
]
??? Je m'appelle François Dume.
layout: false class: layout-arte
.center[]
.pull-left[.center[
]]
.pull-right[.center[
]]
??? Je travaille chez ARTE à Strasbourg. ARTE est une chaine franco-allemande, disponible sur le canal 7 de votre téléviseur.
class: layout-arte background-image: url(./images/capture-artetv.png)
??? Au niveau numérique, ARTE édite un certain nombre de sites internet : arte.tv
class: layout-arte background-image: url(./images/capture-future.png)
??? Future, plateforme dédiée à la science
class: layout-arte background-image: url(./images/capture-creative.png)
??? Creative, plateforme dédiée aux arts visuels et numériques
class: layout-arte background-image: url(./images/capture-concert.png)
??? Concert, anciennement Arte Live Web
class: layout-arte background-image: url(./images/capture-cinema.png)
??? Cinéma, qui permet de retrouver les films diffusés à l'antenne
class: layout-arte background-image: url(./images/capture-tracks.png)
??? Tracks, exemple de site d'une émission
.center[]
.center[...]
.center[
]
??? 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).
class: layout-arte, middle
.pull-left[.center[]]
.pull-right[.center[
]]
??? On édite également des applications mobiles, notamment pour android et Apple.
class: center, middle, inverse
- 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
??? 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",
class: center, middle, inverse
??? Nous disposons déjà d'une API qui permet de mettre à disposition le contenu antenne.
- 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)
??? 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.
- 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
class: center, middle, inverse
???
Mis en production pour 24h Jerusalem en avril 2014. Début du développement décembre. Utilisé par Tracks.
class: center, middle, inverse
class: middle
- 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)
???
- 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.
class: center, middle, inverse
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
}
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
...
curl -I https://.../api1/resource?access_token=MDBjY...
HTTP/1.1 429 Too Many Requests
Server: openresty
class: center
??? 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...
??? 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
??? 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"
...
}
(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)
.small[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).]
(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
(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
(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" : ...
}
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
??? 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
- 1500 requêtes/minute
- temps de réponse moyen : <50ms
- 2 VM load balancés
Prévision :
- au moins 10x cette charge
- 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 .twitter[@ThibZ])
- Implémentation au niveau du backend : il y a un bundle Symfony2 pour ça (.small[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 !)
??? Configuration mutualisée sur toute la plate-forme, difficilement modifiable
class: center, middle, inverse
Standard {json:api}
??? 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.
class: center, middle
{json:api} est un standard pour construire une API
??? 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.
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" ]
}
}]
}
{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",
},
]
}
}
{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"}
}
}
}
]
}
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.
class: center, middle, inverse
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
* )
* )
.small[va générer : https://server/api1/programs?programId=0123456-FZD&language=fr&kind=SHOW]
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)
mise en place d'une classe ExclusionStrategy (ping .twitter[@damienalexandre])
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);
}
class: center, middle, inverse
- création d'un bundle permettant de mettre en cache les "visitors"
- le bundle devrait bientôt être libéré
- ping .twitter[@xavierlacot]
- création d'un bundle permettant de mettre en cache le mécanisme de serialization
- presque libre : https://github.com/ArteGEIE/ArteHateoasBundle
- ping .twitter[@xavierlacot]
- 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 ?
name: inverse class: center, middle, inverse
??? 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 ;-)
- DoctrineMongoDBBundle
- FOSRestBundle
- NelmioApiDocBundle
- EkinoNewrelicbundle
- oldsound/rabbitmq-bundle
- FOSOAuthServerBundle
- ...
.twitter[@\_franek\_
]