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 Podfilter.de. 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:
- 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)
Make sure to have Ansible installed in a non-ancient version, like 1.9:
$ ansible --version ansible 1.9.3
Rolefile.yml and add the roles:
- src: "https://github.com/dresden-weekly/ansible-rails" version: "develop" name: "dresden-weekly.Rails" - src: "debops.users" version: "v0.1.0" - src: 'https://github.com/ANXS/postgresql.git' name: 'ANXS.postgresql' version: 'master' - name: 'geerlingguy.redis' version: 'master' src: 'https://github.com/geerlingguy/ansible-role-redis.git'
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.
[myapp] some.fqdn-host.com ansible_ssh_host=184.108.40.206 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
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: "" comment: "Application user" sshkeys: - "" - role: ANXS.postgresql postgresql_databases: - name: '' postgresql_users: - name: '' 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: "" rails_user_bashrc_lines: - "cd || true" - "cd || 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: - "" 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.yml pubkeys: stefan: 'ssh-rsa AAAAB3Nza........... user@host' somebody_else: 'ssh-rsa AAAAB3Nza........... user@host' deploy: 'ssh-rsa AAAAB3Nza........... user@host'
Additionally, you need the individual app config, e.g.
# group_vars/myapp.yml ruby_version: '2.2.3' # where and whom to deploy app_name: myapp app_user: '' app_path: '/home//app' # what ode to use git_url: 'https://github.com/zealot128/podfilter.git' git_branch: 'master' rails_env: production # nginx settings app_domain: 'myapp.de' app_hosts: 'myapp.de www.myapp.de' # on which host to run migrations rails_primary_node: '' rails_database_name: '_' rails_database_user: '' postgresql_version: '9.4' postgresql_locale: 'de_DE.UTF-8' # add additional folders to symlink, like uploads rails_deploy_custom_shared_folders: [ 'public/uploads' ] # Sidekiq with 5 workers rails_sidekiq_enabled: true sidekiq_configuration_concurrency: 5 install_redis: true # enable cronjobs with whenever whenever: true # global user env variables: You can either use those or the config/secrets.yml for # getting configuration and secrets into your app rails_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: '' # by default, only a couple of files/folders are copied over, you might # whant to add additional rails_deploy_custom_archive: - bin - Gemfile.lock #- private #- engines # automatically backup database daily database_backup_name: "" # provide secret files that are copied over on each deploy rails_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: '' encoding: UTF8 pool: 30 # You might want to symlink additional files # rails_shared_files: # - db/production.sqlite3
$ 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: '' 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 --deploymentto shared/bundle
- Gracefully shutdown any existing Sidekiq workers (Tell sidekiq to not accept any further work)
- compile the assets
- run migrations
- change the
- remove everything but the most recent 5 releases
- Handlers in the end:
- touch tmp/restart.txt and trigger Passenger restart
- restart Sidekiq workers
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.