Hébergement d'un site Internet Serverless sur AWS

By Alexis Villebrun | 30 March 2017

Introduction

Afin de minimiser les coûts d’exploitation et d’hébergement du site Internet scalesquad.io, nous avons souhaité l’héberger sur AWS en Serverless.

Pour cela, différentes options s’offrent à nous :

  • Utiliser API Gateway et Lambda pour faire un site dynamique
  • Utiliser S3 pour un site statique.

N’ayant pas besoin de contenu dynamique (sauf exception), nous avons fait le choix de partir sur un hébergement S3.

Cependant, pour des raisons évidentes de maintenance, il n’était pas question de coder directement les pages HTML à la main.

Nous avons fait le choix de partir sur Hugo, un moteur de génération de contenu statique codé en Go.

La chaîne de mise à jour du site est donc la suivante :

  • Le développeur modifie le code en local sur son poste, et le pousse sur AWS CodeCommit.
  • Le push sur la branche master déclenche une fonction Lambda qui appelle CodeBuild.
  • CodeBuild lance un conteneur docker, install Hugo, génère le site et l’upload sur S3.

Chaine de mise à jour

Les étapes et fonctions utilisées

Developpement du site en local

Le développeur récupère via Git le contenu existant du site sur AWS CodeCommit, le modifie, le test en local et le repousse sur AWS CodeCommit.

alex@mypc:~/website $ git pull
alex@mypc:~/website $ vim content/blog/20170330-ServerlessHostingOnAWS.md
alex@mypc:~/website $ git add content/blog/20170330-ServerlessHostingOnAWS.md
alex@mypc:~/website $ git commit -m 'Updated article 20170330-ServerlessHostingOnAWS'
[master yyyyyyy] Updated article 20170330-ServerlessHostingOnAWS
 Committer: Villebrun Alexis <alexisv@scalesquad.io>
 1 file changed, 1 insertion(+), 2 deletions(-)
alex@mypc:~/website $ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 306 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/xxxxxxxxxxxxx
   xxxxxxx..yyyyyyy  master -> master

Lambda appelle CodeBuild

Un trigger a été positionné sur AWS CodeCommit pour déclencher l’exécution d’une fonction Lambda.

Le code de la fonction Lambda est le suivant :

import json
import boto3

codebuild = boto3.client('codebuild')

def lambda_handler(event, context):
    #Log the updated references from the event
    references = { reference['ref'] for reference in event['Records'][0]['codecommit']['references'] }
    print("References: "  + str(references))
	#Log the commit id from the event
    commitId = str(references[0]['commit'])
    print("CommitId: " + commitId)
    
    try:
        response = codebuild.start_build(
            projectName=str(event['Records'][0]['customData']),
            sourceVersion=str(commitId),
            artifactsOverride={
                'type': 'NO_ARTIFACTS'
            }
        )
    except Exception as e:
        print(e)
        print('Error calling CodeBuild')
        raise e

Comme on peut le voir, cette fonction ne fait pas grand chose :

  • Log la référence de l’évenement : References: set([u'refs/heads/master'])
  • Log l’identifiant du commit : CommitId: 2869245c174cab49bd718fc72e387bf783d06c86
  • Appelle l’API CodeBuild pour démarrer le build, en lui passant l’ID du Commit et le nom du projet à construire.

CodeBuild génère le site statique et l’upload dans S3

AWS CodeBuild commence par télécharger les sources sur AWS CodeCommit.

Il construit ensuite un environnement Docker et utilise le fichier buildspec.yml présent sur le repository pour générer le site.

Enfin, il l’uploade sur S3, en précisant la durée de conservation en cache selon les types de fichiers.

version: 0.1

environment_variables:
  plaintext:
    AWS_DEFAULT_REGION: "eu-west-1"
    HUGO_VERSION: "0.18.1"
    HUGO_SHA256: "cb462f41ff9620df89f69b85ccdea48cd789490bbab7a17d9c349dae76490add"

phases:
  install:
    commands:
      - curl -Ls https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz -o /tmp/hugo.tar.gz
      - echo "${HUGO_SHA256}  /tmp/hugo.tar.gz" | sha256sum -c -
      - tar xf /tmp/hugo.tar.gz -C /tmp
      - mv /tmp/hugo_${HUGO_VERSION}_linux_amd64/hugo_${HUGO_VERSION}_linux_amd64 /usr/bin/hugo
      - rm -rf /tmp/hugo*
  build:
    commands:
      - hugo
  post_build:
    commands:
      - aws s3 sync --delete public s3://BUCKETNAME --cache-control "max-age=3600, public" --exclude "*" --include "*.html" --include "*.xml"
      - aws s3 sync --delete public s3://BUCKETNAME --cache-control "max-age=86400, public" --exclude "*.html" --exclude "*.xml"

Une fois l’upload terminé, l’environnement Docker de Build est détruit.

Conclusion et optimisations possibles

Cette méthode permet de n’avoir pratiquement aucun cout d’infrastructure en dehors des périodes de build.

En effet, le stockage S3 est très peu cher, l’environnement de Build est utilisé uniquement pendant les phases de build et donc payé quelques minutes à chaque commit, de même pour la fonction Lambda.

Cependant, il reste des optimisations possibles :

  • Utiliser AWS CloudFront comme CDN pour distribuer le site (amélioration de la latence et baisse du coût) et permettre l’utilisation de HTTPS au lieu de HTTP
  • Utiliser API Gateway et Lambda à la place de services SaaS pour la gestion des commentaires, des formulaires, …
  • Utiliser Gulp ou autre outil pour minimizer le code généré.