Ghost on Elastic Beanstalk

At Fluencia, we wanted to build a new SpanishDict blog that would empower our team to create engaging and exciting content, and to do so quickly and easily. Ghost is an amazing blogging platform, both because of its easy intuitive interface and its built in collaboration tools. It also allows us to take advantage of technologies our engineering team is already skilled with, including Node, Ember.js and Handlebars templates.

When bringing our new blog application to production, we had several engineering priorities:

  • Easy deployment - Our team follows an Agile process with short development sprints and frequent feature deployment. We wanted an infrastructure that would facilitate this approach instead of hindering it.
  • Zero downtime — SpanishDict.com receives several million requests daily. The new blog would be integrated into the main landing page for the site, and visible to all of our traffic, so we needed an approach that would keep our content live even as changes were being rolled out.
  • Simple source control — Ghost is under active development and we wanted to continue to integrate new features as they were released. Likewise, we wanted to minimize the manual work needed for developers to get any of our three environments (development, staging, and production) up and running.

Given our experience with Amazon Web Services, we chose Elastic Beanstalk as the home for our new blog.

Starting point

We followed a very helpful guide to get Ghost deployed on Elastic Beanstalk. That guide is great for getting started, however its final set up didn't leave us with a production-ready environment for the following reasons:

  • Deploying a new application version had to be done manually from the EB web console.
  • Changes to a custom theme were not propagated to the EB application.
  • The app experienced ~15 seconds of downtime during deploys.

The content directory

We focused most of our efforts in Ghost's content/ directory. It contains four directories:

  • data/ — which we could ignore, since all our data was going to be stored on AWS RDS instead of using SQLite;
  • apps/ — which is going to be part of future development plans for Ghost, but we had no need for right now;
  • themes/ — which we wanted to update on each deploy;
  • images/ — which we wanted to persist across deploys and between instances.

Using AWS S3 to host our images was a perfect solution for this, and using S3FS we could connect the images/ in the application to point to our S3 bucket.

S3FS credentials

S3FS requires a file /etc/passwd-s3fs containing your credentials for AWS. You can create files in the Elastic Beanstalk system through creating a *.config file in a .ebextensions/ directory. However, given that we wanted to add those files to our source control, we didn't want to include any credentials in there.

Elastic Beanstalk allows you to configure environment variables through the console, so that was a logical candidate for storying our credentials. However, the password file couldn't resolve environment variables when called, so our solution was instead to configure a container comand to read them and then write to the password file.

Here's what that solution looks like:

// .ebextensions/97_s3fs-password.config
files:
    "/etc/passwd-s3fs":
        mode: "000640"
        owner: root
        group: root
        content: |
            # placeholder text
container_commands:
    01-create_s3fs_file:
        command: "echo ${AWS_CREDS} | tee /etc/passwd-s3fs"
        cwd: /

Mounting S3FS

After installing S3FS, we needed to mount the directory on the file system. Mounting the directory into content/images/ caused us some trouble since Elastic Beanstalk completely wipes your application directory on each deploy, and it throws an error if it tries to delete a folder connected to S3FS.

Our solution was to mount our bucket in a different location completely outside the application directory, then create a symlink to that location from content/images/. Doing so still allowed Ghost to reach all the images it needed, but removing it on deploy caused no problems.

We did that with the following two config files:

// .ebextensions/98_mount-s3fs.config
files:
    "/var/local/mount_s3_bucket.sh":
        mode: "000777"
        owner: root
        group: root
        content: |
            #!/usr/bin/env bash

            ## Mount S3FS, if it isn't already mounted
            if mount | grep s3fs > /dev/null;
            then :;
            else
                mkdir /var/local/images
                /usr/bin/s3fs $S3_BUCKET /var/local/images -o allow_other -o use_cache=/tmp -o nonempty
            fi

container_commands:
    02-mount-s3fs:
        command: sh /var/local/mount_s3_bucket.sh
        cwd: /

Note that when mounting S3FS, we specify which bucket to mount using an environment variable, allowing us to effortlessly use a different S3 bucket in production and staging.

For creating the symlink:

// .ebextensions/99_create-symlink.config
files:
    "/var/local/create_s3_symlink.sh":
        mode: "000755"
        owner: root
        group: root
        content: |
            #!/usr/bin/env bash

            # Delete any images you uploaded
            rm -rf /tmp/deployment/application/content/images
            # Create symlink
            ln -s /var/local/images /tmp/deployment/application/content/images

container_commands:
    03-create_s3_symlink:
        command: sh /var/local/create_s3_symlink.sh
        cwd: /

One thing to note here is that we do not make a symlink in the actual application directory /var/app/current/. At the point at which container commands execute, Elastic Beanstalk is building up your application in the /tmp/deployment/application/ directory. It the moves that whole directory to the final location. Any changes to the application directory would be overriden by what is in the temp directory.

100% uptime during deployment

With these changes in place, all we had to do was configure batched application deployments. We did this by going to Configuration > Updates and Deployments > Application Deployments in the Elastic Beanstalk console for our environment and setting "Batch type" to "Fixed" and "Batch Size" to "1". In the Autoscaling section below, we also needed to set a minimum instance count of at least 2.

Conclusion

Now, using the Elastic Beanstalk CLI, deploying to each of our environments is a single command. $ eb deploy sd-blog-prod. It creates an archive out of the current directory, uploads it to each instance of our production environment and uses it to create our application directory.

Note that if you want to exclude certain files from this process, you can do so with an .ebignore file

With a little bit of configuration, our deploy process is now quick and painless. Hosting a Ghost blog on Elastic Beanstalk is a great option for a large scale production site.