Overview

At Hakuna Cloud we have some basic rules:

  • test your stuff;
  • configuration is code;
  • test your stuff;
  • automate everything;

A website is like any other software, with the same dignity: it needs a repo to track changes, a full CI/CD pipeline to tests and deploy and a coded infrastructure.

We will use

  • Bitbucket for the private git repo;
  • Bitbucket PIpelines for the “build”, the tests and the deployment;
  • AWS for the hosting (S3 + CloudFront)

The git repo

AKA infrastructure as code + automate all the thingz

The git repo must have the pipeline enabled, some restrictions to allow the merges (eg: only if all test are ok, 1 approver required …) and we must add IAM credentials to the build ENVs to deploy the site in AWs. So…

include all the thingz

As usual, we use bb-create - a tool to create and update bitbucket repos defined in a json file.

First we need to define the configuration of our git repo

{
  "repoName": "my-ideas/blog",
  "repoDefinition": {
    "scm": "git",
    "is_private": true,
    "description": "my-ideas Blog",
    "has_wiki": true,
    "fork_policy": "no_public_forks",
    "mainbranch": "master",
    "project": {
      "key": "WIKI"
    }
  },
  "pipelineEnvs": [
    {
      "key": "AWS_ACCESS_KEY_ID",
      "value": "XXXX",
      "secured": false
    },
    {
      "key": "AWS_SECRET_ACCESS_KEY",
      "value": "YYYY",
      "secured": true
    },
    {
      "key": "AWS_REGION",
      "value": "eu-west-1",
      "secured": false
    },
    {
      "key": "AWS_DEFAULT_REGION",
      "value": "eu-west-1",
      "secured": false
    },
    {
      "key": "NPM_TOKEN",
      "value": "XXXX",
      "secured": true
    }
  ],
  "branchRestrictions": [
    {
      "kind": "require_passing_builds_to_merge",
      "pattern": "master",
      "value": 1,
      "users": [],
      "groups": []
    },
    {
      "kind": "require_approvals_to_merge",
      "pattern": "master",
      "value": 1,
      "users": [],
      "groups": []
    }
  ]
}

then we can create the git repo with bb-create create blog.jsno. Weh we need to rotate the AWS keys, we can simply update the json file and run bb-create update blog.json

AWS Hosting

Hosting a website in AWS means copy the site contents in an S3 bucket, configure S3 as the origin for CloudFront CDN, add a DNS record and configure an SSL certificate. The following AWS Cloudformation template defines al the resources, and wire them together


---
AWSTemplateFormatVersion: '2010-09-09'
Description: MY-IDEAS blog
Metadata:

  # cftpl metadata
  aws:
    region: eu-west-1
    capabilities:
      - CAPABILITY_IAM
      - CAPABILITY_NAMED_IAM
    template:
      name: "my-ideas-{{stages.0.name}}-blog"
      stage: "{{stages.0.name}}"

# Only prod have https, dns and the cdn
# Any other env is accessible at http://${StageEnv}-www.${RootDomainName}.s3-website-eu-west-1.amazonaws.com
Conditions:
  IsLive: !Equals [ "prod", !Ref StageEnv]

Parameters:
  RootDomainName:
    Type: String
    Default: "my-ideas.rocks"
  SiteName:
    Type: String
    Default: "blog"
  AcmCertificateArn:
    Type: String
    Default: "arn:aws:acm:us-east-1:242728094507:certificate/cb081c06-2cc2-49b1-996e-6311cf83c323"
  StageEnv:
    Type: String
    Default: "{{stage}}"

Resources:
  WebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${StageEnv}-${SiteName}.${RootDomainName}"
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: 404.html
    DeletionPolicy: Retain

  WebsiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref 'WebsiteBucket'
      PolicyDocument:
        Statement:
          - Sid: PublicReadForGetBucketObjects
            Effect: Allow
            Principal: '*'
            Action: s3:GetObject
            Resource: !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', /*]]

  WebsiteCloudfront:
    Type: AWS::CloudFront::Distribution
    Condition: IsLive
    DependsOn: [WebsiteBucket]
    Properties:
      DistributionConfig:
        Comment: Cloudfront Distribution pointing to S3 bucket
        Origins:
          - DomainName: !Select [2, !Split ["/", !GetAtt WebsiteBucket.WebsiteURL]]
            Id: S3Origin
            CustomOriginConfig:
              HTTPPort: '80'
              HTTPSPort: '443'
              OriginProtocolPolicy: http-only
        Enabled: true
        HttpVersion: 'http2'
        DefaultRootObject: index.html
        Aliases:
          - !If [IsLive, !Sub "${SiteName}.${RootDomainName}", !Sub "${StageEnv}-${SiteName}.${RootDomainName}"]
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          Compress: true
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: true
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
        PriceClass: PriceClass_All
        ViewerCertificate:
          AcmCertificateArn: !Ref AcmCertificateArn
          SslSupportMethod: sni-only
        CustomErrorResponses:
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /404.html

  WebsiteDNSName:
    Type: AWS::Route53::RecordSetGroup
    Condition: IsLive
    Properties:
      HostedZoneName: !Sub "${RootDomainName}."
      RecordSets:
        - Name: !If [IsLive, !Sub "${SiteName}.${RootDomainName}", !Sub "${StageEnv}-${SiteName}.${RootDomainName}"]
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt [WebsiteCloudfront, DomainName]

Outputs:
  CdnId:
    Description: "My-IDEAS blog web site"
    Value: !If [ IsLive, !Ref "WebsiteCloudfront", "nan" ]
    Export:
      Name: !Sub "my-ideas-{{stage}}-${SiteName}-cdn"


Some points:
* The stack can be created using [cftpl](https://www.npmjs.com/package/@my-ideas/cftpl);
* if the stage is `prod`, the stack will create a CDN and a public record For test stages, you must use the S3 endpoint

# CI/CD Pipeline
Our website are based on [Jekyll](https://jekyllrb.com/), THE best static CMS available - but what we do applies to whatever cms/website (no, wordpress is not a website/cms) 

The pipeline will 
* "compile" the website (as generating CSS from sass files, minify js any task you need to do)
* run some basic tests, like broken links
* deploy the code to the S# bucket and invalidate the CDN cache 

Here we are, our bitbucket-pipeline
```yaml
image: myideas/jekyll-build
options:
  max-time: 10
pipelines:
  default:
    - step:
        name: Test
        script:
          - bundle install
          - bundle exec jekyll build
          - htmlproofer ./_site --allow-hash-href 
  branches:
    master:
      - step:
          name: Test
          script:
            - bundle install
            - bundle exec jekyll build
            - htmlproofer ./_site --allow-hash-href
          artifacts:
            - _site/**
      - step:
          name: Publish
          deployment: production
          script:
            - STAGE=prod
            - cd _site
            - aws s3 sync ./ s3://${STAGE}-blog.my-ideas.rocks/ --delete --acl=public-read
            - cdn=$(aws cloudformation describe-stacks --stack-name my-ideas-${STAGE}-blog | jq -c '.Stacks[0].Outputs | .[] | select(.OutputKey | contains("CdnId") ).OutputValue' -r)
            - aws cloudfront create-invalidation --distribution-id $cdn --paths "/*"

Notes:

  • The docker image myideas/jekyll-build is shipped with ruby, aws cli, htmlproofer and jq;
  • We check if there is a cdn, in which case it is invalidated after the contents are pushed to S3