Beanstalk is a great platform, but has gradually become more complex over the years. I remember setting up my first environment about 4 years ago and being quite impressed by how easy it was to have a working environment in a relatively short amount of time.

I’ve been working on a new app lately and had to set up a brand new environment, so I had to start from scratch once again. And boy oh boy, it’s not so much that the fundamentals have changed, but I found myself constantly hitting walls for things that I didn’t anticipate, things that I just assumed would work out of the box or be easy to configure.

What I thought would be a day of work ended up being a week. A week of paper cuts. Again, the initial deployment didn’t take long, but having a functional and usable app is a different story.

Here are some of the papercuts I encountered and how I ended up fixing them. There are plenty of guides on how to get started with Beanstalk, so I’m assuming some basic knowledge, that you’ve deployed a basic version of your app and that you’re starting to hit the same walls as I did.

Remove Puma from your gemfile for the Production environment

Amazon Linux will come bundled with Puma already and one of the most frustrating aspects at first is that if you have Puma in your Gemfile, it conflicts with the bundled one.

You will get many warning: already initialized constant warnings. It is possible to change how Beanstalk will launch Puma and make it use our provided version from the Gem, but an easier solution is to simply tell your Gemfile to bundle Puma in development and test only.

To do so, change the Puma line in your Gemfile to something like this:

# Use Puma as the app server
gem 'puma', '~> 5.2', groups: [:development, :test]

This won’t change anything to your dev environment, but when running in production, the Puma gem won’t be installed.

Provide your own config for puma

Again, more issues with Puma. It’s going to use the default config file provided by the platform, not the one contained in config/puma.rb.

If you want to change the configuration, create a file named Procfile (no extension) in the root of your app’s folder and add the following line:

web: puma -C /var/app/current/config/puma.rb

The Procfile is used to instruct Beanstalk what process to launch, so for the web process, we’re instructing to launch Puma, but using the file provided in the config folder of the app, not the one provided by Beanstalk.

Setup swap space

You’ll likely make your first deployment on a low-cost machine and chances are that you’ll run out of memory while deploying.

Add the following file to your .ebextensions folder.

00_setup_swap.config

commands:
  01setup_swap:
    test: test ! -e /var/swapfile
    command: |
      /bin/dd if=/dev/zero of=/var/swapfile bs=1M count=2048
      /bin/chmod 600 /var/swapfile
      /sbin/mkswap /var/swapfile
      /sbin/swapon /var/swapfile

Expose environment variables

With Amazon Linux 2 (AL2), environment variables aren’t exposed anymore. As a result, it’s impossible to ssh to the machine and run the Rails console (rails c). You’ll get a bunch of errors related to environment variables not being set.

Not being able to run rails c makes debugging the app much harder, so again, add this file to your .ebextensions folder.

01_set_vars.config

commands:
  setvars:
    command: /opt/elasticbeanstalk/bin/get-config environment | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""' > /etc/profile.d/sh.local
packages:
  yum:
    jq: []

This will execute the Beanstalk tool get-config, which returns the environment variables in JSON format. That’s why this command requires jq to read the output and export the variables.

Install Yarn

You’ll most likely need Yarn to install your front-end dependencies. Fortunately, AL2 comes bundled with Node and we can install Yarn through NPM.

Add this file to your .ebextensions folder.

02_install_yarn.config

commands:
  01_install_yarn:
    command: |
      set -e
      npm i -g yarn
      ln -s "$(npm bin --global)"/yarn /usr/bin/yarn
    test: "! yarn -v"

Configure Health Check path

Beanstalk needs an endpoint to check the health of your app. This is done by configuring the Application Load Balancer default process.

Add this file to your .ebextensions folder.

03_alb_default_process.config

option_settings:
  aws:elasticbeanstalk:environment:process:default:
    DeregistrationDelay: '20'
    HealthCheckInterval: '15'
    HealthCheckPath: /okcomputer
    HealthCheckTimeout: '5'
    HealthyThresholdCount: '3'
    UnhealthyThresholdCount: '5'
    Port: '80'
    Protocol: HTTP
    StickinessEnabled: 'true'
    StickinessLBCookieDuration: '43200'

This is an example using the default path for the OK Computer gem, change HealthCheckPath to whatever suits you best.

Configure the load balancer to serve HTTPS requests

The easiest way to serve HTTPS requests is to generate a SSL certificate using AWS Certificate Manager.

Once you’ve generated a certificate, you’ll need the ARN for the certificate which is listed in the certificate details.

