Placing a Craft CMS application behind the CloudFront.

Why CloudFront + Craft CMS == The solution you should be using

Craft CMS is awesome, I really like the tool. It’s very flexible, I consider building templates for it a sheer pleasure, and it’s pretty easy to customize.

The problem with Craft CMS (and content management tools in general) is that the websites that use them tend to be slow. When a person visits a website generated with Craft CMS, the server needs to communicate with a database in order to put together an HTML document that’s readable for the browser. This is how the Craft CMS template looks like:

<div>
{% block.title %}
</div>

And this is what the browser understands:

<div>
A great title!
</div>

Craft CMS fills the Twig blocks ({% … %}) with the content created via the Craft CMS admin panel; the content is pulled from a database.

Each time you visit a page with the div above, the same operation of putting together the HTML file happens.

visitor <-> Load Balancer <-> Server <-> Database

Not sure about you, but I don’t change titles of pages too often and I don’t really see a point of the whole process is repeated each time.

In order to decrease the load on the server and stop repeating the actions if not needed, I’ve decided to start using a content delivery network (CDN), and more specifically, CloudFront, in order to cache content.

When the content is already in the CloudFront (Hit), there won’t be a need to go to the server:

visitor <-> CloudFront

If it’s not (Miss), we’ll go to the server as if CloudFront didn’t exist.

visitor <-> Cloudfront <-> Load Balancer <-> Server <-> Database

If you don’t like my explanation, check out this AWS diagram:

https://media.amazonwebservices.com/architecturecenter/AWS_ac_ra_web_01.pdf

Implementing CloudFront decreased the loading time of my website by more than half. It also massively improved the website’s resilience in case of high traffic. Check out the results of JMeter load tests that I’ve carried out before and after implementing CloudFront.

Number of threads: 5000; Ramp up period: 30 sec

There are already some useful articles about putting CloudFront in front of WordPress architecture, eg. here and here, but there aren’t any about Craft CMS.

There are also hundreds of articles, tutorials and YT videos regarding setting up a basic CF distribution. The AWS example is quite cool. However, it’s very hard to find resources that go deeper than the very surface.

This is why I’ve decided to focus only on very specific issues and fill some knowledge gaps

1. Connect CF with ELB — fixing the certificate issue

When you’re setting up CloudFront, you need to pick an origin for it. In my case, it was an Elastic Load Balancer (ELB). So I clicked on Create Origin and picked the ELB from the list that is displayed when you click on the Origin Domain Name input field.

Unfortunately, this didn’t work and as a result, when I went to my CloudFront domain, I was given a 502 response.

It was caused by a SSL/TLS negotiationfailure between CF and ELB. It basically meant that ELB was using a different certificate than CF.

In order to give ELB the same certificate, we’ve created a CNAME for the Load Balancer on the same domain for which we were planning to set up the CloudFront.

In order to make use of the new CNAME, you need to write it in the Origin DOmain Name input field. I know, the UX doesn’t suggest that you can write there, but you actually can! Putting ELB on a domain for which I’m setting up CloudFront solved the issue!

You can actually type in the box. Really.

2. Don’t cache POST requests aka make /admin panel and forms work

At this point I had CloudFront talking to Load Balancer, hooray! Progress! The next thing was defining the behaviors. We want to cache a lot, but we don’t want to cache all. For example, we don’t want to cache the Craft CMS admin panel, so that the changes there get updated immediately.

In addition, if you have any redirects on your page, you don’t want CloudFront to cache them, because CloudFront does not follow the redirects. In other words, if you have a redirect, it will get stuck and it happened to me.

Let me tell you how I fixed the issues.

Exclude admin panel from caching

