Capistrano Setup: Initial & Continuous Deployment Guide

by Pedro Alvarez 56 views

Hey guys! Today, we're diving deep into setting up Capistrano for automated deployments, specifically focusing on two crucial deployment types: initial setup and continuous deployment. Our goal is to create a seamless process that takes a brand new EC2 instance from launch to a fully running application with just a single command. We'll also cover continuous deployment strategies for pushing out new features and enhancements. Let's get started!

Initial Deployment (deploy:initial)

Our primary objective with the initial deployment (deploy:initial) is to automate the entire setup process for a fresh EC2 instance. This includes everything from creating the deploy user and setting up permissions to installing dependencies, configuring Nginx, setting up symlinks, and integrating rbenv, Ruby, and Passenger. Think of it as a one-shot operation that transforms a bare-bones EC2 instance into a fully functional application server. This is super important because it saves time and reduces the risk of human error during the initial setup.

Setting Up the Deploy User and Permissions

First, we need to create a dedicated user for deployment. This is a security best practice, as it isolates the deployment process from other system users. We'll create a user, say deploy, and grant it the necessary permissions to deploy our application. This typically involves adding the user to a group that has write access to the deployment directory. The following snippet illustrates how we can achieve this using Capistrano tasks:

namespace :deploy do
  desc 'Create deploy user'
  task :create_deploy_user do
    on roles(:all) do
      execute "sudo adduser deploy --disabled-password --gecos 'Deploy User'"
      execute "sudo usermod -aG sudo deploy"
    end
  end

  desc 'Set up SSH key for deploy user'
  task :setup_ssh_key do
    on roles(:all) do
      execute "sudo mkdir -p /home/deploy/.ssh"
      execute "sudo chown -R deploy:deploy /home/deploy/.ssh"
      upload! StringIO.new(File.read("~/.ssh/id_rsa.pub")), "/tmp/id_rsa.pub"
      execute "sudo mv /tmp/id_rsa.pub /home/deploy/.ssh/authorized_keys"
      execute "sudo chown -R deploy:deploy /home/deploy/.ssh/authorized_keys"
      execute "sudo chmod 600 /home/deploy/.ssh/authorized_keys"
    end
  end

  before :check, :create_deploy_user
  before :check, :setup_ssh_key
end

This code snippet creates a deploy user, adds it to the sudo group, and sets up SSH key authentication. Security is paramount, and using SSH keys instead of passwords significantly enhances the security of our deployment process.

Installing Dependencies

Next up, we need to ensure that all the necessary dependencies are installed on the server. This includes system-level dependencies like git, nginx, and passenger, as well as Ruby-specific dependencies managed by Bundler. We can leverage Capistrano tasks to automate the installation of these dependencies. For instance, we can use apt-get (for Debian-based systems) or yum (for Red Hat-based systems) to install system-level packages.

namespace :deploy do
  desc 'Install system dependencies'
  task :install_dependencies do
    on roles(:all) do
      execute "sudo apt-get update"
      execute "sudo apt-get install -y git nginx passenger libnginx-mod-http-passenger"
    end
  end

  before :check, :install_dependencies
end

This task updates the package list and installs git, nginx, and passenger along with the necessary Nginx module. Automation is key here; by automating dependency installation, we ensure consistency across all our environments.

Setting Up Nginx Configuration

Configuring Nginx is a critical step in setting up our application server. We need to create an Nginx configuration file that tells Nginx how to handle requests for our application. This typically involves setting up a server block that listens on port 80 (or 443 for HTTPS) and proxies requests to our application server (Passenger in this case). We can use Capistrano to upload the Nginx configuration file to the server and create a symlink to enable it.

namespace :deploy do
  desc 'Upload Nginx configuration'
  task :upload_nginx_config do
    on roles(:web) do
      template 'config/nginx.conf.erb', '/tmp/nginx.conf'
      execute "sudo mv /tmp/nginx.conf /etc/nginx/sites-available/#{fetch(:application)}"
      execute "sudo ln -nfs /etc/nginx/sites-available/#{fetch(:application)} /etc/nginx/sites-enabled/#{fetch(:application)}"
      execute "sudo rm /etc/nginx/sites-enabled/default"
      execute "sudo systemctl restart nginx"
    end
  end

  before :deploy, :upload_nginx_config
