Deploying Rails with Ansible with Dresden-Weekly toolbox (PostgreSQL, RVM, Sidekiq, Passenger)


In the past I used both Ansible and Capistrano to deploy my own Rails apps (as well as at my company); Ansible, for installing the provisioning, user accounts, and key and secret management, and Capistrano for each individual deployment.

At Dresden-Weekly, a regularly meetup in Dresden, other developers faced similar deployment situations. Thus, we decided to throw our solutions together and create a suite of Rails deployment recipes (Ansible roles, as you will). As of today, Ansible-Rails consists of than 30 individual roles that are all part of the same repository and Ansible Galaxy role. All these roles are intended to be used together, as they use the same pool of variables.

Now, I want to show you on how to use those roles for a complete deployment of I will use bare Ansible without an intermediate virtual environment, like Vagrant Ansible remote . If you are on Windows or can't install recent versions of Ansible, this might be interesting for you.

With Ansible, we are going to provide:

PostgreSQL Sidekiq Background workers as Userjobs Redis (as needed by Sidekiq) correct Ruby version with RVM and dependencies imagemagick for image upload resizings Logrotate for app logs daily database dumps Nginx + Passenger (without SSL, see bottom for explanation) Table of Contents Installation

Make sure to have Ansible installed in a non-ancient version, like 1.9:

$ ansible --versionansible 1.9.3

Add a Rolefile.yml and add the roles:

- src: "" version: "develop" name: "dresden-weekly.Rails"- src: "debops.users" version: "v0.1.0"- src: '' name: 'ANXS.postgresql' version: 'master'- name: 'geerlingguy.redis' version: 'master' src: ''

For this example, I will install the most recent version of dresden-weekly.rails, as well as debops.user, Redis, and Postgresql roles.

Install the specified roles:

$ ansible-galaxy install -r Rolefile.yml --ignore-errors Inventory & Provisioning

Next, add the server information to your inventory file (any file in your project directory), e.g. production :

[myapp] ansible_ssh_host= ansible_ssh_user=root[apps:children]myapp

I recommend using a group or host for each app. Thus, you can use the same deployment script for all your apps and use a separate configuration for each app as a group_vars or host_vars file.

Here is a provisioning script:

# provision.yml- name: Install Rails hosts: apps sudo: yes tags: - provisioning vars_files: - group_vars/pubkeys.yml vars: database_backup_base_dir: "/backup/databases" pre_tasks: # I've had problems with locale and postgresql, this might help - locale_gen: name=de_DE.UTF-8 state=present - locale_gen: name=en_US.UTF-8 state=present roles: - role: debops.users users_list: - name: "{{ app_user }}" comment: "Application user" sshkeys: - "{{pubkeys.some_user}}" - role: ANXS.postgresql postgresql_databases: - name: '{{rails_database_name}}' postgresql_users: - name: '{{rails_database_user}}' role_attr_flags: CREATEDB,SUPERUSER - role: geerlingguy.redis when: install_redis - dresden-weekly.Rails/ruby/postgresql - dresden-weekly.Rails/ruby/rvm # imagemagick + rmagick dependencies + pngquant jpegoptim - dresden-weekly.Rails/ruby/imagemagick - dresden-weekly.Rails/rails/folders - dresden-weekly.Rails/rails/logrotate # set environment variables and profile script that changes to the app folder on login - role: dresden-weekly.Rails/user/profile rails_user_name: "{{ app_user }}" rails_user_bashrc_lines: - "cd {{ RAILS_APP_BASE_PATH }} || true" - "cd {{ RAILS_APP_CURRENT_PATH }} || true" # install nginx + configure app-site and enable it - dresden-weekly.Rails/nginx/passenger # simple daily crontab dump of pg database - sufficient for many app sizes - dresden-weekly.Rails/database/backup # Sidekiq + Upstart userjobs - role: dresden-weekly.Rails/rails/jobs/sidekiq when: 'rails_sidekiq_enabled is defined and rails_sidekiq_enabled == true' - role: dresden-weekly.Rails/upstart/userjobs users: - "{{app_user}}" when: 'rails_sidekiq_enabled is defined and rails_sidekiq_enabled == true'

I've referenced an additional file, group_vars/pubkeys , where I store various SSH pubkeys. The file looks like this:

