├── .gitignore ├── README.md ├── Vagrantfile ├── ansible.cfg ├── app-vars.yml ├── deploy.yml ├── deploy_tasks └── after_cleanup.yml ├── group_vars └── all │ └── vault.yml ├── images └── ansible-rails-promo.jpg ├── inventories └── development.ini ├── provision.yml ├── roles ├── certbot │ ├── defaults │ │ └── main.yml │ └── tasks │ │ ├── cert.yml │ │ └── main.yml ├── common │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── fluentbit │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── td-agent-bit.conf.j2 ├── logrotate │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── nginx │ ├── defaults │ │ └── main.yml │ ├── files │ │ ├── config │ │ │ ├── general.conf │ │ │ ├── letsencrypt.conf │ │ │ ├── proxy.conf │ │ │ └── security.conf │ │ └── nginx.conf │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── nginx-default.conf.j2 ├── nodejs │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── pgbackup │ ├── defaults │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── postgresql-backup.j2 ├── postgresql │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ ├── pg_hba.conf.j2 │ │ └── postgresql.conf.j2 ├── puma │ └── tasks │ │ └── main.yml ├── redis │ └── tasks │ │ └── main.yml ├── ruby │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── sidekiq │ └── tasks │ │ └── main.yml ├── ssh │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── ufw │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── user │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml └── yarn │ └── tasks │ └── main.yml └── templates ├── .rbenv-vars.j2 ├── database.yml.j2 ├── nginx.conf.j2 ├── puma.service.j2 └── sidekiq.service.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vault_pass 3 | /tmp/ 4 | .vagrant/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Ansible Rails

2 |

3 | Ansible Rails Promo Image 4 |