end

This task uploads an Nginx configuration file (using a template), moves it to the appropriate directory, creates a symlink, and restarts Nginx. Proper configuration is crucial for ensuring our application is accessible and performs optimally.

Setting Up rbenv, Ruby, and Passenger

rbenv is a Ruby version manager that allows us to easily switch between different Ruby versions. Passenger is a web application server that integrates seamlessly with Nginx and provides a robust environment for running Ruby applications. Setting these up correctly is vital for our application's performance and stability. We'll use Capistrano tasks to install rbenv, install the required Ruby version, and configure Passenger.

namespace :deploy do
  desc 'Install rbenv and Ruby'
  task :install_rbenv do
    on roles(:all) do
      execute "sudo apt-get install -y rbenv ruby-build"
      execute "rbenv install #{fetch(:ruby_version)}"
      execute "rbenv global #{fetch(:ruby_version)}"
      execute "gem install bundler"
    end
  end

  desc 'Install Passenger'
  task :install_passenger do
    on roles(:all) do
      execute "gem install passenger"
      execute "passenger-install-nginx-module --auto --auto-download --prefix=/opt/nginx"
    end
  end

  before :deploy, :install_rbenv
  before :deploy, :install_passenger
end

These tasks install rbenv, Ruby, Bundler, and Passenger. Using version managers like rbenv helps us maintain consistency across our development and production environments.

Setting Up Let's Encrypt SSL Certificates

To secure our application with HTTPS, we'll use Let's Encrypt to obtain SSL certificates. Let's Encrypt provides free SSL certificates and an automated process for obtaining and renewing them. We can use the certbot tool to automate this process. Here’s how we can set it up:

namespace :deploy do
  desc 'Install Let's Encrypt certbot'
  task :install_certbot do
    on roles(:web) do
      execute "sudo apt-get install -y certbot"
    end
  end

  desc 'Obtain Let's Encrypt certificate'
  task :obtain_certificate do
    on roles(:web) do
      execute "sudo certbot --nginx -d #{fetch(:domain)}"
    end
  end

  before :deploy, :install_certbot
  after :deploy, :obtain_certificate
end

This task installs certbot and obtains an SSL certificate for our domain. HTTPS is essential for modern web applications, and Let's Encrypt makes it easy to implement.

Enhancing Security Against Modern Web Application Vulnerabilities

Security is a top priority, so we need to implement measures to protect against common web application vulnerabilities. This includes setting appropriate HTTP headers, configuring firewalls, and regularly updating our dependencies. Here are some enhancements we can add:

  • HTTP Headers: Set security-related HTTP headers like Strict-Transport-Security, X-Frame-Options, and Content-Security-Policy in our Nginx configuration.
  • Firewall: Configure a firewall (like ufw on Ubuntu) to restrict access to our server.
  • Regular Updates: Ensure that we regularly update our system packages and Ruby gems to patch security vulnerabilities.
namespace :deploy do
  desc 'Set security headers in Nginx config'
  task :set_security_headers do
    on roles(:web) do
      # Modify Nginx config to include security headers
    end
  end

  desc 'Configure firewall'
  task :configure_firewall do
    on roles(:all) do
      execute "sudo ufw enable"
      execute "sudo ufw allow 'Nginx Full'"
      execute "sudo ufw allow OpenSSH"
    end
  end

  before :deploy, :set_security_headers
  before :deploy, :configure_firewall
end

Proactive security measures are crucial for protecting our application and users.

Continuous Deployment (deploy)

Once our initial setup is complete, we need a streamlined process for deploying new features and enhancements. This is where continuous deployment (deploy) comes in. This deployment type should handle tasks such as updating gems and Ruby (if applicable), applying changes, and running migrations. Here’s how we can set it up:

Updating Gems and Ruby

Keeping our gems and Ruby version up-to-date is crucial for both security and performance. We can use Capistrano tasks to automate this process. This involves checking for updates, installing new versions, and ensuring that our application uses the latest gems.

