# Deploying a Rails Application with AWS Copilot

This post outlines deploying a Rails application using AWS Copilot, a CLI tool designed to provide a Heroku-like experience within the AWS ecosystem. Before proceeding, it’s recommended to familiarize yourself with the basics by following the “Deploy your first application” guide in the Copilot documentation.

I used the Jumpstart Rails project template for this deployment. The steps and configurations discussed here are specific to this template but might be adaptable to other Rails applications.

Important Notes:

  • Each copilot command executes CloudFormation operations in the background. Inspecting the CloudFormation Stacks in the AWS console can be insightful if you encounter issues.
  • copilot commands can be time-consuming.

# Setting Up the Application

First, log into the AWS account and set up the application and environment using the following commands:

# Log into the AWS account (using SSO for this deployment)
aws configure sso

# Initialize the application repository
copilot init \
  -a rails-app \
  -t "Load Balanced Web Service" \
  -n web \
  -d ./Dockerfile.production

# Create a test/development environment
copilot env init \
  --name dev \
  --app rails-app \
  --default-config

This step does not deploy the application but sets up the necessary configuration within your repository’s copilot/ directory.

# Database Configuration

Next, allocate the database as a storage unit:

copilot storage init \
  -n webdb \
  -t Aurora \
  -w web \
  -l environment \
  --engine PostgreSQL \
  --initial-db railsapp

For the application to access database credentials, modify the manifest.yml file as follows:

network:
  vpc:
    security_groups:
      - from_cfn: ${COPILOT_APPLICATION_NAME}-${COPILOT_ENVIRONMENT_NAME}-webdbSecurityGroup
secrets:
  DATABASE_JSON:
    from_cfn: ${COPILOT_APPLICATION_NAME}-${COPILOT_ENVIRONMENT_NAME}-webdbAuroraSecret

# Application Configuration

AWS provides DATABASE_JSON instead of the typical DATABASE_URL. Adjust the database.yml to parse these credentials:

production:
  <<: *default
  # Parse DATABASE_JSON if available
  <% if ENV['DATABASE_JSON'] %>
  <%   require 'json' %>
  <%   db_config = JSON.parse(ENV['DATABASE_JSON']) %>
  username: <%= db_config['username'] %>
  password: <%= db_config['password'] %>
  host: <%= db_config['host'] %>
  port: <%= db_config['port'] %>
  database: <%= db_config['dbname'] %>
  <% else %>
  url: <%= ENV['DATABASE_URL'] %>
  <% end %>

In your Dockerfile.production, include instructions for precompiling assets and starting the Rails server:

RUN SECRET_KEY_BASE=dummy-staging-key bin/rails assets:precompile
CMD ["bin/rails", "server", "-b", "0.0.0.0"]

Set environment variables for production in your manifest.yml:

variables:
  RAILS_ENV: production
  SECRET_KEY_BASE: <value from `rails secret`>

# Service Configuration

Configure the application as a “Load Balanced Web Service” to enable command execution in the container, essential for tasks like database migration.

exec: true

Define routing and health check settings:

http:
  path: '/'
  healthcheck: '/up'

Since SSL support isn’t provided by default, turn off SSL enforcement in config/environments/production.rb:

config.force_ssl = false

# Deploying the Application

Deploy the application and database using the following commands:

copilot deploy \
  --deploy-env \
  --env dev \
  --name web

# Run database migrations post-deployment
copilot svc exec \
  --command "bin/rails db:migrate" \
  -n web

# View application logs
copilot svc logs

# Display deployment information
copilot svc show

You should have a URL in the show command that will expose your app to the public Internet.

We did it! We’ve deployed our application.

This has been the best application tool to deploy into the AWS ecosystem. This is, however, the longest time it has taken me to deploy a hello world Rails application with compromises – i.e., SSL, one of the tasks, etc.

How can I help make this experience better for others after me? I have a blog post to write about the incident. I do want the experience to be more flawless. There are a lot of gothyas and “didn’t you reads” that have been discoverable but disappointing.

Note: To clean it all up copilot app delete.

# Additional Resources

# Update

I was later informed via a chat thread with the team, that I can get SSL using Request-Driven Web Service.

copilot init \
  -a rails-app \
  -t "Request-Driven Web Service" \
  -n web \
  -d ./Dockerfile.production

Note: When setting up the database the instructions are bit different, but the cli will tell you what to add.

The issue that I was running into of not being able database migrations can be solved with copilot task run.

copilot task run \
  --command "bin/rails db:migrate" \
  --dockerfile Dockerfile.production \
  --env dev \
  --app rails-app \
  --follow

However, I found a bug with task run. When it builds the docker image, it uses the host architecture, it is not building for the target architecture. I changed my command to use the latest image of the app I just deployed, using docker images.

copilot task run \
  --command "bin/rails db:migrate" \
  --image <ecr host>/rails-app/web \
  --env dev \
  --app rails-app \
  --follow

This did not work because the task run does not assume the environment variables and secrets from the services. This would include RAILS_ENV and the DATABASE_JSON.

This does not appear to be a viable way for me.