We thought it would be an interesting challenge both from a DevOPS perspective as well as a web development and integration perspective to create our own fully managed content delivery network.
Utilization of the network is geared specifically for WordPress users. The idea being to give users a dead simple way to leverage geographic CDN endpoints across the globe by simply installing a WordPress plugin and activating with the click of a button. You never have to leave your site to register on a 3rd party website, you never have to set up API keys by hand or troubleshoot problems. We wanted it to be simple. And to work!
There are many components that need to be integrated and ultimately tied together in order for a system of this magnitude to work. I’ll try to break it down into high level sections that will touch on systems administration, automation, WordPress plugin development as well as good old fashioned web development.
Why Free?
Why indeed! There aren’t enough solutions that are this easy and simple for the every-day user. Also we designed our network to have a minimal footprint, mitigating our costs and allowing us to ensure it reaches the maximum amount of people for the most minimal cost for us.
This world has enough tiered services, micro payment services all requiring varying degrees of technical experience. Again we just wanted something simple and easy. If people like it, that’s all we want π
What is a CDN?
I thought it would be a good idea to have a brief introduction in case you weren’t familiar with what a content delivery network is. In a nutshell, a CDN is a network of servers that are geographically distributed throughout the world. The idea is that when someone requests a file like www.yoursite.com/file.jpg , the DNS resolution determines which endpoint server is actually closer to you (and thereby having a quicker response time) and forwards the request to that server. Then the server responds and sends www.yoursite.com/file.jpg from that endpoint.
Ultimately if this is done for all static assets on your site (css files, js files, font files, image & media files, etc) , then the general performance of the site improves significantly.
How many endpoints do you have?
Currently there are 14, with plans to add more as usage grows. New endpoints can be quickly added in minutes with the way the deployment system has been automated. Below are the geographic locations of all 14 endpoints to illustrate the global coverage in the screenshot above :
1. USA β Northern California
2. USA β Northern Virginia
3. USA β Dallas, TX
4. USA β Miami, FL
5. Canada β Toronto
6. Europe β London, England
7. Europe β Stockholm, Sweden
8. Europe β Warsaw, Poland
9. Asia Pacific β Hong Kong, China
10. Asia Pacific β Tokyo, Japan
11. Asia Pacific β Sydney, Australia
12. Asia Pacific β Singapore
13. Asia Pacific β Mumbai, India
14. Latin America β Sao Paulo, Brazil
Currently the goal is to have better coverage in North America, South America and Eastern Europe as well as Asia Pacific.
How to create a CDN endpoint server with Nginx
There are a few ways to do this. The two most significant ways would be to create a reverse proxy server using either Nginx or Varnish. Varnish would provide more robust configuration options, cookie and header manipulation and more complicated load balancing and traffic direction. Nginx is robust, simple and stable to set up.
We chose to use Nginx as a reverse proxy to cache, compress and serve content from an endpoint server. We decided to compile nginx from scratch to give us the flexibility to add a few modules and fine-tune the nginx installation the way we wanted it.
We compiled nginx with 3 modules in order to have a robust CDN experience :
Mod Security
The plan is to not necessarily use ModSecurity right away, but build it in in case we wanted to offer a WAF (web application firewall) at a later date as an added service.
ModSecurity would act as a firewall between requests to your wordpress site and the wordpress site itself. If a malicious user attempted to inject code, SQL or something similar, ModSecurity would catch and block the request. This is disabled for us because we are only serving static content with the CDN for now.
Nginx Virtual Host Traffic Status
We wanted to compile nginx with the nginx-module-vts module to monitor and track bandwidth usage per site that uses it. The idea is to collect and aggregate the traffic usage for each site that uses our free service. If someone were to maliciously suck bandwidth from our network, we would be able to tell this way and determine which site it is as well.
Nginx cache purge
This module is important, though at the time of this post is not currently built into the system to be used. Right now the main way to clear a cached item would be to add a query string to indicate a version change (i.e. site.com/style.css?ver=1.1). For an image you would change the image filename. Currently the cache is set to naturally expire after 1 hour with the CDN.
The idea with compiling the nginx_cache_purge module, would be to provide a mechanism for our centralized API to submit URLs to be purged at manual request.
We will ultimately build a section in the WordPress plugin for users to submit specific urls to manually purge. Just as soon as there is time!
Nginx configuration for a CDN
I’ll quickly touch on how nginx is configured to perform as a CDN. If a request comes in for a CDN site assigned to a specific user, we will take the request and serve it from the cache to the user. If the request isnt cached, we’ll proxy a request to the source server (your site), pull the content, store it in cache and serve it to the end user.
We want to compress all requests with the nginx gzip module currently built into most packaged versions of nginx by default :
gzip on; gzip_types *; gzip_min_length 1000; gzip_proxied no-cache no-store private expired auth;
Next we want to catch only any request for a static asset , since this CDN is currently only designed for static assets :
location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff|ttf|eot)$ { proxy_cache CACHE; add_header X-Shift8CDN-Cache $upstream_cache_status; proxy_set_header Host www.shift8web.ca; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass https://shift8web.ca; proxy_read_timeout 120; proxy_redirect off; expires 1d; add_header Pragma public; add_header Cache-Control "public"; }
To understand better about how the nginx reverse proxy works, you should really just read the documentation straight from the nginx website.
In a nutshell, we are setting the origin site in the site header, setting some headers for passing the requesting real ip, scheme as well as the timeout and a cache “hit” header. We are also sending headers for local browser caching of the elements to further speed up requests and offload stress on the network.
How we made a WordPress plugin to use the Shift8 CDN
This is where the magic happens! The strategy for us is to create a dead-simple WordPress plugin to integrate with our CDN, and expand on it as usage potentially grows. New features like purging a URL, setting cache expiration times and even integrating the WAF (mentioned earlier) will be rolled out into the plugin as version updates are pushed out.
There’s two key things that our plugin does : rewrite static URLs to use the CDN’s custom URL and set a DNS prefetch tag for the CDN url
How to rewrite static content to use a CDN in WordPress
I’m going to put together a few functions, from our plugin, to rewrite content for static URLs to use our CDN url. The idea is when the site is loaded in the browser, all the URLs that reflect static content on your site will go through the CDN which will mean the majority of the site will load much quicker.
function shift8_cdn_rewrites( $rewrites ) { $urls = array( home_url( '/wp-content/uploads/' ), home_url( '/wp-content/themes/' ), home_url( '/wp-content/plugins/' ), home_url( '/wp-includes/' ), ); foreach( $urls as $in => $out ) { $rewrites[$out] = str_replace( $shift8_site_url, 'https://' . $shift8_options['cdn_prefix'] . S8CDN_SUFFIX, $urls[$in] ); } return $rewrites; } add_filter( 'shift8_cdn_rewrites', 'shift8_cdn_rewrites' );
In the above declatarion we are building an array with a list of URLs that we want to generate. In this scenario we are generating the site urls for uploads, themes, plugins and the core includes folders. All those folders, when URLs are generated for the browser, are going to include static content.
Next after the array is built, we are replacing the URL from your site to the URL of our CDN. Then we are returning the modified array to use later. Lastly we are adding a filter for this function , to be used later.
add_action( 'template_redirect', template_redirect'); add_filter( 'rewrite_urls', 'filter'); function filter( $content ) { // This is a critical point at which you must add rules for rewriting URL's $rewrites = apply_filters( 'shift8_cdn_rewrites', array() ); // Loop through each rule and process it foreach( $rewrites as $origin => $destination ) { $content = str_replace( $origin, $destination, $content ); } return $content; } function template_redirect() { ob_start( 'ob' ); } function ob( $contents ) { return apply_filters( 'rewrite_urls', $contents, $this ); }
In the above snippet of code , we are basically hooking into WordPress generating the content to be rendered every time a page is loaded. What we are doing once hooked in, we are applying the filtered URLs to search/replace any occurrance of the URLs in the array list from the other snippet above. Once matched, we replace with the URL of our CDN and then return the content back to being rendered and built by WordPress when a page is loaded.
We are using the built-in PHP function ob_start to buffer the output and apply the filters of the modified content.
This is how we rewrite all the necessary URLs to use the CDN! In the future this can be expanded to include other media and URLs in order to grow the plugin to include more services perhaps.
// Add DNS prefetch for faster resolution and loading function shift8_cdn_prefetch() { if (shift8_cdn_check_enabled()) { // Get all options configured as array $shift8_options = shift8_cdn_check_options(); echo ' '; } } add_action('wp_head', 'shift8_cdn_prefetch', 0);
Last thing we want to do is set a dns-prefetch tag in theof your WordPress site. This will ensure that DNS resolution of the CDN url is done once and then cached by your browser to speed up the process even further. Saving having to do DNS queries on the CDN url hundreds of time every time your page loads will definitely add up and contribute to page speed overall.
All we are doing is hooking into the wp_head function and injecting our tags to implement dns prefetching of the CDN url.
Thats it! If your interested in trying out the plugin, head on over to the WordPress plugin directory and install it. Be sure to leave a review!