5 | 6 | Ansible Rails is a playbook for easily deploying Ruby on Rails applications. It uses Vagrant to provision an environment where you can test your deploys. [Ansistrano](https://github.com/ansistrano/deploy) is used for finally deploying our app to staging and production environments. 7 | 8 | While this is meant to work out of the box, you can tweak the files in the `roles` directory in order to satisfy your project-specific requirements. 9 | 10 | > **Shameless plug:** If you're looking for a simple bookmarking tool, try [EmailThis.me](https://www.emailthis.me) - a simpler alternative to Pocket that helps you *save ad-free articles and web pages to your email inbox*. 11 | 12 | --- 13 | 14 | ### What does this do? 15 | * Configure our server with some sensible defaults 16 | * Install required/useful packages. See notes below for more details. 17 | * Auto upgrade all installed packages (TODO) 18 | * Create a new deployment user (called 'deploy') with passwordless login 19 | * SSH hardening 20 | * Prevent password login 21 | * Change the default SSH port 22 | * Prevent root login 23 | * Setup UFW (firewall) 24 | * Setup Fail2ban 25 | * Install Logrotate 26 | * Setup Nginx with some sensible config (thanks to nginxconfig.io) 27 | * Certbot (for Let's encrypt SSL certificates) 28 | * Ruby (using Rbenv). 29 | * Defaults to `2.6.6`. You can change it in the `app-vars.yml` file 30 | * [jemmaloc](https://github.com/jemalloc/jemalloc) is also installed and configured by default 31 | * [rbenv-vars](https://github.com/rbenv/rbenv-vars) is also installed by default 32 | * Node.js 33 | * Defaults to 12.x. You can change it in the `app-vars.yml` file. 34 | * Yarn 35 | * Redis (latest) 36 | * Postgresql. 37 | * Defaults to v12. You can specify the version that you need in the `app-vars.yml` file. 38 | * Puma (with Systemd support for restarting automatically) **See Puma Config section below** 39 | * Sidekiq (with Systemd support for restarting automatically) 40 | * Ansistrano hooks for performing the following tasks - 41 | * Installing all our gems 42 | * Precompiling assets 43 | * Migrating our database (using `run_once`) 44 | 45 | --- 46 | 47 | ### Getting started 48 | Here are the steps that you need to follow in order to get up and running with Ansible Rails. 49 | 50 | #### Step 1. Installation 51 | 52 | ``` 53 | git clone https://github.com/EmailThis/ansible-rails ansible-rails 54 | cd ansible-rails 55 | ``` 56 | 57 | #### Step 2. Configuration 58 | Open `app-vars.yml` and change the following variables. Additionally, please review the `app-vars.yml` and see if there is anything else that you would like to modify (e.g.: install some other packages, change ruby, node or postgresql versions etc.) 59 | 60 | ``` 61 | app_name: YOUR_APP_NAME // Replace with name of your app 62 | app_git_repo: "YOUR_GIT_REPO" // e.g.: github.com/EmailThis/et 63 | app_git_branch: "master" // branch that you want to deploy (e.g: 'production') 64 | 65 | postgresql_db_user: "{{ deploy_user }}_postgresql_user" 66 | postgresql_db_password: "{{ vault_postgresql_db_password }}" # from vault (see next section) 67 | postgresql_db_name: "{{ app_name }}_production" 68 | 69 | nginx_https_enabled: false # change to true if you wish to install SSL certificate 70 | ``` 71 | 72 | 73 | #### Step 3. Storing sensitive information 74 | Create a new `vault` file to store sensitive information 75 | ``` 76 | ansible-vault create group_vars/all/vault.yml 77 | ``` 78 | 79 | Add the following information to this new vault file 80 | ``` 81 | vault_postgresql_db_password: "XXXXX_SUPER_SECURE_PASS_XXXXX" 82 | vault_rails_master_key: "XXXXX_MASTER_KEY_FOR_RAILS_XXXXX" 83 | ``` 84 | 85 | #### Step 4. Deploy 86 | 87 | Now that we have configured everything, lets see if everything is working locally. Run the following command - 88 | ``` 89 | vagrant up 90 | ``` 91 | Now open your browser and navigate to 192.168.50.2. You should see your Rails application. 92 | 93 | If you don't wish to use Vagrant, clone this repo, modify the `inventories/development.ini` file to suit your needs, and then run the following command 94 | ``` 95 | ansible-playbook -i inventories/development.ini provision.yml 96 | ``` 97 | 98 | To deploy this app to your production server, create another file inside `inventories` directory called `production.ini` with the following contents. For this, you would need a VPS. I've used [DigitalOcean](https://m.do.co/c/031c76b9c838) and [Vultr](https://www.vultr.com/?ref=8597223) in production for my apps and both these services are top-notch. 99 | ``` 100 | [web] 101 | 192.168.50.2 # replace with IP address of your server. 102 | 103 | [all:vars] 104 | ansible_ssh_user=deployer 105 | ansible_python_interpreter=/usr/bin/python3 106 | ``` 107 | 108 | --- 109 | 110 | ### Additional Configuration 111 | 112 | #### Installing additional packages 113 | By default, the following packages are installed. You can add/remove packages to this list by changing the `required_package` variable in `app-vars.yml` 114 | ``` 115 | - curl 116 | - ufw 117 | - fail2ban 118 | - git-core 119 | - apt-transport-https 120 | - ca-certificates 121 | - software-properties-common 122 | - python3-pip 123 | - virtualenv 124 | - python3-setuptools 125 | - zlib1g-dev 126 | - build-essential 127 | - libssl-dev 128 | - libreadline-dev 129 | - libyaml-dev 130 | - libxml2-dev 131 | - libxslt1-dev 132 | - libcurl4-openssl-dev 133 | - libffi-dev 134 | - dirmngr 135 | - gnupg 136 | - autoconf 137 | - bison 138 | - libreadline6-dev 139 | - libncurses5-dev 140 | - libgdbm5 141 | - libgdbm-dev 142 | - libpq-dev # postgresql client 143 | - libjemalloc-dev # jemalloc 144 | ``` 145 | 146 | #### Enable UFW 147 | You can enable UFW by adding the role to `provision.yml` like so - 148 | ``` 149 | roles: 150 | ... 151 | ... 152 | - role: ufw 153 | tags: ufw 154 | ``` 155 | 156 | Then you can set up the UFW rules in `app-vars.yml` like so - 157 | ``` 158 | ufw_rules: 159 | - { rule: "allow", proto: "tcp", from: "any", port: "80" } 160 | - { rule: "allow", proto: "tcp", from: "any", port: "443" } 161 | ``` 162 | 163 | #### Enable Certbot (Let's Encrypt SSL certificates) 164 | 165 | Add the role to `provision.yml` 166 | ``` 167 | roles: 168 | ... 169 | ... 170 | - role: certbot 171 | tags: certbot 172 | ``` 173 | 174 | Add the following variables to `app-vars.yml` 175 | ``` 176 | nginx_https_enabled: true 177 | 178 | certbot_email: "you@email.me" 179 | certbot_domains: 180 | - "domain.com" 181 | - "www.domain.com" 182 | ``` 183 | 184 | #### PostgreSQL Database Backups 185 | By default, daily backup is enabled in the `app-vars.yml` file. In order for this to work, the following variables need to be set. If you do not wish to store backups, remove (or uncomment) these lines from `app-vars.yml`. 186 | 187 | ``` 188 | aws_key: "{{ vault_aws_key }}" # store this in group_vars/all/vault.yml that we created earlier 189 | aws_secret: "{{ vault_aws_secret }}" 190 | 191 | postgresql_backup_dir: "{{ deploy_user_path }}/backups" 192 | postgresql_backup_filename_format: >- 193 | {{ app_name }}-%Y%m%d-%H%M%S.pgdump 194 | postgresql_db_backup_healthcheck: "NOTIFICATION_URL (eg: https://healthcheck.io/)" # optional 195 | postgresql_s3_backup_bucket: "DB_BACKUP_BUCKET" # name of the S3 bucket to store backups 196 | postgresql_s3_backup_hour: "3" 197 | postgresql_s3_backup_minute: "*" 198 | postgresql_s3_backup_delete_after: "7 days" # days after which old backups should be deleted 199 | ``` 200 | 201 | 202 | #### Puma config 203 | 204 | Your Rails app needs to have a puma config file (usually in `/config/puma.rb`). Here's a sample - 205 | 206 | ``` 207 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 208 | threads threads_count, threads_count 209 | 210 | port ENV.fetch("PORT") { 3000 } 211 | 212 | rails_env = ENV.fetch("RAILS_ENV") { "development" } 213 | environment rails_env 214 | 215 | if %w[production staging].member?(rails_env) 216 | app_dir = ENV.fetch("APP_DIR") { "YOUR_APP/current" } 217 | directory app_dir 218 | 219 | shared_dir = ENV.fetch("SHARED_DIR") { "YOUR_APP/shared" } 220 | 221 | # Logging 222 | stdout_redirect "#{shared_dir}/log/puma.stdout.log", "#{shared_dir}/log/puma.stderr.log", true 223 | 224 | pidfile "#{shared_dir}/tmp/pids/puma.pid" 225 | state_path "#{shared_dir}/tmp/pids/puma.state" 226 | 227 | # Set up socket location 228 | bind "unix://#{shared_dir}/sockets/puma.sock" 229 | 230 | workers ENV.fetch("WEB_CONCURRENCY") { 2 } 231 | preload_app! 232 | 233 | elsif rails_env == "development" 234 | plugin :tmp_restart 235 | end 236 | ``` 237 | 238 | --- 239 | 240 | ### Motivation 241 | I use Heroku to deploy my Rails apps. It makes deployment really easy and I've got no complaints. However, I always wanted to learn how it all works under the hood. Over the last couple of months, I decided to learn more about how to set up a server and deploy a Rails app to production. This project is a consolidation of my learnings. 242 | 243 | --- 244 | 245 | ### Credits 246 | * [Geerling Guy](https://github.com/geerlingguy) (for his wonderful book on Ansible) 247 | * [dresden-weekly/ansible-rails](https://github.com/dresden-weekly/ansible-rails) 248 | 249 | --- 250 | 251 | ### Questions, comments, suggestions? 252 | Please let me know if you run into any issues or if you have any questions. I'd be happy to help. I would also welcome any improvements/suggestions by way of pull requests. 253 | 254 | 255 | Bharani
256 | Founder @ [EmailThis.me](https://www.emailthis.me) 257 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure(2) do |config| 2 | 3 | config.vm.box = "hashicorp/bionic64" 4 | 5 | config.vm.network "private_network", ip: "192.168.50.2" 6 | 7 | config.vm.provision"ansible" do |ansible| 8 | ansible.compatibility_mode = '2.0' 9 | ansible.playbook = "provision.yml" 10 | ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" } 11 | end 12 | 13 | end -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = False 3 | vault_password_file = ./.vault_pass -------------------------------------------------------------------------------- /app-vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | app_name: YOUR_APP_NAME 3 | deploy_user: deployer 4 | deploy_group: "{{ deploy_user }}" 5 | deploy_user_path: "/home/{{ deploy_user }}" 6 | 7 | # App Git repo 8 | app_git_repo: "YOUR_GIT_REPO" 9 | app_git_branch: "master" 10 | 11 | # Rails app 12 | app_root_path: "{{ deploy_user_path }}/{{ app_name }}" 13 | app_current_path: "{{ app_root_path }}/current" 14 | app_releases_path: "{{ app_root_path }}/releases" 15 | app_shared_path: "{{ app_root_path }}/shared" 16 | app_pids_path: "{{ app_shared_path }}/tmp/pids" 17 | app_logs_path: "{{ app_shared_path }}/logs" 18 | app_sockets_path: "{{ app_shared_path }}/sockets" 19 | rails_db_pool: 20 20 | rails_environment: production 21 | 22 | # Puma 23 | puma_service_file: "puma.service.j2" 24 | puma_config_file: "{{ app_current_path }}/config/puma.rb" 25 | puma_socket: "{{ app_sockets_path }}/puma.sock" 26 | puma_web_concurrency: 2 27 | 28 | # Sidekiq 29 | sidekiq_service_file: "sidekiq.service.j2" 30 | 31 | # Ansistrano 32 | ansistrano_deploy_to: "{{ app_root_path }}" 33 | ansistrano_keep_releases: 3 34 | ansistrano_deploy_via: git 35 | ansistrano_git_repo: "{{ app_git_repo }}" 36 | ansistrano_git_branch: "{{ app_git_branch }}" 37 | ansistrano_after_cleanup_tasks_file: "{{ playbook_dir }}/deploy_tasks/after_cleanup.yml" 38 | ansistrano_git_identity_key_path: "~/.ssh/id_rsa" 39 | ansistrano_ensure_shared_paths_exist: yes 40 | ansistrano_ensure_basedirs_shared_files_exist: yes 41 | 42 | ansistrano_shared_paths: 43 | - log # log -> ../../shared/log 44 | - tmp # tmp -> ../../shared/tmp 45 | - vendor # vendor -> ../../shared/vendor 46 | - public/assets # For rails asset pipeline 47 | - public/packs # For webpacker 48 | - node_modules # For webpacker node_modules -> ../../shared/node_modules 49 | 50 | shared_files_to_copy: 51 | - { src: database.yml.j2, dest: config/database.yml } 52 | 53 | # Common 54 | required_packages: 55 | - zlib1g-dev 56 | - build-essential 57 | - libssl-dev 58 | - libreadline-dev 59 | - libyaml-dev 60 | - libxml2-dev 61 | - libxslt1-dev 62 | - libcurl4-openssl-dev 63 | - libffi-dev 64 | - dirmngr 65 | - gnupg 66 | - autoconf 67 | - bison 68 | - libreadline6-dev 69 | - libncurses5-dev 70 | - libgdbm5 71 | - libgdbm-dev 72 | - libpq-dev # postgresql client 73 | - libjemalloc-dev # jemalloc 74 | 75 | # Ruby 76 | ruby_version: 2.6.6 77 | rbenv_ruby_configure_opts: "RUBY_CONFIGURE_OPTS=--with-jemalloc" 78 | rbenv_root_path: "{{ deploy_user_path }}/.rbenv" 79 | rbenv_shell_rc_path: "{{ deploy_user_path }}/.bashrc" 80 | rubies_path: "{{ rbenv_root_path }}/versions" 81 | ruby_path: "{{ rubies_path }}/{{ ruby_version }}" 82 | rbenv_bin: "{{ rbenv_root_path }}/bin/rbenv" 83 | rbenv_bundle: "{{ rbenv_root_path }}/shims/bundle" 84 | 85 | # Nodejs 86 | nodejs_version: "12.x" 87 | 88 | # Postgresql 89 | postgresql_version: "9.6" 90 | postgresql_db_user: "{{ deploy_user }}_postgresql_user" 91 | postgresql_db_password: "{{ vault_postgresql_db_password }}" # from vault 92 | postgresql_db_name: "{{ app_name }}_production" 93 | postgresql_listen: 94 | - "localhost" 95 | - "{{ ansible_default_ipv4.address }}" # only if db is on a separate server 96 | 97 | 98 | # nginx 99 | nginx_https_enabled: false # replace after setting up certbot 100 | nginx_conf_template: "nginx.conf.j2" 101 | 102 | 103 | # certbot 104 | # certbot_email: "admin@{{ inventory_hostname }}" 105 | # certbot_domains: 106 | # - "{{ inventory_hostname }}" 107 | # - "www.{{ inventory_hostname }}" 108 | 109 | 110 | # PostgreSQL Backup to S3 111 | aws_key: "{{ vault_aws_key }}" 112 | aws_secret: "{{ vault_aws_secret }}" 113 | 114 | postgresql_backup_dir: "{{ deploy_user_path }}/backups" 115 | postgresql_backup_filename_format: >- 116 | {{ app_name }}-%Y%m%d-%H%M%S.pgdump 117 | postgresql_db_backup_healthcheck: "NOTIFICATION_URL (eg: https://healthcheck.io/)" 118 | postgresql_s3_backup_bucket: "DB_BACKUP_BUCKET" 119 | postgresql_s3_backup_hour: "3" 120 | postgresql_s3_backup_minute: "*" 121 | postgresql_s3_backup_delete_after: "7 days" # days after which old backups should be deleted 122 | 123 | # fluentbit 124 | fluentbit_inputs: 125 | - Name: tail 126 | Path: "{{ app_logs_path }}/production.log" 127 | 128 | fluentbit_outputs: 129 | - Name: http 130 | Match: "*" 131 | tls: On 132 | Host: "" # e.g: loggly or sumologic logs endpoint 133 | Port: 443 134 | URI: "" # e.g: /receiver/v1/http/{{ vault_sumologic_token }} 135 | Format: json_lines 136 | Json_Date_Key: timestamp 137 | Json_Date_Format: iso8601 138 | Retry_Limit: False 139 | 140 | logrotate_conf: 141 | - path: "ansible" 142 | conf: | 143 | "{{ app_current_path }}/log/*.log" { 144 | weekly 145 | size 100M 146 | missingok 147 | rotate 12 148 | compress 149 | delaycompress 150 | notifempty 151 | copytruncate 152 | } -------------------------------------------------------------------------------- /deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy our Rails ap 3 | hosts: all 4 | become: true 5 | become_user: "{{ deploy_user }}" 6 | 7 | pre_tasks: 8 | - name: Setup app folder 9 | file: 10 | state: directory 11 | path: "{{ app_root_path }}" 12 | owner: "{{ deploy_user }}" 13 | group: "{{ deploy_group }}" 14 | 15 | - name: Copy rbenv-vars file 16 | template: 17 | src: ".rbenv-vars.j2" 18 | dest: "{{ app_root_path }}/.rbenv-vars" 19 | owner: "{{ deploy_user }}" 20 | group: "{{ deploy_group }}" 21 | 22 | - name: Make shared directories 23 | file: 24 | path: "{{ app_shared_path }}/{{ item }}" 25 | state: directory 26 | owner: "{{ deploy_user }}" 27 | group: "{{ deploy_group }}" 28 | with_items: 29 | - tmp 30 | - tmp/pids 31 | - tmp/cache 32 | - sockets 33 | - log 34 | - public 35 | - public/packs 36 | - vendor 37 | - vendor/bundle 38 | - bin 39 | - config 40 | - config/puma 41 | - assets 42 | - node_modules 43 | 44 | - name: Upload shared files 45 | template: 46 | src: "{{ item.src }}" 47 | dest: "{{ app_shared_path }}/{{ item.dest }}" 48 | owner: "{{ deploy_user }}" 49 | group: "{{ deploy_group }}" 50 | with_items: "{{ shared_files_to_copy }}" 51 | tags: 52 | - copy 53 | 54 | roles: 55 | - role: ansistrano.deploy 56 | 57 | - role: puma 58 | tags: puma 59 | become: true 60 | become_user: root 61 | 62 | - role: sidekiq 63 | tags: sidekiq 64 | become: true 65 | become_user: root -------------------------------------------------------------------------------- /deploy_tasks/after_cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Bundle install with --deploy 3 | bundler: 4 | state: present 5 | deployment_mode: yes 6 | gem_path: "../../shared/vendor/bundle" # relative to chdir 7 | chdir: "{{ ansistrano_release_path.stdout }}" 8 | exclude_groups: ["development", "test"] 9 | executable: "{{ rbenv_bundle }}" 10 | 11 | - name: Running pending migrations 12 | shell: "{{ rbenv_bundle }} exec rake db:migrate" 13 | run_once: true 14 | args: 15 | chdir: "{{ ansistrano_release_path.stdout }}" 16 | 17 | - name: Precompiling assets 18 | shell: "{{ rbenv_bundle }} exec rake assets:precompile" 19 | args: 20 | chdir: "{{ ansistrano_release_path.stdout }}" -------------------------------------------------------------------------------- /group_vars/all/vault.yml: -------------------------------------------------------------------------------- 1 | $ANSIBLE_VAULT;1.1;AES256 2 | 30373963356363633262326462656435666536663065623465313933643862636635313936373330 3 | 3039633537316134626331323735643638326231616465310a383832663835353839643138323862 4 | 31663337326562646630343430356631663261393965386431323134623832336566623561633161 5 | 3733633864613765380a666138373235353465363234343534633966663136666634346234623764 6 | 39346533323631346538393564613637316166656165613034656363623037363033613065356263 7 | 6666343833303934353732653765656530396131383936366266 8 | -------------------------------------------------------------------------------- /images/ansible-rails-promo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmailThis/ansible-rails/ff5b39db6d4e678edc0cd14fbd943f8e50c10e51/images/ansible-rails-promo.jpg -------------------------------------------------------------------------------- /inventories/development.ini: -------------------------------------------------------------------------------- 1 | [web] 2 | 192.168.50.2 3 | 4 | [all:vars] 5 | ansible_ssh_user=deployer 6 | ansible_python_interpreter=/usr/bin/python3 7 | -------------------------------------------------------------------------------- /provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: true 4 | 5 | vars_files: 6 | - app-vars 7 | 8 | roles: 9 | - role: common 10 | - role: user 11 | - role: ssh 12 | - role: ruby 13 | tags: ruby 14 | - role: nodejs 15 | tags: nodejs 16 | - role: yarn 17 | tags: nodejs 18 | - role: postgresql 19 | tags: postgresql 20 | - role: redis 21 | tags: redis 22 | - role: nginx 23 | tags: nginx 24 | - role: logrotate 25 | tags: logrotate -------------------------------------------------------------------------------- /roles/certbot/defaults/main.yml: -------------------------------------------------------------------------------- 1 | certbot_url: https://dl.eff.org/certbot-auto 2 | certbot_dir: /opt/certbot 3 | certbot_email: admin@example.com 4 | certbot_flags: "" 5 | certbot_domains: [] -------------------------------------------------------------------------------- /roles/certbot/tasks/cert.yml: -------------------------------------------------------------------------------- 1 | - name: Check if a certificate already exists 2 | stat: 3 | path: /etc/letsencrypt/live/{{ domain | replace('*.', '') }}/cert.pem 4 | register: letsencrypt_cert 5 | 6 | - name: Get new certificate 7 | command: "{{ certbot_dir }}/certbot-auto certonly --non-interactive --quiet --agree-tos --email {{ certbot_email }} --standalone -d {{ domain }} {{ certbot_flags }}" 8 | when: not letsencrypt_cert.stat.exists 9 | notify: Restart nginx -------------------------------------------------------------------------------- /roles/certbot/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create certbot directory 3 | file: path=/opt/certbot state=directory mode=0755 owner=root group=root 4 | 5 | - name: Install certbot standalone 6 | get_url: 7 | url: "{{ certbot_url }}" 8 | dest: "{{ certbot_dir }}/certbot-auto" 9 | 10 | - name: Ensure that certbot-auto is executable 11 | file: 12 | path: "{{ certbot_dir }}/certbot-auto" 13 | mode: 0755 14 | 15 | - name: Ensure that nginx is stopped 16 | service: 17 | name: nginx 18 | state: stopped 19 | 20 | - name: Check & get new certificate 21 | include_tasks: cert.yml 22 | loop: "{{ certbot_domains }}" 23 | loop_control: 24 | loop_var: domain 25 | 26 | - name: Add certbot renewal cronjob 27 | cron: name="renew letsencrypt certificates" hour="0" minute="0" job="/bin/bash {{ certbot_dir }}/certbot-auto renew --non-interactive --quiet --standalone --pre-hook 'service nginx stop' --post-hook 'service nginx start'" -------------------------------------------------------------------------------- /roles/common/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | required_packages: [] -------------------------------------------------------------------------------- /roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install prerequisites 3 | apt: name=aptitude update_cache=yes state=latest force_apt_get=yes 4 | 5 | - name: Install required system packages 6 | apt: name={{ item }} state=latest update_cache=yes 7 | loop: 8 | - curl 9 | - ufw 10 | - fail2ban 11 | - git-core 12 | - apt-transport-https 13 | - ca-certificates 14 | - software-properties-common 15 | - python3-pip 16 | - virtualenv 17 | - python3-setuptools 18 | - "{{ required_packages }}" -------------------------------------------------------------------------------- /roles/fluentbit/defaults/main.yml: -------------------------------------------------------------------------------- 1 | fluentbit_flush_seconds: 2 2 | 3 | # Default inputs 4 | fluentbit_inputs: [] 5 | 6 | # Default outputs 7 | fluentbit_outputs: [] 8 | 9 | fluentbit_hostname: "{{ hostname }}" -------------------------------------------------------------------------------- /roles/fluentbit/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart fluentbit 3 | service: 4 | name: td-agent-bit 5 | enabled: true 6 | state: restarted 7 | become: true -------------------------------------------------------------------------------- /roles/fluentbit/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add td-agent-bit apt-key 3 | apt_key: 4 | url: https://packages.fluentbit.io/fluentbit.key 5 | state: present 6 | 7 | - name: Add td-agent-bit repository 8 | apt_repository: 9 | repo: 'deb https://packages.fluentbit.io/ubuntu/bionic bionic main' 10 | state: present 11 | filename: td-agent-bit 12 | update_cache: true 13 | 14 | - name: Install fluentbit package 15 | package: 16 | name: td-agent-bit 17 | state: present 18 | update_cache: true 19 | notify: Restart fluentbit 20 | 21 | - name: Configure td-agent-bit.conf file 22 | template: 23 | src: td-agent-bit.conf.j2 24 | dest: /etc/td-agent-bit/td-agent-bit.conf 25 | mode: 0644 26 | notify: Restart fluentbit -------------------------------------------------------------------------------- /roles/fluentbit/templates/td-agent-bit.conf.j2: -------------------------------------------------------------------------------- 1 | [SERVICE] 2 | Flush {{ fluentbit_flush_seconds }} 3 | 4 | {% for input in fluentbit_inputs %} 5 | [INPUT] 6 | {% for key in input %} 7 | {{ key }} {{ input[key] }} 8 | {% endfor %} 9 | {% endfor %} 10 | 11 | {% for output in fluentbit_outputs %} 12 | [OUTPUT] 13 | {% for key in output %} 14 | {{ key }} {{ output[key] }} 15 | {% endfor %} 16 | {% endfor %} 17 | 18 | [FILTER] 19 | Name record_modifier 20 | Match * 21 | Record hostname {{ fluentbit_hostname }} -------------------------------------------------------------------------------- /roles/logrotate/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logrotate_conf: [] -------------------------------------------------------------------------------- /roles/logrotate/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install logrotate 3 | apt: name=logrotate state=latest update_cache=yes 4 | 5 | - blockinfile: 6 | path: "/etc/logrotate.d/{{ item.path }}" 7 | block: "{{ item.conf }}" 8 | create: yes 9 | loop: "{{ logrotate_conf }}" -------------------------------------------------------------------------------- /roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_conf_template: "nginx-default.conf.j2" 3 | nginx_https_enabled: false -------------------------------------------------------------------------------- /roles/nginx/files/config/general.conf: -------------------------------------------------------------------------------- 1 | # favicon.ico 2 | location = /favicon.ico { 3 | log_not_found off; 4 | access_log off; 5 | } 6 | 7 | # robots.txt 8 | location = /robots.txt { 9 | log_not_found off; 10 | access_log off; 11 | } 12 | 13 | # gzip 14 | gzip on; 15 | gzip_vary on; 16 | gzip_proxied any; 17 | gzip_comp_level 6; 18 | gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; 19 | 20 | # remove trailing slashes 21 | rewrite ^/(.*)/$ /$1 permanent; -------------------------------------------------------------------------------- /roles/nginx/files/config/letsencrypt.conf: -------------------------------------------------------------------------------- 1 | # ACME-challenge 2 | location ^~ /.well-known/acme-challenge/ { 3 | root /var/www/_letsencrypt; 4 | } 5 | -------------------------------------------------------------------------------- /roles/nginx/files/config/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_http_version 1.1; 2 | proxy_cache_bypass $http_upgrade; 3 | 4 | proxy_set_header Upgrade $http_upgrade; 5 | proxy_set_header Connection "upgrade"; 6 | proxy_set_header Host $host; 7 | proxy_set_header X-Real-IP $remote_addr; 8 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 9 | proxy_set_header X-Forwarded-Proto $scheme; 10 | proxy_set_header X-Forwarded-Host $host; 11 | proxy_set_header X-Forwarded-Port $server_port; 12 | -------------------------------------------------------------------------------- /roles/nginx/files/config/security.conf: -------------------------------------------------------------------------------- 1 | # security headers 2 | add_header X-Frame-Options "SAMEORIGIN" always; 3 | add_header X-XSS-Protection "1; mode=block" always; 4 | add_header X-Content-Type-Options "nosniff" always; 5 | add_header Referrer-Policy "no-referrer-when-downgrade" always; 6 | add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; 7 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; 8 | 9 | # . files 10 | location ~ /\.(?!well-known) { 11 | deny all; 12 | } 13 | -------------------------------------------------------------------------------- /roles/nginx/files/nginx.conf: -------------------------------------------------------------------------------- 1 | # Generated by nginxconfig.io 2 | # https://www.digitalocean.com/community/tools/nginx#?0.domain=example.com&0.php=false&0.proxy&0.root=false&server_tokens&limit_req&brotli&expires_assets=14d&expires_media=14d&expires_svg=14d&expires_fonts=14d&client_max_body_size=25 3 | 4 | user www-data; 5 | pid /run/nginx.pid; 6 | worker_processes auto; 7 | worker_rlimit_nofile 65535; 8 | 9 | events { 10 | multi_accept on; 11 | worker_connections 65535; 12 | } 13 | 14 | http { 15 | charset utf-8; 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | log_not_found off; 20 | types_hash_max_size 2048; 21 | client_max_body_size 25M; 22 | 23 | # MIME 24 | include mime.types; 25 | default_type application/octet-stream; 26 | 27 | # logging 28 | access_log /var/log/nginx/access.log; 29 | error_log /var/log/nginx/error.log warn; 30 | 31 | # limits 32 | limit_req_log_level warn; 33 | limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m; 34 | 35 | # SSL 36 | ssl_session_timeout 1d; 37 | ssl_session_cache shared:SSL:10m; 38 | ssl_session_tickets off; 39 | 40 | # Diffie-Hellman parameter for DHE ciphersuites 41 | ssl_dhparam /etc/nginx/dhparam.pem; 42 | 43 | # Mozilla Intermediate configuration 44 | ssl_protocols TLSv1.2 TLSv1.3; 45 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 46 | 47 | # OCSP Stapling 48 | ssl_stapling on; 49 | ssl_stapling_verify on; 50 | resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s; 51 | resolver_timeout 2s; 52 | 53 | # load configs 54 | include /etc/nginx/conf.d/*.conf; 55 | include /etc/nginx/sites-enabled/*; 56 | } 57 | -------------------------------------------------------------------------------- /roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart nginx 3 | service: 4 | name: nginx 5 | state: restarted 6 | 7 | - name: Reload nginx 8 | service: 9 | name: nginx 10 | state: reloaded -------------------------------------------------------------------------------- /roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add nginx repo 3 | apt_repository: 4 | repo: ppa:nginx/stable 5 | 6 | - name: Install nginx 7 | apt: 8 | name: nginx 9 | state: present 10 | force: yes 11 | update_cache: yes 12 | 13 | - name: Copy nginx config files 14 | copy: 15 | src: "{{ item }}" 16 | dest: /etc/nginx 17 | owner: "{{ deploy_user }}" 18 | group: "{{ deploy_group }}" 19 | with_items: 20 | - nginx.conf 21 | - config 22 | 23 | - name: Generate DH param (2048 bits) 24 | openssl_dhparam: 25 | path: /etc/nginx/dhparam.pem 26 | size: 2048 27 | when: nginx_https_enabled == true 28 | 29 | - name: Create a directory if it does not exist 30 | file: 31 | path: "/etc/nginx/{{ item }}" 32 | state: directory 33 | owner: "{{ deploy_user }}" 34 | group: "{{ deploy_group }}" 35 | mode: 0644 36 | with_items: 37 | - sites-available 38 | - sites-enabled 39 | 40 | - name: Copy nginx configuration in place 41 | template: 42 | src: "{{ nginx_conf_template }}" 43 | dest: "/etc/nginx/sites-available/default" 44 | owner: "{{ deploy_user }}" 45 | group: "{{ deploy_group }}" 46 | mode: 0644 47 | 48 | - name: Symlink default site 49 | file: 50 | src: /etc/nginx/sites-available/default 51 | dest: /etc/nginx/sites-enabled/default 52 | state: link 53 | 54 | - name: Set nginx user 55 | lineinfile: 56 | dest: /etc/nginx/nginx.conf 57 | regexp: "^user" 58 | line: "user {{ deploy_user }};" 59 | state: present 60 | 61 | # No need to restart nginx nginx_https_enabled is set to true 62 | # because certbot will install the certificates and then restart nginx 63 | - name: Restart nginx (when not running certbot) 64 | command: "/bin/true" 65 | notify: 66 | - Reload nginx 67 | - Restart nginx 68 | when: nginx_https_enabled != true 69 | 70 | -------------------------------------------------------------------------------- /roles/nginx/templates/nginx-default.conf.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name {{ inventory_hostname }}; 6 | 7 | location / { 8 | return 200 "ok"; 9 | add_header Content-Type text/plain; 10 | } 11 | } -------------------------------------------------------------------------------- /roles/nodejs/defaults/main.yml: -------------------------------------------------------------------------------- 1 | nodejs_version: "12.x" 2 | -------------------------------------------------------------------------------- /roles/nodejs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies 3 | apt: 4 | name: 5 | - apt-transport-https 6 | - gnupg2 7 | state: present 8 | 9 | - name: Add Nodesource apt key 10 | apt_key: 11 | url: https://keyserver.ubuntu.com/pks/lookup?op=get&fingerprint=on&search=0x1655A0AB68576280 12 | id: "68576280" 13 | state: present 14 | 15 | - name: Add NodeSource repositories 16 | apt_repository: 17 | repo: "{{ item }}" 18 | state: present 19 | with_items: 20 | - "deb https://deb.nodesource.com/node_{{ nodejs_version }} {{ ansible_distribution_release }} main" 21 | - "deb-src https://deb.nodesource.com/node_{{ nodejs_version }} {{ ansible_distribution_release }} main" 22 | register: node_repo 23 | 24 | - name: Update apt cache if repo was added 25 | apt: update_cache=yes 26 | when: node_repo.changed 27 | 28 | - name: Ensure Node.js and npm are installed 29 | apt: 30 | name: "nodejs={{ nodejs_version|regex_replace('x', '') }}*" 31 | state: present -------------------------------------------------------------------------------- /roles/pgbackup/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | postgresql_backup_dir: "backups" 3 | postgresql_backup_filename_format: >- 4 | {{ app_name }}-%Y%m%d-%H%M%S.pgdump 5 | postgresql_db_backup_healthcheck: "" 6 | postgresql_s3_backup_hour: "3" 7 | postgresql_s3_backup_minute: "*" 8 | postgresql_s3_backup_delete_after: "7 days" -------------------------------------------------------------------------------- /roles/pgbackup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Create postgresql backup directory 2 | file: 3 | path: "{{ postgresql_backup_dir }}" 4 | recurse: true 5 | state: directory 6 | 7 | - name: Set backup directory permissions 8 | file: 9 | path: "{{ postgresql_backup_dir }}" 10 | state: directory 11 | owner: "{{ deploy_user }}" 12 | group: "{{ deploy_group }}" 13 | mode: 0700 14 | 15 | - name: Upload backup script 16 | become: true 17 | template: 18 | src: postgresql-backup.j2 19 | dest: "{{ postgresql_backup_dir }}/postgresql-backup.sh" 20 | mode: 0755 21 | 22 | - name: Configure backup cron job 23 | cron: 24 | name: Backup cron job 25 | minute: "{{ postgresql_s3_backup_minute }}" 26 | hour: "{{ postgresql_s3_backup_hour }}" 27 | user: "{{ deploy_user }}" 28 | job: "bash -lc {{ postgresql_backup_dir }}/postgresql-backup.sh" 29 | cron_file: "postgresql-backup" 30 | state: present 31 | 32 | -------------------------------------------------------------------------------- /roles/pgbackup/templates/postgresql-backup.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 5 | 6 | NOW=$(date +"%Y-%m-%d-at-%H-%M-%S") 7 | FILENAME="{{ postgresql_db_name }}"-"$NOW" 8 | 9 | DELETION_TIMESTAMP=`[ "$(uname)" = Linux ] && date +%s --date="-{{ postgresql_s3_backup_delete_after }}"` 10 | 11 | echo " * Generating backup"; 12 | PGPASSWORD={{ postgresql_db_password }} pg_dump -Fc --no-acl --no-owner -h localhost -U {{ postgresql_db_user }} {{ postgresql_db_name }} > {{ postgresql_backup_dir }}/"$FILENAME".dump 13 | 14 | echo " * Uploading to S3"; 15 | aws s3 cp {{ postgresql_backup_dir }}/"$FILENAME".dump s3://{{ postgresql_s3_backup_bucket }}/"$FILENAME".dump 16 | 17 | echo " * Delete local file"; 18 | # rm {{ postgresql_backup_dir }}/"$FILENAME".dump 19 | 20 | 21 | # Delete old files 22 | echo " * Deleting old backups..."; 23 | 24 | # Loop through files 25 | aws s3 ls s3://{{ postgresql_s3_backup_bucket }}/ | while read -r line; do 26 | # Get file creation date 27 | createDate=`echo $line|awk {'print $1" "$2'}` 28 | createDate=`date -d"$createDate" +%s` 29 | 30 | if [[ $createDate -lt $DELETION_TIMESTAMP ]] 31 | then 32 | # Get file name 33 | FILENAME=`echo $line|awk {'print $4'}` 34 | if [[ $FILENAME != "" ]] 35 | then 36 | echo " -> Deleting $FILENAME" 37 | aws s3 rm s3://{{ postgresql_s3_backup_bucket }}/$FILENAME 38 | fi 39 | fi 40 | done; 41 | 42 | echo " * Ping Healthcheck URL"; 43 | curl -fsS --retry 3 {{ postgresql_db_backup_healthcheck }} 44 | -------------------------------------------------------------------------------- /roles/postgresql/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | postgresql_version: "9.6" 3 | 4 | postgresql_packages: 5 | - "postgresql-{{ postgresql_version }}" 6 | - "postgresql-contrib-{{ postgresql_version }}" 7 | 8 | postgresql_python_packages: 9 | - "python3-psycopg2" 10 | 11 | postgresql_apt_url: "https://apt.postgresql.org/pub/repos/apt" 12 | postgresql_apt_key: "{{ postgresql_apt_url }}/ACCC4CF8.asc" 13 | postgresql_apt_repo: "deb {{ postgresql_apt_url }}/ {{ ansible_lsb.codename }}-pgdg main" 14 | 15 | postgresql_config_path: /etc/postgresql/{{ postgresql_version }}/main 16 | postgresql_data_path: /var/lib/postgresql/{{ postgresql_version }}/main 17 | postgresql_pid_file: /var/run/postgresql/{{ postgresql_version }}-main.pid 18 | 19 | # default admin user 20 | postgresql_admin_user: postgres 21 | 22 | # table locale and character encoding 23 | postgresql_locale: "en_US" 24 | postgresql_encoding: "UTF-8" 25 | 26 | # shell locale and character encoding 27 | postgresql_shell_locale: "{{ postgresql_locale }}" 28 | postgresql_shell_encoding: "{{ postgresql_encoding | replace('-', '') | lower }}" 29 | 30 | pg_hba_template: "pg_hba.conf.j2" 31 | postgresql_parameters_template: "postgresql.conf.j2" 32 | 33 | # default application database 34 | postgresql_db_user: '' # name of the user (empty means no user is created) 35 | postgresql_db_password: '' 36 | postgresql_db_name: '' # name of the database (empty means no database is created) 37 | 38 | # all the created users 39 | postgresql_users: 40 | - name: "{{ postgresql_db_user | default('') }}" 41 | password: "{{ postgresql_db_password | default('') }}" # only needed if user is given 42 | role_attr_flags: "{{ postgresql_users_role_attr_flags }}" 43 | 44 | # all the created databases 45 | postgresql_databases: 46 | - name: "{{ postgresql_db_name }}" 47 | owner: "{{ postgresql_db_user | default('') }}" # empty mean 'postgres' user will own it 48 | # encoding: "{{ postgresql_encoding }}" 49 | # lc_collate: "{{ postgresql_locale }}.{{ postgresql_encoding }}" 50 | # lc_ctype: "{{ postgresql_locale }}.{{ postgresql_encoding }}" 51 | # template: template0 52 | 53 | # default user attr_flags 54 | postgresql_users_role_attr_flags: 55 | - CREATEDB 56 | - NOSUPERUSER 57 | 58 | 59 | # variables to build postgresql.conf 60 | # postgresql_host: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}" 61 | postgresql_listen: 62 | - "localhost" 63 | 64 | postgresql_port: 5432 65 | postgresql_max_connections: 100 66 | 67 | postgresql_connections: 68 | ssl: false 69 | 70 | postgresql_resources: 71 | shared_buffers: 128MB 72 | 73 | postgresql_write_ahead_log: {} 74 | postgresql_replication: {} 75 | postgresql_query_tuning: {} 76 | postgresql_logging: 77 | log_line_prefix: "'%t '" 78 | log_timezone: "'UTC'" 79 | 80 | postgresql_runtime_statistics: {} 81 | postgresql_autovacuum: {} 82 | postgresql_client_connection_defaults: 83 | datestyle: "'iso, mdy'" 84 | timezone: "'UTC'" 85 | lc_messages: "'{{ postgresql_locale }}.{{ postgresql_encoding }}'" 86 | lc_monetary: "'{{ postgresql_locale }}.{{ postgresql_encoding }}'" 87 | lc_numeric: "'{{ postgresql_locale }}.{{ postgresql_encoding }}'" 88 | lc_time: "'{{ postgresql_locale }}.{{ postgresql_encoding }}'" 89 | default_text_search_config: "'pg_catalog.english'" 90 | 91 | postgresql_lock_management: {} 92 | postgresql_cutomized: {} 93 | 94 | postgresql_service: postgresql 95 | 96 | postgresql_socket_directories: 97 | - "/var/run/postgresql" -------------------------------------------------------------------------------- /roles/postgresql/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: Restart postgresql 2 | service: 3 | name: "{{ postgresql_service }}" 4 | state: restarted -------------------------------------------------------------------------------- /roles/postgresql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set shell locales 3 | copy: 4 | dest: /etc/profile.d/lang.sh 5 | content: | 6 | export LANGUAGE="{{ postgresql_locale }}.{{ postgresql_shell_encoding }}" 7 | export LANG="{{ postgresql_locale }}.{{ postgresql_shell_encoding }}" 8 | export LC_ALL="{{ postgresql_locale }}.{{ postgresql_shell_encoding }}" 9 | 10 | - name: Add postgres repo key 11 | apt_key: 12 | url: "{{ postgresql_apt_key }}" 13 | 14 | - name: Add postgres repo 15 | apt_repository: 16 | repo: "{{ postgresql_apt_repo }}" 17 | 18 | - name: Install required postgres packages 19 | apt: name={{ item }} state=latest update_cache=yes cache_valid_time=86400 20 | loop: 21 | - "{{ postgresql_packages }}" 22 | - "{{ postgresql_python_packages }}" 23 | - "postgresql-{{ postgresql_version }}" 24 | - "postgresql-contrib-{{ postgresql_version }}" 25 | 26 | - name: Configure pg_hba.conf 27 | template: 28 | src: "{{ pg_hba_template }}" 29 | dest: "{{ postgresql_config_path }}/pg_hba.conf" 30 | 31 | - name: Configure postgresql.conf 32 | template: 33 | src: "{{ postgresql_parameters_template }}" 34 | dest: "{{ postgresql_config_path }}/postgresql.conf" 35 | notify: 36 | - Restart postgresql 37 | 38 | - meta: flush_handlers 39 | 40 | - name: Template locales 41 | shell: > 42 | psql -c "update pg_database 43 | set 44 | encoding = pg_char_to_encoding('{{ postgresql_encoding }}'), 45 | datctype = '{{ postgresql_locale }}.{{ postgresql_encoding }}', 46 | datcollate = '{{ postgresql_locale }}.{{ postgresql_encoding }}' 47 | where 48 | encoding != pg_char_to_encoding('{{ postgresql_encoding }}') 49 | or datctype != '{{ postgresql_locale }}.{{ postgresql_encoding }}' 50 | or datcollate != '{{ postgresql_locale }}.{{ postgresql_encoding }}';" 51 | register: postgresql_update_template_result 52 | changed_when: > 53 | postgresql_update_template_result.stdout is defined 54 | and 'UPDATE 0' != postgresql_update_template_result.stdout 55 | ignore_errors: yes 56 | become: yes 57 | become_user: "{{ postgresql_admin_user }}" 58 | 59 | - name: Create users 60 | postgresql_user: 61 | name: "{{ item.name }}" 62 | password: "{{ item.password }}" 63 | role_attr_flags: "{{ item.role_attr_flags | default(postgresql_users_role_attr_flags) | join(',') | replace('\\n', '') }}" 64 | when: item.name != '' 65 | with_items: "{{ postgresql_users }}" 66 | become: yes 67 | become_user: "{{ postgresql_admin_user }}" 68 | 69 | - name: Create databases 70 | postgresql_db: 71 | name: "{{ item.name }}" 72 | owner: "{{ item.owner | default(postgresql_user, true) }}" 73 | encoding: "{{ item.encoding | default(postgresql_encoding) }}" 74 | lc_collate: "{{ item.lc_collate | default(postgresql_locale + '.' + postgresql_encoding) }}" 75 | lc_ctype: "{{ item.lc_ctype | default(postgresql_locale + '.' + postgresql_encoding) }}" 76 | template: "{{ item.template | default('template0') }}" 77 | state: present 78 | when: item.name != '' 79 | with_items: "{{ postgresql_databases }}" 80 | become: yes 81 | become_user: "{{ postgresql_admin_user }}" 82 | -------------------------------------------------------------------------------- /roles/postgresql/templates/pg_hba.conf.j2: -------------------------------------------------------------------------------- 1 | local all postgres peer 2 | local all all peer 3 | host all all 127.0.0.1/32 md5 4 | host all all ::1/128 md5 -------------------------------------------------------------------------------- /roles/postgresql/templates/postgresql.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | # ----------------------------- 4 | # PostgreSQL configuration file 5 | # ----------------------------- 6 | 7 | #------------------------------------------------------------------------------ 8 | # FILE LOCATIONS 9 | #------------------------------------------------------------------------------ 10 | 11 | data_directory = '{{ postgresql_data_path }}' 12 | hba_file = '{{ postgresql_config_path }}/pg_hba.conf' 13 | ident_file = '{{ postgresql_config_path }}/pg_ident.conf' 14 | external_pid_file = '{{ postgresql_pid_file }}' 15 | 16 | #------------------------------------------------------------------------------ 17 | # CONNECTIONS AND AUTHENTICATION 18 | #------------------------------------------------------------------------------ 19 | 20 | listen_addresses = '{{ postgresql_listen | join(',') }}' 21 | port = {{ postgresql_port }} 22 | max_connections = {{ postgresql_max_connections }} 23 | unix_socket_directories = '{{ postgresql_socket_directories | join(',') }}' 24 | {% for k,v in postgresql_connections.items() | list %} 25 | {{ k }} = {{ v }} 26 | {% endfor %} 27 | 28 | #------------------------------------------------------------------------------ 29 | # RESOURCE USAGE (except WAL) 30 | #------------------------------------------------------------------------------ 31 | 32 | {% for k,v in postgresql_resources.items() | list %} 33 | {{ k }} = {{ v }} 34 | {% endfor %} 35 | 36 | #------------------------------------------------------------------------------ 37 | # WRITE AHEAD LOG 38 | #------------------------------------------------------------------------------ 39 | 40 | {% for k,v in postgresql_write_ahead_log.items() | list %} 41 | {{ k }} = {{ v }} 42 | {% endfor %} 43 | 44 | #------------------------------------------------------------------------------ 45 | # REPLICATION 46 | #------------------------------------------------------------------------------ 47 | 48 | {% for k,v in postgresql_replication.items() | list %} 49 | {{ k }} = {{ v }} 50 | {% endfor %} 51 | 52 | #------------------------------------------------------------------------------ 53 | # QUERY TUNING 54 | #------------------------------------------------------------------------------ 55 | 56 | {% for k,v in postgresql_query_tuning.items() | list %} 57 | {{ k }} = {{ v }} 58 | {% endfor %} 59 | 60 | #------------------------------------------------------------------------------ 61 | # ERROR REPORTING AND LOGGING 62 | #------------------------------------------------------------------------------ 63 | 64 | {% for k,v in postgresql_logging.items() | list %} 65 | {{ k }} = {{ v }} 66 | {% endfor %} 67 | 68 | #------------------------------------------------------------------------------ 69 | # RUNTIME STATISTICS 70 | #------------------------------------------------------------------------------ 71 | 72 | {% for k,v in postgresql_runtime_statistics.items() | list %} 73 | {{ k }} = {{ v }} 74 | {% endfor %} 75 | 76 | #------------------------------------------------------------------------------ 77 | # AUTOVACUUM PARAMETERS 78 | #------------------------------------------------------------------------------ 79 | 80 | {% for k,v in postgresql_autovacuum.items() | list %} 81 | {{ k }} = {{ v }} 82 | {% endfor %} 83 | 84 | #------------------------------------------------------------------------------ 85 | # CLIENT CONNECTION DEFAULTS 86 | #------------------------------------------------------------------------------ 87 | 88 | {% for k,v in postgresql_client_connection_defaults.items() | list %} 89 | {{ k }} = {{ v }} 90 | {% endfor %} 91 | 92 | #------------------------------------------------------------------------------ 93 | # LOCK MANAGEMENT 94 | #------------------------------------------------------------------------------ 95 | 96 | {% for k,v in postgresql_lock_management.items() | list %} 97 | {{ k }} = {{ v }} 98 | {% endfor %} 99 | 100 | #------------------------------------------------------------------------------ 101 | # CUSTOMIZED OPTIONS 102 | #------------------------------------------------------------------------------ 103 | 104 | {% for k,v in postgresql_cutomized.items() | list %} 105 | {{ k }} = {{ v }} 106 | {% endfor %} -------------------------------------------------------------------------------- /roles/puma/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy puma.service 3 | template: 4 | src: "{{ puma_service_file }}" 5 | dest: /lib/systemd/system/puma.service 6 | force: yes 7 | owner: "{{ deploy_user }}" 8 | group: "{{ deploy_group }}" 9 | mode: 0644 10 | register: puma_service_file 11 | 12 | - name: Ensure that we re-read puma.service 13 | systemd: 14 | daemon_reload: yes 15 | name: "puma" 16 | when: puma_service_file.changed 17 | 18 | - name: Enable puma 19 | service: 20 | name: puma 21 | enabled: yes 22 | 23 | - name: Restart puma 24 | service: 25 | name: puma 26 | state: restarted 27 | -------------------------------------------------------------------------------- /roles/redis/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Redis Server 3 | apt: name=redis-server state=latest 4 | 5 | - name: Install Redis Tools 6 | apt: name=redis-tools state=latest 7 | 8 | - name: Ensure Redis Server is running 9 | service: name=redis-server state=started enabled=yes -------------------------------------------------------------------------------- /roles/ruby/defaults/main.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 2.6.6 2 | additional_rubies: [] -------------------------------------------------------------------------------- /roles/ruby/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install rbenv 3 | become: yes 4 | become_user: "{{ deploy_user }}" 5 | git: 6 | repo: "https://github.com/rbenv/rbenv.git" 7 | dest: "{{ rbenv_root_path }}" 8 | depth: 1 9 | accept_hostkey: yes 10 | clone: yes 11 | update: yes 12 | 13 | - name: Install ruby-build 14 | become: yes 15 | become_user: "{{ deploy_user }}" 16 | git: 17 | repo: "https://github.com/rbenv/ruby-build.git" 18 | dest: "{{ rbenv_root_path }}/plugins/ruby-build" 19 | depth: 1 20 | 21 | - name: Install rbenv-vars 22 | become: yes 23 | become_user: "{{ deploy_user }}" 24 | git: 25 | repo: "https://github.com/rbenv/rbenv-vars.git" 26 | dest: "{{ rbenv_root_path }}/plugins/rbenv-vars" 27 | depth: 1 28 | 29 | - name: Ensure {{ rbenv_shell_rc }} exists 30 | become: true 31 | become_user: "{{ deploy_user }}" 32 | shell: "touch {{ rbenv_shell_rc_path }}" 33 | args: 34 | creates: "{{ rbenv_shell_rc_path }}" 35 | 36 | - name: Export RBENV_ROOT in {{ rbenv_shell_rc_path }} 37 | become: true 38 | become_user: "{{ deploy_user }}" 39 | lineinfile: 40 | dest: "{{ rbenv_shell_rc_path }}" 41 | regexp: "^export RBENV_ROOT=" 42 | line: "export RBENV_ROOT={{ rbenv_root_path }}" 43 | 44 | - name: Put rbenv in users PATH in {{ rbenv_shell_rc_path }} 45 | become: true 46 | become_user: "{{ deploy_user }}" 47 | lineinfile: 48 | dest: "{{ rbenv_shell_rc_path }}" 49 | regexp: "^PATH=\\$PATH:\\$RBENV_ROOT/bin" 50 | line: "PATH=$RBENV_ROOT/bin:$PATH" 51 | 52 | - name: Put $RBENV_ROOT/shims in users $PATH in {{ rbenv_shell_rc_path }} 53 | become: true 54 | become_user: "{{ deploy_user }}" 55 | lineinfile: 56 | dest: "{{ rbenv_shell_rc_path }}" 57 | regexp: "^PATH=\\$RBENV_ROOT/shims:\\$PATH" 58 | line: "PATH=$RBENV_ROOT/shims:$PATH" 59 | 60 | - name: Install Rubies 61 | become: yes 62 | become_user: "{{ deploy_user }}" 63 | shell: "{{ rbenv_ruby_configure_opts | default('') }} {{ rbenv_bin }} install {{ item }}" 64 | args: 65 | creates: "{{ rbenv_root_path }}/versions/{{ item }}" 66 | with_flattened: 67 | - "{{ additional_rubies }}" 68 | - "{{ ruby_version }}" 69 | 70 | 71 | - name: Check default ruby 72 | shell: '{{ rbenv_bin }} version | grep -oE "^[^ ]+"' 73 | changed_when: no 74 | register: rbenv_current_version 75 | become: yes 76 | become_user: "{{ deploy_user }}" 77 | 78 | - name: Set default ruby 79 | shell: "{{ rbenv_bin }} global {{ ruby_version }}" 80 | become: yes 81 | become_user: "{{ deploy_user }}" 82 | when: rbenv_current_version.stdout != ruby_version 83 | 84 | - name: Install Bundler 85 | shell: "{{ rbenv_root_path }}/shims/gem install bundler" 86 | become: yes 87 | become_user: "{{ deploy_user }}" 88 | -------------------------------------------------------------------------------- /roles/sidekiq/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy sidekiq.service 3 | template: 4 | src: "{{ sidekiq_service_file }}" 5 | dest: /lib/systemd/system/sidekiq.service 6 | force: yes 7 | owner: "{{ deploy_user }}" 8 | group: "{{ deploy_group }}" 9 | mode: 0644 10 | register: sidekiq_service_file 11 | 12 | - name: Ensure that we re-read sidekiq.service 13 | systemd: 14 | daemon_reload: yes 15 | name: "sidekiq" 16 | when: sidekiq_service_file.changed 17 | 18 | - name: Enable sidekiq 19 | service: 20 | name: sidekiq 21 | enabled: yes 22 | 23 | - name: Restart sidekiq 24 | service: 25 | name: sidekiq 26 | state: restarted 27 | -------------------------------------------------------------------------------- /roles/ssh/defaults/main.yml: -------------------------------------------------------------------------------- 1 | ssh_port: 22 2 | ssh_password_authentication: "no" 3 | ssh_permit_root_login: "no" 4 | ssh_usedns: "no" 5 | ssh_permit_empty_password: "no" 6 | ssh_challenge_response_auth: "no" 7 | ssh_gss_api_authentication: "no" 8 | ssh_x11_forwarding: "no" -------------------------------------------------------------------------------- /roles/ssh/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart SSH 3 | service: 4 | name: ssh 5 | state: restarted -------------------------------------------------------------------------------- /roles/ssh/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update SSH configuration to be more secure 3 | lineinfile: 4 | dest: "/etc/ssh/sshd_config" 5 | regexp: "{{ item.regexp }}" 6 | line: "{{ item.line }}" 7 | state: present 8 | with_items: 9 | - regexp: "^PasswordAuthentication" 10 | line: "PasswordAuthentication {{ ssh_password_authentication }}" 11 | - regexp: "^PermitRootLogin" 12 | line: "PermitRootLogin {{ ssh_permit_root_login }}" 13 | - regexp: "^Port" 14 | line: "Port {{ ssh_port }}" 15 | - regexp: "^UseDNS" 16 | line: "UseDNS {{ ssh_usedns }}" 17 | - regexp: "^PermitEmptyPasswords" 18 | line: "PermitEmptyPasswords {{ ssh_permit_empty_password }}" 19 | - regexp: "^ChallengeResponseAuthentication" 20 | line: "ChallengeResponseAuthentication {{ ssh_challenge_response_auth }}" 21 | - regexp: "^GSSAPIAuthentication" 22 | line: "GSSAPIAuthentication {{ ssh_gss_api_authentication }}" 23 | - regexp: "^X11Forwarding" 24 | line: "X11Forwarding {{ ssh_x11_forwarding }}" 25 | notify: Restart SSH -------------------------------------------------------------------------------- /roles/ufw/defaults/main.yml: -------------------------------------------------------------------------------- 1 | ufw_rules: [] -------------------------------------------------------------------------------- /roles/ufw/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: Restart UFW 2 | service: name=ufw state=restarted -------------------------------------------------------------------------------- /roles/ufw/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Install UFW 2 | apt: package=ufw state=present 3 | 4 | - name: Reset UFW to defaults 5 | ufw: state=reset 6 | 7 | - name: Configure UFW defaults 8 | ufw: direction={{ item.direction }} policy={{ item.policy }} 9 | with_items: 10 | - { direction: 'incoming', policy: 'deny' } 11 | - { direction: 'outgoing', policy: 'allow' } 12 | notify: 13 | - Restart UFW 14 | 15 | - name: Configure UFW rules 16 | ufw: rule={{ item.rule }} port={{ item.port }} from={{ item.from }} proto={{ item.proto }} 17 | with_items: 18 | - { rule: 'limit', port: '{{ ssh_port | default("22") }}', proto: 'tcp', from: "any" } 19 | - "{{ ufw_rules }}" 20 | notify: 21 | - Restart UFW 22 | 23 | - name: Enable UFW logging 24 | ufw: logging=on 25 | notify: 26 | - Restart UFW 27 | 28 | - name: Enable UFW 29 | ufw: state=enabled -------------------------------------------------------------------------------- /roles/user/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | deploy_group: "{{ deploy_group }}" 3 | deploy_user: "{{ deploy_user }}" 4 | copy_local_key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}" -------------------------------------------------------------------------------- /roles/user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure sudo group is present 3 | group: 4 | name: sudo 5 | state: present 6 | 7 | - name: Ensure sudo group has sudo privileges 8 | lineinfile: 9 | dest: /etc/sudoers 10 | state: present 11 | regexp: "^%sudo" 12 | line: "%sudo ALL=(ALL:ALL) ALL" 13 | validate: "/usr/sbin/visudo -cf %s" 14 | 15 | - name: Make sure we have a deployment group 16 | group: 17 | name: "{{ deploy_group }}" 18 | state: present 19 | 20 | - name: Allow deployment group to have passwordless sudo 21 | lineinfile: 22 | path: /etc/sudoers 23 | state: present 24 | regexp: '^%{{ deploy_group }}' 25 | line: '%{{ deploy_group }} ALL=(ALL) NOPASSWD: ALL' 26 | validate: '/usr/sbin/visudo -cf %s' 27 | 28 | - name: Create a new user with sudo privileges 29 | user: 30 | name: "{{ deploy_user }}" 31 | state: present 32 | groups: "{{ deploy_group }}" 33 | append: true 34 | create_home: true 35 | shell: /bin/bash 36 | 37 | - name: Set authorized key for remote user 38 | authorized_key: 39 | user: "{{ deploy_user }}" 40 | state: present 41 | key: "{{ copy_local_key }}" -------------------------------------------------------------------------------- /roles/yarn/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Yarn apt key 3 | apt_key: 4 | url: https://dl.yarnpkg.com/debian/pubkey.gpg 5 | 6 | - name: Add Yarn repository 7 | apt_repository: 8 | repo: "deb https://dl.yarnpkg.com/debian/ stable main" 9 | filename: yarn 10 | 11 | - name: Install Yarn 12 | apt: 13 | name: yarn -------------------------------------------------------------------------------- /templates/.rbenv-vars.j2: -------------------------------------------------------------------------------- 1 | RAILS_ENV=production 2 | RACK_ENV=production 3 | RAILS_MASTER_KEY={{ vault_rails_master_key }} 4 | DB_POOL={{ rails_db_pool }} 5 | 6 | # Puma 7 | APP_DIR={{ app_current_path }} 8 | SHARED_DIR={{ app_shared_path }} 9 | WEB_CONCURRENCY={{ puma_web_concurrency }} -------------------------------------------------------------------------------- /templates/database.yml.j2: -------------------------------------------------------------------------------- 1 | production: 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 5 | database: "{{ postgresql_db_name }}" 6 | username: "{{ postgresql_db_user }}" 7 | password: "{{ postgresql_db_password }}" 8 | host: "localhost" -------------------------------------------------------------------------------- /templates/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | upstream app { 2 | # Path to Puma SOCK file, as defined previously 3 | server unix:///{{ puma_socket }} fail_timeout=0; 4 | } 5 | 6 | server { 7 | listen 80; 8 | server_name localhost; 9 | 10 | root {{ app_current_path }}/public; 11 | 12 | try_files $uri/index.html $uri @app; 13 | 14 | location @app { 15 | proxy_pass http://app; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | proxy_set_header Host $http_host; 18 | proxy_redirect off; 19 | } 20 | 21 | error_page 500 502 503 504 /500.html; 22 | client_max_body_size 4G; 23 | keepalive_timeout 10; 24 | } -------------------------------------------------------------------------------- /templates/puma.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Puma HTTP Server 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User={{ deploy_user }} 8 | EnvironmentFile={{ app_root_path }}/.rbenv-vars 9 | 10 | WorkingDirectory={{ app_current_path }} 11 | 12 | ExecStart={{ rbenv_bundle }} exec puma -C {{ app_current_path }}/config/puma.rb 13 | ExecStop={{ rbenv_bundle }} exec puma -S {{ app_current_path }}/config/puma.rb 14 | PIDFile={{ app_pids_path }}/puma.pid 15 | 16 | # Should systemd restart puma? 17 | # Use "no" (the default) to ensure no interference when using 18 | # stop/start/restart via `pumactl`. The "on-failure" setting might 19 | # work better for this purpose, but you must test it. 20 | # Use "always" if only `systemctl` is used for start/stop/restart, and 21 | # reconsider if you actually need the forking config. 22 | Restart=always 23 | 24 | [Install] 25 | WantedBy=multi-user.target 26 | 27 | -------------------------------------------------------------------------------- /templates/sidekiq.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sidekiq 3 | After=network.target 4 | 5 | [Service] 6 | Type=notify 7 | User={{ deploy_user }} 8 | 9 | EnvironmentFile={{ app_root_path }}/.rbenv-vars 10 | 11 | WorkingDirectory={{ app_current_path }} 12 | 13 | ExecStart={{ rbenv_bundle }} exec sidekiq -e production -C config/sidekiq.yml 14 | 15 | # Greatly reduce Ruby memory fragmentation and heap usage 16 | # https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ 17 | Environment=MALLOC_ARENA_MAX=2 18 | 19 | # if we crash, restart 20 | RestartSec=1 21 | Restart=on-failure 22 | 23 | [Install] 24 | WantedBy=multi-user.target --------------------------------------------------------------------------------