Tag: CloudFront


Automating the Deployment of Encrypted Web Services with the AWS SDK for PHP (Part 2)

by Joseph Fontes | on | in PHP | | Comments

In the first post of this series, we focused on how to use Amazon Route 53 for domain registration and use Amazon Certificate Manager (ACM) to create SSL certificates. With our newly registered domain available for use, we can proceed to deploy and configure the services we need to host the www.dev-null.link website across an encrypted connection. Once complete, the infrastructure configuration will reflect the diagrams below.

Diagram 1

Diagram 2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The first diagram shows the use of Route 53 to route traffic between AWS Elastic Beanstalk environments across multiple regions. The second example adds Amazon CloudFront support to the design.

AWS Elastic Beanstalk

Our first step is to create the Elastic Beanstalk application, which will provide the necessary infrastructure to host our website. The following is the order of the methods used for the AWS Elastic Beanstalk deployment:

  • createApplication
  • createApplicationVersion
  • createConfigurationTemplate
  • createEnvironment

We start by creating the Elastic Beanstalk application.

$ebCreateApplicationData = [ 'ApplicationName' => "DevNullDemo",
            'Description' => "Demo application for ACM deployment" ];

$ebCreateApplicationResult = $ebClient->createApplication($ebCreateApplicationData);

print_r($ebCreateApplicationResult);

Result

