Precompiling CSS & JavaScript Assets
Some might remember a process where precompiling Rails CSS & JavaScript assets meant having Node and other dependencies installed on our production servers. In contrast, Lambda Containers embraces a modern and secure way by precompiling assets during the SAM build phase. This is why Node.js is installed in our starter's Dockerfile-build
. Afterward the the project, including the public/assets
directory is copied to the final production ECR image. This helps keep the production image small and secure.
CSS & JavaScript Ready!
Our Rails starter is ready to hit the ground running with Webpacker and other Rails defaults like Sass. Here is a quick example using the TailwindCSS Rails gem. First, add tailwindcss-rails
to your Gemfile.
gem 'tailwindcss-rails'
Change the starter project's app/views/application/index.html.erb
file to:
<h1 class="
text-center
text-9xl
text-blue-400
mt-5
">Hello TailwindCSS</h1>
Make sure to setup your Custom Domain Name so the paths to the assets work. Redeploy your application. You should see this "Hello TailwindCSS" page pictured to the right. Not a fan of TailwindCSS? No problem, feel free to use Sass or any other CSS or JavaScript of your choosing. Everything should work as expected.
Serving Static Assets
Our Quick Start cookiecutter project leverages Rails' built in ability to serve static assets. We do this by setting this environment variable in app.rb
.
ENV['RAILS_SERVE_STATIC_FILES'] = '1'
We also add this configuration to your config/environments/production.rb
file. In this case we are setting the cache control to 30 days, which you can change. The X-Lamby-Base64
header signals to the Lamby rack adapter that the content requires base64 binary encoding.
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{30.days.seconds.to_i}",
'X-Lamby-Base64' => '1'
}
Using S3 & CloudFront As An Asset Host
Your application's assets may be impractical to package and serve from Lambda. Or maybe you want a real CDN like CloudFront to globally serve your assets. For whatever reason, using CloundFront with an S3 bucket as the origin is pretty easy to setup.
Configure Your Rails Asset Host
In your production.rb
, or any other environment file that uses an asset host, configure Rails with the domain you intent to use. We also recommend that your asset manifest be removed from the public dir and placed in your application config instead. This way, it can be packed and deployed along side your application. Open up your config/initializers/assets.rb
file and set the manifest.
config.action_controller.asset_host = 'https://assets.example.com'
Rails.application.config.assets.manifest = Rails.root.join('config/manifest.json')
Sync Assets to S3 During Builds
In order to get the compiled assets to our S3 bucket, we need to hook into the bin/_build script. Add this line after the assets:precompile
line. It uses the AWS CLI s3 sync command to copy new assets to your S3 bucket. It also sets a cache control header for a proper origin response.
aws s3 sync ./public/assets "s3://assets.example.com/assets" \
--cache-control "public, max-age=31536000"
Create a CloudFront Distro Backed by S3 with CloudFormation
To create our CloudFront distribution and S3 bucket, we are going to use CloudFormation to express our Infrastructure as Code (IaC). The work here was inspired DJ Walker's Automate Your Static Hosting Environment With AWS CloudFormation tutorial but re-written from the ground up to be more inline with Rails static asset host best practices. Simply log into the AWS Console, and go to CloudFormation -> Create stack -> Template is ready -> Upload a template file.
Prior to executing this template, please use AWS Certificate Manager to create an SSL cert for you domain name and change the AcmCertificateArn
to the ARN of your certificate. Also make sure to change assets.example.com
to your domain name.
AWSTemplateFormatVersion: '2010-09-09'
Description: MyApp Asset Host
Resources:
AssetHostBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: assets.example.com
AssetHostBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref AssetHostBucket
PolicyDocument:
Statement:
- Action: 's3:GetObject'
Effect: Allow
Resource: !Sub 'arn:aws:s3:::${AssetHostBucket}/*'
Principal:
CanonicalUser: !GetAtt AssetHostDistributionIdentity.S3CanonicalUserId
AssetHostDistributionIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Ref AssetHostBucket
AssetHostDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- assets.example.com
Comment: assets.example.com
DefaultCacheBehavior:
AllowedMethods: [ HEAD, GET ]
CachedMethods: [ HEAD, GET ]
Compress: true
DefaultTTL: 31536000
MinTTL: 31536000
ForwardedValues:
Cookies:
Forward: none
QueryString: false
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !GetAtt 'AssetHostBucket.DomainName'
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${AssetHostDistributionIdentity}'
ViewerCertificate:
AcmCertificateArn: YOUR_CERT_ARN_HERE
SslSupportMethod: sni-only