In the part 03 of this series we got the basic infrastructure ready. Now we can continue to build our Ghost host server on that. Before anything, its best to setup AWS-CLI to use Systems Manager (SSM) so that we can connect to the Ghost host without leaving the terminal. Following the AWS documentation gets us setup in few minutes.

Also we need to have a real domain name for test secure site setup etc. I always use jayforweb.com as my test domain and I use google domains as my domain registrar.

Now we have to setup the followings in our Ghost host.

  1. Docker
  2. Docker-compose
  3. Traefik container
  4. Ghost container
  5. Route53 hosted zone
  6. Route53 record set

CloudFormation made this easy by providing us some helper scripts. We use cfn-init:file to create configuration files and copy they to the appropriate places. Also we use cfn-init:commands to install and setup Docker, Docker-compose. Below is the new expanded version of our Ghost host resource.

YAML is great, but there is something else, TOML :-) We use a toml file for traefik to make it an opportunity to learn something different. I really like the idea of not having to worry about indentations.

  MyGhostHostInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install docker, ghost and traefik
      AWS::CloudFormation::Init:
        config:
          files:
            "/data/traefik/docker-compose.yaml":
              content: !Sub |
                version: "3"
                networks:
                  web:
                    external: true

                services:
                  traefik:
                    image: traefik:alpine
                    restart: always
                    ports:
                      - "80:80"
                      - "443:443"
                    volumes:
                      - /var/run/docker.sock:/var/run/docker.sock
                      - /data/traefik/traefik.toml:/traefik.toml
                      - /data/traefik/acme.json:/acme.json
                    labels:
                      - traefik.backend=traefik
                      - traefik.enable=false
                    networks:
                      - web

                  ghost:
                    image: ghost:alpine
                    restart: always
                    environment:
                      - url=https://jayforweb.com
                    volumes:
                      - /data/blogdir:/var/lib/ghost/content
                    labels:
                      - traefik.backend=friendlyghost
                      - traefik.docker.network=web
                      - traefik.frontend.rule=Host:jayforweb.com
                      - traefik.port=2368
                    networks:
                      - web
              mode: '000644'
              owner: root
              group: root

            "/data/traefik/traefik.toml":
              content: !Sub |
                logLevel = "ERROR"

                defaultEntryPoints = ["http", "https"]

                [entryPoints]
                [entryPoints.http]
                address = ":80"

                [entryPoints.http.redirect]
                entryPoint = "https"
                permanent = true

                [entryPoints.https]
                address = ":443"

                [entryPoints.https.redirect]
                regex = "^https://www.(.*)"
                replacement = "https://$1"
                permanent = true

                [entryPoints.https.tls]
                compress = true

                [acme]
                email = "jayanath@gmail.com"
                storage = "acme.json"
                entryPoint = "https"

                [acme.tlsChallenge]
                onHostRule = true

                [acme.httpChallenge]
                entryPoint = "http"

                [[acme.domains]]
                main = "jayforweb.com"
                sans = ["www.jayforweb.com"]

                [docker]
                endpoint = "unix:///var/run/docker.sock"
                domain = "jayforweb.com"
                watch = true
                network = "web"

              mode: '000644'
              owner: root
              group: root

          commands:            
            "a":
              command: "touch /data/traefik/acme.json"
            "b":
              command: "chmod 600 /data/traefik/acme.json"
            "c":
              command: "sudo yum update -y"
            "d":
              command: "yes | sudo amazon-linux-extras install docker"
            "e":
              command: "sudo service docker start"
            "f":
              command: "sudo usermod -a -G docker ec2-user"
            "g":
              command: "sudo yum install -y git"
            "h":
              command: "curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose"
            "i":
              command: "chmod +x /usr/local/bin/docker-compose" 
            "j":
              command: "sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose"
            "k":
              command: "docker network create web"
            "l":
              command: "docker-compose up -d"
              cwd: "/data/traefik/"
Ghost host with cfn-init

[entryPoints.https.redirect] element in the traefik.toml file redirects all the http traffic to https. [command a] acme.json is used by traefik to setup TLS with LetsEncrypt. We have to use a symlink to add the docker-compose to PATH [command j].  

We need to add Route53 hosted zone and the record set with our domain name.

  MyGhostHostedZone: 
    Type: "AWS::Route53::HostedZone"
    Properties: 
      HostedZoneConfig: 
        Comment: 'Hosted zone for jayforweb.com test domain'
      Name: 'jayforweb.com'

  MyGhostMainRecordSet:
    DependsOn: MyGhostHostedZone
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId : !Ref MyGhostHostedZone
      Name: jayforweb.com
      ResourceRecords: 
      - !GetAtt MyGhostHostInstance.PublicIp
      TTL: '300'
      Type: A
Route53 hosted zone and the record set

We need to add an alias record as www.jayforweb.com to point to jayforweb.com. I tried to create a record set for that but didn't work. So we will revisit that later.Now its time run our CloudFormation to see what it creates! I deleted my stack to start fresh. Just imagine if we had to delete the resources manually, one by one :-) This is why I love CloudFormation.

make create-blog-host-stack STACK_NAME=ghost-host-1
Execute the target from make file

Its always handy to know where to look for any errors. On our Ghost host, we can find all the logs under /var/log

Log files under /var/log on Ghost host

cfn-init-cmd.log file is what we should look first. After the deployment our log shows happy faces.

Check all the commands for any errors

Now we can verify whether our configuration files are in place at /data/traefik. Obviously they should be as we had no errors at all.

The site is up, with one small problem. We have to tweak the acme.json file to get the LetsEncryption working properly. I remember got it working after few tries last time with this site, fewmorewords.com but I dont remember how I did it :-). I will update this post when I work it out.  But this is awesome as we got everything as code, no click-ops at all.

YEY!! we got it working!

We can start up, shut down the site very easily with docker-compose via SSM. We have to make sure to use sudo. Also we should run docker-compose in detached mode [-d flag] when we start up the stack. Have a look at the commands in the below screenshot.

Docker-compose at work

WOW! This became a very long post and thanks for staying with me so far. As always the full code is on github if you want to try this out by yourself.

Cheers!!