…
[Application] => Array
	(
	[ApplicationName] => DevNullDemo
	[Description] => Demo application for ACM deployment
…

Now we’ll create the initial version of the application and name it DevNullDemo. You can use the application archive of your choice, although a simple PHP demo site is available here.

$ebCreateAppVersionData = [ 'ApplicationName' => "DevNullDemo",
                            'VersionLabel' => 'v1',
                            'Description' => 'Initial Create',
                            'SourceBundle' => [ 'S3Bucket' => 'pub-materials',
                                                'S3Key' => 'Sample-App.zip' ] ];

$ebCreateAppVersionResult = $ebClient->createApplicationVersion($ebCreateAppVersionData);

print_r($ebCreateAppVersionResult);

Result

…
            [ApplicationVersion] => Array
                (
                    [ApplicationName] => DevNullDemo
                    [Description] => Initial Create
                    [VersionLabel] => v1
                    [SourceBundle] => Array
                        (
                            [S3Bucket] => pub-materials
                            [S3Key] => Sample-App.zip
                        )
                    [Status] => UNPROCESSED
                )
…

Next, we need to create an Elastic Beanstalk configuration template. This template requires the selection of an Elastic Beanstalk solution stack prior to calling the method, createConfigurationTemplate. The solution stack is the platform you choose to run your application within Elastic Beanstalk. You can find a list of available solution stack choices by using the listAvailableSolutionStacks method.

$ebSolutionStacks = $ebClient->listAvailableSolutionStacks();
print_r($ebSolutionStacks);

Result

…
            [SolutionStacks] => Array
                (
                    [0] => 64bit Windows Server Core 2012 R2 v1.2.0 running IIS 8.5
…
                    [5] => 64bit Amazon Linux 2016.03 v2.1.6 running Java 7
…
                    [9] => 64bit Amazon Linux 2014.03 v1.1.0 running Node.js
                    [14] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
                    [15] => 64bit Amazon Linux 2015.09 v2.0.6 running PHP 5.6
                    [16] => 64bit Amazon Linux 2015.09 v2.0.4 running PHP 5.6
…
                    [14] => Array
                        (
                            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
                            [PermittedFileTypes] => Array
                                (
                                    [0] => zip
                                )

                        )
…

For our demonstration, we’ll use the, 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0 solution stack.

$ebConfigTemplateData = [ 'ApplicationName' => "DevNullDemo",
                          'TemplateName' => 'DevNullDemoTemplate',
                          'SolutionStackName' => '64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0',
                          'Description' => 'EB Environment template for blog deployment.' ];

$ebConfigTemplateCreateResult = $ebClient->createConfigurationTemplate($ebConfigTemplateData);

print_r($ebConfigTemplateCreateResult);

Result

…
        (
            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
            [ApplicationName] => DevNullDemo
            [TemplateName] => DevNullDemoTemplate
            [Description] => EB Environment template for blog deployment.
…

Now we can create and start the infrastructure by using the createEnvironment method. The following example sets additional options such as instance type and ACM SSL certificate. You need to replace the [CERTIFICATEARN] value with the AWS ACM certificate ARN created in part 1 of this series. You can also find this value by using the AWS ACM listCertificates method. For these examples, we’ve created a certificate across multiple regions for the host name eb.dev-null.link, in addition to the previously created www.dev-null.link certificate.

$ebCreateEnvData = [ 'ApplicationName' => "DevNullDemo",
                        'EnvironmentName' => "DevNullEnv",
                        'Description' => "Demo environment for ACM EB deployment.",
                        'TemplateName' => "DevNullDemoTemplate",
                        'VersionLabel' => 'v1',
                        'OptionSettings' => [

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'ListenerProtocol',
                                  'Value' => 'HTTPS' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'SSLCertificateId',
                                  'Value' => '[CERTIFICATEARN]' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'InstancePort',
                                  'Value' => '80' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'InstanceProtocol',
                                  'Value' => 'HTTP' ],

                                [ 'Namespace' => 'aws:autoscaling:launchconfiguration',
                                  'OptionName' => 'InstanceType',
                                  'Value' => 't2.nano' ],

                                ],
                        'Tier' => [ 'Name' => 'WebServer',
                                    'Type' => 'Standard',
                                    'Version' => ' ' ],
                        ];

$ebCreateEnvData = $ebClient->createEnvironment($ebCreateEnvData);

print_r($ebCreateEnvData);

Result

…
            [EnvironmentName] => DevNullEnv
            [EnvironmentId] => e-fnvhjptdjd
            [ApplicationName] => DevNullDemo
            [VersionLabel] => v1
            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
            [Description] => Demo environment for ACM EB deployment.

            [Status] => Launching
            [Health] => Grey
            [Tier] => Array
                (
                    [Name] => WebServer
                    [Type] => Standard
                    [Version] =>
                )
…

As the results show, the current status of our environment is Launching. We can periodically check the status with the describeEnvironments method.

$ebDescEnvResult = $ebClient->describeEnvironments();

foreach($ebDescEnvResult['Environments'] as $ebEnvList) {
        print "Name:\t".$ebEnvList['EnvironmentName']."\n";
        print "ID:\t".$ebEnvList['EnvironmentId']."\n";
        print "CNAME:\t".$ebEnvList['CNAME']."\n";
        print "Status:\t".$ebEnvList['Status']."\n\n";
}

Result

Name:  	DevNullEnv
ID:    	[ID]
CNAME: 	DevNullEnv.[ID].[Region].elasticbeanstalk.com
Status:	Ready

When the environment has a status of Ready, we can proceed to create the necessary DNS records. You can also check that the site is functional by pasting the CNAME value into a web browser. Be sure to record this CNAME value so you can use it later.

Demo App

You will want to repeat this process across additional AWS Regions to demonstrate latency-based DNS resolution.

Amazon Route 53

Our next step is to create a Route 53 hosted zone. This hosted zone will define a domain name (or subdomain) for which we are authoritative and, thus, allowed to create DNS records. We’ll start with the createHostedZone method.

$route53Client = $sdk->createRoute53();

$route53Data = [ 'Name' => "dev-null.link",
            'CallerReference' => "BLOGPOSTREF001",
            'HostedZoneConfig' => [ 'Comment' => "AWS SDK sample dev-null.link" ] ];

$route53Result = $route53Client->createHostedZone($route53Data);

Result

            [HostedZone] => Array
                (
                    [Id] => /hostedzone/[Amazon Route 53 Zone ID]
                    [Name] => dev-null.link.
                    [CallerReference] => BLOGPOSTREF001
                    [Config] => Array
                        (
                            [Comment] => AWS SDK sample dev-null.link
                            [PrivateZone] =>
                        )

                    [ResourceRecordSetCount] => 2
                )
…
            [DelegationSet] => Array
                (
                    [NameServers] => Array
                        (
                            [0] => ns-999.awsdns-60.net
		…
)
                )

You should copy the ID of the hosted zone from this result. You can also find a list of all hosted zone ID values by using the listHostedZones method.

$route53ListResult = $route53Client->listHostedZones();

foreach($route53ListResult['HostedZones'] as $zoneItem) {
        print "Name:\t".substr($zoneItem['Name'],0,-1)."\n";
        print "ID:\t".$zoneItem['Id']."\n\n";
}

Result

Name:  	dev-null.link
ID:    	/hostedzone/[Amazon Route 53 Zone ID]

We’ll now create a new DNS entry so that our website is visible via web browser with the eb.dev-null.link host name. For this, we need to use the CNAME value from our Elastic Beanstalk application.

$currentDate = date("r");
$hostedZoneId = "/hostedzone/[Amazon Route 53 Zone ID]";
$subDomain = "eb.dev-null.link";
$ebCname = "DevNullEnv.[EB ID].[Region].elasticbeanstalk.com";

$recordComment = "Created $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $ebCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

print_r($route53ChangeResult);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING
…
                    [Comment] => Created test.jf.unicorn.rentals record on Thu, 15 Sep 2016 12:25:07 -0700
                )
…

As we can see from the result, the status of the change is PENDING. We can check the status with the getChange method using the value of the ChangeInfo ID.

$route53ChangeData = [ 'Id' => "/change/[ChangeInfo ID]" ];
$route53ChangeResult = $route53Client->getChange($route53ChangeData);
print_r($route53ChangeResult);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/<ChangeInfo ID>
                    [Status] => INSYNC
…
                    [Comment] => Created test.jf.unicorn.rentals record on Thu, 15 Sep 2016 12:25:07 -0700
                )
