
Ruby on Rails - Discovering Amazon CloudFront
Whenever we come across implementing file uploading functionality in Rails, we either use the CarrierWave or the PaperClip gem. Fog is the choice for Amazon S3. This setup works fairly well, easy to setup and runs successfully on production in a day.
Background
For security reasons, Amazon S3 configuration for CarrierWave has a configuration option ‘fog_authenticated_url_expiration‘ which defines the seconds to live for the URL, when used with ‘fog_public = true‘. This enables CarrierWave to generate a signed URL which can be accessed for the defined seconds only, after which the S3 document URL expires. This is provided for security reasons. When we want to upload private data on the S3 cloud buckets, such an arrangement keeps the data secure from guest users.
What happens when the URL expires?
In real time, when user has opened up a page which has private S3 download URLs and stayed inactive for sometime, the link timeout will expire it. Now, when user clicks on the link, it tries to open the document but S3 would throw ‘AccessDenied‘ error and show an error page with an XML content.
<Error> <Code>AccessDenied</Code> <Message>Request has expired</Message> <X-Amz-Expires>300</X-Amz-Expires> <Expires>2016-08-05T13:05:37Z</Expires> <ServerTime>2016-08-17T14:01:50Z</ServerTime> <RequestId>5e674uth3q4</RequestId> <HostId> XERYHFXDZHBDFHt7e45ysetrzhdfy </HostId> </Error>
This appears annoying and the user experience deteriorates. To handle such a case gracefully, one of the following needs to be done.
- Make the download URLs public – This is not feasible most of the times.
- Increase the time to expire, which just decreases the probability of this error, but does not really fix it, its just a work around.
- Implement a page instead of this XML response. – Bang!
Solution
For point 3 defined above, Amazon has another service called CloudFrontwhich allows us to define custom error page and rules to redirect to it on specific HTTP errors. But as usual, Amazon has “amazing” documentation to for help!
Never-mind, There are some really nice articles which explain the whole process of configuring CloudFront.
But, What next?
Configuring CloudFront just does not plug it with the S3 configuration for CarrierWave. First thing is, CloudFront has created a CDN URL for us, but to use that URL, we need to replace the domain name in S3 URL.
So, “https://s3.amazonaws.com/exampleBucket/26b8dbda-32a8-4e54-99d8.txt?X-Amz-Date=20160805T130037Z&X-Amz-Expires=300…” should be changed as “https://my-new-id.cloudfront.net/exampleBucket/26b8dbda-32a8-4e54-99d8.txt?X-Amz-Date=20160805T130037Z&X-Amz-Expires=300…“.
This URL would be accessible, and to change it at where its generated we have to use the following property in ‘carrier_wave.rb’ initializer.
config.asset_host = "https://my-new-id.cloudfront.net"
But this does not work with ‘config.fog_public = true‘ option, and we don’t want to make it public! Its a bottleneck!
How to generate CloudFront signed URL with CarrierWave?
cloudfront-signer gem to the rescue! Yes, there is another gem which provides the interface to create signed URL (similar to S3 URLs) with CarrierWave. We need to follow the steps below.
- Create CloudFront keys – From amazon console, click on “<your name>” dropdown -> “security credentials” -> “CloudFront key pairs” -> “create new key pairs”
- Download the key pairs and store it at a secure place.
- Install cloudfront-signer gem, and instead of fog configuration add new cofnguration for AWS as following,
CarrierWave.configure do |config| config.storage = :aws config.aws_bucket = Rails.env.development? ? 'dev0bucket' : 'prod-bucket' config.aws_acl = 'public-read' config.aws_attributes = { expires: 10.minutes.from_now.httpdate, cache_control: 'max-age=604800' } config.aws_credentials = { access_key_id: Settings.carrier_wave.amazon_s3.access_key, secret_access_key: Settings.carrier_wave.amazon_s3.secret_key, region: 'us-standard' # Required } config.asset_host = "https://my-new-id.cloudfront.net" config.aws_signer = -> (unsigned_url, options) { Aws::CF::Signer.sign_url(unsigned_url, options) } end
- Reboot the rails application, and it should generate appropriate CloudFront CDN based URLs.
- Open a document in a new tab, change the URL or wait for 10 minutes till it expires. Then try again accessing it. It should redirect and show the custom error page as you have defined.
This way, we can show a custom error or notification pages for diff. HTTP errors while accessing Amazon S3. CloudFront provides a better interface instead of just showing the XML error, this could be helpful to non-techie users.
References
At BoTree Technologies, we build enterprise applications with our RoR team of 25+ engineers.
We also specialize in Python, RPA, AI, Django, JavaScript and ReactJS.
Consulting is free – let us help you grow!
