Elastic Beans

Elastic Beans orchestrates an Elastic Beanstalk application for a Rails app. It is a CLI that replaces the Elastic Beanstalk CLI, which looks great on the surface but is missing some key pieces.

  • The VPC support in eb create is great, but it ends there. Connecting to instances inside a VPC requires manual SSH gateway setup.
  • The CLI supports multiple environments, but only in a rigid fashion that isn't usable when those environments share the same codebase.
  • Upgrading running applications to a new version of the platform should be easy with eb upgrade, but using a custom AMI to support on-disk encryption means that command can't be used.

Usage

See the Getting Started guide for a complete tutorial on setting up your AWS account.

Elastic Beans makes heavy use of the AWS SDK. Configuration of AWS credentials and region should be completed before using this gem, according to the SDK configuration instructions. As the SDK documentation suggests, using environment variables is recommended.

# Pre-configure the application before creating environments
beans configure -n myapp-networking -a myapp \
  -p INTERNAL_PUBLIC_KEY -s SSL_CERTIFICATE_ARN -k KEYPAIR \
  [--solution-stack '64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)'] \
  [-o 'OPTION_SETTING_NAMESPACE/OPTION_NAME=VALUE'...]
beans setenv -a myapp \
  DATABASE_URL=mysql2://db.example.com:3306/myapp \
  SECRET_KEY_BASE=abc123 \
  REDIS_URL=redis://...

# Create a webserver environment with a pretty DNS name at myapp.TLD (managed by Route53)
beans create -a myapp [-d myapp.TLD] [--tags=Environment:production Team:Unicorn] webserver

# Add additional webserver instances to ensure no downtime
beans scale -a myapp --min 2 --max 4 webserver

# Create a worker environment using an SQS queue created by the CloudFormation template
beans create -a myapp [-q QUEUE] worker

# Create an execution environment using an SQS queue created by the CloudFormation template
beans create -a myapp exec

# Create a periodic task scheduler using cron.yaml
beans create -a myapp scheduler

# Create an application version for the HEAD git commit in the working directory.
# Then deploy that version to each running environment.
beans deploy -a myapp

# Run one-off tasks in the execution environment created earlier
beans exec rake db:migrate -a myapp

# SSH to an instance for debugging, tunneling through a bastion instance to reach the private network
beans ssh -a myapp webserver [-n INDEX] [-i IDENTITY_FILE] [-u USERNAME] \
  [-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]

# Run interactive commands via SSH
beans exec rails console -a myapp --interactive [-i IDENTITY_FILE] [-u USERNAME] \
  [-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]

# Update all existing environments and configuration
beans configure -n myapp-networking -a myapp \
  [-p INTERNAL_PUBLIC_KEY] [-s SSL_CERTIFICATE_ARN] [-k KEYPAIR] \
  [--solution-stack '64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)'] \
  [-o 'OPTION_SETTING_NAMESPACE/OPTION_NAME=VALUE'...]

API

Elastic Beans exposes the classes it uses to manage Elastic Beanstalk so you can call them from your own code. Check out the API documentation.

AWS SDK clients must be passed into Elastic Beans constructors.

Health check

Elastic Beanstalk worker environments have an internal health check performed on the instance that does not support HTTPS. Elastic Beans includes a health check middleware to respond to GET / requests originating from localhost.

In config/initializers/elastic_beans.rb, add the middleware into your stack, below forcing HTTPS:

require "elastic_beans/rack/health_check"

if Rails.configuration.force_ssl
  Rails.configuration.middleware.insert_before(
    ActionDispatch::SSL,
    ElasticBeans::Rack::HealthCheck,
    logger: Rails.logger,
  )
else
  Rails.configuration.middleware.use(ElasticBeans::Rack::HealthCheck, logger: Rails.logger)
end

Periodic tasks

Elastic Beanstalk supports periodic tasks by sending a POST request to an endpoint of your application. Create a periodic scheduler environment to enqueue tasks:

beans create -a myapp scheduler

This environment will enqueue the commands from cron.yaml.

The active-elastic-job gem supports executing background jobs as periodic tasks at a special endpoint. This approach will work fine as long as your jobs can execute within the time allotted by nginx and SQS.

If you need more time, Elastic Beans includes a middleware that can convert specially-crafted paths into commands and enqueue them for execution just like beans exec. "Specially-crafted" meaning with /exec at the front, as the cron.yaml section below explains. The middleware only accepts localhost requests from aws-sqsd.

In config/initializers/elastic_beans.rb, add the middleware into your stack, below forcing HTTPS (aws-sqsd does not use HTTPS):

require "elastic_beans/rack/exec"

if Rails.configuration.force_ssl
  Rails.configuration.middleware.insert_before(
    ActionDispatch::SSL,
    ElasticBeans::Rack::Exec,
    application: ElasticBeans::Application.new(
      name: "myapp",
      cloudformation: Aws::CloudFormation::Client.new,
      elastic_beanstalk: Aws::ElasticBeanstalk::Client.new,
      s3: Aws::S3::Client.new,
      sqs: Aws::SQS::Client.new,
    ),
    logger: Rails.logger,
  )