I wanted to exclude the following path from caching: myDomain.com/admin, as this is how I log into the admin panel. When I’m logged in, the path changes to myDomain.com/admin/dashboard, so I thought that I should exclude the /admin/* path from caching by setting the Cache Based on Selected Request Headers to All.

The whole setup was just right, but wasn’t enough.

It turns out, that the /admin/* path works fine for situations when you’re already logged in, but won’t allow you to log in — for this I needed additional behavior.

What I didn’t know is that the Path Pattern won’t be the same path that you see in the browser, but the path to which your request point (check the Network tab in the dev tools!). The path for logging in wasn’t really /admin, but /index.php?p=admin/actions/users/login. Therefore in order to enable logging in, I needed to add one more behavior, this time for /index.php

Don’t get stuck on 3xx in POST requests.

As mentioned above, logging in wasn’t my only issue. The other one was the fact that when I was trying to make a POST request on my form pages (I’m using Contact Form plugin), my application was getting stuck (I’ve described it on AWS forum but didn’t get any help :<)

The solution to this issue was similar to the one above — instead of disabling caching on the path that you see in the browser, disable it on the very URL to which the request points to. In case of my forms, they were all pointing to the root:

if (inputIsValid('#myForm')) {
$.post({
url: / <--- points to root
...
});
}

I couldn’t disable caching for root, as this is the page that gets the biggest traffic. Instead I’ve added a global to my Craft application, which is a path to which I point all my forms (to avoid a magic string in my code). For this very special path I’ve created another behavior in my CF (similar to the admin and index.php ones).

if (inputIsValid('#myForm')) {
$.post({
url: "{{ craft.globals.getSetByHandle('notCached').label })
...
});
}

3. CloudFront changes to ELB URL on POST requests

Another issue I’ve faced was the fact that CF was changing ELB URL on POST requests (the same problem was described here). When I was doing an action that was triggering POST, I was redirected to the ELB domain.

It was caused by the fact that by default, CloudFront sets the value of the Host reader to the domain name of the origin. Basically, it changes the domain. In order to prevent it, I needed to whitelist the Host, Origin and Referer headers for the Defaultbehavior which had enabled caching.

4. Adding CloudFront error page

CloudFront has a very easy way to set up an error page. All you need to do is to add S3 as an additional origin, pick for what errors you want to show the error page and provide a path to an HTML file. Easier said than done lol because of unclear instructions regarding how to write a Path to the error page. So let me show you how I did it! :D

First, add the new S3 origin

Then add a behavior. In the path pattern, add a path to the directory in which the HTML file resides. In my case, on the root of S3, I have a /errors directory, inside of which I have a static directory, inside of which I have a index.html file.

Last but not least, you need to go to Error Pages tab and link the error page.

One could think (I did at least) that if the Path Pattern is /errors/static/*, then the path to the HTML file should be inde.html. Well, that’s not true. The path should be /errors/static/index.html. The path in the Behavior Path Pattern needs to overlap with the path in Error Pages

God that’s stupid.

5. Connect the DNS to the CF — don’t use a root domain as a CNAME

The final act of my CloudFront adventure was actually connecting the DNS to my CF distribution.

I had two CF distributions (setup in essentially the same way, just the ELB origins were different ofc): one for staging (staging.myDomain.com) and one for production (myDomain.com).

I’ve started changing the DNS endpoint for staging. Went to Route53 and made the staging.myDomain.com domain point to the CloudFront domain instead of pointing to Elastic Beanstalk URL as it used to before.

It worked. I’m almost there I thought. Live is beautiful! I went on and made the same change for production aaaaand it didn’t work I got ERR_SSL_VERSION_OR_CIPHER_MISMATCH. I was investigating certificates and protocols for the whole day and couldn’t find a reason why staging, which works in the same way as production, worked, but production didn’t.

I’ve found the reason by an accident, while reading one of the responses to this thread:

you can't put a CNAME at the root of a domain (ie example.com), but you can on a subdomain (ie www.example.com).

Eureka! This is the only difference in my setup! Staging custom origin doesn’t use a bare domain, but the production does!

What I needed to do at this point was to change the CNAME in my CloudFront distribution so that it didn’t use root. Thankfully, our architecture has a proxy which adds www to the domain. I was then able to change the CNAME from myDomain.com to www.myDomain.com and it worked!

6. Personal takeaways

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store