Déploiement continu d'un site S3 avec Jekyll et TravisCI

Déploiement continu d'un site S3 avec Jekyll et TravisCI

Adeptes du Cloud et convaincus par le principe Eat Your Own Dog Food, nous avons récemment mis en ligne notre nouveau site web. Le site est intégralement hébergé sur AWS S3, sans serveur malgré les quelques formulaires proposés et surtout déployé en continu avec Travis CI.
Je vous propose de découvrir dans cet article quelques clés pour réussir.
Notez tout d’abord que pour un hébergement de site dans S3, nous devons disposer d’un site entièrement statique en HTML et JavaScript, aucune page ne peut-être générée côté serveur.
Dans notre cas, pour simplifier la génération des pages répétant les en-têtes et pieds de page, j’ai choisi d’utiliser Jekyll comme moteur de génération.

Pourquoi Jekyll ?

Pour ce projet, nous voulions éviter d’avoir à gérer un serveur, mais aussi obtenir une plus grande flexibilité sur l’édition du contenu et la charte graphique du site par rapport à un CMS du marché.
Nous avons besoin de modifier et d’éditer facilement le contenu du site. Pour répondre à ce point, Jekyll fonctionne comme un moteur de template orienté web. Nous pouvons très facilement intégrer des pages écrites en Markdown ce qui simplifie grandement l’intégration de texte rédigé dans le HTML du site.
Jekyll permet de générer l’ensemble des fichiers du site. Le résultat de la génération peut alors être uploadé sur un bucket S3 comme une simple copie de fichiers.
Les options de variabilisation offertes ainsi que la possibilité de composer nos pages à partir de différents blocs générés séparément évite de rentrer dans la logique trop répétitive et trop verbeuse d’un site HTML.
Pour finir, GitHub propose via son service GitHub pages d’héberger gratuitement les sites utilisant Jekyll et nous nous servons de cette offre comme plateforme de validation.

Pourquoi S3 ?

Comme je l’ai précisé plus haut, il n’était pas question pour moi d’avoir à gérer un nouveau serveur web pour héberger le site. Le service d’hébergement web des buckets S3 répond parfaitement à notre besoin. Attention toutefois, pour simplifier la tâche, j’ai choisi de ne pas supporter le SSL pour l’hébergement de notre site. Par défaut, vous l’aurez compris, S3 ne supporte pas l’exposition d’un site statique via un protocole sécurisé. Pour adresser ce point, regardez du côté de Cloud Front, qui ne fait pas partie de cet article.
En terme de coût d’hébergement, nous parlons de quelques dollars par mois (0,024 $ pour le stockage, 0,09 $ par Go téléchargé, 0,005 $ pour les uploads des mises à jour).
Les paramètres de configuration sont simples, mais permettent de gérer assez finement la mise en cache navigateur, des redirections ainsi que des pages d’erreurs personnalisées comme les 404 document non trouvé.
Pour finir, AWS S3 est construit pour assurer une disponibilité à 99.99%, ce qui nous permet de ne pas avoir à nous poser la question de la disponibilité du site.

Configuration du bucket S3

Nous ne nous attarderons pas sur cette partie qui est déjà très bien décrite dans la documentation du service, http://docs.aws.amazon.com/fr_fr/AmazonS3/latest/dev/WebsiteHosting.html
Il suffit tout d’abord de créer un bucket S3 portant le même nom que celui qui sera utilisé pour accéder au site. Dans cas, il s’agira de ‘www.wescale.fr’.
Vous pouvez le faire via la console ou en ligne de commande :

aws s3 mb s3://www.wescale.fr --region eu-west-1  

Une fois le bucket créé, vous pouvez activer l’option hébergement de site statique via la console.

aws s3 website s3://www.wescale.fr/ --index-document index.html --error-document error.html  