…

Now that our change has the status of INSYNC, we can view the secure URL in our browser window with the URL https://eb.dev-null.link/.
Demo App

Deploying Across Multiple Regions

This deployment is now serving our website across an encrypted connection in a single AWS Region. For those who would like to use multiple regions, we can expand our current configuration. An AWS ACM certificate is needed in each AWS Region used for the deployment. Because we’ll be using CloudFront, we have to ensure that a certificate is created in the us-east-1 region because CloudFront will source the available AWS ACM certificates from there. You can reference the previous blog post for instructions on creating an AWS ACM certificate in additional regions. Next, run the Elastic Beanstalk creation methods shown earlier in each additional region where you want to deploy the application. Be sure to record the CNAME value for each environment.

After we have all of the necessary Elastic Beanstalk environments running, we need to delete the Route 53 resource record for eb.dev-null.link so that we can replace it with a latency-based record set.

$recordComment = "Deleted $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'DELETE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $ebCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

You might notice that the method and instructions to delete the record are almost identical to the instructions used to create the record. They even use the same method, changeResourceRecordSets.

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING

                    [Comment] => Deleted www.dev-null.link record on Thu, 15 Sep 2016 14:58:51 -0700
                )
…

Our next step is to add the latency-based routing rules. This example provides the CNAME of the Elastic Beanstalk environment via the describeEnvironments method.

$currentDate = date("r");
$hostedZoneId = "HOSTED ZONE ID";

$ebDescEnvData = [ 'EnvironmentNames' => [ 'DevNullEnvProd' ] ];
$ebDescEnvResult = $ebClient->describeEnvironments($ebDescEnvData);

$ebCname = $ebDescEnvResult['Environments'][0]['CNAME'];

$recordComment = "Created www record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [
                                                'Name' => "eb.dev-null.link",
                                                'Type' => 'CNAME',
                                                'TTL' => 60,
                                                'Region' => $region,
                                                'SetIdentifier' => str_replace("-","",$region),
                                                'ResourceRecords' => [ [ 'Value' => $ebCname ], ],
                                                ],
                                        ],
                                ],
                        ],
                ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING
…

Demo App

In the Route 53 console, we can see there are two CNAMEs that now resolve the host name, eb.dev-null.link. When a user visits the website, the URL returned will correspond to the lower of the record latencies.

Amazon CloudFront

