Amazon EC2 Container system with Ruby on Rails [PART-1]

This tutorial is meant for people who want to launch a RoR application using AWS container system and rest of its infrastructure. In the first part of this series, I'm going to focus on how to deploy a Rails application using container system. In the next part we're going to talk about using Background jobs using Sidekiq and Amazon's ElastiCache, as well as making our app secure with SSL and using ELB(Elastic Load balancer) to handle traffic.

Let's get started

1. Setting up Rails app

We're going to use AWS RDS PostgreSQL as our production database so let's create an app with Postgresql as our database. We're going to use Rails 5.1 with webpack for javascript management.

rails new example-app --database=postgresql

Now let's go in our root folder and scaffold some simple model, say Post

rails g scaffold Post name:string description:text

Let's set up the development database

rake db:setup

rake db:migrate

Check to see that everything works in development environment

rails s

And try creating some posts in http://localhost:3000/posts

If everything looks good, then we move on to AWS setup

2.Setting up ECR

Go to https://aws.amazon.com/ and `Sign in to console`. Fill out the registration and payment forms (you will not have to pay for most of the stuff that we're going to use, since it falls into the “free tier” category, but if you choose some other options, you might incur some charges)

When you're asked to choose region, I will use eu-central-1 but it does not really matter.

Ok, now you should get to the main console:

First we're going to set up our EC2 Container service. Type ec2 container service in the search bar and click on the option.

  • press “Get Started”
  • Leave both checkboxes checked for for storing container images and deploying application and press continue
  • make a repository name (for this tutorial it is going to be example-app )
  • Now you should be presented with instructions to push Docker image to ECR. Do not close this page, it will come in handy in a few minutes, but first we should go and make our docker image. Docker itself is out of scope for this tutorial. But to follow along, create Dockerfile with the following content
FROM ruby:2.4.1
# node.js
RUN echo "Installing nodejs ..." && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y curl apt-transport-https && \
curl -sS https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
echo 'deb https://deb.nodesource.com/node_7.x jessie main' > /etc/apt/sources.list.d/nodesource.list && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# yarn
RUN echo "Installing yarn ..." && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y curl apt-transport-https && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y yarn && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN yarn
WORKDIR /app
RUN bundle config --global frozen 1
COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN RAILS_ENV=production bundle install --path /bundle --without development test
COPY . /app
RUN npm rebuild node-sass --force
RUN DISABLE_SPRING=1 RAILS_ENV=production bin/rake assets:precompile

This will create a “recipe” for an image. Then setup Docker on your machine.

Before you can get login credentials for AWS CLI you have to follow these instructions. This includes IAM user and group (I suggest creating group with full administrative privileges for this tutorial), installing AWS CLI and logging in through CLI with your IAM user, but you can do that all from the instructions, so I won't go over the details here.

Once you push your image to ECR You should go to EC2 container service -> Repositories -> example-app and see that there is an image with the tag latest .

3. Setting up Cluster

Now it's time to set up cluster. Cluster consists of 3 components:

  1. ECS instances — ECS instances are just EC2 instances dedicated for a particular cluster where tasks are run
  2. Tasks — Tasks are responsible for mounting containers on ECS instances and running commands (like running application, running sidekiq, migrations and so on)
  3. Services — Services are responsible for running tasks. If, for example, an application container fails for any reason, the service runs another task to start the application server (thus reducing downtime). It also does rolling deployments (explained at the end of this post).

Let's create a new cluster. Click “Create Cluster”

  • Cluster name: choose whatever you like, but here we'll call itproduction-application
  • EC2 instance type: we'll pick t2.micro because it is included in free-tier
  • Number of instances: let's leave it at 1. You can increase the amount later if you need to.
  • click “Create”

It might take a bit of time while AWS creates the cluster. You might notice that there were a few important things that we left empty:

  1. SSH keys — if you want to SSH into your ECS instances you will have to create a key. Here we left this part blank, because you can create it later, and we won't need to do that in this tutorial.
  2. VPC — Virtual Private Cloud is your internal network for AWS services. It consists of Security groups that you assign to your AWS resources to whitelist what kind of ports and connections are allowed with each resource. Here we left it empty so AWS created a VPC for us. We will adjust the security groups later.

Ok, so we have an empty cluster with one Container instance running. Next we will set up Our production Database using Amazon RDS

4. Setting up Amazon RDS

  • Go to 'Services' and type in 'RDS' and click on RDS
  • Press 'Get started now'
  • For this tutorial we're going to select PostgreSQL.
  • For this tutorial we can click checkbox “Only show options that are eligible for RDS Free Tier” which will automatically choose instance size that is eligible for free tier. But you can choose whichever size fits your needs.
  • DB Instance Identifier: Let's call ours 'production-database'. This is just a name for the DB instance we're creating
  • Master username: Username that your Rails app will use to connect to DB instance. Also fill in the password (obv).
  • click “Next Step”
  • VPC: choose VPC that was generated from your cluster creation (open select, and there should be one VPC that is NOT the default VPC)
  • Publicly Accessible: Choose “Yes” so our App can connect to the database. It will not be accessible to everyone when we configure Security group correctly.
  • VPC Security Group: Leave it at “Create new..”
  • Database Name: Name of our database. we'll call it example_app_production
  • Click “Finish” And now you should have a running RDS instance. It takes a few minutes to create.

5. Security Group setup

Now, when we have a running Cluster instance and RDS instance we need to make sure that

  1. People can access our application through port 80
  2. Our application can access RDS instance on port 5432

Let's go to Services -> VPC. Then click on “2 VPCs”. You can see that there are 2 VPC. One was created when you started using AWS, and the other one was created when you made the ECS cluster. To not mix up VPCs when creating more resources, let's delete the default VPC (scroll to right and you will see the default flag column). Check the default VPC, go to actions -> “delete VPC” and check the box saying that you understand that you are deleting the default VPC. Don't worry — All of our resources are running on the other VPC so it won't create any problems.