# group_vars/pubkeys.ymlpubkeys: stefan: 'ssh-rsa AAAAB3Nza........... [email protected]' somebody_else: 'ssh-rsa AAAAB3Nza........... [email protected]' deploy: 'ssh-rsa AAAAB3Nza........... [email protected]'

Additionally, you need the individual app config, e.g. group_vars/myapp.yml :

# group_vars/myapp.ymlruby_version: '2.2.3'# where and whom to deployapp_name: myappapp_user: '{{app_name}}'app_path: '/home/{{app_user}}/app'# what ode to usegit_url: ''git_branch: 'master'rails_env: production# nginx settingsapp_domain: ''app_hosts: ''# on which host to run migrationsrails_primary_node: '{{inventory_hostname}}'rails_database_name: '{{app_name}}_{{rails_env}}'rails_database_user: '{{app_user}}'postgresql_version: '9.4'postgresql_locale: 'de_DE.UTF-8'# add additional folders to symlink, like uploadsrails_deploy_custom_shared_folders: [ 'public/uploads' ]# Sidekiq with 5 workersrails_sidekiq_enabled: truesidekiq_configuration_concurrency: 5install_redis: true# enable cronjobs with wheneverwhenever: true# global user env variables: You can either use those or the config/secrets.yml for# getting configuration and secrets into your apprails_user_env: SIDEKIQ_WEB_USER: admin SECRET_TOKEN: 'rake secret output here' TWITTER_CONSUMER_SECRET: key TWITTER_CONSUMER_KEY: secret SIDEKIQ_WEB_PASSWORD: password RAILS_ENV: '{{rails_env}}'# automatically backup database dailydatabase_backup_name: "{{rails_database_name}}"# provide secret files that are copied over on each deployrails_provisioned_files: - file: config/secrets.yml yaml: production: http_username: admin http_password: somepassword secret_key_base: 'tip: run rake secret in a rails app to get a good secret' twitter_consumer_key: "somekey" twitter_consumer_secret: "somesecret" twitter_access_token: "somekey" twitter_access_token_secret: "somesecret" - file: config/database.yml yaml: production: adapter: postgresql database: '{{rails_database_name}}' encoding: UTF8 pool: 30# You might want to symlink additional files# rails_shared_files:# - db/production.sqlite3

Run it!

$ ansible-playbook -i production provision.yml

You only need to run the provision.yml once or when you change variables, like Passenger/Nginx options (more runs should not be harmful though)..

Run an individual deploy

Now everything is set up. You can deploy the app code. This task is intended to be run every time you want to deploy app changes. Here a deployment playbook:

# deploy.yml- name: 'Deploy app' hosts: apps sudo: yes tags: deploy sudo_user: '{{app_user}}' vars: profile: '/bin/bash -lc -- ' roles: - dresden-weekly.Rails/rails/folders - dresden-weekly.Rails/rails/create-release - role: dresden-weekly.Rails/rails/jobs/sidekiq/restart when: 'rails_sidekiq_enabled is defined and rails_sidekiq_enabled == true' - dresden-weekly.Rails/rails/tasks/bundle - dresden-weekly.Rails/rails/tasks/compile-assets - dresden-weekly.Rails/rails/tasks/migrate-database - dresden-weekly.Rails/rails/update-current - role: dresden-weekly.Rails/rails/tasks/whenever when: whenever - dresden-weekly.Rails/rails/cleanup-old-releases

Yeah, that's it. Run it to deploy the code:

$ ansible-playbook -i production deploy.yml

This will deploy the app similar like Capistrano:

Create folders app/releases app/shared app/repo Checkout Code to app/repo, export code to releases/RELEASE_ID bundle install --deployment to shared/bundle Gracefully shutdown any existing Sidekiq workers (Tell sidekiq to not accept any further work) compile the assets rake assets:precompile run migrations change the current symlink update crontab through whenever remove everything but the most recent 5 releases Handlers in the end: touch tmp/restart.txt and trigger Passenger restart restart Sidekiq workers

Feel free to browse the other roles and each role's different configuration option. Also check out the example deployment repositories .

If you miss some essential roles or configuration options, feel free to add issues (or Pull Requests ;-D). For example, the current Nginx Passenger role does not support SSL, as I use a proxy for my own setup und don't need SSL on the app server.