This post gives you an overview of the key components needed to deploy a Rails app to Amazon ECS on AWS Fargate.
When I started working with AWS, I felt overwhelmed with all the necessary pieces to make everything work. Some resources tell you which buttons to click on the AWS console. But my greatest difficulty was understanding how it all tied together and why. So this guide focuses on explaining that.
Amazon ECS (Elastic Container Service) is a service that allows you to run containers on AWS. However, your containers need to run on virtual machines (i.e., EC2 instances). Before Fargate, you had to operate your virtual machines to run containers. With Amazon Fargate, you can run containers on virtual machines managed by AWS: so you have one fewer thing to worry about.
In my opinion, Amazon ECS on Amazon Fargate is the easiest way to run containerized Rails apps on AWS.
There are three main components in the architecture:
- the Postgres database – this is where you store your data. We’ll host it on Amazon RDS (Relational Database Service);
- A fleet of containers – the Rails application will be running on a set of containers to respond to multiple concurrent requests or process background jobs. Amazon ECS runs these containers.
- A load balancer – this component forwards a client’s request to one of the containers. The client can be a web browser or a mobile app or another web service. This component is called the Application Load Balancer (ALB).
These are the essential pieces of running a Rails app on ECS/Fargate. In my opinion, most configuration done in AWS is to enable these three components to work together.
These components need to be able to communicate with each other. That is where networking comes in. Networking is probably the aspect of AWS that took me the longest to understand.
This architecture needs to support the following communication flows:
- The load balancer needs to receive requests from the client
- The load balancer needs to forward client requests to one of the containers
- The container needs to query the database
- The database needs to send query results to the container
- The container needs to send responses to the client
The purpose of all configuration around VPCs, security groups, subnets, and gateways is to enable those communication flows.
All your components are within your network, the VPC (virtual private cloud). Think of the VPC as your own dedicated space on AWS.
Your network-related tasks are similar to urban planning for a city: You have to decide how big the city is, where the neighborhoods are, how to connect them, and which facilities they have.
You have to decide the size of the VPC
You specify the range of IPs allocated to your VPC using the CIDR notation (e.g., `10.0.0.0/16`). The higher the number of IPs in your VPC, the higher the number of components (e.g., number of databases, etc.) that can be in it.
A VPC belongs to a region which is the geographic area where the datacenter is located.
You have to partition the VPC
You create smaller networks (i.e., subnets) within your VPC. Each subnet spans a lower range of IPs within your VPC (e.g., `10.0.0.0/20`).
Subnets allow you to group components according to their function within your architecture.
Each subnet belongs to an availability zone. An availability zone is an independent data-center. Having subnets in multiple availability zones makes your application resilient to failures in a single availability zone.
For this example, we are going to have four subnets (2 subnets per availability zone). In this example, every component is going to be duplicated (one per subnet) for resiliency purposes.
You have to decide in which subnet each component is placed.
You must assign every component to a subnet. You need to place components on subnets according to criteria chosen by you.
For this example, we will use assign the components to subnets based on the following criteria:
- Private subnet – this subnet is used by our “internal” components, which don’t need direct access to the Internet: the containers and the database.
- Public subnet – this subnet is used by our “external” components, which need direct access to the Internet: the load balancer.
Using public and private subnets is a common way of structuring the VPC of web applications.
You have to allow the VPC to communicate with the Internet
By default, communication is only possible within components of the VPC. You have to explicitly enable communication with the Internet.
To enable traffic to flow to or from the Internet you need:
- An internet gateway, which is another AWS component. This component is part of the VPC but is not part of a specific subnet. This component can send and receive traffic from the Internet.
- A “public” route table, which directs traffic from the public subnet to the Internet through the Internet gateway. Without the route table, the components in the public subnet would be unable to send responses to the clients.
To enable the containers to send responses to clients you need:
- A NAT gateway, which is another AWS component. You place this component in the public subnets. The NAT gateway enables the private subnet’s components to connect to the Internet while preventing the Internet from connecting directly to them.
- A “private” route table, which directs traffic from the private subnet to the Internet through the NAT gateway.
Let me illustrate how a typical request/response will flow through all the components:
- Step A – Request arrives from the Internet, and it hits the internet gateway.
- Step B – The internet gateway is inside the VPC, so it is able to send the request to the load balancer.
- Step C – The load balancer is in the VPC, so it sends the request to a container.
- Step D – The container accesses information from the database which is also in the VPC
- Step E – The container sends the request back to the client through the NAT gateway because of the “private” route table
- Step F – The NAT gateway sends the request back to the client through the Internet gateway because of the “public” route table
You have to allow each component to communicate with each other
You can control which type of traffic a component is allowed to receive or send. These restrictions are specified using security groups, which are like firewalls for AWS components.
When setting up security groups, you should follow the principle of least privilege – don’t give more permissions than needed by the components to perform their function.
You can use security groups to specify the following restrictions:
- The load balancer can only receive TCP traffic through port 80 (HTTP) or port 443 (HTTPS) from all IPs.
- The containers can only receive TCP traffic through the container’s port from IPs within the VPC.
- The Postgres database can only receive TCP traffic through the database port (e.g. 5432) from IPs within the private subnet.
At a high-level ECS is just an AWS service that runs the Rails application in a fleet of containers. However, I wanted to explain some of the key concepts you need to understand to run these containers.
Elastic Container Registry
ECS runs Docker containers. You create Docker containers from Docker images. You create Docker images from a Dockerfile through the
docker build command. These images need to be stored in a repository – through the
docker push command – so they can be used by ECS to run containers.
The Amazon Elastic Container Registry is an AWS service you can use as a Docker repository for your Rails application’s images
An ECS task definition is what describes the containers you want to run on ECS. The task definition specifies the Docker image, the command to run, the allocated memory/cpu, etc.
You’ll have a task definition for each of the different uses of your application:
- Serving web requests
- Running your background job scheduler (e.g. Sidekiq, Resque)
- Running migrations
An ECS task is an instance of the execution of a task definition. You can think of tasks as an ECS wrapper over your running containers.
You can schedule a task in different ways:
- Manually – You run tasks as a one-off. You would do this for running a database migration during a deployment: after the task performs the migration, it stops.
- Service Scheduler – You use this for long-running tasks like the tasks responsible for serving web requests or running background jobs. The service ensures a specified number of tasks is always running. For this example, we would need to define one service for the web request and another service for the background job scheduler.
The ECS cluster is where your tasks run.
Application Load Balancer
The application load balancer (ALB) routes incoming client requests to the ECS service, which schedules your web containers. To configure the ALB, you need a listener and a target group.
The listener ensures the ALB checks for incoming connections from clients on a given port (e.g., 80) and protocol (e.g., TCP). The listener also ensures the ALB forwards incoming requests.
The target group specifies the AWS component the ALB uses to forward requests. In this case, the target group would be the ECS service responsible for the Rails web containers.
The target group also specifies a health check. This health check ensures the load balance only forwards requests to healthy ECS tasks within the ECS service.
This article isn’t a detailed walkthrough of everything you need to configure to run Rails on ECS Fargate. The post focuses on bringing some clarity into how everything ties together.
AWS started to look a bit less daunting to me when I finally understood the key concepts. I hope this article helped you learn a thing or two about running Rails apps on ECS Fargate.
I can send you my posts straight to your e-mail inbox if you sign up for my newsletter.