Recently, my Grio teammates and I supported one of our clients in migrating their Ruby on Rails application from Heroku to AWS. The motivation for the switch — namely, a need for more power and flexibility as the app evolved — is one that many growing companies share; in this blog post, I’ll give a high-level overview of our process and considerations, which I hope will prove helpful (or at least interesting!) to others who are embarking on their own migration journeys.
Amazon’s terminology can be a little confusing. If you’re not an AWS aficionado, the following definitions should help bring your up to speed, at least for the purposes of understanding this blog post:
- A container is a executable image that contains software and dependencies
- A task definition is a description of a collection of one or more containers, commands to run them with, and ports to expose
- A service runs a task definition and controls instance scaling
- A task is an actual running instance of a container inside a surface; note the difference between a task and a task definition
One of the complexities of AWS is that there’s more than one way to host a functional app. Specifically, most developers will use one of four options:
- Manual setup of EC2 instances — in other words, the classic way to do things, which works but has been largely nudged out by more flexible, automated approaches;
- EC2 instances managed with Elastic Beanstalk, which allows for automated instance scaling;
- Containerized instances running in Elastic Container Service, which allows for encapsulation of the app and environment in a Docker container (and also allows for automated scaling); or
- Containerized instances running in Elastic Kubernetes Service, which goes a step further to offer full control over a Kubernetes cluster.
Containerized approaches are generally considered best practice, and will save most DevOps teams a lot of headache. For our project, we opted for Elastic Container Service.
Things to consider when planning an AWS migration
Building the Docker image
If you’re using a containerized approach (which, as noted above, I’d recommend), your first step will be to create a Docker image of your app. There are two key questions to consider in this part of the process — which base image you’ll use, and how you’ll go about adding your code to the image.
- Choosing a base image: An ideal base image uses an OS you and your team are familiar with, is reasonably small (Docker images can inflate very quickly), and includes the dependencies required by your app. You may, of course, have to make tradeoffs between these factors — for example, if a base image includes your key dependencies plus a long list of things you don’t need, you’re better off starting from a simpler (smaller) image and installing dependencies yourself.
- Getting your app code into the image: You have two options here — copy your code directly, or check out code from git. If you go the former route, you’ll need to make sure you have a good method for working around any local uncommitted code. If you choose the latter, you’ll need to make sure Docker has credentials to pull from git. Both hurdles are surmountable, but one may be easier than the other depending on the nature of your app and your existing workflows.
If your app uses any sensitive information (passwords, API keys, etc.) you need a way to manage secrets in your AWS implementation. Technically, you could bake secrets into your Docker image — but please don’t. It’s a terrible security practice, and it also means that you need to fully redeploy the image anytime you want to update secret information. A better solution is to fetch secrets from AWS Parameter Store while the image runs, which you can accomplish by writing a script that wraps running the app and fetching secrets into the same operation.
The best practice for ensuring network security with an AWS-hosted app is to enclose almost all of your code, services, assets, etc. in a Virtual Private Cloud (VPC). Check your setup to be sure that the components inside the VPC can talk to each other (e.g., your app needs to be able to communicate with your database) and that nothing within the VPC is accessible from outside; ideally, your load balancer should be the only point of entry, as shown below:
Aside from your Docker image setup, secrets, and security, it’s helpful to think ahead about:
- Other services you’ll need: Redis, ElasticSearch, databases, etc.
- Hosting and delivering assets: You could opt for runtime asset delivery (which will usually compromise performance), baking assets into your Docker image (which will make the image bigger, possibly too big), or hosting assets on s3 or another external service (which ensures good performance, but adds complexity).
- Handling other environments: You will likely have more than one environment running at a time; you could separate environments into different AWS regions, or different ECS clusters.
AWS Migration: Step by step
As mentioned above, the first hands-on step in your migration will be to create your Docker image. Once you’ve selected a base image, you’ll need to:
- Install dependencies
- Add your app code to the image (see considerations for this step above)
- Make sure the image exposes ports to connect to the app
- Set the command to boot the image
- Clean up — you’ll likely have some extra code from the build process that isn’t actually needed to run the app; taking time to remove it will keep things tidy and reduce your image size
When your image is ready, it’s time to wire everything together as follows:
- Push your image up; we use Elastic Container Registry to host the image, as it’s nice to keep everything in AWS, but you could use dockerhub or another similar service
- Set up a task definition
- Set up an AWS load balancer
- Set up a service in ECS that uses the task definition, and point your load balancer to the service
- Set up or point to your other services (RDS/Redis/ECS)
- Set up your secrets in AWS Parameter Store
- Check the service, and make sure it’s managing to boot your app (if it’s not, you have a little debugging ahead of you)
Any one of these steps, of course, could easily fill its own blog post — but hopefully this overview gives you a good sense of the process, and a starting point for planning your own migration.