namespace :deploy do
  desc 'Update gems'
  task :update_gems do
    on roles(:app) do
      within release_path do
        execute :bundle, :install, '--without development test', '--deploy', '--jobs 4', '--quiet' # Use bundle install for faster deployment without development and test dependencies. Add other arguments for specific needs.        
      end
    end
  end

  desc 'Update Ruby version'
  task :update_ruby do
    on roles(:app) do
      execute "rbenv install #{fetch(:ruby_version)}"
      execute "rbenv global #{fetch(:ruby_version)}"
    end
  end

  after :updated, :update_gems # It is crucial to update gems after the code has been updated.
  before :deploy, :update_ruby # Update ruby before deploying new code. Note: This should be used cautiously to avoid unnecessary updates.
end

The bundle install command installs the necessary gems, and the rbenv commands update the Ruby version. Regular updates ensure we're using the latest features and security patches.

Applying Changes and Running Migrations

Deploying changes involves updating the application code and running any necessary database migrations. Capistrano makes this process straightforward with its built-in tasks. We can use the deploy:migrate task to run migrations and ensure our database schema is up-to-date.

namespace :deploy do
  desc 'Run migrations'
  task :migrate do
    on roles(:db) do
      within release_path do
        execute :rails, 'db:migrate', 'RAILS_ENV=production' # Add your preferred way to execute Rails commands. 
      end
    end
  end

  after :deploy, :migrate # Run migrations after deploying new code.
end

This task runs the rails db:migrate command, ensuring our database schema is in sync with our application code. Database migrations are a critical part of the deployment process, ensuring our application can function correctly with the latest code.

Complete Capistrano Configuration Example

To give you a clearer picture, here’s a complete example of a deploy.rb file that incorporates all the tasks we’ve discussed:

# config valid for current version and patch releases of Capistrano
lock '~> 3.11'

set :application, 'qalab'
set :repo_url, '[email protected]:your-repo/qalab.git' # Replace with your repository URL.

set :deploy_to, '/var/www/qalab'
set :branch, ENV['BRANCH'] || :master
set :ruby_version, '2.7.0' # Replace with your desired Ruby version.
set :domain, 'your-domain.com' # Replace with your domain.

append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'