Ici index.html sera notre page d’accueil par défaut et error.html la page affichée en cas d’erreur.
Nous n’y sommes pas encore, car par défaut les objets stockés dans S3 ne sont visibles que par leur propriétaire. Pour un site public qui doit être visible par tous, nous devons autoriser les utilisateurs non authentifiés à récupérer les objets.
Dans la console il faut éditer la stratégie de comportement du bucket et ajouter cette policy :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::www.wescale.fr/*"
        }
    ]
}

En ligne de commande, il suffit de lancer :

aws s3api put-bucket-policy --bucket www.wescale.fr --policy file://policy.json  

La dernière étape consiste à ajouter une ACL pour nous assurer que tous les objets seront bien visibles par les internautes. Dans la console, il suffit d’ajouter une autorisation pour tout le monde en cochant la case ‘Liste’.

aws s3api put-bucket-acl --bucket www.wescale.fr --acl public-read  

À ce stade, vous pouvez ajouter une page index.html pour vérifier que vous pouvez bien la consulter comme un site web standard. Dans votre navigateur, il vous suffit de saisir l’adresse du point de terminaison web pour votre bucket : http://bucket.name.s3-website-eu-west-1.amazonaws.com Dans notre cas, il s’agit de http://www.wescale.fr.s3-website-eu-west-1.amazonaws.com/ .

Bravo, vous avez mis en place la première version de votre site statique HTML hébergé sur AWS S3.

Développement du site avec Jekyll

Pour le développement d’un site avec Jekyll, j’ai choisi d’utiliser l’image du Docker Hub. La première étape consiste à générer un site d’exemple :

docker run -ti -v$(pwd):/srv/jekyll jekyll/jekyll jekyll new mon-site  

Cette commande va générer un template de site Jekyll dans le répertoire mon-site. Cela fournit une bonne base de démarrage, pour le développement de votre site.
Pour passer à la partie développement, vous pourrez alors lancer :

cd mon-site  
docker run -ti -v$(pwd):/srv/jekyll -p 4000:4000 jekyll/jekyll  

Cette commande lance Jekyll en mode serveur sur le port 4000. Dans ce mode de fonctionnement Jekyll se charge de mettre à jour les pages modifiées directement.
Le site qui en résulte est accessible depuis votre navigateur à l’adresse : http://localhost:4000/.

Une fois que vous êtes satisfait de votre première version, vous pouvez mettre à jour votre bucket avec la version Jekyll de votre site :

aws s3 sync ./_site s3://www.wescale.fr --region eu-west-1  

Cette commande permet de synchroniser les fichiers du site que vous avez en local sous Jekyll vers votre bucket S3.

Plateforme de validation GitHub Pages

Dans notre workflow de développement, nous souhaitons bénéficier d’une plateforme de validation qui nous permette de vérifier en dehors de la production que notre site se génère correctement.
Pour ce faire, j’ai décidé d’utiliser l’hébergement GitHub Pages. Vous pouvez activer cette option en allant dans les settings du dépôt GitHub à la section Pages.
Par défaut, GitHub utilise la branche gh-pages comme source pour le site avec le support natif de Jekyll.

Attention, cette version n’a pas vocation à être référencée dans les moteurs de recherche. Il est donc essentiel d’interdir l’accès aux robots sur cette version :

{% if site.github %}
<meta name="robots" content="noindex">  
{% endif %}

Pensez donc à ajouter ce bloc if dans les en-têtes de vos pages pour que la meta robots soit présente uniquement sur l’hébergement GitHub.

Faites aussi attention à la génération des URLs de votre site car pour pages, le site sera disponible sous la racine https://compteGitHub.github.io/nom-du-depot-git/.
Dans mon cas le site a vocation à être hébergé directement à la racine ancêtre en production.
Je vous recommande donc de rendre les URLs relatives au contexte d’hébergement, en l’occurence Jekyll fournit une macro à cet effet.

<link href="{{ "/css/bootstrap.min.css" | relative_url }}" rel="stylesheet">  

Pensez à appliquer cette règle pour l’ensemble des URLs qui apparaissent dans le site. Avec cette modification le site sera navigable qu’il soit hébergé à la racine ou dans un sous répertoire particulier.

Intégration continue avec Travis CI

Pour automatiser la génération du site depuis github, j’ai choisi d’utiliser le service d’intégration continue bien connu Travis CI.
Dans les configurations du dépôt, il faut tout d’abord activer l’intégration de Travis CI que vous trouverez à la rubrique “integration & services”.

Fichier /.travis.yml

language: ruby  
rvm:  
 - 2.1
install: gem install jekyll -v 3.2.1 && gem install s3_website && gem install minima  
script: jekyll build  

Nous indiquons ici à Travis CI que le build fonctionne en Ruby. Dans la phase install nous installons Jekyll et ses dépendances. Je reviendrai plus tard sur l’utilisation de s3_website.
Le script consiste simplement à lancer un jekyll build qui va se charger de générer sur place le site.

Nous avons maintenant un site statique généré en cas de modification par un service d’intégration continue. La version hébergée sur pages permet de valider nos modifications à partir des sources.

Déploiement sur S3 depuis Travis CI

Dans notre configuration, la branche gh-pages est utilisée pour le développement du site. La branche de production qui doit contenir les sources de la version actuelle du site en production sur le bucket S3 est la branche master.
Lorsque la version de validation est prête pour la production, nous pouvons créer une pull request qui validera la bonne génération du site.
Une fois fusionnée dans la branche master le site devra être publié automatiquement sur S3.
Pour ce faire, il faut ajouter à notre configuration travis CI la publication proprement dite du site vers le bucket. J’ai choisi d’utiliser la gem ruby s3_website que nous avons installée précédemment.

Nous devons tout d’abord nous assurer que s3_website pourra bénéficier des credentials d’accès au bucket s3. Il nous faut donc créer un utilisateur IAM ayant les droits sur le bucket et uniquement sur le bucket.

Dans l’interface de Travis CI, allez dans les settings du job pour ajouter les variables d’environnement S3_ACCESS_KEY_ID et S3_SECRET_KEY correspondant aux accès de l’utilisateur IAM.
Pensez à désactiver l’affichage de la valeur de ces variables dans les logs en positionnant le bouton sur ‘off’.
Profitez aussi de ce passage dans les paramètres pour vérifier que le build est bien activé pour les pull requests et les pushes.

Nous pouvons maintenant configurer s3_website en créant le fichier de configuration s3_website.yml :

s3_id: <%= ENV['S3_ACCESS_KEY_ID'] %>  
s3_secret: <%= ENV['S3_SECRET_KEY'] %>  
s3_bucket: www.wescale.fr  
max-age:  
 "images/*": 864000
 "js/*": 86400
 "css/*": 86400
 "*": 86400

Ce fichier de configuration va chercher les accès à S3 dans les variables d’environnement. Nous y précisons en dur le nom du bucket. J’ai aussi pris soin d’ajouter quelques règles pour définir le temps de mise en cache navigateur souhaité via la directive max-age.

Nous devons maintenant modifier le fichier .travis.yml, pour y ajouter un déploiement conditionnel. Ajouter ce texte à la fin du fichier :

deploy:  
 provider: script
 script: rvm $TRAVIS_RUBY_VERSION do s3_website push --force
 skip_cleanup: true
 on:
   branch: master

Avec cette nouvelle configuration, Travis CI va exécuter la commande s3_website push -- force à chaque modification constatée de la branche master et uniquement pour cette branche.
Avec cette commande, s3_website va synchroniser l’intégralité du site généré vers notre bucket. L’utilitaire va donc s’assurer de supprimer les fichiers qui ont disparu dans la nouvelle version du site et mettre à jour les metadatas des objets S3 (c’est la raison d’être du flag --force).

Configuration du DNS

Pour faire pointer votre nom de domaine vers le nouveau site, vous devez simplement ajouter un alias DNS. Ce CNAME fera pointer le nom www vers le endpoint web complet de votre bucket.

www 10800 IN CNAME www.monsite.fr.s3-website-eu-west-1.amazonaws.com.  

Le temps de publier la modification de zone et de propager sa nouvelle configuration et votre site sera accessible via son nom cible www.monsite.fr par exemple.

Pour aller plus loin

Votre site est désormais disponible directement hébergé sur S3. Mais nous pouvons aller plus loin en ajoutant le CDN CloudFront, pour distribuer plus efficacement le contenu du site et améliorer l’expérience utilisateur. Ce service offre en plus le support du protocole HTTPS pour sécuriser vos visiteurs.
Il suffit dans ce cas de créer une distribution CloudFront via la console AWS.

Pour le support du HTTPS, commencez par créer un certificat pour votre nom de domaine via ACM. L’opération est très simple, pensez juste à valider la demande via le lien que vous recevrez par email.

Lors de la création de votre distribution, pensez à préciser :

  • que vous autorisez la compression
  • que vous autorisez le HTTPS
  • que vous utilisez le certificat généré par AWS
  • que vous utilisez comme origine votre bucket S3
  • que votre objet racine par défaut est ‘index.html’

Lorsque la distribution passera du status InProgress au status Deployed, vous pourrez tester l’accès à votre site via l’url CloudFront que vous trouverez dans la console. Dans mon cas il s’agit de ‘http://d12akylxg884qr.cloudfront.net/’.
Si vous choisissez CloudFront, pensez a désactiver l'accès en lecture publique sur le Bucket S3 via la mise en place d'Origin Access Identification.

Après validation du site dans votre distribution CloudFront, vous pourrez changer le CNAME DNS du site pour le faire pointer vers votre distribution.
Notez que vous pouvez modifier la configuration s3_website pour y inclure la prise en charge de CloudFront :

cloudfront_distribution_id: ID_DISTRIBUTION_CLOUDFRONT  
cloudfront_distribution_config:  
  default_cache_behavior:
    min_TTL: 86400
  aliases:
    quantity: 1
    items:
      - www.monsite.fr
max_age: 120  
gzip: true  

Avec cette configuration, s3_website prendra soin de mettre à jour la distribution et d’invalider les caches lors des déploiements.

Attention, comme pour tout service AWS, CloudFront se paie à l'utilisation en terme de stockage et de bande passante. Si d'aventure une personne malintentionnée se retrouve sur votre site, elle pourrait faire une attaque de type DoS et par la même faire exploser votre consommation CloudFront. La bonne pratique pour se prémunir est de mettre en place un AWS Shield en amont de CloudFront, pensez-y.

Pour finir, vous pouvez ajouter dans S3 l’autorisation des requêtes CORS, pour permettre aux pages du site d’aller rechercher des ressources en dehors du domaine.
Via la console, rendez-vous dans la section Autorisations du bucket et éditer la configuration CORS en y ajoutant le contenu :

<?xml version="1.0" encoding="UTF-8"?>  
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">  
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>  

Avec cette configuration vous autorisez les requêtes cross domain pour toutes les origines.

Pour conclure

Comme vous avez pu le constater, la logique est assez simple. Avec ce système, nous disposons d’un hébergement défiant toute concurrence du côté des CMS et autres Wordpress. À l’usage, cela répond parfaitement à notre besoin de pouvoir éditer facilement le contenu du site.
Notez tout de même une limite, la gestion des dépendances Ruby est instable, il nous arrive régulièrement d’avoir un build cassé. La solution est toujours de mettre à jour le fichier de dépendance pour prendre en compte les nouvelles versions des Gem dans le Gemfile.lock.

Pour s’en affranchir, il faudrait utiliser un conteneur de build portant une version pré-packagée de Jekyll. Le conteneur officiel va télécharger les dépendances au démarrage et ne bénéficie pas d’une version figée comme cela devrait être le cas.