While we're at it let's name our VPC to something, say “myVPC”

Now we go to “Security groups” link on the sidemenu

We are interested in groups:

  1. 'EC2ContainerService..', which was created for our ECS instances. Let’s name it ‘Application’. Also, check that its Inbound Rules specify that all IP addresses (0.0.0.0) can access port 80
  2. 'rds-launch-wizard' which was created when we created our RDS instance. Let's name it 'RDS'. Also, we should edit inbound rules to allow our 'Application' SG to access 'RDS' SG on port 5432. So add another rule, choose 'PostgreSQL' in the first select, and in IP field choose security group “Application”. You might notice another rule which was automatically created to allow connections from your current IP address. Afterwards click “Save”.

6. Setting up Task and Service

To deploy an application we need just two more things — A task that runs the application container, and service that runs the task.

G0 to EC2 Container Service -> Task defininitions -> create new task definition.

  • Task Definition Name: We'll call it run-application
  • I suggest opening another browser tab with AWS resources, so you can look up relevant configuration when creating container, then press “Add container”
  • Container name: We'll call it application
  • Image: In the other tab, go to “Repositories” , click on your app image. You should see “repository URI”. Copy that and paste it in “Image” field. You have to also append latest tag, so Task knows which image to mount. So now your image field should be filled with something like
 some_id.dkr.ecr.eu-central-1.amazonaws.com/example-app:latest
  • Memory Limit: Let's follow the suggestion and Pick “Soft limit” and 300. This should be enough to run 2 tasks simultaneously (explained later)
  • Port Mappings: Since we will run our application from puma, we need to map port 80 to container port 3000 (default puma port)
  • Command: The executable command for the container should be separated by space and we will use puma launch command so something like bundle,exec,puma,-C,config/puma.rb
  • ENV VARIABLES: We're going to define all of our ENV variables here, so we don't have production DB credentials and other sensitive information committed in our version control. These will be our ENV variables:
  1. RAILS_ENV : production
  2. RACK_ENV : production
  3. SECRET_KEY_BASE : Run rake secret in your application root directory and paste the string returned here
  4. DATABASE_NAME : example_app_production
  5. DATABASE_USERNAME : app_production
  6. DATABASE_PASSWORD : your database password
  7. DATABASE_HOST : in the other tab go to RDS -> DB instances -> click on instance -> instance actions -> see details. Copy the endpoint value and paste it in this variable.

Click “Add” and then “Create” for task.

Now we need to adjust our database.yml file like so:

production:
adapter: postgresql
encoding: unicode
pool: 5
host: <%= ENV['DATABASE_HOST'] %>
database: <%= ENV['DATABASE_NAME'] %>
username: <%= ENV['DATABASE_USERNAME'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
port: 5432

Remember, that we need to rebuild and push a new image if we make changes to our code so do that now (if you forgot the commands to do that, you can go to Repositories -> example-app> “push commands”)

Last thing we need to do is create a Service that will launch our application.

So we go to Clusters -> application-production -> Services “Create”

It will automatically select our only task, we just need to give it a name say “application” and number of tasks to run: 1. Press “Create Service”. Now if you go to “View Service” You will see that it automatically tries to launch the task. If it fails, then it will try and launch it again and again until the task reaches “Running” state.

To go to your application go to Clusters ->application-production ->ECS instances -> click on EC2 instance link. There you will find public IP for your application. Now try Open “your_ip/posts” in browser and see it fail…

There still is a small problem. We have not run our migrations. We can do that by doing a one-time task run and overriding the executable command. Problem is that if we override our current “run-application” task, we will get error Reasons : [“RESOURCE:PORTS”]. This is because we are trying to run 2 tasks that connect to the same ports, which is not allowed. To run rake tasks we need to create a very similar task to “run-application” except we will leave the Command field empty to be filled when configuring task to run, and not map any ports. Create the task in “task definitions” called “rake-task”. Open “run-applications” task to copy all the information (you can use JSON config option to copy/paste the whole container info faster. Specifically, copy/paste “containerDefinitions” attribute).

Now go to Clusters ->application-production -> Tasks -> Run new task. Choose “rake-task:1”, Click on “Advanced options”, “container overrides — application” and in Command let’s do bundle,exec,rake,db:migrate . This will launch a new task, running our migrations to the database. Let’s press “Run Task”.

NOTE: if you chose a different “Memory limit” when creating the container for the task, it might be the case that your ECS instance does not have enough resources (lack of Memory). In that case you can update your task container to use less memory or launch more ECS instances.

After the task is finished try your_ip/posts in browser. If you followed the instructions, you should have a working rails app on ECS. :)

7. Deploying updates

Deploying updates through the UI is kind of painful, so I would suggest using something like This script or create your own, but I'm going to show how it works so you get the idea

  1. When you want to deploy new code, you firstly have to build a new image and push it to the repository (was mentioned here already)
  2. Go to Task definitions -> run-application and create a new revision. Revision numbers increment automatically. You don't really have to change anything in the task itself since AWS lets you create a new revision without any other changes.
  3. After creating a new revision you should update your “application” service to use the new revision of your task. just change the “task definition” to the new revision you just created. After saving the updated service, it will try to smoothly change deployed application version by running the new task without killing the old one. If the new task is successfully launched and has state “running” the old task will be killed. Remember to also launch migration task (no need to create new revision for that, since revisions are needed only for automatic tasks used by services).

As mentioned before, I will be extending this tutorial to cover SSL, load balancers, and background jobs with Sidekiq and ElastiCache. When it is ready, I will add a link here.

Have fun coding! :)