Atypical WordPress blog contains a mix of static stuff such as images, javascript, style sheets and dynamic content such as posts, pages and comments posted by users. You can speed up your blog by serving static content via content delivery network such as Akamai, Edgecast and so on. The big boys of CDN business also offered the solution to accelerate dynamic content to improve the performance and reliability of the blog. However, solutions offered by big and traditional CDNs are expensive. Amazon cloudfront recently started to serving dynamic content at lowered price. In this blog post, I will explain:
- How to serve your entire blog using cloudfront.
- DNS settings.
- WordPress settings.
- Documenting limitations of cloudfront.
- Documenting performance improvements.
How does dynamic content delivery network works?
A dynamic content delivery network or content distribution network (CDN) is a system of computers containing copies of data, placed at various points in a network so as to maximize bandwidth for access to the data from clients throughout the network. A client accesses a copy of the data near to the client, as opposed to all clients accessing the same central server, so as to avoid bottleneck near that server. A cdn will increase speed and efficiency for your blog. A typical setup looks as follows:
Amazon cloudfront limitations
From the aws cloudfront faq:
What types of HTTP requests are supported by Amazon CloudFront?
HTTP GET and HEAD requests are currently supported by Amazon CloudFront. Over time, we will add support for POST requests.
Translation: CloudFront will not accept any POST requests i.e. you will not able to post comments, post, or upload images using WordPress. Currently, cloudfront will not act as a proxy, passing all post requests via the custom origin. However, you can configure WordPress to accept POST requests on a subdomain. This will allow you to post comments, upload files and much more.
Our sample setup
You can convert your existing blog or setup a new blog from scratch to use cloudfront. For demonstration purpose, I'm going to setup a new blog called www.contentdeliverynetworklog.com and serve the whole blog via cloudfront as follows:
- Blog domain name: www.contentdeliverynetworklog.com (CNAME to cloudfront)
- Blog origin domain name: cp.contentdeliverynetworklog.com. To fetch web content from this origin web server.
- Static asset domain name: s0.contentdeliverynetworklog.net (CNAME to cloudfront).
- Static asset origin domain name: origin.contentdeliverynetworklog.net.
- Custom origin IP address for both static/dynamic assets: 75.126.153.203
- Caching ruleset for your dynamic and static assets: I am going to use a combination of Batcache+Memcached. However, you can use other plugins such as "W3 Total Cache" (not tested).
- DNS server: Route 53 (or use your existing BIND9 based dns servers).
Step #1: DNS setup for cp.contentdeliverynetworklog.com
First, point your custom origin domain cp.contentdeliverynetworklog.com to 75.126.153.203. A typical BIND 9 entry will look as follows in your zone file:
cp 600 IN A 75.126.153.203
Step #2: Create a bucket
You may want CloudFront to log all viewer requests for files in your distribution. This is useful for stats program such as webalizer and awstats. You need to create a bucket by selecting a bucket name and region. This is required to store web server logs. Login to aws console > open the Amazon S3 console at https://console.aws.amazon.com/s3 > Click Create Bucket:
Please note that access logging is an optional feature of CloudFront. There is no extra charge for enabling access logging. However, you accrue the usual Amazon S3 charges for storing and accessing the files on Amazon S3. Please see this guide for more information on log file formats.
Step #3: Configure AWS Cloudfront Dynamic CDN for www.contentdeliverynetworklog.com
Now, let us see how to configure and use CloudFront to distribute dynamic and static content. I am going to create a CloudFront distribution and configure it to fetch web content from my origin web server called cp.contentdeliverynetworklog.com. First, open the Amazon Cloudfron console at https://console.aws.amazon.com/cloudfront > Click Create Distribution > Set a delivery method to Download > Continue.
Origin settings
- Set Origin Domain Name to cp.contentdeliverynetworklog.com
- Set Origin ID to CustomWWW-cp.contentdeliverynetworklog.com
- Set Origin Protocol Policy to HTTP Only (CloudFront will connect to my origin using only HTTP).
Default cache behavior settings
- Set Viewer Protocol Policy to HTTP and HTTPS.
- Set Object Caching to Use Origin Cache Headers. My origin server is adding a Cache-Control header to control how long your objects stay in the CloudFront cache. However, you can specify a minimum time that objects stay in the CloudFront cache regardless of Cache-Control headers by selecting Customize option and setting Minimum TTL in seconds (default is 24 hours).
- Set Forward Query Strings to Yes.
Distribution settings
The distribution settings affect both cdn performance and pricing. You need to select the price class associated with the maximum price that you want to pay for CloudFront service.
- Set Price Class to Use All Edge Locations.
- Set Alternate Domain Names(CNAMEs) to www.contentdeliverynetworklog.com. I want to use my own domain name instead of the CloudFront domain name for the blog URLs. You need need to create a CNAME record with DNS server to route queries for www.contentdeliverynetworklog.com to *.cloudfront.net.
- Set Logging to On.
- Set Bucket for Logs to webserverlog-contentdeliverynetworklog.s3.amazonaws.com (see step #2 create a bucket for more info).
- Set Log Prefix to stats-logs/.
- Finally, set Distribution State to Enabled.
Finally, click on the Create Distribution button. You will see the status as follows:
Please note down the domain name d3qrb8why8gyke.cloudfront.net.
Step #4: DNS CNAME setup for www.contentdeliverynetworklog.com
You need to create a CNAME record with DNS server to route queries for www.contentdeliverynetworklog.com to d3qrb8why8gyke.cloudfront.net. A typical BIND 9 CNAME entry will look as follows in your zone file:
www 600 IN CNAME d3qrb8why8gyke.cloudfront.net.
Verify DNS settings
Use the following host command dns lookup utility to verify dns settings:
Sample outputs:
$ host cp.contentdeliverynetworklog.com
Sample outputs:
cp.contentdeliverynetworklog.com has address 75.126.153.203
Make sure your cloudfront CDN domain is resolving:
Sample outputs:
$ host d3qrb8why8gyke.cloudfront.net.
Sample outputs:
d3qrb8why8gyke.cloudfront.net has address 54.240.168.62 d3qrb8why8gyke.cloudfront.net has address 54.240.168.82 d3qrb8why8gyke.cloudfront.net has address 54.240.168.98 d3qrb8why8gyke.cloudfront.net has address 54.240.168.190 d3qrb8why8gyke.cloudfront.net has address 54.240.168.214 d3qrb8why8gyke.cloudfront.net has address 54.240.168.30 d3qrb8why8gyke.cloudfront.net has address 54.240.168.38 d3qrb8why8gyke.cloudfront.net has address 54.240.168.54
Also, make sure d3qrb8why8gyke.cloudfront.net set as CNAME to www.contentdeliverynetworklog.com:
Sample outputs:
$ host www.contentdeliverynetworklog.com
Sample outputs:
www.contentdeliverynetworklog.com is an alias for d3qrb8why8gyke.cloudfront.net. d3qrb8why8gyke.cloudfront.net has address 54.240.168.146 d3qrb8why8gyke.cloudfront.net has address 54.240.168.166 d3qrb8why8gyke.cloudfront.net has address 54.240.168.170 d3qrb8why8gyke.cloudfront.net has address 54.240.168.198 d3qrb8why8gyke.cloudfront.net has address 54.240.168.206 d3qrb8why8gyke.cloudfront.net has address 54.240.168.26 d3qrb8why8gyke.cloudfront.net has address 54.240.168.126 d3qrb8why8gyke.cloudfront.net has address 54.240.168.134
Step #5: WordPress configuration
You need to install WordPress at cp.contentdeliverynetworklog.com sub-domain. This domain will be used for the following purpose:
- Blog installation and/or upgradation
- Post / edit comments.
- Post and edit pages or blog entries.
- Manage your blog by visiting the url http://cp.contentdeliverynetworklog.com/wp-admin/ or https://cp.contentdeliverynetworklog.com/wp-admin/
Create the Mysql database and a user
Type the following command:
Type the following sql commands to create the database and a user account:
$ mysql -u root -p
Type the following sql commands to create the database and a user account:
mysql> CREATE DATABASE cdnblog;
mysql> GRANT ALL ON cdnblog.* TO cdnblogadm@localhost IDENTIFIED BY 'aZ7pHqG$b3#';
Where,- DB Name: cdnblog
- DB User: cdnblogadm
- DB Password: aZ7pHqG$b3#
WordPress installation
Download WordPress using the wget command, enter:
cd to your Apache/Lighttpd/Nginx DocumentRoot:
To install WordPress visit the following url:
If you placed the WordPress files in the root directory, you should visit the following url:
Just follow on screen instructions or see installing WordPress page for more information. Make sure you set read-only permission on all files:
$ cd /tmp/
$ wget http://wordpress.org/latest.tar.gz
$ tar xvf latest.tar.gz
cd to your Apache/Lighttpd/Nginx DocumentRoot:
$ cd /home/httpdocs/contentdeliverynetworklog.com/home/http
## create a sud-dir to keep all wordpress files ##
$ mkdir dyn
$ cd dyn
$ cp -avr /tmp/wordpress/* .
## temporary set permission ##
$ chmod -R 0777 ../dyn
To install WordPress visit the following url:
http://cp.contentdeliverynetworklog.com/dyn/wp-admin/install.php
If you placed the WordPress files in the root directory, you should visit the following url:
http://cp.contentdeliverynetworklog.com/wp-admin/install.php
Just follow on screen instructions or see installing WordPress page for more information. Make sure you set read-only permission on all files:
# chown -R www-data:www-data /home/httpdocs/contentdeliverynetworklog.com/home/http
# chmod -R 0444 /home/httpdocs/contentdeliverynetworklog.com/home/http
## make sure web-server and process can read and list the dirs ##
# find /home/httpdocs/contentdeliverynetworklog.com/home/http -type d -print0 | xargs -0 -I {} chmod 0445 {}
index.php
Next, create a file called index.php
Sample outputs:
$ cat /home/httpdocs/contentdeliverynetworklog.com/home/http/index.php
Sample outputs:
<?php /* Short and sweet file for www.contentdeliverynetworklog.com */ define('WP_USE_THEMES', true); define('WP_IN_ROOTDIR', true); require('./dyn/wp-blog-header.php'); ?>
wp-config.php configuration
Edit the file /home/httpdocs/contentdeliverynetworklog.com/home/http/dyn/wp-config.php:
Add the following at the top of the file
Add the following at the top of the file
/** /* WP_SITEURL: the WordPress address (URL) where your WordPress core files reside. */ define('WP_SITEURL', 'http://cp.contentdeliverynetworklog.com/dyn'); /** /* WP_HOME: the WordPress Cloudfront CDN (URL). This is the address you want people * to type in their browser to reach your WordPress blog. */ define('WP_HOME', 'http://www.contentdeliverynetworklog.com/');
Save and close the file.
Install and configure WordPress memcached object caching engine
You need install memcached object cache plugin to speed up dynamic database-driven wordpress blog by caching data and objects in RAM to reduce the number of times an external data source must be read. The WordPress memcached object cache plugin provides a persistent backend for the WordPress object cache. See how to install memcached server andWordPress memcached object cache plugin for more information.
Install and configure Batcache
The Batcache plugin uses Memcached to store and serve rendered pages. Grab Batcache using wget command, enter:
cd into plugins directory:
Copy batcache.php using cp command, run:
Copy advanced-cache.php using cp command, run:
Set values as per your requirements:
$ cd /tmp
$ wget http://downloads.wordpress.org/plugin/batcache.1.2.zip
$ unzip batcache.1.2.zip
cd into plugins directory:
$ cd /home/httpdocs/contentdeliverynetworklog.com/home/http/dyn/wp-content/plugins/
Copy batcache.php using cp command, run:
$ cp /tmp/batcache/batcache.php .
Copy advanced-cache.php using cp command, run:
$ cp /tmp/batcache/advanced-cache.php ..
$ cd ..
## configure advanced cache max-header ##
$ vi advanced-cache.php
Set values as per your requirements:
var $max_age = 300; // Expire batcache items aged this many seconds (zero to disable batcache) var $group = 'cp_wp_cdn_log_r1'; // Name of memcached group. You can simulate a cache flush by changing this.
Save and close the file. Edit the file/home/httpdocs/contentdeliverynetworklog.com/home/http/dyn/wp-config.php. Add the following line the top of wp-config.php to activate Batcache:
define('WP_CACHE', true);
Save and close the file.
Testing headers
Type the following curl command to test headers
Sample outputs:
$ curl -I -H 'Accept-Encoding: gzip,deflate' http://cp.contentdeliverynetworklog.com
Sample outputs:
HTTP/1.1 200 OK Server: nginx Date: Wed, 27 Feb 2013 10:40:29 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 3488 Connection: keep-alive Keep-Alive: timeout=60 X-Whom: l1-com-conte Last-Modified: Wed, 27 Feb 2013 10:39:41 GMT Cache-Control: max-age=252, must-revalidate Vary: Cookie Vary: Accept-Encoding X-Pingback: http://cp.contentdeliverynetworklog.com/dyn/xmlrpc.php Content-Encoding: gzip X-Origin-Type: Dynamic
Step #6: Configure WordPress to use a CDN for static assets
You need to configure s0.contentdeliverynetworklog.net to serve static assets such as css, js, and images using cloudfront. First, open the Amazon Cloudfron console at https://console.aws.amazon.com/cloudfront > Click Create Distribution > Set a delivery method to Download > Continue.
- Set Origin Domain Name to origin.contentdeliverynetworklog.net
- Set Origin ID to CustomWWW-origin.contentdeliverynetworklog.net
- Set Origin Protocol Policy to HTTP Only (CloudFront will connect to my origin using only HTTP).
You need to set default cache behavior settings as follows:
- Set Viewer Protocol Policy to HTTP and HTTPS.
- Set Object Caching to Use Origin Cache Headers.
- Set Forward Query Strings to No.
Next, set the distribution settings:
- Set Price Class to Use All Edge Locations.
- Set Alternate Domain Names(CNAMEs) to s0.contentdeliverynetworklog.net.
- Set Logging to Off.
- Finally, set Distribution State to Enabled.
You need to create a CNAME record with DNS server to route queries for s0.contentdeliverynetworklog.net to de10i7qbzuz6c.cloudfront.net. A typical BIND 9 CNAME entry will look as follows in
your zone file:
your zone file:
; zone: c/contentdeliverynetworklog.net s0 600 IN CNAME de10i7qbzuz6c.cloudfront.net. origin 600 IN A 75.126.153.203
Verify dns settings with the dig or host command:
$ host s0.contentdeliverynetworklog.net
s0.contentdeliverynetworklog.net is an alias for de10i7qbzuz6c.cloudfront.net.
de10i7qbzuz6c.cloudfront.net has address 54.240.168.210
de10i7qbzuz6c.cloudfront.net has address 54.240.168.234
de10i7qbzuz6c.cloudfront.net has address 54.240.168.250
de10i7qbzuz6c.cloudfront.net has address 54.240.168.10
de10i7qbzuz6c.cloudfront.net has address 54.240.168.58
de10i7qbzuz6c.cloudfront.net has address 54.240.168.70
de10i7qbzuz6c.cloudfront.net has address 54.240.168.150
$ host origin.contentdeliverynetworklog.net
origin.contentdeliverynetworklog.net has address 75.126.153.203
Configure WordPress to upload files to CDN server
Edit the file /home/httpdocs/contentdeliverynetworklog.com/home/http/dyn/wp-config.php and add the following line:
define( 'UPLOADS', '/home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http/uploads' );
Save and close the file. Create a directory:
Finally, creeate a symbolic link using the ln command:
Copy required images and css files in assets directory:
Finally, set read only permissions:
Finally, serve all WordPress media files via CDN url http://s0.contentdeliverynetworklog.net/uploads. Edit your themes functions.php and append the following code:
# mkdir -p /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http
Finally, creeate a symbolic link using the ln command:
# cd /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http
# ln -s ../http/wp-content/uploads .
Copy required images and css files in assets directory:
# cd /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http
# mkdir assets
# mkdir assets/lib/
# cp /path/to/your/ie.css assets/
# cp /path/to/your/style.css assets/
# cp /path/to/your/layout.css assets/
### copy all css images ###
# cp -avr /path/to/your/lib/images assets/lib/
Finally, set read only permissions:
# chown -R www-data:www-data /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http
# chmod -R 0444 /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http
## make sure web-server and process can read and list the dirs ##
# find /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http -type d -print0 | xargs -0 -I {} chmod 0445 {}
Finally, serve all WordPress media files via CDN url http://s0.contentdeliverynetworklog.net/uploads. Edit your themes functions.php and append the following code:
/** * Origin server: origin.contentdeliverynetworklog.net * CDN url: s0.contentdeliverynetworklog.net/uploads * WordPress Media Upload path: /home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http/uploads */ function wp_cdn_log_upload_url() { return 'http://s0.contentdeliverynetworklog.net/uploads'; } add_filter( 'pre_option_upload_url_path', 'wp_cdn_log_upload_url' );
Lighttpd max-age settings for static assets
You need to use a combination of mod_expire and mod_setenv to setup caching control headers:
### Origin host config for CDN s0.contentdeliverynetworklog.net ### $HTTP["host"] == "origin.contentdeliverynetworklog.net"{ server.document-root = "/home/httpdocs/contentdeliverynetworklog.com/home/origin-cdn-http" accesslog.filename = "/var/log/lighttpd/origin.contentdeliverynetworklog.net/access.log" $HTTP["url"] =~ "^/" { expire.url = ( "" => "access 365 days" ) } setenv.add-response-header += ( "Cache-Control" => "public" ) } #### end CDN ##
Configure preview post button
You need use the hook or filter called preview_post_link under wordpress to change the default "preview" button when posting. Edit your themes functions.php and append the following code:
function wp_cdn_log_preview_link($link) { $link = preg_replace('/www/', 'cp', $link); $link = preg_replace('/\?p=/', 'dyn/\?p=', $link); return $link; } add_filter( 'preview_post_link', 'wp_cdn_log_preview_link' );
Save and close the file. Finally, edit your themes file (header.php) to use cdn stylesheet urls. For example:
<link rel="stylesheet" href="http://s0.contentdeliverynetworklog.net/assets/layout.css" type="text/css" media="screen, projection" />
Putting it all together
You can test working setup at the following urls:
- CDN URL: http://www.contentdeliverynetworklog.com/
- Origin URL: http://cp.contentdeliverynetworklog.com/
I ran the following three basic tests:
wget download test
wgeting CDN url http://www.contentdeliverynetworklog.com/uncategorized/hello-world-13.html vs origin url http://cp.contentdeliverynetworklog.com/uncategorized/hello-world-13.html from my home:
- CDN URL download time: 0.07s
- Origin url download time: 0.3s
ICMP ping test
Ping test for cdn domain www.contentdeliverynetworklog.com vs origin domain cp.contentdeliverynetworklog.com (ave. rrt):
Location | AWS Cloudfront [1] | Origin server at Dallas, TX [2] |
Amsterdam2, Netherlands | 0.8 | 114.5 |
Florida, U.S.A | 16.3 | 32.6 |
Sydney, Australia | 123.0 | 192.3 |
Vancouver, Canada | 4.6 | 45.5 |
London, United Kingdom | 2.1 | 108.3 |
Singapore | 3.0 | 218.3 |
Hong Kong, China | 3.1 | 230.2 |
Mumbai, India | 75.3 | 278.9 |
Dublin, Ireland | 12.5 | 128.6 |
Cape Town, South Africa | 351.7 | 261.8 |
Tampere, Finland | 11.0 | 156.4 |
Web page performance test
I used webpagetest.org from Singapore (IE8/DSL):
First run: CDN [3] vs origin server [4]
The first time content is requested, it's pulled from the origin server to the CDN network and stays there for other users to access it as per TTL (max-age header):
- CDN #1 test
- Origin #1 test
Second run: CDN [5] vs origin server [6]
The second time content is requested, the CDN network will serve the content directly from geographically closer to my end-users. If max-age header is expired the CDN server will pull the fresh copy from the origin server again.
- CDN #2 test
- Origin #2 test
Conclusion
With cloudfront, I can get my blog post and other static content to users faster and more efficiently. I set the max-age ttl to 300 seconds for all blog posts. It should be increased to 1800 seconds or more for better performance. However, higher ttl means comments will not get rendered immediately. A better solution is to use third party commenting systems. This is my first attempt running a WordPress based site using Amazon CloudFront to deliver a dynamic and static content. Amazon CloudFront is really easy to use. The cost of running such a site from AWS cloudfront for 4 weeks:
- United States - $68.91 (343 GB with 36,969,170 HTTP requests)
- Europe - $81.79 (372 GB with 41,252,596 HTTP requests)
- Total - $150.70
REFERENCES:
- Ping CDN test result.
- Ping origin server result.
- CDN test # 1 (without edge cache) cdn server result.
- CDN test # 2 (with edge cache)cdn server result.
- Origin test # 1 origin server result.
- Origin test # 2 origin server result.
- AWS cloudfront guide.
Disqus comments