When you have the ARN of the certificate you want to use, add the following file to your .ebextensions folder.

04_alb_securelistener.config

option_settings:
  aws:elbv2:listener:443:
    ListenerEnabled: 'true'
    Protocol: HTTPS
    SSLCertificateArns: [YOUR-CERTIFICATE-ARN]

Skip force_ssl for health checks

SSL termination is done at the load balancer level, so if the load balancer tries to reach your health check URL on the EC2 instance using HTTPS, it’s going to fail.

So, if you have “force_ssl” configured in your production.rb environment file, you’ll want to add an exception for your health check path. Here’s an example for the default OK Computer gem path:

# production.rb
config.force_ssl = true
config.ssl_options = { redirect: { exclude: -> request { request.path =~ /okcomputer/ } } }

Serve Javascript assets from the /packs path

Beanstalk if configured by default to serve assets from the /assets path. If you’re using Webpack and serve JS files from the /packs folder, you’ll find yourself hitting 404 errors for these resources.

Under the hood, Beanstalk has a webapp.conf file located under /etc/nginx/conf.d/elasticbeanstalk that contains the following location directive:

location /assets {
    alias /var/app/current/public/assets;
    gzip_static on;
    gzip on;
    expires max;
    add_header Cache-Control public;
}

We can use the same pattern for packs by creating a file in our app under the .platform/nginx/conf.d/elasticbeanstalk folder. Create the full path folder if you haven’t yet.

Then, under that folder, create a packs.conf file with:

packs.conf

location /packs {
    alias /var/app/current/public/packs;
    gzip_static on;
    gzip on;
    expires max;
    add_header Cache-Control public;
}

Add Access-Control-Allow-Origin to font files if using a CDN (e.g. Cloudfront)

That’s a tricky one. If you’re using a CDN, you’ll potentially get CORS errors from certain browsers, basically saying that the app is not allowed to load the font files since they’re blocked by CORS policy.

Your fonts are most likely served from the /assets path, and again, here’s the default location directive provided by Beanstalk:

location /assets {
    alias /var/app/current/public/assets;
    gzip_static on;
    gzip on;
    expires max;
    add_header Cache-Control public;
}

It’s not containing the Access-Control-Allow-Origin header, so that’s why you get the error.

Now it’s a tricky one because this directive is contained on the server under the file /etc/nginx/conf.d/elasticbeanstalk/webapp.conf and also contains other directives.

I’m not sure if it’s possible to overwrite it by providing a file with the same name in our app’s .platform/nginx/conf.d/elasticbeanstalk folder, but even if it was, I generally don’t like changing default files provided by the platform. Should they make changes in the future, you’ll not be getting the changes.

So the solution is to create our own config file with our location directive.

Unfortunately, location directives don’t work like building blocks. An easy solution would be to create a fonts.conf file under the app’s .platform/nginx/conf.d/elasticbeanstalk folder with the following directive:

location ~* \.(eot|ttf|woff|woff2)$ {
  add_header Access-Control-Allow-Origin *;
}

But what will actually happen for fonts is that this location directive will be executed instead of the assets one, resulting in 404 errors. The reason is that the assets directive contains the important alias to /var/app/current/public/assets, which we lose here.

nginx can only execute one location directive per request, so we’ll have to make a completely new directive just for fonts that also contains the alias the assets folder.

So how are we going to solve this and make sure nginx picks our custom location directive for fonts over the default assets one? Long story short, nginx will pick a regex location alias over a static one like the default assets. So if we configure a regex location specifically for fonts, nginx is going to choose that one.

Add the following file to your .platform/nginx/conf.d/elasticbeanstalk folder.

fonts.conf

location ~* /assets/(?<filename>.+\.(eot|otf|ttf|woff|woff2))$ {
    alias /var/app/current/public/assets/$filename;
    gzip_static on;
    gzip on;
    expires max;
    add_header Cache-Control public;
    add_header Access-Control-Allow-Origin *;
}

It is more or less the same location directive as the assets one, but it has a regex matching pattern, a dynamic alias and the important Access-Control-Allow-Origin header.

Feel free to change the CORS header to something more specific or to change the regex.

Accept files more than 1 MB in size

If your app accepts file uploads, you’ll be limited to 1 MB by default, which isn’t much.

Add the following file to your app’s .platform/nginx/conf.d folder.

proxy.conf

client_max_body_size 500M;

Change the value to whatever suits you best.

That’s it! More work went into configuring the environement, but the rest isn’t necessarily specific to Beanstalk or is unique the the deployed app.