namespace :deploy do
  desc 'Create deploy user'
  task :create_deploy_user do
    on roles(:all) do
      execute "sudo adduser deploy --disabled-password --gecos 'Deploy User'"
      execute "sudo usermod -aG sudo deploy"
    end
  end

  desc 'Set up SSH key for deploy user'
  task :setup_ssh_key do
    on roles(:all) do
      execute "sudo mkdir -p /home/deploy/.ssh"
      execute "sudo chown -R deploy:deploy /home/deploy/.ssh"
      upload! StringIO.new(File.read("~/.ssh/id_rsa.pub")), "/tmp/id_rsa.pub"
      execute "sudo mv /tmp/id_rsa.pub /home/deploy/.ssh/authorized_keys"
      execute "sudo chown -R deploy:deploy /home/deploy/.ssh/authorized_keys"
      execute "sudo chmod 600 /home/deploy/.ssh/authorized_keys"
    end
  end

  desc 'Install system dependencies'
  task :install_dependencies do
    on roles(:all) do
      execute "sudo apt-get update"
      execute "sudo apt-get install -y git nginx passenger libnginx-mod-http-passenger"
    end
  end

  desc 'Upload Nginx configuration'
  task :upload_nginx_config do
    on roles(:web) do
      template 'config/nginx.conf.erb', '/tmp/nginx.conf'
      execute "sudo mv /tmp/nginx.conf /etc/nginx/sites-available/#{fetch(:application)}"
      execute "sudo ln -nfs /etc/nginx/sites-available/#{fetch(:application)} /etc/nginx/sites-enabled/#{fetch(:application)}"
      execute "sudo rm /etc/nginx/sites-enabled/default"
      execute "sudo systemctl restart nginx"
    end
  end

  desc 'Install rbenv and Ruby'
  task :install_rbenv do
    on roles(:all) do
      execute "sudo apt-get install -y rbenv ruby-build"
      execute "rbenv install #{fetch(:ruby_version)}"
      execute "rbenv global #{fetch(:ruby_version)}"
      execute "gem install bundler"
    end
  end

  desc 'Install Passenger'
  task :install_passenger do
    on roles(:all) do
      execute "gem install passenger"
      execute "passenger-install-nginx-module --auto --auto-download --prefix=/opt/nginx"
    end
  end

  desc 'Install Let's Encrypt certbot'
  task :install_certbot do
    on roles(:web) do
      execute "sudo apt-get install -y certbot"
    end
  end

  desc 'Obtain Let's Encrypt certificate'
  task :obtain_certificate do
    on roles(:web) do
      execute "sudo certbot --nginx -d #{fetch(:domain)}"
    end
  end

  desc 'Set security headers in Nginx config'
  task :set_security_headers do
    on roles(:web) do
      # Modify Nginx config to include security headers
    end
  end

  desc 'Configure firewall'
  task :configure_firewall do
    on roles(:all) do
      execute "sudo ufw enable"
      execute "sudo ufw allow 'Nginx Full'"
      execute "sudo ufw allow OpenSSH"
    end
  end

  desc 'Update gems'
  task :update_gems do
    on roles(:app) do
      within release_path do
        execute :bundle, :install, '--without development test', '--deploy', '--jobs 4', '--quiet' # Use bundle install for faster deployment without development and test dependencies. Add other arguments for specific needs.        
      end
    end
  end

  desc 'Run migrations'
  task :migrate do
    on roles(:db) do
      within release_path do
        execute :rails, 'db:migrate', 'RAILS_ENV=production' # Add your preferred way to execute Rails commands. 
      end
    end
  end

  before :check, :create_deploy_user
  before :check, :setup_ssh_key
  before :check, :install_dependencies
  before :deploy, :upload_nginx_config
  before :deploy, :install_rbenv
  before :deploy, :install_passenger
  before :deploy, :install_certbot
  before :deploy, :set_security_headers
  before :deploy, :configure_firewall
  after :deploy, :obtain_certificate
  after :updated, :update_gems # It is crucial to update gems after the code has been updated.
  after :deploy, :migrate # Run migrations after deploying new code.
end

And here’s an example of a nginx.conf.erb template:

server {
  listen 80;
  server_name <%= fetch(:domain) %>;

  root /var/www/<%= fetch(:application) %>/current/public;

  # Security Headers
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options "SAMEORIGIN";
  add_header X-Content-Type-Options "nosniff";
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";

  # Pass all requests to Passenger
  passenger_enabled on;
  passenger_user deploy;

  rails_env production;

  location ~ ^/assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  # Redirect HTTP to HTTPS
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name <%= fetch(:domain) %>;

  root /var/www/<%= fetch(:application) %>/current/public;

  # Security Headers
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options "SAMEORIGIN";
  add_header X-Content-Type-Options "nosniff";
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";

  # SSL Configuration
  ssl_certificate /etc/letsencrypt/live/<%= fetch(:domain) %>/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/<%= fetch(:domain) %>/privkey.pem;

  # Pass all requests to Passenger
  passenger_enabled on;
  passenger_user deploy;

  rails_env production;

  location ~ ^/assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }
}

This setup ensures our application is served over HTTPS with essential security headers. Using templates allows us to dynamically generate configuration files, making our deployments more flexible and maintainable.

Running the Deployments

With everything configured, you should be able to set the EC2_HOST environment variable or add the host to targets.rb and then run:

bundle exec cap production deploy:initial

This command will perform the initial setup. For continuous deployments, you can use:

bundle exec cap production deploy

This will deploy the latest changes to your application. Simple commands for complex processes make deployments much easier and less error-prone.

Conclusion

Setting up Capistrano for automated deployments can seem daunting at first, but it’s a worthwhile investment. By automating our deployment process, we reduce the risk of errors, save time, and ensure consistency across our environments. We’ve covered both initial setup and continuous deployment, along with security enhancements to protect our application. Now you're equipped to streamline your deployment workflow! Keep coding, guys!