Gérer son blog en Serverless avec S3, CloudFront, GitHub, CodePipeline et CodeBuild


Problématique

Les CMS sont aujourd'hui le choix principal lorsqu'il s'agit de créer son blog, mais l'apparition de nombreux projets de générateurs de sites/blogs statiques est en train de changer la donne. Ces générateurs de sites permettent de séparer la structure, le design, et l'écriture du contenu en Markdown ou en reStructuredText. Le but de cet article est d'automatiser le déploiement et la gestion du'un blog réalisé à partir d'un générateur de blog statique. Pour cet article on utilisera Hugo qui est un des générateurs de sites les plus rapides du marché. Vous pouvez évidemment utiliser le moteur de rendu HTML qui vous convient, il faudra juste adapter les paramètres et le buildspec.

Nous avions déjà présenté il y a quelques temps le système faisant tourner notre blog en serverless lorsque nous utilisions la CI Travis. Depuis la sortie de AWS CodeBuild, nous avons migré, d'où ce second article.

On ne s'attardera pas sur l'utilisation de Hugo en lui même mais plutôt sur la façon de mettre en ligne le blog et y ajouter du contenu depuis votre dépot Github grâce à Amazon CloudFormation et CodePipeline notamment. Pour en savoir plus sur Hugo vous pouvez visiter le site d'Hugo ou le repo GitHub du projet.


Architecture

Notre architecture a pour objectif de permettre un déploiement continu (à chaque modification du dépôt) des fichiers statiques du blog sur un bucket S3. Le contenu ajouté ou modifié depuis le dépôt GitHub déclenche la mise à jour d'un CodePipeline (1) qui va récupérer les données du dépôt, les transmettre (2) à CodeBuild pour générer les pages statiques qui seront ensuite déposées (3) dans un bucket S3 avec le web hosting activé.

Architecture

Pour servir nos pages web, on crée une distribution CloudFront associée à un certificat TLS qui pointe sur le endpoint du bucket s3. L'accès au site se fait par un enregistrement dns qui est en fait un alias de la distribution CloudFront.

Pour automatiser tout ça, nous avons un template CloudFormation avec les paramètres et les ressources nécessaires sur ce dépôt GitHub.

Quelques prérequis

- Créer votre nom de domaine

Plus précisément, créer votre zone hébergée publique. La documentation AWS explique comment s'y prendre.

- Créer un certificat public

Voir comment effectuer une demande de certificat public. Et surtout penser à récupérer l'ARN du certificat pour les paramètres.

- AWS S3

On déclare notre bucket S3 qui va servir nos pages web et les policies permettant l'accès aux fichiers du bucket depuis Internet.

S3WebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html
    DeletionPolicy: Retain

  S3WebsiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3WebsiteBucket
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                -
                  - "arn:aws:s3:::"
                  -
                    Ref: "S3WebsiteBucket"
                  - "/*"
            Principal: "*"

- AWS CodeBuild:

CodeBuild a besoin d'un Service Role que l'on déclare avant de créer la ressource CodeBuild en elle même. Les propriétés de l'environnement de build sont décrites ensuite. Lors de l'exécution de CodeBuild, les spécifications de génération du code sont déclarées dans un fichier buildspec.yml qui se trouve à la racine de la source (le repo GitHub).

CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: HugoAWSCodeBuildServiceRole
      Path: /
      AssumeRolePolicyDocument: |
        {
            "Statement": [{
                "Effect": "Allow",
                "Principal": { "Service": [ "codebuild.amazonaws.com" ]},
                "Action": [ "sts:AssumeRole" ]
            }]
        }
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource: "*"
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Resource:
                  - !Sub arn:aws:s3:::${BucketName}/*
                  - !Sub arn:aws:s3:::${BucketName}
                Effect: Allow
                Action:
                  - s3:*
              - Resource:
                  - !Sub arn:aws:s3:::${BuildBucket}/*
                  - !Sub arn:aws:s3:::${BuildBucket}
                Effect: Allow
                Action:
                  - s3:Get*

CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: "CODEPIPELINE"
      Source:
        Type: "CODEPIPELINE"
      Environment:
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "node:8-alpine"
        Type: "LINUX_CONTAINER"
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: BUCKET_NAME
            Value: !Ref BucketName
          - Name: CACHE_CONTROL_MAX_AGE
            Value: !Ref CacheControlMaxAge
          - Name: BRANCH
            Value: !Ref GitHubBranch
          - Name: REPO
            Value: !Ref GitHubRepo
          - Name: USER
            Value: !Ref GitHubUser
      Name: !Ref GitHubRepo
      ServiceRole: !GetAtt CodeBuildServiceRole.Arn

Le buildspec

Plusieurs phases sont définies :

  • L'installation de l'environnement
  • La copie de fichiers
  • La génération du site statique
  • La copie des pages générées à la dernière étape sur le bucket S3 dédié
version: 0.2
phases:
  install:
    commands:
     - apk update
     - apk add ca-certificates wget
     - update-ca-certificates
     - apk add --no-cache --update python3
     - pip3 install awscli

  pre_build:
   commands:
    - wget https://github.com/gohugoio/hugo/releases/download/v0.40.3/hugo_0.40.3_Linux-64bit.tar.gz
    - tar -xvzf hugo_0.40.3_Linux-64bit.tar.gz
    - mv hugo /usr/bin/
    - rm -f hugo_0.40.3_Linux-64bit.tar.gz

  build:
    commands:
      - cd .. && mkdir blog_osones
      - hugo new site blog_osones/
      - cp -r src/* blog_osones/
      - cd blog_osones
      - hugo

  post_build:
    commands:
      - aws s3 sync ./public s3://$BUCKET_NAME/ --delete --cache-control max-age=$CACHE_CONTROL_MAX_AGE

- AWS CodePipeline

La déclaration du CodePipeline est aussi précédée d'un Service Role permettant au service d'avoir accès aux ressources nécessaires, notamment le bucket S3 de stockage des artifacts. Les différentes étapes du pipeline sont ensuite définies. La source qui correspond au dépôt GitHub est récupérée et constitue l'artifact de sortie de l'étape. Dans la partie build, cet artifact est transmis comme input au CodeBuild précédemment déclaré.

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: HugoAWSCodePipelineServiceRole
      Path: /
      AssumeRolePolicyDocument: |
        {
            "Statement": [{
                "Effect": "Allow",
                "Principal": { "Service": [ "codepipeline.amazonaws.com" ]},
                "Action": [ "sts:AssumeRole" ]
            }]
        }
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${BuildBucket}/*
                  - !Sub arn:aws:s3:::${BuildBucket}
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: "*"
                Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Ref GitHubRepo
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref BuildBucket
      Stages:
        - Name: "Source"
          Actions:
            - Name: "Download_Source"
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: 1
                Provider: GitHub
              Configuration:
                Owner: !Ref GitHubUser
                Repo: !Ref GitHubRepo
                Branch: !Ref GitHubBranch
                OAuthToken: !Ref GitHubToken
              OutputArtifacts:
                - Name: source
              RunOrder: 1
        - Name: "Build"
          Actions:
            - Name: "Build_and_deploy"
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuild
              InputArtifacts:
                - Name: source
              OutputArtifacts:
                - Name: build
              RunOrder: 1

- AWS CloudFront

On déclare ensuite une distribution Amazon CloudFront qui pointe sur le endpoint du bucket S3. On lui fournit aussi l'ARN du certificat généré dans les prérequis.

  CloudFront:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Origins:
        - DomainName: !Select [2, !Split ["/", !GetAtt S3WebsiteBucket.WebsiteURL]]
          Id: S3Website
          CustomOriginConfig:
            OriginProtocolPolicy: http-only
        Enabled: true
        HttpVersion: 'http2'
        DefaultRootObject: index.html
        Aliases: 
        - !Join ['', ['hugoblog.', !Ref DomainName]]
        DefaultCacheBehavior:
          AllowedMethods: ["HEAD", "GET"]
          Compress: false
          TargetOriginId: S3Website
          ForwardedValues:
            QueryString: 'true'
          ViewerProtocolPolicy: redirect-to-https
        PriceClass: PriceClass_All
        ViewerCertificate:
          AcmCertificateArn: !Ref AcmCertificateArn
          SslSupportMethod: sni-only

- AWS Route 53

Enfin, nous ajoutons des RecordSet IPv4 et IPv6 Amazon Route 53 qui est en fait un enregistrement DNS qui définit un alias vers la distribution Cloudfront.

  DNSRecordSetIPv4
      AliasTarget:
        DNSName: !GetAtt CloudfrontDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      Comment: Recordset to the cloudfront distribution
      HostedZoneName: !Join ['',[!Ref DomainName,'.']]
      Name: !Join ['', ['hugoblog.', !Ref DomainName]]
Type: A

DNSRecordSetIPv6
      AliasTarget:
        DNSName: !GetAtt CloudfrontDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      Comment: Recordset to the cloudfront distribution
      HostedZoneName: !Join ['',[!Ref DomainName,'.']]
      Name: !Join ['', ['hugoblog.', !Ref DomainName]]
Type: AAAA


Marche à suivre

- Créer votre "fork" du repo git

Pour avoir votre propre copie du projet dans votre repo Git. Le repository se trouve ici.

- Adapter le fichier de paramètres

Remplacer les valeurs par celles qui vous correspondent.

- Déployer le template CloudFormation

Et voilà !! Vous avez tout ce qu'il faut pour lancer votre blog en serverless sur AWS avec une mise en ligne automatique des nouveaux articles.


Didier NANDIEGOU

La discussion continue !

Nous attendons vos questions, remarques & mots doux sur notre Twitter :