Deploying Bullet Train to AWS with AppPack

Bullet Train is an open-source SaaS framework for Ruby on Rails.

It provides a starter repo which we can build on to take it from development-ready to production-ready and scalable on AWS. Since the app is already built to run on Heroku, there are very few changes necessary to get it running with AppPack on AWS.

The code for this post lives in ipmb/bullet_train-demo on GitHub.

note: parts of the screencast are sped up for brevity.

Adding healthchecks

AppPack deploys apps to ECS which requires a healthcheck endpoint to determine if the app is healthy before it starts routing traffic to it. To do this, we:

  1. Add a simple route to that always returns a 200 when the app is running
  2. Disable the SSL redirect on that route

That last step is necessary so the healthcheck returns a 200 instead of a 301. The result looks like this:

diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb
index 9ca0a16..3fc9dbf 100644
--- a/config/initializers/new_framework_defaults.rb
+++ b/config/initializers/new_framework_defaults.rb
@@ -23,2 +23,5 @@ Rails.application.config.active_record.belongs_to_required_by_default = true
# Configure SSL options to enable HSTS with subdomains. Previous versions had false.
-Rails.application.config.ssl_options = {hsts: {subdomains: true}}
+Rails.application.config.ssl_options = {
+ hsts: {subdomains: true},
+ redirect: { exclude: ->(request) { /^\/healthcheck$/.match?(request.path) }}
+}
diff --git a/config/routes.rb b/config/routes.rb
index ab4c7bb..4f3dbd7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,2 +1,3 @@
Rails.application.routes.draw do
+ get "/healthcheck", to: proc { [200, {}, ["ok"]] }
# See `config/routes/*.rb` to customize these configurations.

This change may land upstream in Bullet Train soon. See bullet-train-co/bullet_train#632.

Connecting to Redis

AppPack sets up Redis clusters (via Elasticache) using Redis ACLs so they can be shared across multiple apps with different namespaces. This is great for cost savings in development/review app environments where performance is less of a concern. For production you can still use a dedicated cluster if you prefer. We added redis-namespace[1] to support the ACL key prefixes (passed in as the environment variable REDIS_PREFIX):

  1. Added redis-namespace to the Gemfile
  2. Ran bundle install
  3. Edited config/initializers/sidekiq.rb
  4. Edited config/cable.yml
diff --git a/Gemfile.lock b/Gemfile.lock
index 0711960..7b4cc13 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -457,2 +457,4 @@ GEM
redis (4.8.0)
+ redis-namespace (1.10.0)
+ redis (>= 4)
regexp_parser (2.6.1)
@@ -614,2 +616,3 @@ DEPENDENCIES
redis (~> 4.0)
+ redis-namespace
rqrcode
diff --git a/config/cable.yml b/config/cable.yml
index fc14aab..5ca1c58 100644
--- a/config/cable.yml
+++ b/config/cable.yml
@@ -9,3 +9,3 @@ production:
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
- channel_prefix: bullet_train_apppack_demo_production
+ channel_prefix: <%= ENV['REDIS_PREFIX'] || '' %>bullet_train_apppack_demo_production
driver: :ruby
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index e1129e8..b5f0738 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,3 +1,3 @@
redis_conn = proc {
- Redis.new(
+ redis = Redis.new(
url: ENV["REDIS_URL"] || "redis://localhost:6379",
@@ -6,2 +6,7 @@ redis_conn = proc {
)
+ if ENV["REDIS_PREFIX"]
+ Redis::Namespace.new(ENV["REDIS_PREFIX"], :redis => redis)
+ else
+ redis
+ end
}

Storing file uploads in S3

This is already built-in, but assumes the presence of static access keys in the environment which aren't needed on ECS and our environment variable name does not match. That change looks like this:

diff --git a/config/storage.yml b/config/storage.yml
index 8426d2c..143f7b9 100644
--- a/config/storage.yml
+++ b/config/storage.yml
@@ -10,5 +10,5 @@ amazon:
service: S3
- access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
- secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>

- bucket: <%= ENV['AWS_S3_BUCKET'] %>
+ # access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
+ # secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
+ bucket: <%= ENV['PRIVATE_S3_BUCKET_NAME'] %>
region: <%= ENV['AWS_S3_REGION'] %>

Avoiding database access during the build

The AppPack build pipeline is isolated from the app's live resources (e.g., database, Redis, etc.). During the Rails asset pipeline, the app tries to connect to the database and times out due to the isolation. We can fix this by telling it to connect to an in-memory SQLite database during the build process. That requires adding the sqlite3 gem to the project and changing the database URL based on the precense of a CI environment variable:

diff --git a/Gemfile b/Gemfile
index bf9d2bb..413b596 100644
--- a/Gemfile
+++ b/Gemfile
@@ -178,0 +179 @@ gem "redis-namespace"
+gem "sqlite3"
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b4cc13..2ef534d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -524,0 +525,3 @@ GEM
+ sqlite3 (1.6.0)
+ mini_portile2 (~> 2.8.0)
+ sqlite3 (1.6.0-x86_64-linux)
@@ -624,0 +628 @@ DEPENDENCIES
+ sqlite3
diff --git a/config/database.yml b/config/database.yml
index 2a88eea..d548a82 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -85,0 +86,3 @@ production:
+ <% if ENV["CI"] %>
+ url: "sqlite3::memory:"
+ <% else %>

@@ -86,0 +90 @@ production:
+ <% end %>

Ready to get on the fast track to AWS?

AppPack is the easiest way to deploy your Rails apps to AWS.

Bullet train in

AppPack deployment

Prior to creating your application, you'll need to complete the Initial Setup tutorial and create a Postgres database and Redis instance in your cluster.

You can view a screencast of the entire app creation process above or continue reading for additional details.

To create the application, we'll run:

apppack create app bullet-train

Be sure to:

  • Set the healthcheck endpoint to /healthcheck
  • Enable the database add-on
  • Enable the Redis add-on
  • Enable the private S3 bucket add-on

Once the app is created, we'll set a few config variables the app is expecting:

apppack -a bullet-train config set AWS_S3_REGION=us-east-2  # replace with your AWS region
apppack -a bullet-train config set SECRET_BASE_KEY=your-secret-base-key # replace with random string
apppack -a bullet-train config set BASE_URL=https://bullet-train.cluster.apppack.rocks # replace with app's URL

Then we can kick off the first build (future builds will happen automatically on pushes to the repo):

apppack -a bullet-train build start --watch

You can also view your build in the web interface:

screenshot of apppack build pipeline

Finally, we're ready to start working with our app. To open it in your default browser, run:

apppack -a bullet-train open

And there you have it. Your Bullet Train site is live. With AppPack, there are no servers to manage or maintain. Everything runs in your AWS account using AWS-native managed services.


  1. Sidekiq 7.0 removed support for namespaces. This breaks compatibility with AppPack's Redis add-on, but because AppPack runs in your own AWS account, there's an easy work-around. If you add your own Redis user with an ACL of on ~* &* +@all and manually set the REDIS_URL on your app, you'll have access to the full keyspace. In this scenario, you'll be responsible for managing isolation on each Redis instance. ↩︎