To enhance the user experience, we’ll now configure and deploy a content delivery network solution named Amazon CloudFront. This AWS service provides content delivery acceleration for the media provided with our web application. Each CloudFront deployment is composed of a CloudFront distribution with each distribution having one or more origins. An origin defines the mapping of a URL to a particular destination. The host name, www.dev-null.link, will resolve to our CloudFront distribution, which will then serve pages from the backend eb.dev-null.link load-balanced site. We first create our distribution with the createDistribution method. The value of $cfCreateDistData can be found in this Github Gist.

$cfCreateDistResult = $cfClient->createDistribution($cfCreateDistData);
print_r($cfCreateDistResult);

Result

Aws\Result Object
(
    [data:Aws\Result:private] => Array
        (
            [Distribution] => Array
                (
                    	[Id] => [CF ID Value]
[DomainName] => [CloudFront ID].cloudfront.net
…

Once complete, we need to save the value of DomainName from the result returned. Next, we’ll create a new Route 53 CNAME record that points www.dev-null.link to our CloudFront distribution.

$subDomain = "www.dev-null.link";
$cfCname = "[CloudFront ID].cloudfront.net";

$recordComment = "Created $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $cfCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

Result

Aws\Result Object
(
    [data:Aws\Result:private] => Array
        (
            [ChangeInfo] => Array
                (
[Status] => PENDING
…

Once complete, we can test the new deployment by navigating to the URL (https://www.dev-null.link/) in a web browser.

Conclusion

With our infrastructure configuration completed, we now have a globally load balanced web application that’s accessible via encrypted communications. We’ve shown we can use the AWS SDK for PHP to automate these deployments, which provides the agility to reproduce these environments for customers on demand. Next, we’ll continue this series by reviewing deployments that use Amazon S3 for static content hosting, and the AWS Application Load Balancer and Elastic Load Balancing with Amazon EC2 instance deployments.

Serving Private Content Through Amazon CloudFront Using Signed Cookies

by Milind Gokarn | on | in .NET | | Comments

Private content can be served through Amazon CloudFront in two ways: through signed URLs or signed cookies. For information about which approach to choose, see Choosing Between Signed URLs and Signed Cookies.

The AWS SDK for .NET includes an Amazon.CloudFront.AmazonCloudFrontUrlSigner utility class that can be used to generate signed URLs. Based on a customer request, we recently added the Amazon.CloudFront.AmazonCloudFrontCookieSigner utility class to make it easier to generate the cookies required to access private content through Amazon CloudFront.

To start serving private content through Amazon CloudFront:

  • Creating CloudFront Key Pairs for Your Trusted Signers. You can either create a new key pair using the AWS Management Console or, if you have your own RSA key pair, you can upload the public key to create a key pair. Each key pair has a key pair ID, which will be used to create the signed cookies.
  • The RSA key pair file (.pem file) must be available when creating signed cookies. If you created the key pair using the AWS Management Console, you can download the key pair file and store it locally.
  • Adding Trusted Signers to Your Distribution. You can do this through the AWS Management Console or programmatically, through the Amazon.CloudFront.IAmazonCloudFront.CreateDistribution or Amazon.CloudFront.IAmazonCloudFront.UpdateDistribution APIs.

 

Creating Signed Cookies for Canned Policies

Canned policies allow you to specify an expiration date only. Custom policies allow more complex restrictions. For a comparison between the two types of policies, see Choosing Between Canned and Custom Policies for Signed Cookies.

The following code snippet shows the use of the Amazon.CloudFront.AmazonCloudFrontCookieSigner.GetCookiesForCannedPolicy method to create signed cookies for canned policies.

// The key pair Id for the CloudFront key pair
var cloudFrontKeyPairId = "key_pair_id";

// The RSA key pair file (.pem file) that contains the private key    
var privateKeyFile = new FileInfo(@"rsa_file_path"); 

// Path to resource served through a CloudFront distribution
var resourceUri = "http://xyz.cloudfront.net/image1.jpeg" 
    
var cookies = AmazonCloudFrontCookieSigner.GetCookiesForCannedPolicy(
    resourceUri,
    cloudFrontKeyPairId,
    privateKeyFile,
    DateTime.Today.AddYears(1)); // Date until which the signed cookies are valid

Creating Signed Cookies for Custom Policies

You should use custom policies to apply complex restrictions to the accessing of private content. In addition to an expiration date, custom policies allow you to set resource paths with wildcards, activation time, and IP address/address ranges.

The following code snippet shows how to generate signed cookies for custom policies.

// The key pair Id for the CloudFront key pair
var cloudFrontKeyPairId = "key_pair_id"; 

// The RSA key pair file (.pem file) that contains the private key    
var privateKeyFile = new FileInfo(@"rsa_file_path"); 

// Path to resource served through a CloudFront distribution
var resourceUri = "http://xyz.cloudfront.net/image1.jpeg" 

var cookies = AmazonCloudFrontCookieSigner.GetCookiesForCustomPolicy(
    AmazonCloudFrontCookieSigner.Protocols.Http | 
    AmazonCloudFrontCookieSigner.Protocols.Https, // Allow either http or https

    "xyz.cloudfront.net",      // CloudFront distribution domain
    privateKeyFile,
    "content/*.jpeg",          // Allows use of wildcards
    cloudFrontKeyPairId, 
    DateTime.Today.AddDays(1), // Date till which the signed cookies are valid
    DateTime.MinValue,         // Date from which the signed cookies are valid,
	                       // a value of DateTime.MinValue is ignored			
    "192.0.2.0/24");           // Source IP or range of IP addresses,
                               // a value of string.Empty or null is ignored

Send Cookies to a User’s Browser

Typically, you would create signed cookies when a user visits your website and signs in (or meets some other criteria). At that point, the cookies are generated on the web server and included in response. The user’s browser caches these cookies and includes them in subsequent requests to Amazon CloudFront, when the user accesses the URL for private content hosted on Amazon CloudFront.

The following snippet sends the generated cookies back in the HTTP response to the browser in an ASP.NET web application.

using System.Web;
...
// Set signed cookies for precanned policies
Response.Cookies.Add(new HttpCookie(cookies.Expires.Key, cookies.Expires.Value));
Response.Cookies.Add(new HttpCookie(cookies.Signature.Key, cookies.Signature.Value));
Response.Cookies.Add(new HttpCookie(cookies.KeyPairId.Key, cookies.KeyPairId.Value));

//Or set signed cookies for custom policies 
Response.Cookies.Add(new HttpCookie(cookies.Policy.Key, cookies.Policy.Value));
Response.Cookies.Add(new HttpCookie(cookies.Signature.Key, cookies.Signature.Value));
Response.Cookies.Add(new HttpCookie(cookies.KeyPairId.Key, cookies.KeyPairId.Value));

In this blog post, we showed how to use the customer-suggested Amazon.CloudFront.AmazonCloudFrontCookieSigner utility class to generate signed cookies to access private content from Amazon CloudFront. If you have ideas for new utilities or high-level APIs to add to the SDK, please provide your feedback here.

Caching the Rails Asset Pipeline with Amazon CloudFront

by Alex Wood | on | in Ruby | | Comments

Amazon CloudFront is a content delivery web service. It integrates with other Amazon Web Services to give developers and businesses an easy way to distribute content to end users with low latency, high data transfer speeds, and no minimum usage commitments.

Ruby on Rails introduced the asset pipeline in version 3.1. The Rails asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB.

With CloudFront’s support for custom origin servers, and features of the Rails asset pipeline, building a CDN for your static assets is simple. In this blog post, we will show you how to set this up for your environment.

Do You Have Geographically Diverse Users?

Amazon CloudFront provides 52 (as of when this was written*) edge locations around the world. Your static content can be cached by these edge locations to reduce the latency of your web application. Additionally, this can reduce the load on your app servers, as it limits the number of times your app server needs to serve large static files.

* See the current list of edge locations here.

Prerequisites

You should be able to deploy your Ruby on Rails application to the Internet, and you should know the hostname or IP address for where your application is hosted. If you have followed along with the series and deployed our sample application on AWS OpsWorks, you can complete this tutorial. If not, consider trying out a deployment first.

Creating a CloudFront Distribution

First, we will create a new CloudFront distribution that uses our app as the custom origin. From the Amazon CloudFront console, click Create Distribution. Under "Web", click Get Started.

Within this form, call the Origin ID "Rails App Server", and for the Origin Domain Name, we will point to the URL of our Rails application. Here is how:

  • If you have a domain name (e.g., "www.example.com"), then use that.
  • If not, you should use as stable of a hostname as possible. For example, the hostname of your ELB instance, or at least an Elastic IP. For demonstration purposes, the public host name of your app server instance will also work.

If you’re using something other than a domain name, don’t worry, you can change the origin address later if you need to. All other options can be left at their default values, though you can turn on logging if you want. We aren’t going to talk about using your own domain for the CDN just yet. Once you have your origin options set, click Create Distribution.

Configuring the Ruby on Rails App to Use CloudFront

Using CloudFront as the asset Host for your static assets is truly a one line change.

In config/environments/production.rb:

config.action_controller.asset_host = ENV['CLOUDFRONT_ENDPOINT']

This tells Rails to use your CloudFront endpoint as the hostname for static assets. Your endpoint hostname will be specified in a host environment variable.

To pick up that change if you’re following along at home, go in to the OpsWorks console and edit your app:

  • Under "Application Source", point to the cloudfront branch.
  • Add a new environment variable pair:

    • Key: CLOUDFRONT_ENDPOINT
    • Value: The URL of your CloudFront endpoint, available in the CloudFront console. For e.g., "lettersandnumbers.cloudfront.net"
    • You do not need to "Protect" this value.

Now, deploy your app! You do not need to run a database migration.

How It Works

While we wait for the deployment to complete, how does all of this work?

If you look at the page source of our application before adding the CloudFront CDN, you’ll see lines like this:

<link data-turbolinks-track="true" href="/assets/application-0f3bf7fe135e88baa2cb9deb7a660251.css" media="all" rel="stylesheet" />
<script data-turbolinks-track="true" src="/assets/application-2ab5007aba477451ae5c38028892fd78.js"></script>

Those lines are how the page is including your application.css and application.js files. In app/views/layouts/application.html.erb, they correspond to these lines:

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

In turn, these include statements source from app/assets/stylesheets/application.css.scss and app/assets/javascripts/application.js. If you run the command rake assets:precompile, these files will be compiled and a fingerprint will be added to the filename. For example, I ran rake assets:precompile and the following files were generated:

  • public/assets/application-f3fd37796ac920546df412f68b0d9820.js
  • public/assets/application-68a6279b040bd09341327b6c951d74bc.css

The fingerprinting is a big part of what makes all of this work so smoothly. Let’s take a look at the page source after our latest deployment:

<link data-turbolinks-track="true" href="http://lettersandnumbers.cloudfront.net/assets/application-bfe54945dee8eb9f51b20d52b93aa177.css" media="all" rel="stylesheet" />
<script data-turbolinks-track="true" src="http://lettersandnumbers.cloudfront.net/assets/application-4984ddfbabfbae63ef17d0c8dca28d6c.js"></script>

You can see that we are now sourcing our static assets from CloudFront, and that nothing broke in the process. You can also see the compiled assets with fingerprints added to the filenames. When we loaded the page, the stylesheet_link_tag and javascript_include_tag used our asset host as the host, adding the expected asset filenames to the end of the hostname. When CloudFront received the request, these assets did not exist in the cache, so it forwarded the request to the Rails server, which served the files to CloudFront, which cached the files and sent them to you, the requestor. Future requests would simply hit the CDN, see the file present, and serve it to you from the fastest edge node.

Because fingerprinting is included out of the box, we do not need to deal with cache invalidations. When the assets change, the fingerprint will change. When that happens, CloudFront will not have the new file, and it will make a request to the origin server to get it. Eventually, the old, unused files will expire. It just works.

Wrap-Up

In this post, we took a Ruby on Rails application and cached its static assets using Amazon CloudFront and the Ruby on Rails asset pipeline. We also discussed the broad strokes of how CloudFront and Rails work together to make this simple to do.

Have any questions, comments, or problems getting your application to cache static content with Amazon CloudFront? Suggestions for topics you would like to see next? Please let us know in the comments!