Automate Rails 6.1 Provision and Deployment on Ubuntu 20.04 using Ansible

February 08, 2020

Configure an Ubuntu Server manually to host a Ruby on Rails application is a time-consuming task. For this reason, I’m going to show you how I’m using Ansible to configure the server:

  • Sidekiq for background tasks.
  • Requesting/renewing an SSL certificate automatically from Let’s Encrypt.
  • Redis.
  • Puma application server.
  • Nginx reverse proxy.
  • PostgreSQL.
  • Deployment script to deploy the application and updates to the server.

If you get your VPS on Digital Ocean, get $100 free credit for 60 days using my referral link.

What is Ansible?

Ansible is a simple tool for cloud provisioning, configuration management, and application deployment. Ansible works by connecting to the server or servers, called nodes, over SSH, and runs a set of tasks that we define using YAML. That’s called a playbook. We will be using two Playbooks as part of this tutorial to install and set up our Ubuntu server and another one to deploy the Rails Application from a Git repository.

ansible-deploy

As shown above, Ansible also uses the concept of inventory, which is a list of servers. We can group them by type, so if we have a more complex infrastructure, we can have different playbooks to run on every kind of server. In this tutorial, I am only using one server.

Provision Ubuntu Server 20.04

You can see the full source code in this Gihub Repository. The ansible code is inside the folder .ansible-deploy.

As we’re only using one server, we only define one server IP in the Inventory file located on .ansible-deploy/inventories/development.ini.

.ansible-deploy/inventories/development.ini
[web]
1.1.1.1 # Your server public IP

[all:vars]
ansible_ssh_user=ubuntu # Instance user to log in via SSH
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_private_key_file="~/.ssh/id_rsa"

The main script to provision and configure Ubuntu Server 20.04 is below:

.ansible-deploy/provision.yml
---
- hosts: all
  become: true

  vars_files:
    - app-vars.yml
  
  roles: 
    - role: common
    - role: user
    - role: ssh
    - role: ufw
    - role: ruby
      tags: ruby
    - role: nodejs
      tags: nodejs
    - role: yarn
      tags: nodejs
    - role: postgresql
      tags: postgresql
    - role: redis
      tags: redis
    - role: nginx
      tags: nginx
    - role: logrotate
      tags: logrotate
    - role: certbot
      when: nginx_https_enabled == true
      tags: certbot

In the first line, we specify that this playbook will run in all hosts; we only have one defined in the previous inventory. The second line, become: true is elevating the privilege to run as root. Next, we have a file app-vars.yml, where we define certain variables to customize each role, which are tasks to run as part of this playbook.

The purpose of each role in this playbook is:

  • Common: Auto upgrade all installed packages and installed all the libraries needed.
  • User: Create a new deployment user, called deploy with passwordless login.
  • SSH: Improve SSH security, disable password login, change SSH port and disable root login.
  • Ruby: Installs Ruby, using rbenv:

    • Defaults to 2.7.2. You can change it in the app-vars.yml file
    • jemmaloc is also installed and configured by default
    • rbenv-vars is also installed by default
  • Node.js: Defaults to 15.x. You can change it in the app-vars.yml file.
  • Yarn: Fast, reliable, and secure dependency management for javascript.
  • PostgreSQL: Defaults to v13. You can specify the version that you need in the app-vars.yml file.
  • Redis: In-memory data structure store. Used for Sidekiq and caching in Rails.
  • Nginx: Nginx reverse proxy based on config from nginxconfig.io
  • Puma: With Systemd support for restarting automatically.
  • Sidekiq: With Systemd support for restarting automatically.
  • LogRotate: System utility that manages the automatic rotation and compression of log files. If log files were not rotated, compressed, and periodically pruned, they could eventually consume all available disk space on a system.
  • UFW: Uncomplicated Firewall, is an interface to iptables towards simplifying the process of configuring a firewall. Fail2Ban is also installed. It is an intrusion prevention software framework that protects computer servers from brute-force attacks. As I’m using AWS, I’m setting this from the Security Group. If you’re using any other VPS provider, feel free to enable it and adjust your settings in app-vars.yml.
  • Certbot: Enable Certbot, Let’s Encrypt SSL certificates, and sets up a CRON job auto-renew the certificate when it expires. Add the following variables to app-vars.yml.

    nginx_https_enabled: true
    
    certbot_email: "you@email.me"
    certbot_domains:
    - "domain.com"
    - "www.domain.com"

How to Provision your Server

Once you have created your server, and you know the public IP, and you also have enabled SSH access; here are the steps that you need to follow in order to get up and running with Ansible Rails:

1. Copy scripts

You can just copy the .ansible-deploy folder in your Rails application folder.

2. Storing sensitive data for Ansible

