Precompiling CSS & JavaScript Assets

⚠️ Assets require a Custom Domain Name to link properly.

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>

Hello TailwindCSS Demo Page 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