Abhinav Rai

Devops phase 2

We made the CRUD app which Creates, Reads, Updates and Deleted the user by api. We made user as resource in rails. Rspec for testing (100% code coverage) with BDD principle and coding etiquettes followed.

Now, we are to build a CI pipeline for the same. I used Gitlab as a Git service and it provides its in built CI pipeline. Make 2 stages for CI pipeline. Use ruby image 2.5

  1. Test — All the tests commands go here
test:
  stage: test
  script:
  - gem install bundler -v 1.16.3
  - bundle install
  - bundle exec rake db:migrate
  - bundle exec rspec
  1. Package
pakage:
  stage: package
  script:
  - rm -rf .git
  - gem install bundler -v 1.16.3
  - bundle install --deployment
  artifacts:
      name: "$CI_COMMIT_REF_NAME-$CI_JOB_NAME_$-$CI_JOB_STAGE"
      paths:
        - ./

Follow the documentation for more details of build stages of pipeline here.

The Vagrantfile for running the rails server is given below.

PS: All the secret variables are dummy variables so don’t waste your time on any hacking stuff!

Cool! We could get our server running by this vagrant file which installs dependencies of our rails app.

Now lets talk about chef! A person who makes the dishes based on the recipes you create. Based on Infrastructure as Code principle. Meaning installing ruby 2.5.1 is an instruction which needs a spec too (Using kitchen)! Also to implement DRY principle.

We create the user_api cookbook which has a lot of recipes. Recipes include installing ruby, installing artifact, installing bundler, installing openssl, running server, etc. Its based on the principle that every instruction is code.

Recipe for installing artifact from gitlab.

Integration test for the same.

describe file('/tmp/user_api_artifact.zip') do
  it { should exist }
end

describe command('unzip -v') do
  its('stdout') { should match(/UnZip 6.00/) }
end

describe file('/srv/user_api/') do
  its('type') { should eq :directory }
  it { should be_directory }
end

describe file('/srv/user_api/') do
  its('owner') { should eq 'vagrant' }
  its('group') { should eq 'vagrant' }
end

describe file('/srv/user_api/.bundle/config') do
  its('content') { should include "BUNDLE_PATH:" }
end

Recipe for starting the server as service and restarting on failure.

file '/etc/secret_key' do
  content 'SECRET_KEY_BASE=SECRET_KEY_FOR_PROD'
  mode '0755'
  owner 'vagrant'
  group 'vagrant'
  action :create
end

file '/etc/user_api_runner.sh' do
  content "#!/bin/bash\n\nbundle exec rails db:migrate RAILS_ENV=production\nbundle exec rails s -e production"
  mode '0755'
  owner 'vagrant'
  group 'vagrant'
  action :create
end

systemd_unit 'user_api.service' do
  content({
              Unit: {
                  Description: 'User_api rails service',
              },
              Service: {
                  WorkingDirectory: '/srv/user_api',
                  EnvironmentFile: '/etc/secret_key',
                  ExecStart: '/etc/user_api_runner.sh'
              }
          })
  action [:create, :enable, :start]
end

It starts the user_api_runner script.

Now, The install commands in vagrant file is replaced by fully automated chef recipes in it. Instead of shell provisioners, we will be using chef as provisioner.

All this automation is for one box!

Our server is up and running. But we need more than one instance of our application running. To distribute the load when the requests increase. We create another box which has a load balancer and balances the load. We are using HAProxy for the same with round robin algorithm to distribute the requests.

Vagrant.configure(2) do |config|
  (1..2).each do |i|
    config.vm.define "vm#{i}" do |worker|
      worker.vm.box = "ubuntu/xenial64"
      worker.vm.network "private_network", ip: "192.168.1.#{i}0"
      worker.vm.provision :chef_solo do |chef|
        chef.add_recipe "rails::ruby"
        chef.add_recipe "rails::bundler"
        chef.add_recipe "rails::artifact"
        chef.add_recipe "rails::openssl"
        chef.add_recipe "rails::run_server"
      end
    end
  end

  config.vm.define "vm_balancer" do |vm_balancer|
    vm_balancer.vm.box = "ubuntu/xenial64"
    vm_balancer.vm.network "private_network", ip: "192.168.1.30"
    config.vm.network "forwarded_port", guest: 80, host: 8080
    vm_balancer.vm.provision :chef_solo do |chef|
      chef.add_recipe "load_balancer::install_haproxy"
      chef.add_recipe "load_balancer::config_haproxy"
      chef.add_recipe "load_balancer::restart_haproxy"
    end
  end
end

As in above, we have 2 cookbooks. One is load_balancer to install and configure and restart HAProxy (on error) and other is rails to install ruby, bundler, openssl, download artifact from gitlab and then run the server.

In the next part, I will cover doing these things with Kubernetes instead of Vagrant VMs and why? Tune in for the 3rd blog.

You may contact the author at me@abhinavrai.com