├── .gitignore ├── README.md ├── ansible.cfg ├── hosts.ini ├── server-env.yml ├── server-provision.yml ├── templates ├── env.j2 ├── master.key.j2 ├── nginx_app.conf.j2 ├── rbenv.j2 ├── sidekiq.service.j2 └── sudoers_passenger.j2 └── vars ├── envs.yml └── vars.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rails Ansible 2 | 3 | Note: Storing secret in plaintext in playbook isn't a good practice, I have written this originally for absolute beginner to DevOps. If you are using this on production, I would advise look into using secrets management, like Ansible vault : https://docs.ansible.com/ansible/latest/user_guide/vault.html 4 | 5 | 6 | 7 | Here's an introductory guide on using ansible vault 8 | 9 | To encrypt your environment vars, you can use the `ansible-vault encrypt` command like this : 10 | `ansible-vault encrypt vars/envs.yml vars/vars.yml` 11 | 12 | 13 | 14 | You can view the content of the encrypted files like this : 15 | `ansible-vault view vars/envs.yml vars/vars.yml` 16 | 17 | 18 | 19 | When running playbook with encrypted file, you need to provide the password you used to encrypt the file. 20 | You can use `--ask-vault-pass` to supply the vault password at runtime, a prompt will appear for you input the password. 21 | `ansible-playbook server-provision.yml --ask-vault-pass` 22 | 23 | 24 | 25 | 26 | ### About 27 | 28 | Note: This Ansible script is for Ubuntu 20.04 LTS server. 29 | This Ansible script will setup a server with the following components 30 | 31 | 1. Ruby 32 | 2. Nginx web server 33 | 3. Passenger app server 34 | 4. PostgreSQL 35 | 5. Sidekiq (Optional) 36 | 37 | 38 | To learn how to use this script, refer to this post : [https://rubyyagi.com/rails-deploy-automate-ansible/](https://rubyyagi.com/rails-deploy-automate-ansible/) -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory=hosts.ini 3 | 4 | [ssh_connection] 5 | ssh_args = -o ControlMaster=auto -o ControlPersist=300s 6 | control_path = none -------------------------------------------------------------------------------- /hosts.ini: -------------------------------------------------------------------------------- 1 | # comment 2 | [web] 3 | 128.199.213.18 4 | 5 | [all:vars] 6 | ansible_user=root 7 | ansible_ssh_private_key_file="~/.ssh/id_rsa" -------------------------------------------------------------------------------- /server-env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: 'Update environment variables, master key and restart nginx' 3 | hosts: web 4 | user: "{{ deploy_user }}" 5 | 6 | vars_files: 7 | - vars/vars.yml 8 | - vars/envs.yml 9 | 10 | tasks: 11 | - name: Create rbenv directory 12 | file: 13 | path: "/home/{{ deploy_user }}/{{ app_name }}" 14 | state: directory 15 | owner: "{{ deploy_user }}" 16 | group: "{{ deploy_user }}" 17 | 18 | - name: Update rbenv vars 19 | template: 20 | src: templates/rbenv.j2 21 | dest: "/home/{{ deploy_user }}/{{ app_name }}/.rbenv-vars" 22 | owner: "{{ deploy_user }}" 23 | group: "{{ deploy_user }}" 24 | 25 | - name: Update /etc/environment 26 | template: 27 | src: templates/env.j2 28 | dest: "/etc/environment" 29 | become: true 30 | 31 | - name: Create shared config directory 32 | file: 33 | path: "/home/{{ deploy_user }}/{{ app_name }}/shared/config" 34 | state: directory 35 | owner: "{{ deploy_user }}" 36 | group: "{{ deploy_user }}" 37 | 38 | - name: Update master key 39 | template: 40 | src: templates/master.key.j2 41 | dest: "/home/{{ deploy_user }}/{{ app_name }}/shared/config/master.key" 42 | owner: "{{ deploy_user }}" 43 | group: "{{ deploy_user }}" 44 | 45 | - name: Restart nginx service 46 | service: 47 | name: nginx 48 | state: restarted 49 | become: true -------------------------------------------------------------------------------- /server-provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: web 3 | become: true 4 | 5 | vars_files: 6 | - vars/vars.yml 7 | 8 | pre_tasks: 9 | - name: Update apt cache if needed. 10 | apt: update_cache=yes cache_valid_time=3600 11 | 12 | handlers: 13 | - name: restart sshd 14 | service: 15 | name: sshd 16 | state: restarted 17 | 18 | tasks: 19 | - name: Create the user for deployment purpose 20 | user: 21 | name: "{{ deploy_user }}" 22 | password: "{{ deploy_user_password | password_hash('sha512') }}" 23 | groups: 24 | - sudo 25 | state: present 26 | shell: /bin/bash 27 | become: true 28 | 29 | - name: Set up ssh key login for the deployment user 30 | authorized_key: 31 | user: "{{ deploy_user }}" 32 | state: present 33 | key: "{{ lookup('file', deploy_user_public_key_local_path) }}" 34 | become: true 35 | 36 | - name: Disable password based login 37 | lineinfile: dest=/etc/ssh/sshd_config regexp="^PasswordAuthentication" line="PasswordAuthentication no" state=present 38 | notify: 39 | - restart sshd 40 | 41 | - name: Get software for apt repository management. 42 | apt: 43 | state: present 44 | name: 45 | - python3-apt 46 | - python3-pycurl 47 | - apt-transport-https 48 | - gnupg2 49 | 50 | - name: Add chris lea repository for redis 51 | apt_repository: repo='ppa:chris-lea/redis-server' update_cache=yes 52 | 53 | - name: Add Nodesource apt key. 54 | apt_key: 55 | url: https://keyserver.ubuntu.com/pks/lookup?op=get&fingerprint=on&search=0x1655A0AB68576280 56 | id: "68576280" 57 | state: present 58 | 59 | - name: Install the nodejs LTS repos 60 | apt_repository: 61 | repo: "deb https://deb.nodesource.com/node_12.x {{ ansible_distribution_release }} main" 62 | state: present 63 | register: node_repo 64 | 65 | - name: Update apt cache if repo was added. 66 | apt: update_cache=yes 67 | when: node_repo.changed 68 | 69 | - name: Add Yarn GPG public key 70 | apt_key: 71 | url: https://dl.yarnpkg.com/debian/pubkey.gpg 72 | state: present 73 | 74 | - name: Ensure Debian sources list file exists for Yarn 75 | file: 76 | path: /etc/apt/sources.list.d/yarn.list 77 | owner: root 78 | mode: 0644 79 | state: touch 80 | 81 | - name: Ensure Debian package is in sources list for Yarn 82 | lineinfile: 83 | dest: /etc/apt/sources.list.d/yarn.list 84 | regexp: 'deb http://dl.yarnpkg.com/debian/ stable main' 85 | line: 'deb http://dl.yarnpkg.com/debian/ stable main' 86 | state: present 87 | 88 | - name: Update apt cache 89 | apt: 90 | update_cache: yes 91 | 92 | - name: Install dependencies for compiling Ruby along with Node.js and Yarn 93 | apt: 94 | state: present 95 | name: 96 | - git-core 97 | - curl 98 | - zlib1g-dev 99 | - build-essential 100 | - libssl-dev 101 | - libreadline-dev 102 | - libyaml-dev 103 | - libsqlite3-dev 104 | - sqlite3 105 | - libxml2-dev 106 | - libxslt1-dev 107 | - libcurl4-openssl-dev 108 | - software-properties-common 109 | - libffi-dev 110 | - dirmngr 111 | - gnupg 112 | - apt-transport-https 113 | - ca-certificates 114 | - redis-server 115 | - redis-tools 116 | - nodejs 117 | - yarn 118 | 119 | - name: Log in as deploy user and setup ruby, passenger and nginx 120 | hosts: web 121 | vars_files: 122 | - vars/vars.yml 123 | - vars/envs.yml 124 | user: "{{ deploy_user }}" 125 | become: true 126 | become_user: "{{ deploy_user }}" 127 | 128 | handlers: 129 | - name: restart nginx 130 | service: name=nginx state=restarted 131 | 132 | - name: restart postgresql 133 | service: 134 | name: postgresql 135 | state: restart 136 | sleep: 5 137 | 138 | tasks: 139 | - name: Clone Rbenv 140 | git: repo=git://github.com/rbenv/rbenv.git dest=~{{ deploy_user }}/.rbenv 141 | 142 | - name: Add Rbenv path to .bashrc 143 | lineinfile: 144 | dest: "/home/{{ deploy_user }}/.bashrc" 145 | regexp: 'export PATH="\$HOME/.rbenv/bin:\$PATH"' 146 | line: 'export PATH="$HOME/.rbenv/bin:$PATH"' 147 | state: present 148 | 149 | - name: Add Rbenv eval init to .bashrc 150 | lineinfile: 151 | dest: "/home/{{ deploy_user }}/.bashrc" 152 | regexp: 'eval "\$\(rbenv init -\)"' 153 | line: 'eval "$(rbenv init -)"' 154 | state: present 155 | 156 | - name: Clone rbenv build 157 | git: repo=git://github.com/rbenv/ruby-build.git dest=~{{ deploy_user }}/.rbenv/plugins/ruby-build 158 | 159 | - name: Add Rbenv build to .bashrc 160 | lineinfile: 161 | dest: "/home/{{ deploy_user }}/.bashrc" 162 | regexp: 'export PATH="\$HOME/.rbenv/plugins/ruby-build/bin:\$PATH"' 163 | line: 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' 164 | state: present 165 | 166 | - name: Clone rbenv vars 167 | git: repo=git://github.com/rbenv/rbenv-vars.git dest=~{{ deploy_user }}/.rbenv/plugins/rbenv-vars 168 | 169 | - name: source bashrc 170 | shell: . /home/{{ deploy_user }}/.bashrc 171 | 172 | - name: check ruby {{ ruby_version }} is installed for system 173 | shell: "/home/{{ deploy_user }}/.rbenv/bin/rbenv versions | grep {{ruby_version}}" 174 | register: ruby_installed 175 | changed_when: false 176 | ignore_errors: yes 177 | check_mode: no 178 | 179 | - name: rbenv install ruby 180 | command: "/home/{{ deploy_user }}/.rbenv/bin/rbenv install --verbose {{ruby_version}}" 181 | when: 182 | - ruby_installed.rc != 0 183 | async: 3600 184 | poll: 10 185 | 186 | - name: check if current system ruby version is {{ ruby_version }} 187 | shell: "/home/{{ deploy_user }}/.rbenv/bin/rbenv version | cut -d ' ' -f 1 | grep -Fx '{{ ruby_version }}'" 188 | register: current_ruby_selected 189 | changed_when: false 190 | ignore_errors: yes 191 | check_mode: no 192 | 193 | - name: rbenv set global ruby version and rehash 194 | command: "/home/{{ deploy_user }}/.rbenv/bin/rbenv global {{ruby_version}} && rbenv rehash" 195 | when: 196 | - current_ruby_selected.rc != 0 197 | 198 | - name: 'install bundler v1' 199 | command: "/home/{{ deploy_user }}/.rbenv/shims/gem install bundler -v 1.17.3" 200 | 201 | - name: 'install bundler v2' 202 | command: "/home/{{ deploy_user }}/.rbenv/shims/gem install bundler" 203 | 204 | - name: Add Passenger apt key. 205 | apt_key: 206 | keyserver: keyserver.ubuntu.com 207 | id: 561F9B9CAC40B2F7 208 | state: present 209 | become: true 210 | become_user: root 211 | 212 | - name: Add Phusion apt repo. 213 | apt_repository: 214 | repo: 'deb https://oss-binaries.phusionpassenger.com/apt/passenger {{ ansible_distribution_release }} main' 215 | state: present 216 | update_cache: true 217 | become: true 218 | become_user: root 219 | 220 | - name: Install Nginx and Passenger 221 | apt: 222 | name: 223 | - nginx 224 | - libnginx-mod-http-passenger 225 | state: present 226 | become: true 227 | become_user: root 228 | 229 | - name: Ensure passenger module is enabled. 230 | file: 231 | src: /usr/share/nginx/modules-available/mod-http-passenger.load 232 | dest: /etc/nginx/modules-enabled/50-mod-http-passenger.conf 233 | state: link 234 | 235 | - name: Ask Passenger to use the Rbenv ruby 236 | lineinfile: 237 | dest: /etc/nginx/conf.d/mod-http-passenger.conf 238 | regexp: '^passenger_ruby' 239 | line: "passenger_ruby /home/{{ deploy_user }}/.rbenv/shims/ruby;" 240 | state: present 241 | become: true 242 | become_user: root 243 | 244 | - name: Copy app nginx conf 245 | template: 246 | src: templates/nginx_app.conf.j2 247 | dest: /etc/nginx/sites-enabled/{{ app_name }} 248 | become: true 249 | become_user: root 250 | 251 | - name: Ensure default virtual host is removed. 252 | file: 253 | path: /etc/nginx/sites-enabled/default 254 | state: absent 255 | become: true 256 | become_user: root 257 | 258 | - name: Restart nginx service 259 | service: 260 | name: nginx 261 | state: restarted 262 | become: true 263 | become_user: root 264 | 265 | - name: Let deploy user restart passenger without sudo 266 | template: 267 | src: templates/sudoers_passenger.j2 268 | dest: /etc/sudoers.d/passenger 269 | validate: 'visudo -cf %s' 270 | mode: 0440 271 | become: true 272 | become_user: root 273 | 274 | - name: Install postgres packages 275 | apt: 276 | name: 277 | - libpq-dev 278 | - "postgresql-{{ postgresql_version }}" 279 | - postgresql-contrib 280 | - python3-psycopg2 281 | state: present 282 | become: true 283 | become_user: root 284 | 285 | - name: Ensure all configured locales are present. 286 | locale_gen: "name={{ item }} state=present" 287 | with_items: "{{ postgresql_locales }}" 288 | register: locale_gen_result 289 | 290 | - name: Force-restart PostgreSQL after new locales are generated. 291 | service: 292 | name: postgresql 293 | state: restarted 294 | when: locale_gen_result.changed 295 | become: true 296 | become_user: root 297 | 298 | - name: Ensure PostgreSQL is started and enabled on boot. 299 | service: 300 | name: postgresql 301 | state: started 302 | enabled: true 303 | become: true 304 | become_user: root 305 | 306 | - name: Create postgresql database 307 | postgresql_db: name={{ postgres_db_name }} 308 | become: true 309 | become_user: postgres 310 | # See: https://github.com/ansible/ansible/issues/16048#issuecomment-229012509 311 | vars: 312 | ansible_ssh_pipelining: true 313 | 314 | - name: Create postgresql user 315 | postgresql_user: name={{ postgres_db_user }} password={{ postgres_db_password }} 316 | become: true 317 | become_user: postgres 318 | 319 | - name: Ensure Redis is started on boot 320 | service: name=redis-server state=started enabled=yes 321 | become: true 322 | become_user: root 323 | 324 | - name: Ensure the directory of user sidekiq service exists 325 | file: 326 | path: "/home/{{ deploy_user }}/.config/systemd/user" 327 | state: directory 328 | owner: "{{ deploy_user }}" 329 | group: "{{ deploy_user }}" 330 | 331 | - name: Copy sidekiq service file to user service 332 | template: 333 | src: templates/sidekiq.service.j2 334 | dest: "/home/{{ deploy_user }}/.config/systemd/user/sidekiq.service" 335 | 336 | - name: enable linger for user service 337 | command: "loginctl enable-linger {{ deploy_user }}" 338 | -------------------------------------------------------------------------------- /templates/env.j2: -------------------------------------------------------------------------------- 1 | {% for (key,value) in environment_vars.items() %} 2 | {{ key }}="{{ value }}" 3 | {% endfor %} -------------------------------------------------------------------------------- /templates/master.key.j2: -------------------------------------------------------------------------------- 1 | {{ environment_vars.RAILS_MASTER_KEY }} -------------------------------------------------------------------------------- /templates/nginx_app.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | server { 4 | listen 80; 5 | listen [::]:80; 6 | 7 | server_name _; 8 | root /home/{{ deploy_user }}/{{ app_name }}/current/public; 9 | 10 | passenger_enabled on; 11 | passenger_app_env production; 12 | 13 | location /cable { 14 | passenger_app_group_name {{ app_name }}_websocket; 15 | passenger_force_max_concurrent_requests_per_process 0; 16 | } 17 | 18 | # Allow uploads up to 100MB in size 19 | client_max_body_size 100m; 20 | 21 | location ~ ^/(assets|packs) { 22 | expires max; 23 | gzip_static on; 24 | } 25 | } -------------------------------------------------------------------------------- /templates/rbenv.j2: -------------------------------------------------------------------------------- 1 | {% for (key,value) in environment_vars.items() %} 2 | {{ key }}={{ value }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /templates/sidekiq.service.j2: -------------------------------------------------------------------------------- 1 | # 2 | # This file tells systemd how to run Sidekiq as a 24/7 long-running daemon. 3 | # 4 | # Customize this file based on your bundler location, app directory, etc. 5 | # Customize and copy this into /usr/lib/systemd/system (CentOS) or /lib/systemd/system (Ubuntu). 6 | # Then run: 7 | # - systemctl enable sidekiq 8 | # - systemctl {start,stop,restart} sidekiq 9 | # 10 | # This file corresponds to a single Sidekiq process. Add multiple copies 11 | # to run multiple processes (sidekiq-1, sidekiq-2, etc). 12 | # 13 | # Use `journalctl -u sidekiq -rn 100` to view the last 100 lines of log output. 14 | # 15 | [Unit] 16 | Description=sidekiq 17 | # start us only once the network and logging subsystems are available, 18 | # consider adding redis-server.service if Redis is local and systemd-managed. 19 | After=syslog.target network.target 20 | 21 | # See these pages for lots of options: 22 | # 23 | # https://www.freedesktop.org/software/systemd/man/systemd.service.html 24 | # https://www.freedesktop.org/software/systemd/man/systemd.exec.html 25 | # 26 | # THOSE PAGES ARE CRITICAL FOR ANY LINUX DEVOPS WORK; read them multiple 27 | # times! systemd is a critical tool for all developers to know and understand. 28 | # 29 | [Service] 30 | # 31 | # !!!! !!!! !!!! 32 | # 33 | # As of v6.0.6, Sidekiq automatically supports systemd's `Type=notify` and watchdog service 34 | # monitoring. If you are using an earlier version of Sidekiq, change this to `Type=simple` 35 | # and remove the `WatchdogSec` line. 36 | # 37 | # !!!! !!!! !!!! 38 | # 39 | Type=notify 40 | # If your Sidekiq process locks up, systemd's watchdog will restart it within seconds. 41 | WatchdogSec=10 42 | 43 | WorkingDirectory= /home/{{ deploy_user }}/{{ app_name }}/current 44 | 45 | ExecStart=/bin/bash -lc 'exec /home/{{ deploy_user }}/.rbenv/shims/bundle exec sidekiq -e production' 46 | ExecReload=/bin/kill -TSTP $MAINPID 47 | ExecStop=/bin/kill -TERM $MAINPID 48 | 49 | # Use `systemctl kill -s TSTP sidekiq` to quiet the Sidekiq process 50 | 51 | # Greatly reduce Ruby memory fragmentation and heap usage 52 | # https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ 53 | Environment=MALLOC_ARENA_MAX=2 54 | 55 | EnvironmentFile=/etc/environment 56 | 57 | # if we crash, restart 58 | RestartSec=1 59 | Restart=on-failure 60 | 61 | # output goes to /var/log/syslog (Ubuntu) or /var/log/messages (CentOS) 62 | StandardOutput=syslog 63 | StandardError=syslog 64 | 65 | # This will default to "bundler" if we don't specify it 66 | SyslogIdentifier=sidekiq 67 | 68 | [Install] 69 | WantedBy=default.target -------------------------------------------------------------------------------- /templates/sudoers_passenger.j2: -------------------------------------------------------------------------------- 1 | {{ deploy_user }} ALL=(ALL:ALL) NOPASSWD: /usr/bin/passenger-config restart-app, /usr/bin/env passenger-config restart-app /home/{{ deploy_user }}/{{app_name}}, /usr/bin/env passenger-config restart-app /home/{{ deploy_user }}/{{app_name}} --ignore-app-not-running 2 | -------------------------------------------------------------------------------- /vars/envs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | environment_vars: 3 | TEST_NAME: some_value 4 | DATABASE_HOST: localhost 5 | DATABASE_NAME: "{{ postgres_db_name }}" 6 | DATABASE_USERNAME: "{{ postgres_db_user }}" 7 | DATABASE_PASSWORD: "{{ postgres_db_password }}" 8 | 9 | RAILS_MASTER_KEY: your_rails_app_master_key 10 | SECRET_KEY_BASE: your_rails_app_secret_key_base -------------------------------------------------------------------------------- /vars/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ruby_version: '2.6.6' 4 | 5 | # Your rails app name 6 | app_name: 'your_app_name' 7 | 8 | deploy_user: 'your_deploy_user' 9 | deploy_user_password: 'your_deploy_user_password' 10 | 11 | # The path to your local public key file (ie. your current computer) 12 | deploy_user_public_key_local_path: '~/.ssh/id_rsa.pub' 13 | 14 | # credentials of postgres user which your rails app will use to connect to the database 15 | postgres_db_user: 'your_db_user' 16 | postgres_db_password: 'your_db_password' 17 | postgres_db_name: 'your_db_name' 18 | 19 | # you can leave these untouched 20 | postgresql_version: '12' 21 | postgresql_locales: 22 | - 'en_US.UTF-8' 23 | --------------------------------------------------------------------------------