Automating deployments of your web applications brings many benefits, especially when you run them on AWS. You standardize the process, prevent human errors and can integrate with other AWS services like EC2 Autoscaling and Elastic Load Balancing. Today I will talk about how you can automate your Ruby on Rails deployments using AWS CodeDeploy.
Why to use AWS CodeDeploy?
Manual deployments is no longer a choice when you maintain a production application on AWS. In a reliable and scalable architecture, you will have multiple, stateless, redundant instances and cannot know when one of them fail or a load increase will happen and a new instance will be launched. Also even if you know and run the same commands every time using SSH, human errors can always happen. Hence, you should automate your deployment process in the best way you can.
AWS CodeDeploy is the automated deployment service provided by AWS. Using CodeDeploy you can deploy your applications on EC2 instances, on-premise servers, as well as serverless Lambda functions. To list some of its benefits and features:
- It is well integrated with EC2 Autoscaling Group, the latest version is deployed automatically to new instances upon launch.
- It avoids downtime during deployments by providing you rolling in-place and blue/green deployments
- You can integrate your Elastic Load Balancer so that before the deployment the connections on the instance can be drained and new connections are blocked during the deployment. It takes the instance back to service after deployment succeeds. This helps avoiding downtime further.
- You can receive notifications upon deployment actions through Amazon SNS.
- You can set your CodeDeploy application and deployment group as a deployment step for your CI/CD pipeline implemented using AWS CodePipeline.
In the following sections I will describe how you can implement AWS CodeDeploy integration for your Ruby on Rails application running on AWS with Autoscaling enabled and behind an Elastic Load Balancer.
Implementing AWS CodeDeploy integration for a Ruby on Rails application
Some of the steps of the integration are general to all type of applications that are being integrated with CodeDeploy. I will not dive into them as there are AWS resources in the references section of this post that you can get more information. These are:
- You should create an application on AWS CodeDeploy using AWS CLI or AWS Management Console.
- You should add a deployment group to your CodeDeploy application by selecting your Autoscaling Group.
- AWS CodeDeploy agent should be installed on your EC2 instances that are used in this deployment.
- I assume that Passenger-Nginx is installed on your instances and your web application is served from
pisirpaylasis the name of our application. You can rename your application folder with your application name or use Apache instead of Nginx. Then you will need to customize
appspec.ymland scripts below accordingly. But the theory is the same for the rest.
After you prepare these settings, you should add files and folders in the root of your application repository along with other application code. The most crucial one is
appspec.yml which defines the deployment steps.
Every CodeDeploy application should have this file in the root of their code repository. It defines which scripts to run in which step. I generally define a folder named
deployment_scripts under the root to place these scripts. You are free to rename this folder, but
appspec.yml is a must.
Here is how our
appspec.yml file look like:
The first two lines are standard.
version is specific to AWS CodeDeploy and not related to your application. It should be
0.0. I deploy into Linux instances so
os attribute defines this.
files attribute is a list where your application will be installed. For this example, we have only one destination.
sourcedescribes which folder contains the application in code repository. For Ruby on Rails, we deploy the folder as it is so it is root,
destinationis where to deploy the application on the instance. As I mentioned before, I assume that our application is loaded from
permission section defines which OS user and group should have permissions in folder and file level after the installation. This is about all the files and folder installed under
hooks section is where the magic happens. Here, you list CodeDeploy specific lifecycle events and the order of the scripts in them. Besides, as I did, you can define which OS user should the script be ran by. It should be a passwordless user. I recommend running command that makes installations using a non-root user to avoid possible security problems.
Lifescycle events and hooks
Now let’s describe lifescyle events and the scripts run in them. There are more lifecycle events on AWS CodeDeploy, but the ones below are enough for this deployment.
This lifescycle event is just before the code installation on the instance. In other words, before our code is copied from repository and placed under
/var/www/pisirpaylas/deployment. This step is a perfect place to back up previous deployment and initialize an empty deployment folder.
We simply remove the previous deployment backup and rename existing deployment folder as prev-deployment to back it up. Normally, we do not need this and it will not be used in anyway if our automation works correctly. If deployment fails, CodeDeploy will redeploy the latest successful release to this instance assuming that we activated rollbacks on the deployment group. However, if some disruption occurs and you have to rollback manually by connecting via SSH into the instance, you can find it backed up as prev-deployment. It is just a caution.
You can perform any type of backup logic in this step. After this lifescycle is completed, installation will be performed and our code will be deployed into fresh deployment folder. The last command makes nginx user as the owner of the folder.
As our code installed, we need to make configurations and installations to make it ready for production service. This is where AfterInstall lifecycle is used for. Below we will examine each script one by one. As you will see, in all scripts we first go to the deployment folder using
cd command to run commands.
While configuring Ruby on Rails in production, using shared folders for configuration and gems is a best practice. In our instance, /var/www/pisirpaylas/shared folder is used for this purpose. Your code repository should not contain any database passwords or any other credentials. Your database.yml should not be included in your code repository. I recommend to use CloudFormation templates and init scripts to create database.yml file during provisioning. Using CfnHup is a good way to update this file with the same CloudFormation template. These are about the maintenance of your environment and you can select a different path to configure database.yml.
This script creates a link for database.yml to the main database.yml file in the shared folder.
This script installs the gems into /var/www/pisirpaylas/shared/bundle folder. Using a shared folder increases the speed of upcoming deployments as it will not download existing gem versions.
As you may notice, I added for accessing AWS CodeCommit. It allows to install your gems in your CodeCommit repositories if you have any. Before you can add this step, your instance’s IAM role should have read access to your CodeCommit repositories. You can skip this part if you do not have any gem or Rails engine in your application accessing your CodeCommit repositories.
The last command runs our famous
bundle install command for production. I think the comments should explain the options well. We provide shared locations for bundle and binary, exclude development and test gems.
This step compiles assets and places it in the project’s public folder. It is a Rails specific command.
As you can see I reloaded environment variables before running the command. I generally use /etc/profile/ folder to place environment variables globally on the instance. The script usually does not load this folder automatically. Some gems such as devise require you to provide secret keysin environment variables or configuration files. As we do not store any credentails in the repository, it is better to use environment variables via CloudFormation templates as I described in the database.yml related section.
Actually, you might choose to deploy migrations in a managed way as it effects your database directly. Hence, you might not need this step. However, if you use Ruby on Rails’ built in migrations, this step will allow you to run activerecord migrations in your deployment. If you perform deployments one by one on your instances, the first instance will perform the migrations and running this command more than once in the instances followed will not perform any further actions in your database. Nevertheless, you should be careful while running your migrations. I recommend you to back it up your database before you run amy db changes for the first time.
This is the final lefecycle event where you make final touches to finish the deployment and make additional configurations or clean-ups according to your needs. For example, I add the deployment time as a text file in the public folder which is completely optional. I rearrange application folder permissions and restart Nginx.
When I first started integrating AWS CodeDeploy to our Pisirpaylas application, there was not many examples on the Internet about how we can perform this in a Ruby on Rails application. Many decisions are based on how you configure your AWS environment and your application. For example, if you need to deploy applications developed using another programming language such as PHP or Node.js; you will need to adjust your hooks section and customize your scripts accordingly. I hope this post will give you a good idea about how you can customize your steps upon your needs.
Of course, you might need to allocate some time to understand AWS CodeDeploy and other automation tools; but, when you succeed it worths the effort. When you start automated, you will never want to stop :)
From this point, you can build a pipeline on AWS CodePipeline for your Ruby on Rails project. Then, you can enhance it more by adding test actions before deploying it to production. I hope to discuss them in the upcoming posts.
Thanks for reading!