As mentioned earlier, we have one Ansible Playbook to setup the server, so the secret variables that we need are stored in an Ansible Vault. The secrets related to the Rails app, should be stored using Custom Credentials.

To create a new Ansible Vault to store sensitive information, run in the terminal:

$ ansible-vault create .ansible-deploy/group_vars/all/vault.yml

Add the following information to this new vault file, and save it.

vault.yml
vault_postgresql_db_password: "XXXXX_SUPER_SECURE_PASS_XXXXX"
vault_rails_master_key: "XXXXX_MASTER_KEY_FOR_RAILS_XXXXX"

3. Configuration

The main configuration file is .ansible-deploy/app-vars.yml. It’s pre-populated with sensible defaults, but you need to set some values that are only relevant to your project. Here is a description of the main parts:

Git repository, Database credentials
app_name: YOUR_APP_NAME # Replace with name of your app
app_git_repo: "YOUR_GIT_REPO"
app_git_branch: "main" # branch that you want to deploy (e.g: 'production')

postgresql_db_user:     "{{ deploy_user }}_postgresql_user"
postgresql_db_password: "{{ vault_postgresql_db_password }}" # from vault (see previous section)
postgresql_db_name:     "{{ app_name }}_production"
Uncomplicated Firewall Configuration - UFW

Uncomplicated Firewall is enabled and accepting connections from any IP on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). Feel free to update it.

Configure Certbot - Let’s Encrypt SSL certificates

Certboot is configured to request, install and set up a CRON job to update your certificate when it expires. Setup your domain DNS as well as the details for your domain/email to request the certificate in app-vars.yml.

# certbot: details to request SSL certificate
certbot_email: "your email"
certbot_domains:
  - "domain.com"
PostgreSQL Database Backups

By default, daily backup is enabled. In order for this to work, the following variables need to be set. If you do not wish to store backups, remove or comment on these lines from app-vars.yml.

aws_key: "{{ vault_aws_key }}" # store this in group_vars/all/vault.yml that we created earlier
aws_secret: "{{ vault_aws_secret }}"

postgresql_backup_dir: "{{ deploy_user_path }}/backups"
postgresql_backup_filename_format: >-
  {{ app_name }}-%Y%m%d-%H%M%S.pgdump
postgresql_db_backup_healthcheck: "NOTIFICATION_URL (eg: https://healthcheck.io/)" # optional
postgresql_s3_backup_bucket: "DB_BACKUP_BUCKET" # name of the S3 bucket to store backups
postgresql_s3_backup_hour: "3"
postgresql_s3_backup_minute: "*"
postgresql_s3_backup_delete_after: "7 days" # days after which old backups should be deleted

You can customize the rest of the file variables as well.

4. Execute Playbook in your Ubuntu Server

If you have booted up a clean Ubuntu Server, you can install all the dependencies for your Rails application running:

$ cd .ansible-deploy
$ ansible-playbook -i inventories/development.ini provision.yml

Deploy Ruby on Rails Application

To deploy the Ruby on Rails application, I’m using Ansistrano. It’s an extension built on top of Ansible, and it’s performing the following tasks:

  • Installing all Gems dependencies.
  • Precompiling assets.
  • Running database migrations, using run_once.

Puma config

Update your config/puma.rb with settings for production.

config/puma.rb
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

port ENV.fetch("PORT") { 3000 }

rails_env = ENV.fetch("RAILS_ENV") { "development" }
environment rails_env

if %w[production staging].member?(rails_env)
    app_dir = ENV.fetch("APP_DIR") { "YOUR_APP/current" }
    directory app_dir

    shared_dir = ENV.fetch("SHARED_DIR") { "YOUR_APP/shared" }

    # Logging
    stdout_redirect "#{shared_dir}/log/puma.stdout.log", "#{shared_dir}/log/puma.stderr.log", true
    
    pidfile "#{shared_dir}/tmp/pids/puma.pid"
    state_path "#{shared_dir}/tmp/pids/puma.state"
    
    # Set up socket location
    bind "unix://#{shared_dir}/sockets/puma.sock"
    
    workers ENV.fetch("WEB_CONCURRENCY") { 2 }
    preload_app!

elsif rails_env == "development"
    # Specifies the `worker_timeout` threshold that Puma will use to wait before
    # terminating a worker in development environments.
    worker_timeout 3600
    plugin :tmp_restart
end

Now, you can deploy your application running:

$ cd .ansible-deploy
$ ansible-playbook -i inventories/development.ini deploy.yml

When you update your Rails codebase, you push your changes to your Git repository; then you can redeploy using the same command above to update your changes in your server.


Software developer and consultant. I help companies build great products. I've worked with all kinds of companies. Contact me by email.

Get my new content delivered straight to your inbox. No spam, ever.

© 2021 Pedro Alonso