Deploying Ruby Apps with Bare Metal: A New Type of VM

    Glenn Goodrich
    Share

    This article was sponsored by CenturyLink Cloud. Thank you for supporting the sponsors who make SitePoint possible.

    CenturyLink is a company providing multiple Platform-as-a-Service (PaaS) offerings. When choosing which service to use for a particular application, there are basically two tracks. The first track is AppFog, which is a pure PaaS like you know and love. AppFog provides the infrastructure and you supply the application and data. AppFog supplies a Command Line Interface (CLI) for deployment, as well as a nice dashboard for monitoring your resources.

    The other tract offered by CenturyLink is Infrastructure-as-a-Service, both with virtual machines and another product called Bare Metal, which we’ll cover in this article. Bare Metal offers “the computing power of a physical server, plus the automation and pay-as-you-go flexibility of virtual machines”. Bare Metal servers are NOT shared VMs, so you don’t have to worry about sharing resources. However, they operate like a VM, so you get the responsiveness and rapid deployment of VMs with the isolation of a physical machine.

    You might use Bare Metal for a database server or an application that doesn’t fit well into other virtualized environments. Tasks like batch computing, where you need a large amount of computational resources for short bursts are great for Bare Metal. Also, items like analytics are a good fit, as you can manage the complexity of software like Hadoop and the unique needs of analytics computing.

    Another interesting feature is that Bare Metal servers are integrated into the CenturyLink Cloud, right along services like AppFog. This allows you to mix PaaS applications, databases, expensive computing tasks, and just about anything else, managing them all from the same dashboard. To my knowledge, no other PaaS offers such a menu, and you’d have to do a ton of work on Amazon Web Services (AWS) to get the same convenience.

    Here is a comparison of Bare Metal servers to other server options.

    In today’s post, I will walk through creating a Bare Metal server and deploying a (very) simple Rails application to it.

    Setup

    Before we start on our journey, you’ll need an account on CenturyLink in order to follow along. There are free trials (they do require a payment method, FYI). So, head over to the CenturyLink site and click “Free Trial”. Follow the sign up procedure and you’re ready to go.

    Provision a Bare Metal Server

    After logging into the CenturyLink Control Portal, the dashboard is presented:

    CenturyLink control portal dashboard

    To start the process of creating a server, click on the large “Create a Server” or the “+” on the sidebar and choose “Server”.

    CenturyLink control portal dashboard

    Bare Metal servers are not available in all Data Centers. I had to choose “VA1 – US East (Sterling)” from the “data center” drop down in order to have ‘Bare Metal’ as an option for “server type”. If you’re in a data center where Bare Metal servers should be available, but you can’t see them as an option, contact Customer Care to make sure they’re enabled for your account.

    CenturyLink server creation

    CenturyLink has some great documentation on how to provision a server, which you can follow here. I used the following options:

    • group member of: Default Group
    • server type: Bare Metal
    • configuration: 4 cores (the smallest option)
    • operating system: Ubuntu 14 64-bit
    • server name: sprail
    • primary dns: 8.8.8.8 (Google DNS)
    • secondary dns: 8.8.4.4 (Google DNS)

    After hitting ‘Create Server’, you’ll see the following:

    CenturyLink server request details

    If you stay on this page, the server will go through 3 steps of provisioning where it is validated, the resources are requested, and it is started.

    When we provisioned our server, the configuration requires that a group is selected. In our example, we chose ‘Default Group’. I bet you were wondering what a “group” is, weren’t you? On CenturyLink Cloud, groups allow you to manage multiple servers in bulk. Examples of what can be done in groups are:

    • Bulk Operations, such as power up/down, etc.
    • Group your servers and resources by project, or any other logical reason.
    • Create parent-child relationships between servers, with cascading tasks, etc.
    • Support for complex billing, where you might charge a client for their usage.

    Groups are very powerful, and you should read up on them to learn more.

    It’s worth noting that CenturyLink does offer a REST API that provides endpoints for just about anything you can do via the dashboard, including provisioning servers.

    Once you have chosen and sized your server, we’ll start down the path of creating a Rails application.

    Deploying a Rails Application

    When I return to the dashboard, my data center is now listed:

    CenturyLink control portal dashboard

    Clicking through on that data center leads to a data center specific view, which isn’t very interesting yet. To get to the server, expand the “Default Group” folder in the left-hand list of Data Centers and select your server:

    Data center specific view

    Here, you can retrieve the admin credentials (setup during provisioning) and see the configuration of the server, including the IP address of the server to connect to. Make sure you are connected to the VPN that is provided for you when you create the server. (See the instructions at “How To Configure Client VPN”.).

    Now you will be able to SSH into the box using ssh root@. When you’re prompted for a password, use the password that you specified during provisioning.

    As I mentioned, Bare Metal servers act like VMs, but they are not shared. As such, deploying Rails to a Bare Metal Server is exactly the same as setting up any POSIX box as a Rails server. The steps are:

    1. Create a deploy user.
    2. Install a web server (Nginx, in our case).
    3. Capify your Rails app
    4. Add the Rails app to source control.
    5. Push your changes.
    6. Run the deploy task.

    The first 4 steps are one-time only, so once the deployment is working, it’s a simple (and very scriptable) two-step process to deploy your app to a very powerful standalone machine.

    Server-Side Setup

    For these tasks, you need to be SSH’d into the Bare Metal Server.

    Create a Deploy User

    If you aren’t familiar with basic Unix tasks, like creating a user, it’s not too hard. Type the following on the server:

    $ adduser deploy
    ...Answer the prompts, give the user a good password...
    $ gpasswd -a demo sudo

    That last command adds the deploy user to the sudoers group so we can run higher privileged commands when needed.

    Create/Use Public Key

    Your deployments will go much more smoothly if you use Public Key Authentication to SSH into the box as the deploy user. If you have a key pair on your LOCAL machine, you can use it, otherwise use ssh-keygen to create one. Again, this is on your local/development machine:

    $ ssh-keygen
    ...ssh-keygen output...
    Generating public/private rsa key pair.
    Enter file in which to save the key (/Users/your-user-name/.ssh/id_rsa):

    Just hit return to accept that file name. You’ll be prompted for a pass phrase, which I recommend you leave blank for now. If you choose to add a pass phrase (which is, btw, more secure) you will be prompted for it every time you deploy.

    You should now have a id_rsa.pub file in your ~/.ssh directory. It needs to be copied to the server. This .pub file needs to be copied to the server. The easiest way to do this is via the ssh-copy-id command, which was made for just this purpose. On your LOCAL machine:

    $ ssh-copy-id deploy@SERVER-IP-ADDRESS

    you will be prompted for the deploy user’s password, and the public key file will then be copied.

    At this point, ssh deploy@SERVER-IP should “just work” without prompting for a password.

    Install Nginx

    Let’s get Nginx installed. Packages managers make this simple. As root on the server, type:

    $ apt-get install nginx git-core nodejs -y
    ... All kinds of output..

    OK, Nginx is installed. If you open up a browser and go to **http://SERVER-IP-ADDRESS, you should see Nginx’s welcome page:

    Install Ruby (RVM) and Friends

    I love RVM. It makes life easier. Let’s install it on the server so we easily upgrade Ruby as our incredible app grows and lives forever.

    SSH into the box as the deploy user.

    $ gpg --keyserver hkp://keys.gnupg.net --recv-keys
    409B6B1796C275462A1703113804BB82D39DC0E3
    ...output..
    gpg: Total number processed: 1
    gpg:               imported: 1  (RSA: 1)
    
    $ \curl -sSL https://get.rvm.io | bash -s stable --ruby
    ...this will prompt for the deploy user password...
    ...then install ruby 2.2.1...
    Creating alias default for ruby-2.2.1...
    
    * To start using RVM you need to run `source /home/deploy/.rvm/scripts/rvm`
      in all your open shell windows, in rare cases you need to reopen all shell windows.
    
    $ source /home/deploy/.rvm/scripts/rvm
    $ ruby -v
    ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux]

    Excellent. Ruby is installed. We’ll need Bundler too, since Gemfiles rule the Ruby world.

    $ gem install bundler --no-ri --no-rdoc
    Successfully installed bundler-1.10.6
    1 gem installed

    Git

    Our deploy will pull the latest code from source control. For this app, I am going to use Github. Remember, we installed git in a previous apt-get install step when we installed nginx. The deployment process will need to be able to access our git repository without logging in, so here’s another public key authentication scenario. The deploy user doesn’t have a key file yet, so generate one:

    # AS THE DEPLOY USER ON THE SERVER
    $ ssh-keygen -t rsa
    Generating public/private rsa key pair.
    Enter file in which to save the key (/home/deploy/.ssh/id_rsa):
    Enter passphrase (empty for no passphrase):
    Enter same passphrase again:
    Your identification has been saved in /home/deploy/.ssh/id_rsa.
    Your public key has been saved in /home/deploy/.ssh/id_rsa.pub.
    The key fingerprint is:
    90:4d:56:9c:50:7d:b3:05:26:ad:61:64:4b:84:05:26 deploy@VA1SPGGSPRAIL01
    The key's randomart image is:
    ...a really weird piece of ASCII art...

    With our key pair in place, the public portion of the key needs to be added to Github. Basically, login to Github, go to your account settings Click on ‘SSH keys’, then ‘Add SSH Key’:

    If you’ve done it right, typing ssh -T git@github.com yields:

    $ ssh -T git@github.com
    Warning: Permanently added the RSA host key for IP address '192.0.2.0' to the list of known hosts.
    Hi! You've successfully authenticated, but GitHub does not provide shell access.

    The Rails App

    Since the focus of this article is deployment, the Rails app will be relatively simple. We’ll create a single controller and view, and change/add some gems to represent a somewhat “real” deployment.

    I am using Ruby 2.2 and Rails 4.2.4, and I am back on my local machine. After typing rails new, it’s time to modify the Gemfile. I simply added Puma and the various Capistrano gems. It looks something like this:

    
    ...other gems...
    gem 'puma'
    
    group :development do
      gem 'web-console', '~> 2.0'  # this was already here
      gem 'pry-rails' # I love Pry
    
      gem 'spring'
    
      gem 'capistrano',         require: false
      gem 'capistrano-rvm',     require: false
      gem 'capistrano-rails',   require: false
      gem 'capistrano-bundler', require: false
      gem 'capistrano3-puma',   require: false
    end
    

    Make these changes and bundle away.

    I mentioned this is going to be a simple app. Quickly generate a Home scaffold:

    $ rails g scaffold Thing names:string purpose:string
    ...lots of output...
    $ rake db:migrate
    ...more output...

    Change the root route to point at our list of Things:

    # config/routes.rb
    Rails.application.routes.draw do
      resources :things
      root to: 'things#index'
    end

    Last file change is to add a value in secrets

    Staring the server (rails s) and opening http://localhost:3000 will allow you to see our progress and make things to your heart’s content.

    Stop your local server (CTRL-C) and let’s get this Things app under source control.

    Git

    In the root of your application, type:

    git init .
    git add .
    gc -m "Initial commit"

    Now, we have a local git repository, which we need to push up to Github. Open a browser, go to http://github.com, login, and create a repository for your application. Add that repository as the origin remote to your local and push up your changes. You should now have a Github repository with the Rails app. Here’s mine.

    Capistrano

    Return to your application root on your local machine and type:

    $ cap install
    mkdir -p config/deploy
    create config/deploy.rb
    create config/deploy/staging.rb
    create config/deploy/production.rb
    mkdir -p lib/capistrano/tasks
    create Capfile
    Capified

    In the Capfile (created by the above command), require the various capistrano gems we included to support our deployment:

    # Capfile
    require 'capistrano/setup'
    
    # Include default deployment tasks
    require 'capistrano/deploy'
    require 'capistrano/rails'
    require 'capistrano/bundler'
    require 'capistrano/rvm'
    require 'capistrano/puma'
    
    # Load custom tasks from `lib/capistrano/tasks` if you have any defined
    Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

    These tasks will setup Ruby, install the bundle, etc. Capistrano is nice.

    cap install also creates a config/deploy.rb file and a config/deploy directory. Change the config/deploy.rb file to look like:

    # config valid only for current version of Capistrano
    lock '3.4.0'
    
    set :application,     'Bare Metal Things'
    set :repo_url,        'git@github.com:sitepoint-editors/bare-metal-fun.git'
    set :server           '206.128.156.201', roles: [:web, :app, :db], primary: true
    
    set :user             'deploy'
    set :puma_threads,    [4, 16]
    set :puma_workers,    0
    
    # Default branch is :master
    # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
    
    # Default deploy_to directory is /var/www/my_app_name
    set :deploy_to,     "/home/#{fetch(:user)}/apps/#{fetch(:application)}"
    set :use_sudo,      false
    set :deploy_via,    :remote_cache
    
    # Puma
    set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
    set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
    set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
    set :puma_access_log, "#{release_path}/log/puma.error.log"
    set :puma_error_log,  "#{release_path}/log/puma.access.log"
    set :puma_preload_app, true
    set :puma_worker_timeout, nil
    set :puma_init_active_record, true  
    set :ssh_options,     { forward_agent: true, user: fetch(:user) }
    
    # Default value for :scm is :git
    # set :scm, :git
    
    # Default value for :format is :pretty
    # set :format, :pretty
    
    # Default value for :log_level is :debug
    # set :log_level, :debug
    
    # Default value for :pty is false
    # set :pty, true
    
    # Default value for :linked_files is []
    # set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
    
    # Default value for linked_dirs is []
    # set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
    
    # Default value for default_env is {}
    # set :default_env, { path: "/opt/ruby/bin:$PATH" }
    
    # Default value for keep_releases is 5
    set :keep_releases,   5
    
    namespace :deploy do
      after :restart, :clear_cache do
        on roles(:web), in: :groups, limit: 3, wait: 10 do
          # Here we can do anything such as:
          # within release_path do
          #   execute :rake, 'cache:clear'
          # end
        end
      end
    end

    Commit your changes to git, push it to the origin repository, then deploy the application. Deploying the application is simply a matter of typing:

    $ cap production deploy
    ...loads of output...
    DEBUG [29269dca] Command: cd /home/deploy/apps/bare_metal/current && ( RACK_ENV=production ~/.rvm/bin/rvm default do bundle exec puma -C /home/deploy/apps/bare_metal/shared/puma.rb --daemon )
    DEBUG [29269dca]        Puma starting in single mode...
    DEBUG [29269dca]        * Version 2.14.0 (ruby 2.2.1-p85), codename: Fuchsia Friday
    DEBUG [29269dca]        * Min threads: 4, max threads: 16
    DEBUG [29269dca]        * Environment: production
    DEBUG [29269dca]        * Daemonizing...
    INFO [29269dca] Finished in 0.480 seconds with exit status 0 (successful)

    Unfortunately, this won’t work entirely, even if it seems like it did. I run the first production deploy to make sure permissions are OK and get the Capistrano directory structure made. However, we need to add a couple of directories based on our Puma configuration. SSH into the server as deploy and type:

    $ mkdir apps/bare_metal/shared/tmp/sockets -p
    $ mkdir apps/bare_metal/shared/tmp/pids -p
    $ mkdir apps/bare_metal/shared/config -p

    Now, Puma has a permanent spot to write the files it needs. Although, that last directory is for something else: secrets.

    Secrets

    Handling secrets in Rails has always been, well, fun. For our simple app, we just need to have a local value forSECRET_KEY_BASE, so I recommend we put a copy of config/secrets.yml on the server and then symlink it on deployment. So, open up that file locally, and put a real token value in for production. Change:

    production:
      secret_key_base: 

    to

    production: 
      secret_key_base: 0c2e91d623cd62510e1ba6fc9ed7313461dc13b2068ff692f3a1803891870e6bb77c05bcfe27f7065e4fb1c380bd7fc720a336ea0ae231bf3bd32ecc34f8282b

    You should probably use your own token, which you can generate with rake secret.

    Now, copy that secrets.yml to the shared config directory on the server:

    scp config/secrets.yml deploy@SERVER-IP:/home/deploy/apps/bare_metal/shared/config

    Finally, add a task to the config/deploy.rb file to symlink that file on deployment:

    ## config/deploy.rb
    namespace :deploy do
      ...other tasks...
    
      desc "Link shared files"
      task :symlink_config_files do
        on roles(:web) do
          symlinks = {
            #"#{shared_path}/config/database.yml" => "#{release_path}/config/database.yml",
            "#{shared_path}/config/secrets.yml" => "#{release_path}/config/secrets.yml"
          }
          execute symlinks.map{|from, to| "ln -nfs #{from} #{to}"}.join(" && ")
        end
      end
    
      before 'deploy:assets:precompile', :symlink_config_files

    And then remove that file from git:

    git rm secrets.yml

    Now, a cap production deploy should get it done.

    Nginx Configuration

    SSH into the server as root and type

    vi /etc/nginx/sites-enabled/default

    Replace the entire contents of this file with:

    
    upstream puma {
      server unix:///home/deploy/apps/bare_metal/shared/tmp/sockets/bare_metal-puma.sock;
    }
    
    server {
      listen 80 default_server deferred;
      # server_name example.com;
    
      root /home/deploy/apps/bare_metal/current/public;
      access_log /home/deploy/apps/bare_metal/current/log/nginx.access.log;
      error_log /home/deploy/apps/bare_metal/current/log/nginx.error.log info;
    
      location ^~ /assets/ {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
      }
    
      try_files $uri/index.html $uri @puma;
      location @puma {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    
        proxy_pass http://puma;
      }
    
      error_page 500 502 503 504 /500.html;
      client_max_body_size 10M;
      keepalive_timeout 10;
    }
    

    You’ll need to restart nginx:

    nginx -s stop
    nginx

    Success! The application is now running on our Bare Metal server. Going forward, deploying a new application is a simple as:

    1. Make changes
    2. Commit changes to git and push to Github.
    3. cap production deploy

    Conclusion

    This tutorial walked through deploying a Rails application to a CenturyLink Bare Metal server. The process of deploying the application really wasn’t much different than a regular server, once the Bare Metal server was provisioned. The advantages of using a Bare Metal server make this environment superior to a vanilla, cloud-based virtual machine. There is no worrying about shared resources, as Bare Metal servers are isolated like a physical machine. Bare Metal servers deploy faster, so you’ll be able to scale up when needed. Add in all the services that CenturyLink offers, and your entire DevOps needs can be completely met with a single provider.