Django avec CloudFront et S3, PEP 476

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

J'ai décidé d'écrire ce billet suite à la mise en place d'une solution basée sur CloudFront et S3 pour gérer les fichiers statiques et médias d'un projet Django.

Il y a déjà pas mal de littérature sur le sujet mais je me suis retrouvé dans une situation un peu particulière avec des noms de buckets comportant des . (points) et Python 2.7.9, version dans laquelle la PEP 476 a été rétroportée (elle arrivera dans Python 3.4.3).

CDN et domain sharding

La mise en place d'un CDN est une solution rapide et efficace pour gagner en performances.

Il s'agit de soulager le serveur applicatif sans aller jusqu'au domain sharding que je comprends davantage comme une répartition des requêtes HTTP(S) vers les fichiers statiques et/ou médias sur plusieurs sous-domaines.

Je note tout de même qu'une stratégie de domain sharding est tout à fait envisageable avec CloudFront et un moteur de stockage Django personnalisé.

Configurer CloudFront et S3

Pour cela on fait le choix de créer un bucket S3 pour les fichiers statiques et un autre bucket S3 pour les fichiers médias ainsi que leurs distributions CloudFront associées.

Nous allons nous servir des distributions CloudFront en guise de CDN et leur indiquer d'aller chercher le contenu dans les buckets S3 correspondants.

Je ne vais pas détailler tout le processus de configuration car il est relativement long, déjà détaillé ailleurs, et qu'il y a la nébuleuse documentation d'AWS :

Configurer Django

Il va nous falloir configurer les settings DEFAULT_FILE_STORAGE pour les fichiers médias et STATICFILES_STORAGE pour les fichiers statiques.

Ces settings prennent comme valeur des classes de moteurs de stockage que nous alons personnaliser.

Plutôt que de récrire les moteurs de zéro, on peut se baser sur django-storages-redux compatible Python 3 (fork du Django storages de David) qui propose plusieurs moteurs de stockage pour S3 dont un basé sur boto.

Attention aux noms des buckets

J'ouvre une parenthèse qui intéressera ceux qui ont créé des buckets avec des noms comportant des points (par ex. my.bucket.name) et qui constatent des problèmes de connexion avec boto :

CertificateError: hostname 'my.bucket.name.s3.amazonaws.com' doesn't match either of 's3.amazonaws.com', '*.s3.amazonaws.com'

Si vous êtes dans ce cas c'est que vous utilisez une version de Python post PEP 476 (l'on ne s'étendra pas là-dessus), c'est à dire une version supérieure ou égale à la 2.7.9 ou à la 3.4.3rc1 qui active la vérification des certificats.

Le code de Python se base sur la RFC 6125, section 6.4.3 et semble bien faire le job :

If the wildcard character is the only character of the left-most label in the presented identifier, the client SHOULD NOT compare against anything but the left-most label of the reference identifier (e.g., *.example.com would match foo.example.com but not bar.foo.example.com or example.com).

Le traitement du joker DNS (*) est clairement différent entre la RFC 6125 et la documentation AWS :

For example, with an alternate domain name of *.example.com, you can use any domain name that ends with example.com in your object URLs, such as www.example.com, product-name.example.com, and marketing.product-name.example.com.

Bref.

Il faut savoir qu'il y a deux styles de syntaxes pour accéder aux buckets qu'AWS nomme :

  • virtual-hosted-style (http://my.bucket.name.s3.amazonaws.com)
  • path-style (http://s3.amazonaws.com/my.bucket.name)

Or le backend s3boto de django-storages-redux utilise par défaut la connexion SubdomainCallingFormat de boto qui correspond au virtual-hosted-style.

Une solution possible lorsqu'on a des noms de buckets comportant des points est donc d'utiliser la connexion OrdinaryCallingFormat de boto qui correspond au path-style d'AWS et passe la vérification du certificat avec succès.

Pour fermer cette parenthèse : évitez les points dans les noms des buckets.

URL avec path-style et region-specific endpoint

Attention aux URLs, car avec le path-style vous devez préciser le region-specific endpoint :

"Amazon S3 supports virtual-hosted-style and path-style access in all regions. The path-style syntax, however, requires that you use the region-specific endpoint when attempting to access a bucket."

En clair on doit utiliser s3-eu-west-1.amazonaws.com plutôt que s3.amazonaws.com en mode path-style !

Les moteurs de stockage

On hérite au passage de CachedFilesMixin pour le moteur de stockage des fichiers statiques :

from storages.backends.s3boto import S3BotoStorage

from django.conf import settings
from django.contrib.staticfiles.storage import CachedFilesMixin


class CachedStaticS3BotoStorage(CachedFilesMixin, S3BotoStorage):
    """
    S3BotoStorage backend which also saves a hashed copies of the files it saves.
    """
    bucket_name = settings.BUCKET_STATIC_NAME
    custom_domain = settings.CLOUDFRONT_STATIC_DOMAIN


class MediaS3BotoStorage(S3BotoStorage):
    """
    S3BotoStorage backend for media files.
    """
    bucket_name = settings.BUCKET_MEDIA_NAME
    custom_domain = settings.CLOUDFRONT_MEDIA_DOMAIN

Vous pouvez avoir autant de buckets pour les fichiers médias que vous avez de FileField en utilisant l'argument storage et des moteurs de stockage personnalisés.

Les settings

Et pour finir un exemple de settings permettant d'utiliser un bucket pour les Static et un bucket pour les Media :

from boto.s3.connection import OrdinaryCallingFormat

AWS_ACCESS_KEY_ID = 'yourKey'
AWS_SECRET_ACCESS_KEY = 'yourSecret'
AWS_S3_CALLING_FORMAT = OrdinaryCallingFormat()
AWS_S3_HOST = 's3-eu-west-1.amazonaws.com'
AWS_HEADERS = {
    'Cache-Control': 'max-age=1209600, no-transform'
}

BUCKET_STATIC_NAME = 'bucket.static.com'
CLOUDFRONT_STATIC_DOMAIN = 'your.static.dist.cloudfront.net'

BUCKET_MEDIA_NAME = 'bucket.media.com'
CLOUDFRONT_MEDIA_DOMAIN = 'your.media.dist.cloudfront.net'

DEFAULT_FILE_STORAGE = 'myapp.utils.storage.MediaS3BotoStorage'
STATICFILES_STORAGE = 'myapp.utils.storage.CachedStaticS3BotoStorage'

MEDIA_URL = 'https://%s/' % CLOUDFRONT_MEDIA_DOMAIN

Ressources

Avant Peupler un FileField Django par programmation Après Browserify

Tag Kemar Joint