else
  Rails.configuration.middleware.use(
    ElasticBeans::Rack::Exec,
    application: ElasticBeans::Application.new(
      name: "myapp",
      cloudformation: Aws::CloudFormation::Client.new,
      elastic_beanstalk: Aws::ElasticBeanstalk::Client.new,
      s3: Aws::S3::Client.new,
      sqs: Aws::SQS::Client.new,
    ),
    logger: Rails.logger,
  )
end

In cron.yaml, name the full command to be run and use /exec as the url:

- name: rake myapp:mytask[arg1,arg2]
  url: "/exec"

What elastic_beans does differently than awsebcli

Cleanup of old versions

Elastic Beanstalk has a 1,000 version limit that when reached will not allow any more versions to be deployed. To avoid this, Elastic Beans will clean up versions older than 1 week when deploying a new version. It will always leave a minimum of 5 versions behind in case you need to roll back from the Elastic Beanstalk console.

End-to-end encryption

Elastic Beans sets up end-to-end encryption by default. The ELB is configured with an SSL certificate on an HTTPS listener, as well as a backend key policy.

Environment variables

Elastic Beanstalk sets a 4096-byte limit on the environment variable string, which is passed to CloudFormation. The string is of the form KEY=VALUE,KEY2=VALUE2.... If your application has a lot of environment variables, there is no easy way around this. Elastic Beans avoids this by managing your environment variables in S3 from the start. Before loading your application, the configuration is fetched and loaded.

One-off and periodic commands

Elastic Beans supports the execution of one-off and periodic commands in an isolated environment. This way they can be computationally expensive without affecting application performance. Additionally, they are not limited to the visibility timeout of an application background job queue.

Persistent configuration

Elastic Beans stores your configuration in configuration templates. This means that you can terminate and create an environment and it will remember its setup from before. In addition, changes to the configuration will affect existing and future environments.

In other words, changes made using beans are permanent and can be applied to all current and future environments. Changes made in the Elastic Beanstalk web console only last until the environment is terminated and cannot affect other environments.

Shared code between environments

A Rails app supports multiple contexts with the same codebase. Elastic Beans supports webserver, worker, one-off command, and scheduled command environments.

VPC support

Elastic Beans enforces the use of a VPC and supports running your application in a private network. Commands that need to connect to an instance can use a bastion server as an SSH gateway.

Requirements

Elastic Beans assumes that you use CloudFormation to manage your infrastructure. It requires two stacks for each application: a networking stack, and an application stack. The settings for your application are discovered from the outputs of these CloudFormation stacks.

The networking stack can be shared between many applications, but each application should have its own application stack. For example, you might create separate "eb-development" and "eb-production" network stacks. Then, create an Elastic Beanstalk application named "myapp" from a stack named "myapp." Finally, you can set it up with beans:

$ beans configure -n eb-development -a myapp ...

Networking

Elastic Beans uses VPC networking and will run your application from a private subnet. Networking settings are discovered from a CloudFormation stack that you must set up before running the configure command. It must have the following outputs, which will be used for the corresponding settings:

You must create the default instance profile and service role for Elastic Beanstalk. Elastic Beans will use the default names: aws-elasticbeanstalk-ec2-role and aws-elasticbeanstalk-service-role.

Application

Your Elastic Beanstalk application must already exist and also be created using CloudFormation. The Elastic Beanstalk application name and CloudFormation stack name must be identical. Its details will be discovered from the following outputs:

  • ExecQueueUrl
  • Worker[Name]QueueUrl

A separate worker environment will be configured for each queue [Name] that appears. A default worker queue, i.e. WorkerDefaultQueueUrl, must exist.

The ExecQueueUrl is used by beans exec to enqueue one-off commands. It is also by beans ps to inspect scheduled commands that have not yet run. Make sure that its redrive policy allows such inspection before considering a message failed.

Code

Your application must use the active-elastic-job gem for background job processing. Elastic Beans will set the PROCESS_ACTIVE_ELASTIC_JOBS environment variable appropriately in your environments.

Installation

Add this line to your application's Gemfile:

gem 'elastic_beans'

And then execute:

$ bundle

Or install it yourself as:

$ gem install elastic_beans

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run all the tests, including feature tests. This will take about 1.5–2 hours. To run only the unit tests, run rake spec:unit.

You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Philosophy

beans is an opinionated but flexible tool. It is written for Rails applications and can make assumptions based on that. Additionally, it assumes your application was set up by beans and will not handle one set up by hand very well. During setup it will configure your application to meet the best-practice criteria for a HIPAA-compliant Rails application. However, it will assume future customization is permanent and attempt not to override your choices.

Whenever possible, operations are idempotent.

Elastic Beanstalk is an evolving platform, albeit slowly. It has many gaps and surprises in functionality that beans addresses. Whenever possible, beans adds behavior using publicized and approved methods, such as commands in an ebextension. But sometimes, beans must reach under the hood and tweak things in a less-safe manner.

Beans uses Ruby objects to represent Elastic Beanstalk concepts, like Application or Environment. Those objects should be constructed with enough information to locate an existing entity. However, it is expected that a Ruby object can represent an Elastic Beanstalk entity that does not exist. This is most common when creating them.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/onemedical/elastic_beans.