├── roles ├── php │ ├── tasks │ │ ├── main.yml │ │ └── php.yml │ ├── handlers │ │ └── main.yml │ ├── templates │ │ ├── php-fpm.conf.j2 │ │ └── php.ini.j2 │ └── defaults │ │ └── main.yml ├── basics │ ├── templates │ │ ├── locale.gen.j2 │ │ ├── locale.j2 │ │ └── 50unattended-upgrades.j2 │ └── tasks │ │ ├── locale.yml │ │ ├── web.yml │ │ ├── main.yml │ │ ├── unattended-upgrades.yml │ │ ├── ensure-os-version.yml │ │ ├── users.yml │ │ └── essential-packages.yml ├── network-basics │ ├── tasks │ │ ├── main.yml │ │ ├── fail2ban.yml │ │ └── firewall.yml │ ├── handlers │ │ └── main.yml │ ├── templates │ │ ├── fail2ban.local.j2 │ │ └── jail.local.j2 │ └── defaults │ │ └── main.yml ├── mariadb │ ├── templates │ │ ├── disable-binary-logging.cnf │ │ ├── my.cnf.j2 │ │ └── mb4strings.cnf.j2 │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── ferm │ ├── handlers │ │ └── main.yml │ ├── templates │ │ └── etc │ │ │ ├── default │ │ │ └── ferm.j2 │ │ │ └── ferm │ │ │ ├── filter-input.d │ │ │ ├── dport_accept.conf.j2 │ │ │ └── dport_limit.conf.j2 │ │ │ └── ferm.conf.j2 │ ├── defaults │ │ └── main.yml │ ├── LICENSE │ ├── tasks │ │ └── main.yml │ └── README.md ├── per-app │ ├── tasks │ │ ├── reload-nginx.yml │ │ ├── folders.yml │ │ ├── main.yml │ │ ├── workers.yml │ │ ├── cron.yml │ │ ├── self-signed-certificate.yml │ │ ├── nginx.yml │ │ └── nginx-includes.yml │ ├── templates │ │ ├── ext.j2 │ │ ├── queue.conf │ │ ├── https.conf.j2 │ │ └── laravan-app.conf.j2 │ └── defaults │ │ └── main.yml ├── docker │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── databases │ └── tasks │ │ ├── main.yml │ │ ├── postgres.yml │ │ └── mysql.yml ├── deploy │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── env.yml │ │ ├── migrate.yml │ │ ├── deploy-spa.yml │ │ ├── post-deploy-docker.yml │ │ ├── post-deploy.yml │ │ ├── restart-workers.yml │ │ ├── finish.yml │ │ ├── start.yml │ │ ├── composer.yml │ │ ├── main.yml │ │ ├── passport.yml │ │ ├── clone.yml │ │ ├── deploy-laravel.yml │ │ ├── optimize.yml │ │ ├── files.yml │ │ └── deploy-docker.yml │ ├── templates │ │ └── .env.j2 │ └── defaults │ │ └── main.yml ├── users │ ├── templates │ │ └── sudoers.d.j2 │ └── tasks │ │ └── main.yml ├── backup │ ├── tasks │ │ ├── main.yml │ │ ├── install.yml │ │ └── configure.yml │ ├── defaults │ │ └── main.yml │ └── templates │ │ ├── models │ │ ├── mysql.rb.j2 │ │ ├── postgres.rb.j2 │ │ └── store-and-notify.j2 │ │ └── config.rb.j2 ├── letsencrypt │ ├── templates │ │ ├── acme-challenge-location.conf.j2 │ │ ├── nginx-challenge-site.conf.j2 │ │ └── renew-certs.py │ ├── README.md │ ├── tasks │ │ ├── main.yml │ │ ├── setup.yml │ │ ├── certificates.yml │ │ └── nginx.yml │ ├── library │ │ └── test_challenges.py │ └── defaults │ │ └── main.yml ├── nginx │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── disable_challenge_sites.yml │ │ ├── reload_nginx.yml │ │ └── main.yml │ ├── defaults │ │ └── main.yml │ └── templates │ │ ├── laravan.conf.j2 │ │ └── nginx.conf.j2 ├── postgres │ ├── defaults │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── pg_hba.conf.j2 ├── logrotate │ └── tasks │ │ └── main.yml └── queue │ ├── tasks │ └── main.yml │ └── templates │ └── supervisord.conf.j2 ├── hosts ├── .gitignore ├── setup.sh ├── group_vars ├── development │ ├── databases.yml │ ├── vault.yml │ └── apps.yml ├── all │ ├── www.yml │ ├── queue.yml │ ├── software.yml │ ├── network.yml │ ├── locale.yml │ ├── logrotate.yml │ ├── swapfile.yml │ ├── users.yml │ └── helpers.yml └── production │ ├── databases.yml │ ├── vault.yml │ └── apps.yml ├── ansible.cfg ├── Pipfile ├── deploy.yml ├── requirements.yml ├── Vagrantfile ├── provision.yml ├── LICENSE ├── .gitlab-ci.yml.example ├── README.md └── Pipfile.lock /roles/php/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: php.yml 3 | when: php_required 4 | -------------------------------------------------------------------------------- /roles/basics/templates/locale.gen.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | {{ locale }} UTF-8 4 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | [development] 2 | 10.10.0.42 3 | 4 | [production] 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/* 2 | .vault_pass 3 | vendor/* 4 | deploy.retry 5 | provision.retry 6 | .idea 7 | -------------------------------------------------------------------------------- /roles/network-basics/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: firewall.yml 3 | - include: fail2ban.yml 4 | -------------------------------------------------------------------------------- /roles/mariadb/templates/disable-binary-logging.cnf: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [mysqld] 4 | skip-log-bin 5 | -------------------------------------------------------------------------------- /roles/ferm/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart ferm 3 | service: name=ferm state=restarted 4 | when: ferm_enabled -------------------------------------------------------------------------------- /roles/per-app/tasks/reload-nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reload nginx 3 | service: 4 | name: nginx 5 | state: reloaded 6 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Installing vendor roles from Ansible Galaxy…" 4 | ansible-galaxy install -r requirements.yml -------------------------------------------------------------------------------- /roles/network-basics/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart fail2ban 3 | service: 4 | name: fail2ban 5 | state: restarted -------------------------------------------------------------------------------- /roles/basics/templates/locale.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | #LANG={{ locale }} 4 | LANGUAGE={{ locale }} 5 | LC_ALL={{ locale }} 6 | -------------------------------------------------------------------------------- /roles/basics/tasks/locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure the locale exists 3 | locale_gen: 4 | name: "{{ locale }}" 5 | state: present 6 | -------------------------------------------------------------------------------- /roles/docker/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | docker_required: "'docker' in [{% for item in apps.values() %}'{{ item.type | default('') }}',{% endfor %}]" 3 | -------------------------------------------------------------------------------- /roles/databases/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: mysql.yml 3 | tags: [database, mysql] 4 | - include: postgres.yml 5 | tags: [database, postgres] 6 | -------------------------------------------------------------------------------- /roles/mariadb/templates/my.cnf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [client] 4 | user={{ mysql_root_user }} 5 | password="{{ vault.db_passwords.root }}" 6 | -------------------------------------------------------------------------------- /roles/deploy/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: end maintenance 3 | shell: php artisan up 4 | args: 5 | chdir: "{{ webroot }}/{{ app_name }}/current" 6 | -------------------------------------------------------------------------------- /roles/deploy/tasks/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install .env file 3 | template: 4 | src: .env.j2 5 | dest: "{{ deploy_helper.new_release_path }}/.env" 6 | -------------------------------------------------------------------------------- /roles/deploy/templates/.env.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | {% for key, value in current_app['env'].items() %} 4 | {{ key }}="{{ value }}" 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /roles/users/templates/sudoers.d.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | {% for service in web_sudoers %} 4 | {{ web_user }} ALL=(root) NOPASSWD: {{ service }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /roles/backup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include: install.yml 4 | when: ansible_os_family == "Debian" 5 | 6 | - include: configure.yml 7 | when: ansible_os_family == "Debian" -------------------------------------------------------------------------------- /roles/letsencrypt/templates/acme-challenge-location.conf.j2: -------------------------------------------------------------------------------- 1 | location ^~ /.well-known/acme-challenge/ { 2 | alias {{ acme_tiny_challenges_directory }}/; 3 | try_files $uri =404; 4 | } 5 | -------------------------------------------------------------------------------- /group_vars/development/databases.yml: -------------------------------------------------------------------------------- 1 | --- 2 | databases: 3 | - type: postgres 4 | # - type: mysql 5 | name: myapp 6 | user: myapp 7 | password: "{{ vault.db_passwords.myapp_postgres }}" 8 | -------------------------------------------------------------------------------- /roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: reload nginx 3 | include: reload_nginx.yml 4 | 5 | - name: disable temporary challenge sites 6 | import_tasks: disable_challenge_sites.yml 7 | 8 | -------------------------------------------------------------------------------- /roles/postgres/defaults/main.yml: -------------------------------------------------------------------------------- 1 | postgres_required: "'postgres' in [{% for item in databases %}'{{ item.type | default('') }}',{% endfor %}]" 2 | postgres_root_user: postgres 3 | postgres_version: 11 4 | -------------------------------------------------------------------------------- /roles/deploy/tasks/migrate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Migrate the database 3 | shell: "{{ current_php_executable }} artisan migrate --force" 4 | args: 5 | chdir: "{{ deploy_helper.new_release_path }}" 6 | -------------------------------------------------------------------------------- /roles/mariadb/defaults/main.yml: -------------------------------------------------------------------------------- 1 | mariadb_required: "'mysql' in [{% for item in databases %}'{{ item.type | default('') }}',{% endfor %}]" 2 | mariadb_binary_logging_disabled: true 3 | mysql_root_user: root 4 | -------------------------------------------------------------------------------- /roles/basics/tasks/web.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create web root 3 | file: 4 | path: "{{ webroot }}" 5 | owner: "{{ web_user }}" 6 | group: "{{ web_group }}" 7 | mode: 0755 8 | state: directory 9 | -------------------------------------------------------------------------------- /roles/deploy/tasks/deploy-spa.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: start.yml 3 | - include: clone.yml 4 | - include: env.yml 5 | when: current_app.env is defined 6 | - include: post-deploy.yml 7 | - include: finish.yml 8 | -------------------------------------------------------------------------------- /roles/letsencrypt/templates/nginx-challenge-site.conf.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen [::]:80; 3 | listen 80; 4 | server_name {{ missing_hosts | join(' ') }}; 5 | include acme-challenge-location.conf; 6 | } 7 | -------------------------------------------------------------------------------- /roles/backup/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backup_setup: 3 | user: 'root' 4 | config_dir: '/etc/backup' 5 | tmp_dir: '/tmp/backup' 6 | data_dir: '/var/lib/backup' 7 | log_dir: '/var/log/backup' 8 | -------------------------------------------------------------------------------- /roles/network-basics/templates/fail2ban.local.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [Definition] 4 | 5 | loglevel = {{ fail2ban_loglevel }} 6 | logtarget = {{ fail2ban_logtarget }} 7 | socket = {{ fail2ban_socket }} 8 | -------------------------------------------------------------------------------- /roles/basics/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: ensure-os-version.yml 3 | - include: essential-packages.yml 4 | - include: locale.yml 5 | - include: users.yml 6 | - include: unattended-upgrades.yml 7 | - include: web.yml 8 | -------------------------------------------------------------------------------- /group_vars/all/www.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # WWW 4 | #------------------------------------------------------------------------------- 5 | webroot: /www 6 | -------------------------------------------------------------------------------- /roles/deploy/tasks/post-deploy-docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run post-deploy commands 3 | shell: "{{ item }}" 4 | args: 5 | chdir: "{{ webroot }}/{{ app_name }}" 6 | loop: "{{ current_app.post_deploy | default([]) }}" 7 | -------------------------------------------------------------------------------- /roles/ferm/templates/etc/default/ferm.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # manual customization of this file is not recommended 3 | 4 | FAST=no 5 | CACHE=no 6 | OPTIONS= 7 | ENABLED="{% if ferm_enabled %}yes{% else %}no{% endif %}" -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = ./hosts 3 | nocows = 1 4 | vault_password_file = .vault_pass 5 | roles_path = vendor 6 | 7 | [ssh_connection] 8 | ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s 9 | -------------------------------------------------------------------------------- /roles/deploy/tasks/post-deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run post-deploy commands 3 | shell: "{{ item }}" 4 | args: 5 | chdir: "{{ deploy_helper.new_release_path }}" 6 | loop: "{{ current_app.post_deploy | default([]) }}" 7 | -------------------------------------------------------------------------------- /roles/deploy/tasks/restart-workers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart Queue Workers 3 | supervisorctl: 4 | name: "{{ app_name }}-{{ item.0 + 1 }}" 5 | state: restarted 6 | with_indexed_items: "{{ current_app.workers | default([]) }}" 7 | -------------------------------------------------------------------------------- /group_vars/all/queue.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Beanstalkd 4 | #------------------------------------------------------------------------------- 5 | enable_beanstalkd: True 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | ansible = ">=2.6.14,<2.7" 10 | jmespath = "*" 11 | 12 | [requires] 13 | python_version = "3" 14 | -------------------------------------------------------------------------------- /roles/deploy/tasks/finish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Finalize the deploy, removing the unfinished file and switching the symlink 3 | deploy_helper: 4 | path: "{{ webroot }}/{{ app_name }}" 5 | release: "{{ deploy_helper.new_release }}" 6 | state: finalize 7 | -------------------------------------------------------------------------------- /group_vars/all/software.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Composer 4 | #------------------------------------------------------------------------------- 5 | composer_keep_updated: true 6 | composer_version: '1.7.5' 7 | -------------------------------------------------------------------------------- /roles/nginx/tasks/disable_challenge_sites.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: disable temporary challenge sites 3 | file: 4 | path: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item }}.conf" 5 | state: absent 6 | with_items: "{{ apps.keys() | list }}" 7 | notify: reload nginx 8 | -------------------------------------------------------------------------------- /deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Deploy {{ app_name }} to {{ env }}" 3 | hosts: "{{ env }}" 4 | remote_user: "{{ web_user }}" 5 | vars: 6 | app_name: "{{ app }}" 7 | current_app: "{{ apps[app_name] }}" 8 | roles: 9 | - role: deploy 10 | tags: deploy 11 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | - src: nickhammond.logrotate 2 | version: master 3 | - src: geerlingguy.composer 4 | version: 1.6.1 5 | - src: geerlingguy.docker 6 | version: 2.5.1 7 | - src: geerlingguy.ntp 8 | version: 1.5.2 9 | - src: kamaln7.swapfile 10 | version: master 11 | -------------------------------------------------------------------------------- /roles/per-app/tasks/folders.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create logs folder of sites 3 | file: 4 | path: "{{ webroot }}/{{ item.key }}/logs" 5 | owner: "{{ web_user }}" 6 | group: "{{ web_group }}" 7 | mode: 0755 8 | state: directory 9 | with_dict: "{{ apps }}" 10 | -------------------------------------------------------------------------------- /group_vars/all/network.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Network Settings 4 | #------------------------------------------------------------------------------- 5 | 6 | # Additional open ports (besides 22, 80 & 443) 7 | open_ports: [] 8 | -------------------------------------------------------------------------------- /roles/deploy/tasks/start.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Initialize the deploy root and gather facts 3 | deploy_helper: 4 | path: "{{ webroot }}/{{ app_name }}" 5 | 6 | - name: Create the new deployment path 7 | file: 8 | path: "{{ deploy_helper.new_release_path }}" 9 | state: directory 10 | -------------------------------------------------------------------------------- /roles/per-app/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: self-signed-certificate.yml 3 | tags: certs 4 | - import_tasks: folders.yml 5 | - import_tasks: nginx-includes.yml 6 | - import_tasks: nginx.yml 7 | - import_tasks: cron.yml 8 | - import_tasks: workers.yml 9 | - import_tasks: reload-nginx.yml 10 | -------------------------------------------------------------------------------- /group_vars/all/locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Locale 4 | #------------------------------------------------------------------------------- 5 | locale: de_DE.UTF-8 6 | ntp_timezone: Europe/Berlin 7 | default_timezone: "{{ ntp_timezone }}" 8 | -------------------------------------------------------------------------------- /roles/basics/tasks/unattended-upgrades.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure unattended-upgrades 3 | template: 4 | src: 50unattended-upgrades.j2 5 | dest: /etc/apt/apt.conf.d/50unattended-upgrades 6 | 7 | - name: Install unattended-upgrades 8 | apt: 9 | name: unattended-upgrades 10 | state: present 11 | -------------------------------------------------------------------------------- /roles/per-app/templates/ext.j2: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | {% for host in app_hosts %} 8 | DNS.{{ loop.index }} = {{ host }} 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /roles/deploy/tasks/composer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Composer dependencies 3 | composer: 4 | command: install 5 | executable: "{{ current_php_executable }}" 6 | arguments: --optimize-autoloader 7 | working_dir: "{{ deploy_helper.new_release_path }}" 8 | no_dev: "{{ 'no' if env == 'development' else 'yes' }}" 9 | -------------------------------------------------------------------------------- /roles/ferm/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ferm_package: ferm 3 | 4 | ferm_enabled: true 5 | ferm_limit_portscans: false 6 | 7 | ferm_default_policy_input: DROP 8 | ferm_default_policy_output: ACCEPT 9 | ferm_default_policy_forward: DROP 10 | 11 | ferm_input_list: [] 12 | ferm_input_group_list: [] 13 | ferm_input_host_list: [] 14 | -------------------------------------------------------------------------------- /roles/letsencrypt/README.md: -------------------------------------------------------------------------------- 1 | # Let’s encrypt/acme-tiny role for Ansible 2 | 3 | ## License 4 | 5 | MIT 6 | 7 | ## Author Information 8 | 9 | This role was created by Andreas Wolf. Visit my [website](http://a-w.io) and [Github profile](https://github.com/andreaswolf/) or follow me on [Twitter](https://twitter.com/andreaswo). 10 | -------------------------------------------------------------------------------- /roles/php/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: reload php-fpm 3 | service: 4 | name: "php{{ item }}-fpm" 5 | state: reloaded 6 | with_items: "{{ php_versions_required }}" 7 | 8 | - name: restart php-fpm 9 | service: 10 | name: "php{{ item }}-fpm" 11 | state: restarted 12 | with_items: "{{ php_versions_required }}" 13 | -------------------------------------------------------------------------------- /roles/basics/tasks/ensure-os-version.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Make sure we're on Ubuntu 16.04 or Debian Stretch 3 | fail: 4 | msg: "Laravan is designed to run on Ubuntu 16.04 or Debian only. Your OS is '{{ ansible_distribution }}' in version '{{ ansible_distribution_release }}'" 5 | when: not is_ubuntu and not is_debian 6 | tags: ensure-os-version 7 | -------------------------------------------------------------------------------- /roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: deploy-laravel.yml 3 | when: current_app.type is not defined or current_app.type == 'laravel' 4 | - include_tasks: deploy-spa.yml 5 | when: current_app.type is defined and current_app.type == 'spa' 6 | - include_tasks: deploy-docker.yml 7 | when: current_app.type is defined and current_app.type == 'docker' 8 | -------------------------------------------------------------------------------- /group_vars/all/logrotate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Logrotate per-app options 4 | #------------------------------------------------------------------------------- 5 | logrotate_options: 6 | - weekly 7 | - size 25M 8 | - missingok 9 | - compress 10 | - delaycompress 11 | - copytruncate 12 | -------------------------------------------------------------------------------- /roles/nginx/tasks/reload_nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: reload nginx 3 | command: nginx -t 4 | register: nginx_test 5 | notify: "{{ (ansible_version.full | version_compare('2.1.1.0', '>=') and role_path | basename == 'nginx') | ternary('perform nginx reload', omit) }}" 6 | 7 | - name: perform nginx reload 8 | service: 9 | name: nginx 10 | state: reloaded 11 | -------------------------------------------------------------------------------- /roles/per-app/tasks/workers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create supervisor config directory 3 | file: 4 | path: "/etc/supervisor/conf.d/" 5 | state: directory 6 | 7 | - name: Set up queue workers 8 | template: 9 | src: queue.conf 10 | dest: "/etc/supervisor/conf.d/{{ item.key }}.conf" 11 | with_dict: "{{ apps }}" 12 | when: "{{ 'workers' in item.value }}" -------------------------------------------------------------------------------- /group_vars/all/swapfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # Swapfile 4 | #------------------------------------------------------------------------------- 5 | swapfile_location: /swapfile 6 | swapfile_size: 2048MB 7 | swapfile_swappiness: False 8 | swapfile_vfs_cache_pressure: False 9 | swapfile_use_dd: False 10 | -------------------------------------------------------------------------------- /roles/mariadb/templates/mb4strings.cnf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [mysqld] 4 | # default character set and collation 5 | collation_server = utf8mb4_unicode_ci 6 | character_set_server = utf8mb4 7 | 8 | # utf8mb4 long key index 9 | innodb_large_prefix = 1 10 | innodb_file_format = barracuda 11 | innodb_file_format_max = barracuda 12 | innodb_file_per_table = 1 13 | -------------------------------------------------------------------------------- /roles/docker/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install docker dependencies 3 | apt: 4 | name: python-docker 5 | state: present 6 | tags: essential-packages 7 | when: docker_required 8 | 9 | - name: Debug 10 | debug: 11 | var: docker_required 12 | 13 | - name: Install docker-ce 14 | include_role: 15 | name: geerlingguy.docker 16 | when: docker_required 17 | -------------------------------------------------------------------------------- /roles/basics/tasks/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create groups 3 | group: 4 | name: "{{ item }}" 5 | state: present 6 | with_items: 7 | "{{ user_groups }}" 8 | 9 | - name: Create users 10 | user: 11 | name: "{{ item.name }}" 12 | group: "{{ item.groups[0] }}" 13 | groups: "{{ item.groups }}" 14 | state: present 15 | shell: /bin/bash 16 | with_items: 17 | "{{ users }}" 18 | -------------------------------------------------------------------------------- /roles/per-app/templates/queue.conf: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | {% for command in item.value.workers %} 4 | [program:{{ item.key }}-{{ loop.index }}] 5 | user={{ web_user }} 6 | command={{ command }} 7 | directory={{ webroot }}/{{ item.key }}/current 8 | stdout_logfile={{ webroot }}/{{ item.key }}/logs/workers.log 9 | redirect_stderr=true 10 | autostart=true 11 | autorestart=true 12 | 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /roles/per-app/tasks/cron.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup a cronjob for Laravel 3 | cron: 4 | name: "{{ item.key }} Laravel Cron" 5 | minute: "*" 6 | user: web 7 | job: "cd {{ webroot }}/{{ item.key }}/current && php artisan schedule:run" 8 | cron_file: "laravan-{{ item.key | replace('.', '_') }}" 9 | with_dict: "{{ apps }}" 10 | when: item.value.type is not defined or item.value.type == 'laravel' 11 | -------------------------------------------------------------------------------- /roles/deploy/tasks/passport.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if keys for Passport are present 3 | stat: 4 | path: "{{ deploy_helper.shared_path }}/storage/oauth-private.key" 5 | register: key_file 6 | 7 | - name: Generate keys for Laravel Passport 8 | shell: "{{ current_php_executable }} artisan passport:keys" 9 | args: 10 | chdir: "{{ deploy_helper.new_release_path }}" 11 | when: not key_file.stat.exists 12 | -------------------------------------------------------------------------------- /roles/logrotate/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate logrotate config for apps 3 | set_fact: 4 | logrotate_scripts: "{{ logrotate_scripts|default([]) | union( [{ 'name': 'laravan-'+item.key, 'path': webroot+'/'+item.key+'/logs/*.log', 'options': logrotate_options }] ) }}" 5 | with_dict: "{{ apps }}" 6 | 7 | - name: Setup logrotate using nickhammond.logrotate 8 | include_role: 9 | name: nickhammond.logrotate 10 | -------------------------------------------------------------------------------- /roles/deploy/tasks/clone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Clone the project to the new release folder 3 | git: 4 | repo: "{{ current_app.source.url }}" 5 | dest: "{{ deploy_helper.new_release_path }}" 6 | version: "{{ current_app.source.version }}" 7 | accept_hostkey: True 8 | 9 | - name: Add an unfinished file, to allow cleanup on successful finalize 10 | file: 11 | path: "{{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }}" 12 | state: touch 13 | -------------------------------------------------------------------------------- /roles/deploy/tasks/deploy-laravel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: start.yml 3 | - import_tasks: clone.yml 4 | - import_tasks: files.yml 5 | - import_tasks: env.yml 6 | when: current_app.env is defined 7 | - import_tasks: composer.yml 8 | - import_tasks: passport.yml 9 | when: current_app.passport is defined and current_app.passport == True 10 | - import_tasks: migrate.yml 11 | - import_tasks: optimize.yml 12 | - import_tasks: post-deploy.yml 13 | - import_tasks: finish.yml 14 | - import_tasks: restart-workers.yml 15 | -------------------------------------------------------------------------------- /roles/ferm/templates/etc/ferm/filter-input.d/dport_accept.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # manual customization of this file is not recommended 3 | 4 | protocol {{ item.protocol | default('tcp') }} dport ({{ item.dport | join(' ') }}) { 5 | {% if item.saddr is defined and item.saddr %} 6 | saddr ({{ item.saddr | join(' ') }}) ACCEPT; 7 | {% else %} 8 | {% if item.accept_any | default(True) %} 9 | ACCEPT; 10 | {% else %} 11 | # connections from any IP address not allowed 12 | {% endif %} 13 | {% endif %} 14 | } -------------------------------------------------------------------------------- /roles/letsencrypt/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: setup.yml 3 | - import_tasks: nginx.yml 4 | - import_tasks: certificates.yml 5 | 6 | - name: Install cronjob for key generation 7 | cron: 8 | cron_file: letsencrypt-certificate-renewal 9 | name: letsencrypt certificate renewal 10 | user: root 11 | job: cd {{ acme_tiny_data_directory }} && ./renew-certs.py && /usr/sbin/service nginx reload 12 | day: "{{ letsencrypt_cronjob_daysofmonth }}" 13 | hour: 4 14 | minute: 30 15 | state: present 16 | -------------------------------------------------------------------------------- /group_vars/development/vault.yml: -------------------------------------------------------------------------------- 1 | --- 2 | vault: 3 | # Passwords for databases 4 | db_passwords: 5 | root: supersecretPassword-goes-here 6 | myapp_postgres: secretPassword-goes-here 7 | 8 | # The secret app key used by Laravel 9 | app_key: "base64:Iu1g5NjYRMKWDZyBFg0kdSuy09EPstGGQwYU1dSWNYQ=" 10 | 11 | # SMTP Credentials 12 | mail: 13 | password: yourMailtrapPassword-goes-here 14 | 15 | # S3 Credentials for backups 16 | s3: 17 | access_key_id: s3AccessKeyId-goes-here 18 | secret_access_key: s3SecretAccessKey-goes-here 19 | -------------------------------------------------------------------------------- /roles/per-app/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Auth Basic 3 | app_uses_auth_basic: "{{ 'auth_basic' in item.value and item.value.auth_basic.enabled | default(False) }}" 4 | auth_basic_htpasswd_file: "{{ nginx_path }}/htpasswd-{{ item.key | regex_replace('[^a-zA-Z]+', '_') }}" 5 | 6 | # Nginx includes 7 | nginx_includes_templates_path: nginx-includes 8 | nginx_includes_deprecated: roles/per-app/templates/includes.d 9 | nginx_includes_pattern: "^({{ nginx_includes_templates_path | regex_escape }}|{{ nginx_includes_deprecated | regex_escape }})/(.*)\\.j2$" 10 | nginx_includes_d_cleanup: true 11 | -------------------------------------------------------------------------------- /roles/php/templates/php-fpm.conf.j2: -------------------------------------------------------------------------------- 1 | ; {{ ansible_managed }} 2 | 3 | [laravan-{{ item }}] 4 | listen = /var/run/php{{ item }}-laravan.sock 5 | listen.owner = {{ web_group }} 6 | listen.group = {{ web_group }} 7 | user = {{ web_user }} 8 | group = {{ web_group }} 9 | pm = dynamic 10 | pm.max_children = 10 11 | pm.start_servers = 1 12 | pm.min_spare_servers = 1 13 | pm.max_spare_servers = 3 14 | pm.max_requests = 500 15 | chdir = {{ webroot }}/ 16 | php_flag[log_errors] = on 17 | php_flag[display_errors] = {{ php_display_errors }} 18 | php_admin_value[open_basedir] = {{ webroot }}/:/tmp 19 | -------------------------------------------------------------------------------- /roles/backup/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Make sure ruby and the build dependencies for our gem are present 3 | apt: 4 | pkg: "{{ item }}" 5 | state: present 6 | update_cache: yes 7 | cache_valid_time: 3600 8 | with_items: 9 | - ruby 10 | - ruby-dev 11 | - autoconf 12 | - binutils-doc 13 | - bison 14 | - build-essential 15 | - flex 16 | - gettext 17 | - ncurses-dev 18 | - zlib1g-dev 19 | 20 | - name: Install the 'backup' gem 21 | gem: 22 | name: backup 23 | state: present 24 | user_install: false 25 | version: 5.0.0.beta.2 26 | -------------------------------------------------------------------------------- /roles/deploy/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | laravel_shared_folders: 3 | - storage/app 4 | - storage/app/public 5 | - storage/framework 6 | - storage/framework/cache 7 | - storage/framework/sessions 8 | - storage/framework/views 9 | extra_shared_folders: "{{ current_app.extra_shared_folders | default([]) }}" 10 | 11 | laravel_shared_symlinks: 12 | - { path: "storage", src: "storage" } 13 | - { path: "public/storage", src: "storage/app/public" } 14 | extra_symlinks: "{{ current_app.extra_symlinks | default([]) }}" 15 | 16 | docker_networks: "{{ (current_app | json_query('networks[*].name')) or [] }}" 17 | -------------------------------------------------------------------------------- /roles/network-basics/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fail2ban_loglevel: 3 3 | fail2ban_logtarget: /var/log/fail2ban.log 4 | fail2ban_socket: /var/run/fail2ban/fail2ban.sock 5 | 6 | fail2ban_ignoreip: 127.0.0.1/8 7 | fail2ban_bantime: 600 8 | fail2ban_maxretry: 6 9 | 10 | fail2ban_backend: polling 11 | 12 | fail2ban_destemail: root@localhost 13 | fail2ban_banaction: iptables-multiport 14 | fail2ban_mta: sendmail 15 | fail2ban_protocol: tcp 16 | fail2ban_chain: INPUT 17 | 18 | fail2ban_action: action_ 19 | 20 | fail2ban_services: 21 | - name: ssh 22 | port: ssh 23 | filter: sshd 24 | logpath: /var/log/auth.log 25 | -------------------------------------------------------------------------------- /roles/basics/tasks/essential-packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Refresh package lists 3 | apt: 4 | update_cache: yes 5 | tags: essential-packages 6 | 7 | - name: Install some essential packages 8 | apt: 9 | name: 10 | - apt-transport-https 11 | - build-essential 12 | - ca-certificates 13 | - curl 14 | - git-core 15 | - graphviz 16 | - lsb-release 17 | - python-mysqldb 18 | - python-passlib 19 | - python-psycopg2 20 | - python-pycurl 21 | - python3-software-properties 22 | - sudo 23 | - vim 24 | state: present 25 | tags: essential-packages 26 | -------------------------------------------------------------------------------- /roles/databases/tasks/postgres.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Postgres: Create users" 3 | become: true 4 | become_user: postgres 5 | postgresql_user: 6 | name: "{{ item.user }}" 7 | password: "{{ item.password }}" 8 | expires: infinity 9 | with_items: "{{ databases }}" 10 | when: item.type == 'postgres' 11 | 12 | - name: "Postgres: Create databases" 13 | become: true 14 | become_user: postgres 15 | postgresql_db: 16 | name: "{{ item.name }}" 17 | state: present 18 | owner: "{{ item.user }}" 19 | login_user: "{{ postgres_root_user }}" 20 | with_items: "{{ databases }}" 21 | when: item.type == 'postgres' 22 | -------------------------------------------------------------------------------- /roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_path: /etc/nginx 3 | nginx_logs_root: /var/log/nginx 4 | nginx_user: "{{ web_user }}" 5 | nginx_group: "{{ web_group }}" 6 | nginx_fastcgi_buffers: 8 8k 7 | nginx_fastcgi_buffer_size: 8k 8 | nginx_ssl_path: "{{ nginx_path }}/ssl" 9 | server_names_hash_bucket_size: 64 10 | 11 | # HSTS defaults 12 | nginx_hsts_max_age: 31536000 13 | nginx_hsts_include_subdomains: true 14 | nginx_hsts_preload: true 15 | 16 | # Fastcgi cache params 17 | nginx_cache_path: /var/cache/nginx 18 | nginx_cache_duration: 30s 19 | nginx_cache_key_storage_size: 10m 20 | nginx_cache_size: 250m 21 | nginx_cache_inactive: 1h 22 | -------------------------------------------------------------------------------- /roles/network-basics/tasks/fail2ban.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure fail2ban is installed and up-to-date 3 | apt: 4 | name: fail2ban 5 | state: latest 6 | update_cache: true 7 | notify: 8 | - restart fail2ban 9 | tags: fail2ban 10 | 11 | - name: Configure fail2ban 12 | template: 13 | src: "{{ item }}.j2" 14 | dest: "/etc/fail2ban/{{ item }}" 15 | with_items: 16 | - jail.local 17 | - fail2ban.local 18 | notify: 19 | - restart fail2ban 20 | tags: fail2ban 21 | 22 | - name: Start fail2ban now and on reboot 23 | service: 24 | name: fail2ban 25 | state: started 26 | enabled: yes 27 | tags: fail2ban 28 | -------------------------------------------------------------------------------- /roles/nginx/templates/laravan.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | # Prevent scripts from being executed inside the storage folder. 4 | location ~* /storage/.*\.php$ { 5 | deny all; 6 | } 7 | 8 | # # Browser-cache static files 9 | # location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf)$ { 10 | # expires 7d; 11 | # } 12 | 13 | # Set the max body size equal to PHP's max POST size. 14 | client_max_body_size {{ php_post_max_size | default('25m') | lower }}; 15 | 16 | include h5bp/directive-only/x-ua-compatible.conf; 17 | include h5bp/directive-only/extra-security.conf; 18 | include h5bp/location/cross-domain-fonts.conf; 19 | include h5bp/location/protect-system-files.conf; 20 | -------------------------------------------------------------------------------- /group_vars/production/databases.yml: -------------------------------------------------------------------------------- 1 | --- 2 | databases: 3 | - type: postgres 4 | # - type: mysql 5 | name: myapp 6 | user: myapp 7 | password: "{{ vault.db_passwords.myapp_postgres }}" 8 | backup: 9 | s3: 10 | access_key_id: "{{ vault.s3.access_key_id }}" 11 | secret_access_key: "{{ vault.s3.secret_access_key }}" 12 | region: eu-central-1 13 | bucket: myapp-backups 14 | path: daily 15 | # Uncomment the following line and enter a valid slack webhook url in order 16 | # to receive notifications about backup success / failure through slack: 17 | # slack: 18 | # webhook_url: https://hooks.slack.com/services/xxxxxxxx/xxxxxxxxx/xxxxxxxxxx 19 | -------------------------------------------------------------------------------- /roles/databases/tasks/mysql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "MySQL: Create databases" 3 | mysql_db: 4 | name: "{{ item.name }}" 5 | state: present 6 | login_user: "{{ mysql_root_user }}" 7 | login_password: "{{ vault.db_passwords.root }}" 8 | with_items: "{{ databases }}" 9 | when: item.type == 'mysql' 10 | 11 | - name: "MySQL: Create users and grant permissions" 12 | mysql_user: 13 | name: "{{ item.user }}" 14 | password: "{{ item.password }}" 15 | append_privs: yes 16 | priv: "{{ item.name }}.*:ALL" 17 | state: present 18 | login_user: "{{ mysql_root_user }}" 19 | login_password: "{{ vault.db_passwords.root }}" 20 | host: "localhost" 21 | with_items: "{{ databases }}" 22 | when: item.type == 'mysql' 23 | -------------------------------------------------------------------------------- /group_vars/production/vault.yml: -------------------------------------------------------------------------------- 1 | --- 2 | vault: 3 | # Passwords for databases 4 | db_passwords: 5 | root: supersecretPassword-goes-here 6 | myapp_postgres: secretPassword-goes-here 7 | 8 | # The secret app key used by Laravel 9 | app_key: "base64:M3K69g0qqUJ2NWMxIqdGrunovDRnepog0qkw8Bev0Dk=" 10 | 11 | # SMTP username & password 12 | mail: 13 | username: smtpUsername-goes-here 14 | password: smtpPassword-goes-here 15 | 16 | # S3 credentials for backups 17 | s3: 18 | access_key_id: s3AccessKeyId-goes-here 19 | secret_access_key: s3SecretAccessKey-goes-here 20 | 21 | # Credentials for Pusher 22 | pusher: 23 | app_id: pusherAppId-goes-here 24 | app_key: pusherAppKey-goes-here 25 | app_secret: pusherAppSecret-goes-here 26 | cluster: eu 27 | -------------------------------------------------------------------------------- /group_vars/development/apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apps: 3 | myapp: 4 | hosts: 5 | - canonical: myapp.test 6 | ssl: 7 | enabled: True 8 | provider: self-signed 9 | source: 10 | url: git@github.com:acme/myapp.git 11 | version: master 12 | env: 13 | APP_URL: http://myapp.test 14 | APP_DEBUG: true 15 | APP_KEY: "{{ vault.app_key }}" 16 | DB_CONNECTION: pgsql 17 | DB_HOST: "127.0.0.1" 18 | DB_DATABASE: myapp 19 | DB_USERNAME: myapp 20 | DB_PASSWORD: "{{ vault.db_passwords.myapp_postgres }}" 21 | QUEUE_DRIVER: sync 22 | MAIL_DRIVER: smtp 23 | MAIL_HOST: mailtrap.io 24 | MAIL_PORT: 587 25 | MAIL_USERNAME: mailUsername@myapp.test 26 | MAIL_PASSWORD: "{{ vault.mail.password }}" 27 | MAIL_ENCRYPTION: tls 28 | passport: False 29 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | 6 | config.vm.box_check_update = false 7 | config.ssh.insert_key = false 8 | 9 | config.vm.box = "bento/ubuntu-16.04" 10 | 11 | config.vm.network "private_network", ip: "10.10.0.42" 12 | config.vm.network "forwarded_port", guest: 80, host: 8000 13 | config.vm.network "forwarded_port", guest: 3306, host: 33060 14 | 15 | config.vm.synced_folder ".", "/vagrant" #, disabled: true 16 | # config.vm.synced_folder "/var/www/domains", "/var/www/domains", type: "nfs", :mount_option => ['actimeo=2'] 17 | 18 | config.vm.provider :virtualbox do |v| 19 | v.memory = 2048 20 | v.cpus = 2 21 | v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 22 | v.customize ["modifyvm", :id, "--ioapic", "on"] 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /roles/network-basics/tasks/firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Make sure UFW is installed 3 | apt: 4 | name: ufw 5 | state: present 6 | tags: firewall 7 | 8 | - name: Allow SSH and HTTP(S) incoming traffic 9 | ufw: 10 | rule: allow 11 | port: "{{ item }}" 12 | proto: tcp 13 | with_items: 14 | - 22 15 | - 80 16 | - 443 17 | tags: firewall 18 | 19 | - block: 20 | - name: Open additional ports 21 | ufw: 22 | rule: allow 23 | port: "{{ item }}" 24 | proto: tcp 25 | with_items: "{{ open_ports }}" 26 | when: "{{ len(open_ports) > 0 }}" 27 | tags: firewall 28 | 29 | - block: 30 | - name: Enable UFW 31 | ufw: 32 | state: enabled 33 | policy: deny 34 | - name: Ensure UFW is started now and on boot 35 | service: 36 | name: ufw 37 | state: started 38 | enabled: yes 39 | tags: firewall 40 | 41 | -------------------------------------------------------------------------------- /group_vars/all/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # User accounts configuration 4 | #------------------------------------------------------------------------------- 5 | user_groups: 6 | - docker 7 | - "{{ web_group }}" 8 | 9 | users: 10 | - name: "{{ web_user }}" 11 | groups: 12 | - docker 13 | - "{{ web_group }}" 14 | keys: 15 | # Uncomment and edit either of the following lines in order to authorize yourself to log in as the "web" user on the servers. 16 | # It's required for deployment. Add lines for your coworkers that also require deployment permissions accordingly. 17 | # - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" 18 | # - https://github.com/username.keys 19 | 20 | web_user: web 21 | web_group: www-data 22 | web_sudoers: 23 | - "/usr/sbin/service php7.0-fpm *" 24 | - "/usr/sbin/service php7.1-fpm *" 25 | - "/usr/sbin/service php7.2-fpm *" 26 | -------------------------------------------------------------------------------- /roles/deploy/tasks/optimize.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Build laravel config cache 3 | shell: "{{ current_php_executable }} artisan config:cache" 4 | args: 5 | chdir: "{{ deploy_helper.new_release_path }}" 6 | register: config_cache_result 7 | ignore_errors: yes 8 | 9 | - name: Inform about inability to cache config 10 | debug: 11 | msg: Config could not be cached. Are you using lumen? 12 | when: '"no commands defined" in config_cache_result.stdout' 13 | 14 | - name: Build laravel route cache 15 | shell: "{{ current_php_executable }} artisan route:cache" 16 | args: 17 | chdir: "{{ deploy_helper.new_release_path }}" 18 | register: route_cache_result 19 | ignore_errors: yes 20 | 21 | - name: Inform about inability to cache routes due to Closure 22 | debug: 23 | msg: Routes could not be cached due to a Closure. In order to have routes cached, you may only define controller routes. 24 | when: '"Closure" in route_cache_result.stdout' 25 | -------------------------------------------------------------------------------- /roles/backup/templates/models/mysql.rb.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | ## 4 | # Backup Generated: backup_files 5 | # Once configured, you can run the backup with the following command: 6 | # 7 | # $ backup perform -t backup_files [-c ] 8 | # 9 | # For more information about Backup's components, see the documentation at: 10 | # http://backup.github.io/backup 11 | # 12 | Model.new(:{{ item.name | regex_replace('[^a-zA-Z]+', '_') }}, '{{ item.name }} MariaDB dump') do 13 | 14 | ## 15 | # MySQL [Database] 16 | # 17 | database MySQL do |db| 18 | # To dump all databases, set `db.name = :all` (or leave blank) 19 | db.name = "{{ item.name }}" 20 | db.username = "{{ item.user }}" 21 | db.password = "{{ item.password }}" 22 | db.host = "localhost" 23 | db.port = 3306 24 | end 25 | 26 | {{ lookup('template', 'store-and-notify.j2') }} 27 | 28 | end 29 | -------------------------------------------------------------------------------- /roles/backup/templates/models/postgres.rb.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | ## 4 | # Backup Generated: backup_files 5 | # Once configured, you can run the backup with the following command: 6 | # 7 | # $ backup perform -t backup_files [-c ] 8 | # 9 | # For more information about Backup's components, see the documentation at: 10 | # http://backup.github.io/backup 11 | # 12 | Model.new(:{{ item.name | regex_replace('[^a-zA-Z]+', '_') }}, '{{ item.name }} MariaDB dump') do 13 | 14 | ## 15 | # PostgreSQL [Database] 16 | # 17 | database PostgreSQL do |db| 18 | # To dump all databases, set `db.name = :all` (or leave blank) 19 | db.name = "{{ item.name }}" 20 | db.username = "{{ item.user }}" 21 | db.password = "{{ item.password }}" 22 | db.host = "localhost" 23 | db.port = 5432 24 | end 25 | 26 | {{ lookup('template', 'store-and-notify.j2') }} 27 | 28 | end 29 | -------------------------------------------------------------------------------- /roles/deploy/tasks/files.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create some folders in the shared path 3 | file: 4 | path: "{{ deploy_helper.shared_path }}/{{ item }}" 5 | state: directory 6 | owner: "{{ web_user }}" 7 | group: "{{ web_group }}" 8 | with_items: "{{ laravel_shared_folders | union(extra_shared_folders) }}" 9 | 10 | - name: Remove symlink targets in app folder 11 | file: 12 | path: "{{ deploy_helper.new_release_path }}/{{ item.path }}" 13 | state: absent 14 | with_items: "{{ laravel_shared_symlinks | union(extra_symlinks) }}" 15 | 16 | - name: Add symlinks from the new release to the shared folder 17 | file: 18 | path: "{{ deploy_helper.new_release_path }}/{{ item.path }}" 19 | src: '{{ deploy_helper.shared_path }}/{{ item.src }}' 20 | state: link 21 | with_items: "{{ laravel_shared_symlinks | union(extra_symlinks) }}" 22 | 23 | - name: Symlink the logs folder 24 | file: 25 | path: "{{ deploy_helper.shared_path }}/storage/logs" 26 | src: "{{ webroot }}/{{ app_name }}/logs" 27 | state: link 28 | -------------------------------------------------------------------------------- /roles/ferm/templates/etc/ferm/filter-input.d/dport_limit.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # manual customization of this file is not recommended 3 | 4 | {% if item.disabled is undefined or (item.disabled is defined and not item.disabled) or (item.disabled is defined and item.disabled == 'false') %} 5 | protocol {{ item.protocol | default('tcp syn') }} dport ({{ item.dport | join(' ') }}) { 6 | 7 | @subchain "dport-limit-{{ item.dport[0] }}" { 8 | mod recent name {{ item.dport[0] | upper }} { 9 | set NOP; 10 | update seconds {{ item.seconds | default('300') }} hitcount {{ item.hits | default('5') }} @subchain "dport-log-{{ item.dport[0] }}" { 11 | mod recent set name "badguys" { 12 | mod limit limit 3/hour limit-burst 5 { 13 | LOG log-prefix "iptables-blocked-{{ item.dport[0] }}: " log-level warning; 14 | } 15 | DROP; 16 | } 17 | } 18 | } 19 | } 20 | 21 | ACCEPT; 22 | } 23 | {% else %} 24 | # dport_limit rule has been disabled by item.disabled variable 25 | {% endif %} -------------------------------------------------------------------------------- /provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Provision servers in {{ env }} 3 | hosts: "{{ env }}" 4 | become: yes 5 | remote_user: root 6 | roles: 7 | - role: basics 8 | tags: basics 9 | - role: users 10 | tags: users 11 | - role: geerlingguy.ntp 12 | tags: [basics, ntp] 13 | - role: kamaln7.swapfile 14 | tags: [basics, swapfile] 15 | - role: network-basics 16 | tags: network-basics 17 | - role: postgres 18 | tags: [database, postgres] 19 | when: postgres_required 20 | - role: mariadb 21 | tags: [database, mariadb] 22 | when: mariadb_required 23 | - role: php 24 | tags: php 25 | - role: nginx 26 | tags: nginx 27 | - role: geerlingguy.composer 28 | tags: composer 29 | - role: docker 30 | tags: docker 31 | - role: letsencrypt 32 | tags: [letsencrypt, certs, per-app] 33 | - role: databases 34 | tags: databases 35 | - role: per-app 36 | tags: per-app 37 | - role: logrotate 38 | tags: [logrotate, per-app] 39 | - role: queue 40 | tags: queue 41 | - role: backup 42 | tags: backup 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joseph Paul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /roles/ferm/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nick Janetakis nick.janetakis@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /roles/queue/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Make sure Supervisor is installed 3 | apt: 4 | name: supervisor 5 | state: present 6 | 7 | - name: Install supervisord configuration 8 | template: 9 | src: supervisord.conf.j2 10 | dest: /etc/supervisor/supervisord.conf 11 | 12 | - name: Start and enable Supervisor via systemctl as workaround for a bug in the 16.04 supervisor package 13 | shell: "{{ item }}" 14 | with_items: 15 | - systemctl enable supervisor 16 | - systemctl start supervisor 17 | 18 | - name: Make sure Supervisor is being run 19 | service: 20 | name: supervisor 21 | state: restarted 22 | enabled: yes 23 | 24 | - name: Install Redis 25 | apt: 26 | name: redis-server 27 | state: present 28 | 29 | - name: Install Beanstalkd memory queue 30 | apt: 31 | name: beanstalkd 32 | state: present 33 | when: enable_beanstalkd 34 | 35 | - name: Make sure Beanstalkd is being run 36 | service: 37 | name: beanstalkd 38 | state: restarted 39 | enabled: yes 40 | when: enable_beanstalkd 41 | 42 | - name: Make sure Beanstalkd is not being run unnecessarily 43 | service: 44 | name: beanstalkd 45 | state: stopped 46 | enabled: no 47 | when: not enable_beanstalkd 48 | -------------------------------------------------------------------------------- /roles/php/templates/php.ini.j2: -------------------------------------------------------------------------------- 1 | ; {{ ansible_managed }} 2 | 3 | [PHP] 4 | error_reporting = {{ php_error_reporting }} 5 | display_errors = {{ php_display_errors }} 6 | display_startup_errors = {{ php_display_startup_errors }} 7 | max_execution_time = {{ php_max_execution_time }} 8 | max_input_time = {{ php_max_input_time }} 9 | max_input_vars = {{ php_max_input_vars }} 10 | memory_limit = {{ php_memory_limit }} 11 | post_max_size = {{ php_post_max_size }} 12 | sendmail_path = {{ php_sendmail_path }} 13 | session.save_path = {{ php_session_save_path }} 14 | track_errors = {{ php_track_errors }} 15 | upload_max_filesize = {{ php_upload_max_filesize }} 16 | expose_php = Off 17 | date.timezone = {{ php_default_timezone }} 18 | 19 | [mysqlnd] 20 | mysqlnd.collect_memory_statistics = {{ php_mysqlnd_collect_memory_statistics }} 21 | 22 | [opcache] 23 | opcache.enable = {{ php_opcache_enable }} 24 | opcache.enable_cli = {{ php_opcache_enable_cli }} 25 | opcache.memory_consumption = {{ php_opcache_memory_consumption }} 26 | opcache.interned_strings_buffer = {{ php_opcache_interned_strings_buffer }} 27 | opcache.max_accelerated_files = {{ php_opcache_max_accelerated_files }} 28 | opcache.revalidate_freq = {{ php_opcache_revalidate_freq }} 29 | opcache.fast_shutdown = {{ php_opcache_fast_shutdown }} 30 | -------------------------------------------------------------------------------- /roles/postgres/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install key for Posgresql repository (Ubuntu) 3 | apt_key: 4 | url: "https://www.postgresql.org/media/keys/ACCC4CF8.asc" 5 | state: present 6 | when: is_ubuntu == True and is_arm == False 7 | 8 | - name: Add Postgresql PPA (Ubuntu) 9 | apt_repository: 10 | repo: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main" 11 | update_cache: yes 12 | when: is_ubuntu == True and is_arm == False 13 | 14 | - name: Install Postgresql 15 | apt: 16 | update_cache: yes 17 | name: postgresql-{{ postgres_version }} 18 | state: present 19 | 20 | - name: Install pg_hba.conf file 21 | template: 22 | src: pg_hba.conf.j2 23 | dest: /etc/postgresql/{{ postgres_version }}/main/pg_hba.conf 24 | 25 | - name: Start Postgresql Server 26 | service: 27 | name: postgresql 28 | state: restarted 29 | enabled: true 30 | 31 | - name: Configure Postgresql root user 32 | become: true 33 | become_user: postgres 34 | postgresql_user: 35 | name: "{{ postgres_root_user }}" 36 | password: "{{ vault.db_passwords.root }}" 37 | role_attr_flags: SUPERUSER 38 | 39 | - name: Restart Postgresql Server 40 | service: 41 | name: postgresql 42 | state: restarted 43 | enabled: true 44 | -------------------------------------------------------------------------------- /roles/deploy/tasks/deploy-docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Authenticate with docker registry 3 | docker_login: 4 | url: "{{ current_app.login.url | default('https://index.docker.io/v1/')}}" 5 | email: "{{ current_app.login.email | default(None) }}" 6 | username: "{{ current_app.login.username }}" 7 | password: "{{ current_app.login.password }}" 8 | when: "{{ 'login' in current_app and 'username' in current_app.login and 'password' in current_app.login }}" 9 | 10 | - name: Create docker networks 11 | docker_network: 12 | name: "{{ item }}" 13 | loop: "{{ docker_networks }}" 14 | 15 | - name: Bring up docker container 16 | docker_container: 17 | state: started 18 | pull: True 19 | name: "{{ app_name }}" 20 | image: "{{ current_app.image }}" 21 | restart_policy: "{{ current_app.restart | default(True) }}" 22 | ports: "{{ current_app.ports | default([]) }}" 23 | links: "{{ current_app.links | default([]) }}" 24 | volumes: "{{ current_app.volumes | default([]) }}" 25 | env: "{{ current_app.env | default({}) }}" 26 | networks: "{{ current_app.networks | default([]) }}" 27 | entrypoint: "{{ current_app.entrypoint | default([]) }}" 28 | capabilities: "{{ current_app.capabilities | default([]) }}" 29 | 30 | - import_tasks: post-deploy-docker.yml 31 | 32 | -------------------------------------------------------------------------------- /roles/queue/templates/supervisord.conf.j2: -------------------------------------------------------------------------------- 1 | ; supervisor config file 2 | ; {{ ansible_managed }} 3 | 4 | [unix_http_server] 5 | file=/var/run/supervisor.sock ; path to the socket file 6 | chmod=0770 ; socket file mode (default 0700) 7 | chown=nobody:{{ web_group }} ; owner of the socket file 8 | 9 | [supervisord] 10 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 11 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 12 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) 13 | 14 | ; the below section must remain in the config file for RPC 15 | ; (supervisorctl/web interface) to work, additional interfaces may be 16 | ; added by defining them in separate rpcinterface: sections 17 | [rpcinterface:supervisor] 18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 19 | 20 | [supervisorctl] 21 | serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket 22 | 23 | ; The [include] section can just contain the "files" setting. This 24 | ; setting can list multiple files (separated by whitespace or 25 | ; newlines). It can also contain wildcards. The filenames are 26 | ; interpreted as relative to this file. Included files *cannot* 27 | ; include files themselves. 28 | 29 | [include] 30 | files = /etc/supervisor/conf.d/*.conf 31 | -------------------------------------------------------------------------------- /roles/users/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: Setup users 16 | user: 17 | name: "{{ item.name }}" 18 | group: "{{ item.groups[0] }}" 19 | groups: "{{ item.groups | join(',') }}" 20 | password: '{% for user in vault_users | default([]) if user.name == item.name and user.password is defined %}{% if loop.first %}{{ user.password | password_hash("sha512", user.salt[:16] | default(None) | regex_replace("[^\.\/a-zA-Z0-9]", "x")) }}{% endif %}{% else %}{{ None }}{% endfor %}' 21 | state: present 22 | shell: /bin/bash 23 | update_password: always 24 | with_items: "{{ users }}" 25 | 26 | - name: Add web user sudoers items for services 27 | template: 28 | src: sudoers.d.j2 29 | dest: "/etc/sudoers.d/{{ web_user }}-services" 30 | mode: 0440 31 | owner: root 32 | group: root 33 | validate: "/usr/sbin/visudo -cf %s" 34 | when: web_sudoers 35 | 36 | - name: Add SSH keys 37 | authorized_key: 38 | user: "{{ item.0.name }}" 39 | key: "{{ item.1 }}" 40 | with_subelements: 41 | - "{{ users | default([]) }}" 42 | - keys 43 | -------------------------------------------------------------------------------- /roles/letsencrypt/tasks/setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create directories and set permissions 3 | file: 4 | mode: "{{ item.mode | default(omit) }}" 5 | path: "{{ item.path }}" 6 | state: directory 7 | with_items: 8 | - path: "{{ acme_tiny_data_directory }}" 9 | mode: '0700' 10 | - path: "{{ acme_tiny_data_directory }}/csrs" 11 | - path: "{{ acme_tiny_software_directory }}" 12 | - path: "{{ acme_tiny_challenges_directory }}" 13 | - path: "{{ letsencrypt_certs_dir }}" 14 | mode: '0700' 15 | 16 | - name: Clone acme-tiny repository 17 | git: 18 | dest: "{{ acme_tiny_software_directory }}" 19 | repo: "{{ acme_tiny_repo }}" 20 | version: "{{ acme_tiny_commit }}" 21 | accept_hostkey: yes 22 | 23 | - name: Copy Lets Encrypt account key source file 24 | copy: 25 | src: "{{ letsencrypt_account_key_source_file }}" 26 | dest: "{{ letsencrypt_account_key }}" 27 | when: letsencrypt_account_key_source_file is defined 28 | 29 | - name: Copy Lets Encrypt account key source contents 30 | copy: 31 | content: "{{ letsencrypt_account_key_source_content | trim }}" 32 | dest: "{{ letsencrypt_account_key }}" 33 | when: letsencrypt_account_key_source_content is defined 34 | 35 | - name: Generate a new account key 36 | shell: openssl genrsa 4096 > {{ letsencrypt_account_key }} 37 | args: 38 | creates: "{{ letsencrypt_account_key }}" 39 | when: letsencrypt_account_key_source_content is not defined and letsencrypt_account_key_source_file is not defined 40 | -------------------------------------------------------------------------------- /roles/backup/templates/models/store-and-notify.j2: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # Compress with BZip2 4 | # 5 | compress_with Bzip2 do |compression| 6 | compression.level = 9 7 | end 8 | 9 | {% if 's3' in item.backup %} 10 | ## 11 | # Amazon Simple Storage Service [Storage] 12 | # 13 | store_with S3 do |s3| 14 | # AWS Credentials 15 | s3.access_key_id = "{{ item.backup.s3.access_key_id }}" 16 | s3.secret_access_key = "{{ item.backup.s3.secret_access_key }}" 17 | # Or, to use a IAM Profile: 18 | # s3.use_iam_profile = true 19 | 20 | s3.region = "{{ item.backup.s3.region }}" 21 | s3.bucket = "{{ item.backup.s3.bucket }}" 22 | s3.path = "{{ item.backup.s3.path }}" 23 | # s3.keep = 5 24 | # s3.keep = Time.now - 2592000 # Remove all backups older than 1 month. 25 | end 26 | {% endif %} 27 | 28 | {% if 'slack' in item.backup %} 29 | ## 30 | # Slack [Notifier] 31 | # 32 | notify_by Slack do |slack| 33 | slack.on_success = true 34 | slack.on_warning = true 35 | slack.on_failure = true 36 | 37 | # The incoming webhook url 38 | # https://hooks.slack.com/services/xxxxxxxx/xxxxxxxxx/xxxxxxxxxx 39 | slack.webhook_url = '{{ item.backup.slack.webhook_url }}' 40 | 41 | 42 | ## 43 | # Optional 44 | # 45 | # The channel to which messages will be sent 46 | # slack.channel = 'ops-log' 47 | # 48 | # The username to display along with the notification 49 | # slack.username = 'backup@example.org' 50 | end 51 | {% endif %} 52 | -------------------------------------------------------------------------------- /roles/php/defaults/main.yml: -------------------------------------------------------------------------------- 1 | 2 | php_required: "'laravel' in app_types" 3 | php_versions_used: "[{% for item in laravel_apps %}'{{ item.php_version if 'php_version' in item else php_version_default }}',{% endfor %}]" 4 | php_versions_required: "{{ php_versions_used | unique }}" 5 | 6 | disable_default_pool: true 7 | memcached_sessions: false 8 | 9 | php_versions: 10 | - "7.2" 11 | - "7.3" 12 | - "7.4" 13 | 14 | php_packages: 15 | - "bcmath" 16 | - "cli" 17 | - "common" 18 | - "curl" 19 | - "dev" 20 | - "fpm" 21 | - "gd" 22 | - "intl" 23 | - "mbstring" 24 | - "mysql" 25 | - "opcache" 26 | - "pgsql" 27 | - "sqlite3" 28 | - "xml" 29 | - "xmlrpc" 30 | - "zip" 31 | 32 | php_packages_times_versions: "[{% for item in php_versions_required | product(php_packages) | list %}'php{{item.0}}-{{item.1}}',{%endfor%}]" 33 | 34 | php_error_reporting: 'E_ALL & ~E_DEPRECATED & ~E_STRICT' 35 | php_display_errors: 'Off' 36 | php_display_startup_errors: 'Off' 37 | php_max_execution_time: 120 38 | php_max_input_time: 300 39 | php_max_input_vars: 1000 40 | php_memory_limit: 128M 41 | php_mysqlnd_collect_memory_statistics: 'Off' 42 | php_post_max_size: 25M 43 | php_sendmail_path: /usr/sbin/ssmtp -t 44 | php_session_save_path: /tmp 45 | php_upload_max_filesize: 25M 46 | php_track_errors: 'Off' 47 | php_default_timezone: '{{ default_timezone }}' 48 | 49 | php_opcache_enable: 1 50 | php_opcache_enable_cli: 1 51 | php_opcache_fast_shutdown: 1 52 | php_opcache_interned_strings_buffer: 8 53 | php_opcache_max_accelerated_files: 4000 54 | php_opcache_memory_consumption: 128 55 | php_opcache_revalidate_freq: 60 56 | -------------------------------------------------------------------------------- /roles/per-app/templates/https.conf.j2: -------------------------------------------------------------------------------- 1 | include h5bp/directive-only/ssl.conf; 2 | include h5bp/directive-only/ssl-stapling.conf; 3 | 4 | ssl_dhparam /etc/nginx/ssl/dhparams.pem; 5 | ssl_buffer_size 1400; # 1400 bytes to fit in one MTU 6 | 7 | {% set hsts_max_age = item.value.ssl.hsts_max_age | default(nginx_hsts_max_age) %} 8 | {% set hsts_include_subdomains = item.value.ssl.hsts_include_subdomains | default(nginx_hsts_include_subdomains) | ternary('includeSubDomains', None) %} 9 | {% set hsts_preload = item.value.ssl.hsts_preload | default(nginx_hsts_preload) | ternary('preload', None) %} 10 | add_header Strict-Transport-Security "max-age={{ [hsts_max_age, hsts_include_subdomains, hsts_preload] | reject('none') | join('; ') }}" always; 11 | 12 | {% if item.value.ssl.provider | default('manual') == 'manual' and item.value.ssl.cert is defined and item.value.ssl.key is defined -%} 13 | ssl_certificate {{ nginx_path }}/ssl/{{ item.value.ssl.cert | basename }}; 14 | ssl_certificate_key {{ nginx_path }}/ssl/{{ item.value.ssl.key | basename }}; 15 | {%- elif item.value.ssl.provider | default('manual') == 'letsencrypt' -%} 16 | ssl_certificate {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}-bundled.cert; 17 | ssl_certificate_key {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}.key; 18 | {%- elif item.value.ssl.provider | default('manual') == 'self-signed' -%} 19 | ssl_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; 20 | ssl_trusted_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; 21 | ssl_certificate_key {{ nginx_path }}/ssl/{{ item.key }}.key; 22 | {%- endif -%} 23 | -------------------------------------------------------------------------------- /roles/per-app/tasks/self-signed-certificate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate private key for the CA 3 | shell: openssl genrsa -out CA.key 2048 4 | args: 5 | chdir: "{{ nginx_path }}/ssl" 6 | creates: "CA.key" 7 | 8 | - name: Generate root certificate for the CA 9 | shell: > 10 | openssl req -x509 -new -nodes -key CA.key -sha256 -days 3650 -out CA.pem 11 | -subj "/CN=Development Certificate Authority" 12 | args: 13 | chdir: "{{ nginx_path }}/ssl" 14 | creates: "CA.pem" 15 | 16 | - name: Generate certificate private keys 17 | shell: "openssl genrsa -out {{ item.key }}.key 2048" 18 | args: 19 | chdir: "{{ nginx_path }}/ssl" 20 | creates: "{{ item.key }}.key" 21 | with_dict: "{{ apps }}" 22 | when: needs_self_signed_cert 23 | 24 | - name: Generate CSRs 25 | shell: openssl req -new -key {{ item.key }}.key -subj "/CN={{ item.key }}" -out {{ item.key }}.csr 26 | args: 27 | chdir: "{{ nginx_path }}/ssl" 28 | creates: "{{ item.key }}.csr" 29 | with_dict: "{{ apps }}" 30 | when: needs_self_signed_cert 31 | 32 | - name: Certificate generation configs 33 | template: 34 | src: ext.j2 35 | dest: "{{ nginx_path }}/ssl/{{ item.key }}.ext" 36 | with_dict: "{{ apps }}" 37 | when: needs_self_signed_cert 38 | 39 | - name: Generate self-signed certificates 40 | shell: > 41 | openssl x509 -req -in {{ item.key }}.csr 42 | -CA CA.pem -CAkey CA.key -CAcreateserial 43 | -out {{ item.key }}.cert 44 | -days 3650 -sha256 45 | -extfile {{ item.key }}.ext 46 | args: 47 | chdir: "{{ nginx_path }}/ssl" 48 | creates: "{{ item.key }}.cert" 49 | with_dict: "{{ apps }}" 50 | when: needs_self_signed_cert 51 | notify: 52 | - reload nginx 53 | -------------------------------------------------------------------------------- /roles/network-basics/templates/jail.local.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | [DEFAULT] 4 | 5 | ignoreip = {{ fail2ban_ignoreip }} 6 | bantime = {{ fail2ban_bantime }} 7 | maxretry = {{ fail2ban_maxretry }} 8 | 9 | backend = {{ fail2ban_backend }} 10 | 11 | destemail = {{ fail2ban_destemail }} 12 | banaction = {{ fail2ban_banaction }} 13 | mta = {{ fail2ban_mta }} 14 | protocol = {{ fail2ban_protocol }} 15 | chain = {{ fail2ban_chain }} 16 | 17 | action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 18 | 19 | action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 20 | %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] 21 | 22 | action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 23 | %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] 24 | 25 | action = %({{ fail2ban_action }})s 26 | 27 | {% if fail2ban_services is iterable %} 28 | {% for service in fail2ban_services %} 29 | [{{ service.name }}] 30 | 31 | enabled = {{ service.enabled | default("true") }} 32 | port = {{ service.port }} 33 | filter = {{ service.filter }} 34 | logpath = {{ service.logpath }} 35 | {% if service.maxretry is defined %} 36 | maxretry = {{ service.maxretry }} 37 | {% endif %} 38 | {% if service.protocol is defined %} 39 | protocol = {{ service.protocol }} 40 | {% endif %} 41 | {% if service.action is defined %} 42 | action = %({{ service.action }})s 43 | {% endif %} 44 | {% if service.banaction is defined %} 45 | banaction = {{ service.banaction }} 46 | {% endif %} 47 | 48 | {% endfor %} 49 | {% endif %} 50 | -------------------------------------------------------------------------------- /roles/ferm/templates/etc/ferm/ferm.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # manual customization of this file is not recommended 3 | 4 | table filter { 5 | chain INPUT { 6 | policy {{ ferm_default_policy_input }}; 7 | 8 | # connection tracking 9 | mod conntrack ctstate INVALID DROP; 10 | mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT; 11 | 12 | # allow local connections 13 | interface lo ACCEPT; 14 | 15 | # drop connections from bad guys 16 | mod recent name "badguys" update seconds 3600 { 17 | mod limit limit 3/hour limit-burst 5 { 18 | LOG log-prefix "iptables-recent-badguys: " log-level warning; 19 | } 20 | DROP; 21 | } 22 | 23 | # allow ICMP protocol 24 | protocol icmp ACCEPT; 25 | 26 | @include 'filter-input.d/'; 27 | 28 | {% if ferm_limit_portscans %} 29 | # catch bad guys (port scanners) 30 | mod recent set name "badguys" { 31 | mod limit limit 3/hour limit-burst 5 { 32 | LOG log-prefix "iptables-portscan: " log-level warning; 33 | } 34 | } 35 | 36 | {% endif %} 37 | # reject everything else 38 | protocol udp REJECT reject-with icmp-port-unreachable; 39 | protocol tcp REJECT reject-with tcp-reset; 40 | REJECT reject-with icmp-proto-unreachable; 41 | } 42 | 43 | chain OUTPUT { 44 | policy {{ ferm_default_policy_output }}; 45 | 46 | # connection tracking 47 | mod conntrack ctstate INVALID DROP; 48 | mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT; 49 | } 50 | 51 | chain FORWARD { 52 | policy {{ ferm_default_policy_forward }}; 53 | 54 | # connection tracking 55 | mod conntrack ctstate INVALID DROP; 56 | mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT; 57 | } 58 | } 59 | 60 | @include 'ferm.d/'; -------------------------------------------------------------------------------- /roles/letsencrypt/templates/renew-certs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import time 6 | 7 | from subprocess import CalledProcessError, check_output, STDOUT 8 | 9 | failed = False 10 | letsencrypt_cert_ids = {{ letsencrypt_cert_ids }} 11 | 12 | for site in {{ apps_using_letsencrypt }}: 13 | bundled_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', site + '-' + letsencrypt_cert_ids[site] + '-bundled.cert') 14 | 15 | if os.access(bundled_cert_path, os.F_OK): 16 | stat = os.stat(bundled_cert_path) 17 | print('Certificate file ' + bundled_cert_path + ' already exists') 18 | 19 | if time.time() - stat.st_mtime < {{ letsencrypt_min_renewal_age }} * 86400: 20 | print(' The certificate is younger than {{ letsencrypt_min_renewal_age }} days. Not creating a new certificate.\n') 21 | continue 22 | 23 | print('Generating certificate for ' + site) 24 | 25 | cmd = ( 26 | '/usr/bin/env python {{ acme_tiny_software_directory }}/acme_tiny.py ' 27 | '--quiet ' 28 | '--ca {{ letsencrypt_ca }} ' 29 | '--account-key {{ letsencrypt_account_key }} ' 30 | '--csr {{ acme_tiny_data_directory }}/csrs/{0}-{1}.csr ' 31 | '--acme-dir {{ acme_tiny_challenges_directory }}' 32 | ).format(site, letsencrypt_cert_ids[site]) 33 | 34 | try: 35 | cert = check_output(cmd, stderr=STDOUT, shell=True) 36 | except CalledProcessError as e: 37 | failed = True 38 | print('Error while generating certificate for ' + site) 39 | print(e.output) 40 | else: 41 | with open(bundled_cert_path, 'w') as cert_file: 42 | cert_file.write(cert) 43 | 44 | print('Created certificate for ' + site) 45 | 46 | if failed: 47 | sys.exit(1) 48 | -------------------------------------------------------------------------------- /group_vars/all/helpers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #------------------------------------------------------------------------------- 3 | # These are just helpers, don't change them… 4 | #------------------------------------------------------------------------------- 5 | app_hosts_canonical: "{{ item.value.hosts | map(attribute='canonical') | list }}" 6 | app_hosts_redirects: "{{ item.value.hosts | selectattr('redirects', 'defined') | sum(attribute='redirects', start=[]) | list }}" 7 | app_hosts: "{{ app_hosts_canonical | union(app_hosts_redirects) }}" 8 | 9 | app_needs_nginx: item.value.type is not defined or 10 | item.value.type == 'laravel' or 11 | item.value.type == 'spa' or 12 | (item.value.type == 'docker' and item.value.proxy_port is defined) 13 | 14 | php_version_default: "7.4" 15 | app_php_version: "{{ item.value.php_version | default(php_version_default) }}" 16 | current_php_version: "{{ current_app.php_version | default(php_version_default) }}" 17 | current_php_executable: "/usr/bin/php{{ current_php_version }}" 18 | 19 | ssl_enabled: "{{ item.value.ssl is defined and item.value.ssl.enabled is defined and item.value.ssl.enabled }}" 20 | needs_self_signed_cert: "{{ ssl_enabled and item.value.ssl.provider is defined and item.value.ssl.provider == 'self-signed' }}" 21 | 22 | laravel_apps: "{{ apps.values() | flatten | json_query('[? type==`laravel`||type==None]') }}" 23 | 24 | app_types: "[{% for item in apps.values() %}'{{ item.type | default('laravel') }}',{% endfor %}]" 25 | 26 | 27 | is_ubuntu: "{{ ansible_distribution == 'Ubuntu' and (ansible_distribution_release == 'xenial' or ansible_distribution_release == 'bionic') }}" 28 | is_debian: "{{ ansible_distribution == 'Debian' }}" 29 | is_arm: "{{ ansible_machine == 'aarch64' or ansible_machine == 'armv7l' }}" 30 | 31 | -------------------------------------------------------------------------------- /roles/per-app/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy SSL cert 3 | copy: 4 | src: "{{ item.value.ssl.cert }}" 5 | dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.cert | basename }}" 6 | mode: 0640 7 | with_dict: "{{ apps }}" 8 | when: app_needs_nginx and ssl_enabled and item.value.ssl.cert is defined 9 | tags: certs 10 | 11 | - name: Copy SSL key 12 | copy: 13 | src: "{{ item.value.ssl.key }}" 14 | dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.key | basename }}" 15 | mode: 0600 16 | with_dict: "{{ apps }}" 17 | when: app_needs_nginx and ssl_enabled and item.value.ssl.key is defined 18 | tags: certs 19 | 20 | - name: Create Nginx conf for challenges location 21 | template: 22 | src: "{{ playbook_dir }}/roles/letsencrypt/templates/acme-challenge-location.conf.j2" 23 | dest: "{{ nginx_path }}/acme-challenge-location.conf" 24 | notify: reload nginx 25 | 26 | - name: Create user file for auth basic 27 | htpasswd: 28 | path: "{{ auth_basic_htpasswd_file }}" 29 | name: "{{ item.value.auth_basic.username }}" 30 | password: "{{ item.value.auth_basic.password }}" 31 | when: app_uses_auth_basic 32 | with_dict: "{{ apps }}" 33 | 34 | - name: Create Laravan configurations for Nginx 35 | template: 36 | src: "{{ item.value.nginx_conf | default('laravan-app.conf.j2') }}" 37 | dest: "{{ nginx_path }}/sites-available/{{ item.key }}.conf" 38 | with_dict: "{{ apps }}" 39 | when: app_needs_nginx 40 | notify: reload nginx 41 | tags: [certs, letsencrypt] 42 | 43 | - name: Enable Laravan sites 44 | file: 45 | src: "{{ nginx_path }}/sites-available/{{ item.key }}.conf" 46 | dest: "{{ nginx_path }}/sites-enabled/{{ item.key }}.conf" 47 | owner: root 48 | group: root 49 | state: link 50 | with_dict: "{{ apps }}" 51 | when: app_needs_nginx 52 | notify: reload nginx 53 | -------------------------------------------------------------------------------- /roles/postgres/templates/pg_hba.conf.j2: -------------------------------------------------------------------------------- 1 | # This file is read on server startup and when the postmaster receives 2 | # a SIGHUP signal. If you edit the file on a running system, you have 3 | # to SIGHUP the postmaster for the changes to take effect. You can 4 | # use "pg_ctl reload" to do that. 5 | 6 | # Put your actual configuration here 7 | # ---------------------------------- 8 | # 9 | # If you want to allow non-local connections, you need to add more 10 | # "host" records. In that case you will also need to make PostgreSQL 11 | # listen on a non-local interface via the listen_addresses 12 | # configuration parameter, or via the -i or -h command line switches. 13 | 14 | 15 | 16 | 17 | # DO NOT DISABLE! 18 | # If you change this first entry you will need to make sure that the 19 | # database superuser can access the database using some other method. 20 | # Noninteractive access to all databases is required during automatic 21 | # maintenance (custom daily cronjobs, replication, and similar tasks). 22 | # 23 | # Database administrative login by Unix domain socket 24 | local all postgres peer 25 | 26 | # TYPE DATABASE USER ADDRESS METHOD 27 | 28 | # "local" is for Unix domain socket connections only 29 | local all all peer 30 | # IPv4 local connections: 31 | host all all 127.0.0.1/32 md5 32 | # IPv6 local connections: 33 | host all all ::1/128 md5 34 | # Allow replication connections from localhost, by a user with the 35 | # replication privilege. 36 | #local replication postgres peer 37 | #host replication postgres 127.0.0.1/32 md5 38 | #host replication postgres ::1/128 md5 39 | -------------------------------------------------------------------------------- /roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Nginx PPA (Ubuntu) 3 | apt_repository: 4 | repo: "ppa:nginx/development" 5 | update_cache: yes 6 | when: is_ubuntu and not is_arm 7 | 8 | - name: Install Nginx 9 | apt: 10 | name: nginx 11 | state: present 12 | force: yes 13 | 14 | - name: Create SSL directory 15 | file: 16 | mode: 0700 17 | path: "{{ nginx_path }}/ssl" 18 | state: directory 19 | 20 | - name: Generate strong unique Diffie-Hellman group. 21 | command: openssl dhparam -out dhparams.pem 2048 22 | args: 23 | chdir: "{{ nginx_path }}/ssl" 24 | creates: "{{ nginx_path }}/ssl/dhparams.pem" 25 | notify: reload nginx 26 | tags: [diffie-hellman] 27 | 28 | - name: Grab h5bp/server-configs-nginx 29 | git: 30 | repo: "https://github.com/h5bp/server-configs-nginx.git" 31 | dest: "{{ nginx_path }}/h5bp-server-configs" 32 | version: 82181a672a7c26f9bc8744fead80318d8a2520b1 33 | force: yes 34 | 35 | - name: Move h5bp configs 36 | command: cp -R {{ nginx_path }}/h5bp-server-configs/h5bp {{ nginx_path }}/h5bp 37 | args: 38 | creates: "{{ nginx_path }}/h5bp/" 39 | 40 | - name: Create nginx.conf 41 | template: 42 | src: nginx.conf.j2 43 | dest: "{{ nginx_path }}/nginx.conf" 44 | notify: reload nginx 45 | 46 | - name: Disable default server 47 | file: 48 | path: "{{ nginx_path }}/sites-enabled/default" 49 | state: absent 50 | notify: reload nginx 51 | 52 | - name: Enable better default site to drop unknown requests 53 | command: cp {{ nginx_path }}/h5bp-server-configs/sites-available/no-default {{ nginx_path }}/sites-enabled/no-default.conf 54 | args: 55 | creates: "{{ nginx_path }}/sites-enabled/no-default.conf" 56 | notify: reload nginx 57 | 58 | - name: Create base Laravan config 59 | template: 60 | src: laravan.conf.j2 61 | dest: "{{ nginx_path }}/laravan.conf" 62 | -------------------------------------------------------------------------------- /roles/letsencrypt/tasks/certificates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate private keys 3 | shell: openssl genrsa 4096 > {{ letsencrypt_keys_dir }}/{{ item.key }}.key 4 | args: 5 | creates: "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" 6 | when: app_uses_letsencrypt 7 | with_dict: "{{ apps }}" 8 | 9 | - name: Ensure correct permissions on private keys 10 | file: 11 | path: "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" 12 | mode: 0600 13 | when: app_uses_letsencrypt 14 | with_dict: "{{ apps }}" 15 | 16 | - name: Generate Lets Encrypt certificate IDs 17 | shell: | 18 | echo "{{ [app_hosts | join(' '), letsencrypt_ca, acme_tiny_commit] | join('\n') }}" | 19 | cat {{ letsencrypt_account_key }} {{ letsencrypt_keys_dir }}/{{ item.key }}.key - | 20 | md5sum | cut -c -7 21 | register: generate_cert_ids 22 | changed_when: false 23 | when: app_uses_letsencrypt 24 | with_dict: "{{ apps }}" 25 | tags: [per-app, nginx-includes, nginx-sites] 26 | 27 | - name: Generate CSRs 28 | shell: "openssl req -new -sha256 -key '{{ letsencrypt_keys_dir }}/{{ item.key }}.key' -subj '/' -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{{ app_hosts | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}.csr" 29 | args: 30 | executable: /bin/bash 31 | creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}.csr" 32 | when: app_uses_letsencrypt 33 | with_dict: "{{ apps }}" 34 | 35 | - name: Generate certificate renewal script 36 | template: 37 | src: renew-certs.py 38 | dest: "{{ acme_tiny_data_directory }}/renew-certs.py" 39 | mode: 0700 40 | 41 | - name: Generate the certificates 42 | command: ./renew-certs.py 43 | args: 44 | chdir: "{{ acme_tiny_data_directory }}" 45 | register: generate_certs 46 | changed_when: generate_certs.stdout is defined and 'Created' in generate_certs.stdout 47 | notify: reload nginx 48 | -------------------------------------------------------------------------------- /roles/backup/tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create backup config directories 4 | file: 5 | path: "{{ item }}" 6 | owner: "{{ backup_setup.user }}" 7 | group: "{{ backup_setup.user }}" 8 | state: directory 9 | with_items: 10 | - "{{ backup_setup.config_dir }}" 11 | - "{{ backup_setup.config_dir }}/models" 12 | - "{{ backup_setup.tmp_dir }}" 13 | - "{{ backup_setup.data_dir }}" 14 | - "{{ backup_setup.log_dir }}" 15 | 16 | - name: Install global backup config 17 | template: 18 | src: config.rb.j2 19 | dest: "{{ backup_setup.config_dir }}/config.rb" 20 | owner: "{{ backup_setup.user }}" 21 | group: "{{ backup_setup.user }}" 22 | mode: 0600 23 | 24 | - name: Configure individual backup models 25 | template: 26 | src: models/{{ item.type }}.rb.j2 27 | dest: "{{ backup_setup.config_dir }}/models/{{ item.name.replace('.', '_') }}.rb" 28 | owner: "{{ backup_setup.user }}" 29 | group: "{{ backup_setup.user }}" 30 | mode: 0600 31 | with_items: "{{ databases }}" 32 | when: "'backup' in item" 33 | 34 | - name: Configure cron to run our backups daily 35 | cron: 36 | name: "Laravan Backup {{ item.name }}" 37 | cron_file: "laravan-backup-{{ item.name.replace('.', '_') }}" 38 | job: "nice -n 20 /usr/local/bin/backup perform --trigger \"{{ item.name | regex_replace('[^a-zA-Z]+', '_') }}\" --config-file \"{{ backup_setup.config_dir }}/config.rb\" --data-path \"{{ backup_setup.data_dir }}\" --tmp-path \"{{ backup_setup.tmp_dir }}\" > \"{{ backup_setup.log_dir }}/backup-{{ item.name | regex_replace('[^a-zA-Z]+', '_') }}-{{ item.type }}.log\" 2>&1" 39 | month: "{{ item.backup.month | default('*') }}" 40 | day: "{{ item.backup.day | default('*') }}" 41 | hour: "{{ item.backup.hour | default(0) }}" 42 | minute: "{{ item.backup.minute | default(0) }}" 43 | weekday: "{{ item.backup.weekday | default('*') }}" 44 | user: root 45 | with_items: "{{ databases }}" 46 | when: "'backup' in item" 47 | -------------------------------------------------------------------------------- /roles/php/tasks/php.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add PHP PPA (Ubuntu) 3 | apt_repository: 4 | repo: "ppa:ondrej/php" 5 | update_cache: yes 6 | when: is_ubuntu == True and is_arm == False 7 | 8 | - name: Install key for PHP repository (Debian) 9 | get_url: 10 | url: https://packages.sury.org/php/apt.gpg 11 | dest: /etc/apt/trusted.gpg.d/php.gpg 12 | when: is_debian == True and is_arm == False 13 | 14 | - name: Add PHP Repository (Debian) 15 | apt_repository: 16 | repo: "deb https://packages.sury.org/php {{ ansible_distribution_release }} main" 17 | update_cache: yes 18 | when: is_debian == True and is_arm == False 19 | 20 | - name: Add PHP Repository (ARM) 21 | apt_repository: 22 | repo: "deb http://mirrordirector.raspbian.org/raspbian/ buster main contrib non-free rpi" 23 | update_cache: yes 24 | when: is_arm == True 25 | 26 | - name: "Install PHP version(s) {{ ', '.join(php_versions_required) }}" 27 | apt: 28 | name: "{{ php_packages_times_versions }}" 29 | state: present 30 | force: yes 31 | 32 | - name: "Start php-fpm service" 33 | service: 34 | name: "php{{ item }}-fpm" 35 | state: started 36 | enabled: true 37 | with_items: "{{ php_versions_required }}" 38 | 39 | - name: Create socket directory 40 | file: 41 | path: "/var/run/php{{ item }}-fpm/" 42 | state: directory 43 | with_items: "{{ php_versions_required }}" 44 | 45 | - name: Disable default pool 46 | command: "mv /etc/php/{{ item }}/fpm/pool.d/www.conf /etc/php/{{ item }}/fpm/pool.d/www.disabled" 47 | args: 48 | creates: "/etc/php/{{ item }}/fpm/pool.d/www.disabled" 49 | when: disable_default_pool 50 | notify: reload php-fpm 51 | with_items: "{{ php_versions_required }}" 52 | 53 | - name: PHP configuration file 54 | template: 55 | src: php.ini.j2 56 | dest: "/etc/php/{{ item }}/fpm/php.ini" 57 | notify: reload php-fpm 58 | with_items: "{{ php_versions_required }}" 59 | 60 | - name: php-fpm configuration file 61 | template: 62 | src: php-fpm.conf.j2 63 | dest: "/etc/php/{{ item }}/fpm/pool.d/laravan.conf" 64 | notify: restart php-fpm 65 | with_items: "{{ php_versions_required }}" 66 | -------------------------------------------------------------------------------- /roles/mariadb/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install key for MariaDB repository (Ubuntu) 3 | apt_key: 4 | keyserver: "hkp://keyserver.ubuntu.com:80" 5 | id: "0xF1656F24C74CD1D8" 6 | when: is_ubuntu == True and is_arm == False 7 | 8 | - name: Add MariaDB PPA (Ubuntu) 9 | apt_repository: 10 | repo: "deb [arch=amd64,i386] https://mirrors.evowise.com/mariadb/repo/10.2/ubuntu {{ ansible_distribution_release }} main" 11 | update_cache: yes 12 | when: is_ubuntu == True and is_arm == False 13 | 14 | - name: Install MariaDB 15 | apt: 16 | name: 17 | - mariadb-client 18 | - mariadb-server 19 | state: present 20 | 21 | - name: Disable MySQL binary logging 22 | template: 23 | src: disable-binary-logging.cnf 24 | dest: /etc/mysql/conf.d 25 | owner: root 26 | group: root 27 | when: mariadb_binary_logging_disabled 28 | 29 | - name: Restart MySQL Server 30 | service: 31 | name: mysql 32 | state: restarted 33 | enabled: true 34 | 35 | - name: Set root user password 36 | mysql_user: 37 | name: "{{ mysql_root_user }}" 38 | host: "{{ item }}" 39 | password: "{{ vault.db_passwords.root }}" 40 | check_implicit_admin: yes 41 | state: present 42 | with_items: 43 | - "{{ inventory_hostname }}" 44 | - 127.0.0.1 45 | - ::1 46 | - localhost 47 | 48 | - name: Copy .my.cnf file with root password credentials. 49 | template: 50 | src: my.cnf.j2 51 | dest: ~/.my.cnf 52 | owner: root 53 | group: root 54 | mode: 0600 55 | 56 | - name: Copy config template for mb4 strings 57 | template: 58 | src: mb4strings.cnf.j2 59 | dest: /etc/mysql/conf.d/mb4strings.cnf 60 | owner: root 61 | group: root 62 | 63 | - name: Delete anonymous MySQL server users 64 | mysql_user: 65 | user: "" 66 | host: "{{ item }}" 67 | state: absent 68 | with_items: 69 | - localhost 70 | - "{{ inventory_hostname }}" 71 | - "{{ ansible_hostname }}" 72 | 73 | - name: Remove the test database 74 | mysql_db: 75 | name: test 76 | state: absent 77 | 78 | - name: Restart MySQL Server 79 | service: 80 | name: mysql 81 | state: restarted 82 | enabled: true 83 | -------------------------------------------------------------------------------- /roles/per-app/tasks/nginx-includes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Build list of Nginx includes templates 3 | find: 4 | paths: 5 | - "{{ nginx_includes_templates_path }}" 6 | - "{{ nginx_includes_deprecated }}" 7 | pattern: "*.conf.j2" 8 | recurse: yes 9 | become: no 10 | delegate_to: localhost 11 | register: nginx_includes_templates 12 | 13 | - name: Warn about deprecated Nginx includes directory 14 | debug: 15 | msg: "[DEPRECATION WARNING]: The `{{ nginx_includes_deprecated }}` directory for is deprecated. Please move these templates to a directory named `{{ nginx_includes_templates_path }}` in the root of this project. For more information, see https://roots.io/trellis/docs/nginx-includes/" 16 | when: True in nginx_includes_templates.files | map(attribute='path') | map('search', nginx_includes_deprecated | regex_escape) | list 17 | 18 | - name: Create includes.d directories 19 | file: 20 | path: "{{ nginx_path }}/includes.d/{{ item }}" 21 | state: directory 22 | mode: 0755 23 | with_items: "{{ nginx_includes_templates.files | map(attribute='path') | 24 | map('regex_replace', nginx_includes_pattern, '\\2') | 25 | map('dirname') | unique | list | sort 26 | }}" 27 | when: nginx_includes_templates.files | count 28 | 29 | - name: Template files out to includes.d 30 | template: 31 | src: "{{ item }}" 32 | dest: "{{ nginx_path }}/includes.d/{{ item | regex_replace(nginx_includes_pattern, '\\2') }}" 33 | with_items: "{{ nginx_includes_templates.files | map(attribute='path') | list | sort(True) }}" 34 | notify: reload nginx 35 | 36 | - name: Retrieve list of existing files in includes.d 37 | find: 38 | paths: "{{ nginx_path }}/includes.d" 39 | pattern: "*.conf" 40 | recurse: yes 41 | register: nginx_includes_existing 42 | when: nginx_includes_d_cleanup 43 | 44 | - name: Remove unmanaged files from includes.d 45 | file: 46 | path: "{{ item }}" 47 | state: absent 48 | with_items: "{{ nginx_includes_existing.files | default({}) | map(attribute='path') | 49 | difference(nginx_includes_templates.files | map(attribute='path') | 50 | map('regex_replace', nginx_includes_pattern, nginx_path + '/includes.d/\\2') | unique 51 | ) | list 52 | }}" 53 | when: nginx_includes_d_cleanup 54 | notify: reload nginx 55 | -------------------------------------------------------------------------------- /roles/letsencrypt/library/test_challenges.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import socket 5 | 6 | try: 7 | from httplib import HTTPConnection, HTTPException 8 | except ImportError: 9 | # Python 3 10 | from http.client import HTTPConnection, HTTPException 11 | 12 | DOCUMENTATION = ''' 13 | --- 14 | module: test_challenges 15 | short_description: Tests Let's Encrypt web server challenges 16 | description: 17 | - The M(test_challenges) module verifies a list of hosts can access acme challenges for Let's Encrypt. 18 | options: 19 | hosts: 20 | description: 21 | - A list of hostnames/domains to test. 22 | required: true 23 | default: null 24 | type: list 25 | file: 26 | description: 27 | - The dummy filename in the URL to test. 28 | required: no 29 | default: ping.txt 30 | path: 31 | description: 32 | - The path to the challenges in the URL. 33 | required: no 34 | default: /.well-known/acme-challenge 35 | author: 36 | - Scott Walkinshaw 37 | ''' 38 | 39 | EXAMPLES = ''' 40 | # Example from Ansible Playbooks. 41 | - test_challenges: 42 | hosts: 43 | - example.com 44 | - www.example.com 45 | - www.mydomain.com 46 | ''' 47 | 48 | def get_status(host, path, file): 49 | try: 50 | conn = HTTPConnection(host) 51 | conn.request('HEAD', '/{0}/{1}'.format(path, file)) 52 | res = conn.getresponse() 53 | except (HTTPException, socket.timeout, socket.error): 54 | return 0 55 | else: 56 | return res.status 57 | 58 | def main(): 59 | module = AnsibleModule( 60 | argument_spec = dict( 61 | file = dict(default='ping.txt'), 62 | hosts = dict(required=True, type='list'), 63 | path = dict(default='.well-known/acme-challenge') 64 | ) 65 | ) 66 | 67 | hosts = module.params['hosts'] 68 | path = module.params['path'] 69 | file = module.params['file'] 70 | 71 | failed_hosts = [] 72 | 73 | for host in hosts: 74 | status = get_status(host, path, file) 75 | if int(status) != 200: 76 | failed_hosts.append(host) 77 | 78 | rc = int(len(failed_hosts) > 0) 79 | 80 | module.exit_json( 81 | changed=False, 82 | rc=rc, 83 | failed_hosts=failed_hosts 84 | ) 85 | 86 | from ansible.module_utils.basic import * 87 | main() 88 | -------------------------------------------------------------------------------- /.gitlab-ci.yml.example: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - release 4 | - coverage 5 | 6 | 7 | .test_template: &test_definition 8 | image: jsphpl/php-node-python:7.4 9 | services: 10 | - postgres:12.1 11 | - redis:5 12 | variables: 13 | POSTGRES_DB: testing 14 | POSTGRES_USER: testing 15 | POSTGRES_PASSWORD: secret 16 | before_script: 17 | - composer install 18 | - php artisan migrate:fresh --seed --env=testing 19 | artifacts: 20 | when: on_failure 21 | name: log 22 | paths: 23 | - storage/logs/laravel.log 24 | 25 | test: 26 | <<: *test_definition 27 | stage: test 28 | script: 29 | - ./vendor/bin/phpunit 30 | 31 | coverage: 32 | <<: *test_definition 33 | stage: coverage 34 | only: 35 | - master 36 | script: 37 | # Enable xdebug for coverage report 38 | - docker-php-ext-enable xdebug.so 39 | - ./vendor/bin/phpunit --coverage-text 40 | 41 | 42 | .release_template: &release_definition 43 | stage: release 44 | image: jsphpl/ansible-git 45 | before_script: 46 | # Trust the host key of the deploy target and the gitlab instance 47 | - mkdir -m 700 ~/.ssh 48 | - ssh-keyscan -H YOUR_APP_SERVER_HOSTNAME_GOES_HERE >> ~/.ssh/known_hosts 49 | - ssh-keyscan -H YOUR_GITLAB_SERVER_HOSTNAME_GOES_HERE >> ~/.ssh/known_hosts 50 | 51 | # Install private key for deployment 52 | - echo "$DEPLOY_KEY" > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa 53 | 54 | # Start the ssh agent 55 | - eval $(ssh-agent -s) 56 | - ssh-add ~/.ssh/id_rsa 57 | script: 58 | - git clone YOUR_LARAVAN_PROJECT_GIT_URL_GOES_HERE laravan 59 | - cd laravan 60 | - echo "$VAULT_PASS" > .vault_pass 61 | - ansible-playbook deploy.yml -e env=$ENVIRONMENT_NAME -e app=YOUR_APP_NAME_GOES_HERE 62 | environment: 63 | name: $ENVIRONMENT_NAME 64 | url: https://$TARGET_HOST 65 | 66 | release to staging: 67 | <<: *release_definition 68 | only: 69 | - master 70 | variables: 71 | VERSION: $CI_COMMIT_SHA 72 | TARGET_HOST: YOUR_STAGING_HOSTNAME_GOES_HERE 73 | ENVIRONMENT_NAME: staging 74 | DEPLOY_KEY: $STAGING_DEPLOY_KEY 75 | 76 | release to production: 77 | <<: *release_definition 78 | only: 79 | - /^v\d+\.\d+\.\d+$/ # version tags (vx.y.z) 80 | when: manual 81 | variables: 82 | VERSION: $CI_COMMIT_TAG 83 | TARGET_HOST: YOUR_PRODUCTION_HOSTNAME_GOES_HERE 84 | ENVIRONMENT_NAME: production 85 | DEPLOY_KEY: $PRODUCTION_DEPLOY_KEY 86 | -------------------------------------------------------------------------------- /roles/letsencrypt/defaults/main.yml: -------------------------------------------------------------------------------- 1 | apps_using_letsencrypt: "[{% for name, app in apps.items() | list if app.ssl is defined and app.ssl.enabled and app.ssl.provider | default('manual') == 'letsencrypt' %}'{{ name }}',{% endfor %}]" 2 | app_uses_letsencrypt: ssl_enabled and item.value.ssl.provider | default('manual') == 'letsencrypt' 3 | missing_hosts: "{{ app_hosts | difference((current_hosts.results | selectattr('item.key', 'equalto', item.key) | selectattr('stdout_lines', 'defined') | sum(attribute='stdout_lines', start=[]) | map('trim') | list | join(' ')).split(' ')) }}" 4 | letsencrypt_cert_ids: "{ {% for item in (generate_cert_ids | default({'results':[{'skipped':True}]})).results if item is not skipped %}'{{ item.item.key }}':'{{ item.stdout }}', {% endfor %} }" 5 | 6 | acme_tiny_repo: 'https://github.com/diafygi/acme-tiny.git' 7 | acme_tiny_commit: 'cb094cf3efa34acef8c7139c8480e2135422e755' 8 | 9 | acme_tiny_software_directory: /usr/local/letsencrypt 10 | acme_tiny_data_directory: /var/lib/letsencrypt 11 | acme_tiny_challenges_directory: "{{ webroot }}/letsencrypt" 12 | 13 | # Path to the local file containing the account key to copy to the server. 14 | # Secure this file using Git-crypt for example. 15 | # Leave this blank to generate a new account key that will need to be registered manually with Letsencrypt.org 16 | #letsencrypt_account_key_source_file: /my/account.key 17 | 18 | # Content of the account key to copy to the server. 19 | # Secure this key using Ansible Vault for example. 20 | # Leave this blank to generate a new account key that will need to be registered manually with Letsencrypt.org 21 | #letsencrypt_account_key_source_content: | 22 | # -----BEGIN RSA PRIVATE KEY----- 23 | # MIIJKAJBBBKCaGEA63J7t9dqyua5+Q+P6M3iHtLEKpF/AZcZNBHr1F2Oo8+Hfyvl 24 | # KWXliiWjUORxDxI1c56Rw2VCIExnFjWJAdSLv6/XaQWo2T7U28bkKbAlCF9= 25 | # -----END RSA PRIVATE KEY----- 26 | 27 | letsencrypt_ca: 'https://acme-v02.api.letsencrypt.org' 28 | 29 | letsencrypt_account_key: '{{ acme_tiny_data_directory }}/account.key' 30 | 31 | letsencrypt_keys_dir: "{{ nginx_ssl_path }}/letsencrypt" 32 | letsencrypt_certs_dir: "{{ nginx_ssl_path }}/letsencrypt" 33 | 34 | # the minimum age (in days) after which a certificate will be renewed 35 | letsencrypt_min_renewal_age: 60 36 | 37 | # the days of a month the cronjob should be run. Make sure to run it rather often, three times per month is a pretty 38 | # good value. It does not harm to run it often, as it will only regenerate certificates that have passed a certain age 39 | # (60 days by default). 40 | letsencrypt_cronjob_daysofmonth: 1,11,21 41 | -------------------------------------------------------------------------------- /roles/letsencrypt/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create Nginx conf for challenges location 3 | template: 4 | src: acme-challenge-location.conf.j2 5 | dest: "{{ nginx_path }}/acme-challenge-location.conf" 6 | 7 | - name: Get list of hosts in current Nginx conf 8 | shell: | 9 | [ ! -f {{ nginx_path }}/sites-enabled/{{ item.key }}.conf ] || 10 | sed -n -e "/listen 80/,/server_name/{s/server_name \(.*\);/\1/p}" {{ nginx_path }}/sites-enabled/{{ item.key }}.conf 11 | register: current_hosts 12 | changed_when: false 13 | when: app_uses_letsencrypt 14 | with_dict: "{{ apps }}" 15 | 16 | - name: Create needed Nginx confs for challenges 17 | template: 18 | src: nginx-challenge-site.conf.j2 19 | dest: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.key }}.conf" 20 | register: challenge_site_confs 21 | when: 22 | - app_uses_letsencrypt 23 | - missing_hosts | count 24 | with_dict: "{{ apps }}" 25 | 26 | - name: Enable Nginx sites 27 | file: 28 | src: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.key }}.conf" 29 | dest: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item.key }}.conf" 30 | state: link 31 | register: challenge_sites_enabled 32 | when: 33 | - app_uses_letsencrypt 34 | - missing_hosts | count 35 | with_dict: "{{ apps }}" 36 | notify: disable temporary challenge sites 37 | 38 | - import_tasks: "{{ playbook_dir }}/roles/nginx/tasks/reload_nginx.yml" 39 | when: challenge_site_confs is changed or challenge_sites_enabled is changed 40 | 41 | - name: Create test Acme Challenge file 42 | shell: touch {{ acme_tiny_challenges_directory }}/ping.txt 43 | args: 44 | creates: "{{ acme_tiny_challenges_directory }}/ping.txt" 45 | warn: false 46 | 47 | - name: Test Acme Challenges 48 | test_challenges: 49 | hosts: "{{ app_hosts }}" 50 | register: letsencrypt_test_challenges 51 | ignore_errors: true 52 | when: app_uses_letsencrypt 53 | with_dict: "{{ apps }}" 54 | 55 | - name: Notify of challenge failures 56 | fail: 57 | msg: > 58 | Could not access the challenge file for the hosts/domains: {{ item.failed_hosts | join(', ') }}. 59 | Let's Encrypt requires every domain/host be publicly accessible. 60 | Make sure that a valid DNS record exists for {{ item.failed_hosts | join(', ') }} and that they point to this server's IP. 61 | If you don't want these domains in your SSL certificate, then remove them from `app_hosts`. 62 | See https://roots.io/trellis/docs/ssl for more details. 63 | when: item is not skipped and item is failed 64 | with_items: "{{ letsencrypt_test_challenges.results }}" 65 | -------------------------------------------------------------------------------- /roles/ferm/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: ensure ferm status is in debconf 3 | debconf: 4 | name: ferm 5 | question: ferm/enable 6 | vtype: boolean 7 | value: "{{ ferm_enabled | lower }}" 8 | 9 | - name: ensure ferm is installed 10 | apt: 11 | name: "{{ ferm_package }}" 12 | state: "{{ ferm_package_state | default(apt_security_package_state) }}" 13 | cache_valid_time: "{{ apt_cache_valid_time }}" 14 | install_recommends: no 15 | notify: 16 | - restart ferm 17 | 18 | - name: ensure configuration directories exist 19 | file: 20 | path: "{{ item }}" 21 | state: directory 22 | mode: 0750 23 | with_items: 24 | - /etc/ferm/ferm.d 25 | - /etc/ferm/filter-input.d 26 | 27 | - name: ensure firewall is configured 28 | template: 29 | src: "{{ item }}.j2" 30 | dest: /{{ item }} 31 | with_items: 32 | - etc/default/ferm 33 | - etc/ferm/ferm.conf 34 | notify: 35 | - restart ferm 36 | 37 | - name: ensure iptables INPUT rules are removed 38 | file: state=absent 39 | {% if item.filename is defined and item.filename %} 40 | path=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.filename }}.conf 41 | {% else %} 42 | path=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.type }}_{{ item.dport[0] }}.conf 43 | {% endif %} 44 | with_flattened: 45 | - "{{ ferm_input_list }}" 46 | - "{{ ferm_input_group_list }}" 47 | - "{{ ferm_input_host_list }}" 48 | when: ((item.type is defined and item.type) and (item.dport is defined and item.dport)) and 49 | (item.delete is defined and item.delete) 50 | 51 | - name: ensure iptables INPUT rules are added 52 | template: src=etc/ferm/filter-input.d/{{ item.type }}.conf.j2 53 | {% if item.filename is defined and item.filename %} 54 | dest=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.filename }}.conf 55 | {% else %} 56 | dest=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.type }}_{{ item.dport[0] }}.conf 57 | {% endif %} 58 | with_flattened: 59 | - "{{ ferm_input_list }}" 60 | - "{{ ferm_input_group_list }}" 61 | - "{{ ferm_input_host_list }}" 62 | when: (item.type is defined and item.type and item.dport is defined and item.dport) and 63 | (item.delete is undefined or (item.delete is defined and not item.delete)) 64 | 65 | - name: ensure iptables rules are enabled 66 | command: ferm --slow /etc/ferm/ferm.conf 67 | changed_when: false 68 | when: ferm_enabled 69 | 70 | - name: ensure iptables rules are disabled 71 | command: ferm --flush /etc/ferm/ferm.conf 72 | changed_when: false 73 | when: not ferm_enabled 74 | -------------------------------------------------------------------------------- /group_vars/production/apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apps: 3 | # Choose a unique key for each app. It is required later 4 | # in order to specify which app you want to deploy. 5 | myapp-backend: 6 | hosts: 7 | - canonical: api.acme.com 8 | # HTTP redirects to the canonical domain will be set up for the following domains: 9 | # In case TLS is enabled, certificates will be provisioned for all domains. 10 | # You can delete them entirely, if you don't need any redirects set up. 11 | redirects: 12 | - api-v1.acme.com 13 | - api-v2.acme.com 14 | ssl: 15 | enabled: True 16 | provider: letsencrypt 17 | source: 18 | url: git@github.com:acme/myapp-backend 19 | version: master 20 | env: 21 | # App 22 | APP_NAME: MyApp 23 | APP_ENV: production 24 | APP_URL: https://api.acme.com 25 | APP_DEBUG: false 26 | APP_KEY: "{{ vault.app_key }}" 27 | # Database 28 | DB_CONNECTION: pgsql 29 | DB_HOST: "127.0.0.1" 30 | DB_DATABASE: myapp 31 | DB_USERNAME: myapp 32 | DB_PASSWORD: "{{ vault.db_passwords.myapp_postgres }}" 33 | # Queue & Broadcast 34 | QUEUE_DRIVER: beanstalkd 35 | BROADCAST_DRIVER: pusher 36 | PUSHER_APP_ID: "{{ vault.pusher.app_id }}" 37 | PUSHER_APP_KEY: "{{ vault.pusher.app_key }}" 38 | PUSHER_APP_SECRET: "{{ vault.pusher.app_secret }}" 39 | PUSHER_APP_CLUSTER: "{{ vault.pusher.cluster }}" 40 | # Email 41 | MAIL_DRIVER: smtp 42 | MAIL_HOST: smtp.mailgun.org 43 | MAIL_PORT: 587 44 | MAIL_USERNAME: "{{ vault.mail.username }}" 45 | MAIL_PASSWORD: "{{ vault.mail.password }}" 46 | MAIL_ENCRYPTION: tls 47 | passport: True 48 | workers: 49 | - "php artisan queue:work beanstalkd --daemon --queue=broadcast --env=production --tries=3 --sleep=1" 50 | - "php artisan queue:work beanstalkd --daemon --queue=default --env=production --tries=3 --sleep=1" 51 | post_deploy: 52 | - "php artisan post-deploy-command" 53 | - "php artisan another-post-deploy-command" 54 | 55 | myapp-frontend: 56 | type: spa 57 | hosts: 58 | - canonical: app.acme.com 59 | ssl: 60 | enabled: True 61 | provider: letsencrypt 62 | source: 63 | url: git@github.com:acme/myapp-frontend 64 | version: master 65 | public_dir: dist 66 | 67 | myapp-frontend-docker: 68 | type: docker 69 | hosts: 70 | - canonical: dockerapp.acme.com 71 | proxy_port: 3000 72 | image: acme/myapp-frontend:latest 73 | watch: False 74 | restart: always 75 | ports: 76 | - 3000:3000 77 | env: 78 | NODE_ENV: production 79 | post_deploy: 80 | - "docker image prune -af" 81 | -------------------------------------------------------------------------------- /roles/basics/templates/50unattended-upgrades.j2: -------------------------------------------------------------------------------- 1 | // {{ ansible_managed }} 2 | 3 | // Automatically upgrade packages from these (origin:archive) pairs 4 | Unattended-Upgrade::Allowed-Origins { 5 | "${distro_id}:${distro_codename}"; 6 | "${distro_id}:${distro_codename}-security"; 7 | // Extended Security Maintenance; doesn't necessarily exist for 8 | // every release and this system may not have it installed, but if 9 | // available, the policy for updates is such that unattended-upgrades 10 | // should also install from here by default. 11 | "${distro_id}ESM:${distro_codename}"; 12 | // "${distro_id}:${distro_codename}-updates"; 13 | // "${distro_id}:${distro_codename}-proposed"; 14 | // "${distro_id}:${distro_codename}-backports"; 15 | }; 16 | 17 | // List of packages to not update (regexp are supported) 18 | Unattended-Upgrade::Package-Blacklist { 19 | // "vim"; 20 | // "libc6"; 21 | // "libc6-dev"; 22 | // "libc6-i686"; 23 | }; 24 | 25 | // This option allows you to control if on a unclean dpkg exit 26 | // unattended-upgrades will automatically run 27 | // dpkg --force-confold --configure -a 28 | // The default is true, to ensure updates keep getting installed 29 | //Unattended-Upgrade::AutoFixInterruptedDpkg "false"; 30 | 31 | // Split the upgrade into the smallest possible chunks so that 32 | // they can be interrupted with SIGUSR1. This makes the upgrade 33 | // a bit slower but it has the benefit that shutdown while a upgrade 34 | // is running is possible (with a small delay) 35 | //Unattended-Upgrade::MinimalSteps "true"; 36 | 37 | // Install all unattended-upgrades when the machine is shuting down 38 | // instead of doing it in the background while the machine is running 39 | // This will (obviously) make shutdown slower 40 | //Unattended-Upgrade::InstallOnShutdown "true"; 41 | 42 | // Send email to this address for problems or packages upgrades 43 | // If empty or unset then no email is sent, make sure that you 44 | // have a working mail setup on your system. A package that provides 45 | // 'mailx' must be installed. E.g. "user@example.com" 46 | //Unattended-Upgrade::Mail "root"; 47 | 48 | // Set this value to "true" to get emails only on errors. Default 49 | // is to always send a mail if Unattended-Upgrade::Mail is set 50 | //Unattended-Upgrade::MailOnlyOnError "true"; 51 | 52 | // Do automatic removal of new unused dependencies after the upgrade 53 | // (equivalent to apt-get autoremove) 54 | //Unattended-Upgrade::Remove-Unused-Dependencies "false"; 55 | 56 | // Automatically reboot *WITHOUT CONFIRMATION* 57 | // if the file /var/run/reboot-required is found after the upgrade 58 | //Unattended-Upgrade::Automatic-Reboot "false"; 59 | 60 | // If automatic reboot is enabled and needed, reboot at the specific 61 | // time instead of immediately 62 | // Default: "now" 63 | //Unattended-Upgrade::Automatic-Reboot-Time "02:00"; 64 | 65 | // Use apt bandwidth limit feature, this example limits the download 66 | // speed to 70kb/sec 67 | //Acquire::http::Dl-Limit "70"; 68 | -------------------------------------------------------------------------------- /roles/backup/templates/config.rb.j2: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # {{ ansible_managed }} 4 | 5 | ## 6 | # Backup v5.x Configuration 7 | # 8 | # Documentation: http://backup.github.io/backup 9 | # Issue Tracker: https://github.com/backup/backup/issues 10 | 11 | ## 12 | # Config Options 13 | # 14 | # The options here may be overridden on the command line, but the result 15 | # will depend on the use of --root-path on the command line. 16 | # 17 | # If --root-path is used on the command line, then all paths set here 18 | # will be overridden. If a path (like --tmp-path) is not given along with 19 | # --root-path, that path will use it's default location _relative to --root-path_. 20 | # 21 | # If --root-path is not used on the command line, a path option (like --tmp-path) 22 | # given on the command line will override the tmp_path set here, but all other 23 | # paths set here will be used. 24 | # 25 | # Note that relative paths given on the command line without --root-path 26 | # are relative to the current directory. The root_path set here only applies 27 | # to relative paths set here. 28 | # 29 | # --- 30 | # 31 | # Sets the root path for all relative paths, including default paths. 32 | # May be an absolute path, or relative to the current working directory. 33 | # 34 | # root_path 'my/root' 35 | # 36 | # Sets the path where backups are processed until they're stored. 37 | # This must have enough free space to hold apx. 2 backups. 38 | # May be an absolute path, or relative to the current directory or +root_path+. 39 | # 40 | # tmp_path 'my/tmp' 41 | # 42 | # Sets the path where backup stores persistent information. 43 | # When Backup's Cycler is used, small YAML files are stored here. 44 | # May be an absolute path, or relative to the current directory or +root_path+. 45 | # 46 | # data_path 'my/data' 47 | 48 | ## 49 | # Utilities 50 | # 51 | # If you need to use a utility other than the one Backup detects, 52 | # or a utility can not be found in your $PATH. 53 | # 54 | # Utilities.configure do 55 | # tar '/usr/bin/gnutar' 56 | # redis_cli '/opt/redis/redis-cli' 57 | # end 58 | 59 | ## 60 | # Logging 61 | # 62 | # Logging options may be set on the command line, but certain settings 63 | # may only be configured here. 64 | # 65 | # Logger.configure do 66 | # console.quiet = true # Same as command line: --quiet 67 | # logfile.max_bytes = 2_000_000 # Default: 500_000 68 | # syslog.enabled = true # Same as command line: --syslog 69 | # syslog.ident = 'my_app_backup' # Default: 'backup' 70 | # end 71 | # 72 | # Command line options will override those set here. 73 | # For example, the following would override the example settings above 74 | # to disable syslog and enable console output. 75 | # backup perform --trigger my_backup --no-syslog --no-quiet 76 | 77 | ## 78 | # Component Defaults 79 | # 80 | # Set default options to be applied to components in all models. 81 | # Options set within a model will override those set here. 82 | # 83 | # Storage::S3.defaults do |s3| 84 | # s3.access_key_id = "my_access_key_id" 85 | # s3.secret_access_key = "my_secret_access_key" 86 | # end 87 | # 88 | # Notifier::Mail.defaults do |mail| 89 | # mail.from = 'sender@email.com' 90 | # mail.to = 'receiver@email.com' 91 | # mail.address = 'smtp.gmail.com' 92 | # mail.port = 587 93 | # mail.domain = 'your.host.name' 94 | # mail.user_name = 'sender@email.com' 95 | # mail.password = 'my_password' 96 | # mail.authentication = 'plain' 97 | # mail.encryption = :starttls 98 | # end 99 | 100 | ## 101 | # Preconfigured Models 102 | # 103 | # Create custom models with preconfigured components. 104 | # Components added within the model definition will 105 | # +add to+ the preconfigured components. 106 | # 107 | # preconfigure 'MyModel' do 108 | # archive :user_pictures do |archive| 109 | # archive.add '~/pictures' 110 | # end 111 | # 112 | # notify_by Mail do |mail| 113 | # mail.to = 'admin@email.com' 114 | # end 115 | # end 116 | # 117 | # MyModel.new(:john_smith, 'John Smith Backup') do 118 | # archive :user_music do |archive| 119 | # archive.add '~/music' 120 | # end 121 | # 122 | # notify_by Mail do |mail| 123 | # mail.to = 'john.smith@email.com' 124 | # end 125 | # end 126 | -------------------------------------------------------------------------------- /roles/ferm/README.md: -------------------------------------------------------------------------------- 1 | ## What is ansible-ferm? [![Build Status](https://secure.travis-ci.org/nickjj/ansible-ferm.png)](http://travis-ci.org/nickjj/ansible-ferm) 2 | 3 | It is an [ansible](http://www.ansible.com/home) role to manage iptables using the ever so flexible ferm tool. 4 | 5 | ### What problem does it solve and why is it useful? 6 | 7 | Working with iptables directly can be really painful and the ufw module is decent for basic needs but sometimes you need a bit more control. I also like the approach of writing templates rather than executing allow/deny commands with ufw. I feel like it sets the tone for a more idempotent setup. 8 | 9 | ## Role variables 10 | 11 | Below is a list of default values along with a description of what they do. 12 | 13 | ``` 14 | # Should the firewall be enabled? 15 | ferm_enabled: true 16 | 17 | # Should ferm do ip-based tagging/locking when it detects someone is trying to port scan you? 18 | ferm_limit_portscans: false 19 | 20 | # The default actions to take for certain policies. You likely want to keep them at the default values. 21 | # This ensures all ports are blocked until you white list them. 22 | ferm_default_policy_input: DROP 23 | ferm_default_policy_output: ACCEPT 24 | ferm_default_policy_forward: DROP 25 | 26 | # The lists to use to provide your own rules. This is explained more below. 27 | ferm_input_list: [] 28 | ferm_input_group_list: [] 29 | ferm_input_host_list: [] 30 | 31 | # The amount in seconds to cache apt-update. 32 | apt_cache_valid_time: 86400 33 | ``` 34 | 35 | ### `ferm_input_list` with the `dport_accept` template 36 | 37 | The use case for this would be to white list ports to be opened. 38 | 39 | ``` 40 | ferm_input_list: 41 | # Choose the template to use. 42 | # REQUIRED: It can be either `dport_accept` or `dport_limit`. 43 | - type: "dport_accept" 44 | 45 | # Which protocol should be used? 46 | # OPTIONAL: Defaults to tcp. 47 | protocol: "tcp" 48 | 49 | # Which ports should be open? 50 | # REQUIRED: It can be the port value or a service in `/etc/services`. 51 | dport: ["http", "https"] 52 | 53 | # Which IP addresses should be white listed? 54 | # OPTIONAL: Defaults to an empty list. 55 | saddr: [] 56 | 57 | # Should all IP addresses be white listed? 58 | # OPTIONAL: Defaults to true. 59 | accept_any: true 60 | 61 | # Which filename should be written out? 62 | # OPTIONAL: Defaults to the first port listed in `dport`. 63 | 64 | # The filename which will get written to `/etc/ferm/filter-input.d/nginx_accept`. 65 | filename: "nginx_accept" 66 | 67 | # Should this rule be deleted? 68 | # OPTIONAL: Defaults to false. 69 | delete: false 70 | ``` 71 | 72 | ### `ferm_input_list` with the `dport_limit` template 73 | 74 | The use case for this would be to limit connections on specific ports based on an amount of time. This could be used to harden your security. 75 | 76 | ``` 77 | ferm_input_list: 78 | # Choose the template to use. 79 | # REQUIRED: It can be either `dport_accept` or `dport_limit`. 80 | - type: "dport_limit" 81 | 82 | # Which protocol should be used? 83 | # OPTIONAL: Defaults to tcp. 84 | protocol: "tcp" 85 | 86 | # Which ports should be open? 87 | # REQUIRED: It can be the port value or a service in `/etc/services`. 88 | dport: ["ssh"] 89 | 90 | # How many seconds to count in between the hits? 91 | # OPTIONAL: Defaults to 300. 92 | seconds: "300" 93 | 94 | # How many connections should be allowed per the amount of seconds you specified. 95 | # OPTIONAL: Defaults to 5. 96 | hits: "5" 97 | 98 | # Should this rule be disabled? 99 | # OPTIONAL: Defaults to false. 100 | disabled: false 101 | ``` 102 | 103 | #### `ferm_input_group_list` / `ferm_input_host_list` with either template 104 | 105 | This would be the same as above except it would be scoped to the groups and hosts list. 106 | 107 | ## Example app play in your playbook 108 | 109 | To open the http/https ports on your server add the following to the appropriate group or host vars file: 110 | 111 | ``` 112 | ferm_input_group_list: 113 | - type: "dport_accept" 114 | dport: ["http", "https"] 115 | filename: "nginx_accept" 116 | ``` 117 | 118 | I only chose the `nginx_accept` filename because I use nginx. You can name it whatever you want or even remove the filename to have this role automatically generate a filename for you. 119 | 120 | This file will be written to `/etc/ferm/filter-input.d/nginx_accept.conf` and it will contain the rules necessary to open the `http` and `https` ports. 121 | 122 | ## Attribution 123 | 124 | Many thanks to [nickjj](https://github.com/nickjj/) for creating the [original version](https://github.com/nickjj/ansible-ferm/) of this role. 125 | -------------------------------------------------------------------------------- /roles/per-app/templates/laravan-app.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | {% block upstream %} 4 | {% if item.value.type is defined and item.value.type == 'docker' -%} 5 | upstream {{ item.key }} { 6 | server localhost:{{ item.value.proxy_port }}; 7 | keepalive 32; 8 | } 9 | {% endif -%} 10 | {% endblock %} 11 | 12 | {% block before_server %}{% endblock %} 13 | 14 | server { 15 | {% block server_listen %} 16 | listen {{ ssl_enabled | ternary('[::]:443 ssl http2', '[::]:80') }}; 17 | listen {{ ssl_enabled | ternary('443 ssl http2', '80') }}; 18 | server_name {% for host in app_hosts_canonical %}{{ host }} {% endfor %}; 19 | {% endblock %} 20 | 21 | {% block server_logfiles %} 22 | access_log {{ webroot }}/{{ item.key }}/logs/access.log; 23 | error_log {{ webroot }}/{{ item.key }}/logs/error.log; 24 | {% endblock %} 25 | 26 | {% block server_root %} 27 | root {{ webroot }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/{% if item.value.source is defined %}{{ item.value.source.public_dir | default('public') }}{% endif %}; 28 | index index.php index.htm index.html; 29 | charset utf-8; 30 | {% endblock %} 31 | 32 | {% block server_auth_basic %} 33 | {% if app_uses_auth_basic -%} 34 | auth_basic "Password Required"; 35 | auth_basic_user_file {{ auth_basic_htpasswd_file }}; 36 | {% endif %} 37 | {% endblock %} 38 | 39 | {% block server_dev %} 40 | {% if env == 'development' -%} 41 | # See Virtualbox section at http://wiki.nginx.org/Pitfalls 42 | sendfile off; 43 | {%- endif %} 44 | {% endblock %} 45 | 46 | {% block server_ssl %} 47 | {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) -%} 48 | {{ lookup('template', 'https.conf.j2') }} 49 | {% endif %} 50 | 51 | {% if item.value.ssl is not defined or not item.value.ssl.enabled | default(false) -%} 52 | include acme-challenge-location.conf; 53 | {% endif %} 54 | {% endblock %} 55 | 56 | {% block server_includes %} 57 | include includes.d/{{ item.key }}/*.conf; 58 | {% endblock %} 59 | 60 | {% block server_location %} 61 | {% if item.value.type is defined and item.value.type == 'spa' -%} 62 | add_header Fastcgi-Cache $upstream_cache_status; 63 | include laravan.conf; 64 | location / { 65 | try_files $uri $uri/ /index.html; 66 | } 67 | {% elif item.value.type is defined and item.value.type == 'docker' -%} 68 | proxy_set_header Host $host; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | proxy_set_header X-Forwarded-Proto $scheme; 72 | 73 | location / { 74 | proxy_pass http://localhost:{{ item.value.proxy_port }}; 75 | } 76 | {% else -%} 77 | include laravan.conf; 78 | location / { 79 | try_files $uri $uri/ /index.php$is_args$args; 80 | } 81 | 82 | location ~ \.php$ { 83 | try_files $uri /index.php =404; 84 | error_page 404 /index.php; 85 | 86 | {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%} 87 | set $skip_cache 0; 88 | 89 | if ($query_string != "") { 90 | set $skip_cache 1; 91 | } 92 | 93 | fastcgi_cache laravan-{{ item.key }}; 94 | fastcgi_cache_valid {{ item.value.cache.duration | default(nginx_cache_duration) }}; 95 | fastcgi_cache_bypass $skip_cache; 96 | fastcgi_no_cache $skip_cache; 97 | {% endif -%} 98 | 99 | include fastcgi_params; 100 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 101 | fastcgi_param DOCUMENT_ROOT $realpath_root; 102 | fastcgi_pass unix:/var/run/php{{ app_php_version }}-laravan.sock; 103 | fastcgi_index index.php; 104 | } 105 | {% endif -%} 106 | {% endblock %} 107 | } 108 | 109 | {% block plain_http_redirect %} 110 | {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) %} 111 | server { 112 | listen 80; 113 | 114 | server_name {{ app_hosts | join(' ') }}; 115 | 116 | {% if item.value.ssl.provider | default('manual') == 'letsencrypt' -%} 117 | include acme-challenge-location.conf; 118 | 119 | location / { 120 | return 301 https://$host$request_uri; 121 | } 122 | {% else %} 123 | return 301 https://$host$request_uri; 124 | {% endif -%} 125 | } 126 | {% endif %} 127 | {% endblock %} 128 | 129 | {% block hostname_redirect %} 130 | {% for host in item.value.hosts if host.redirects | default([]) %} 131 | server { 132 | {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) -%} 133 | listen 443 ssl http2; 134 | 135 | {{ lookup('template', 'https.conf.j2') }} 136 | {% else -%} 137 | listen 80; 138 | {% endif -%} 139 | 140 | server_name {{ host.redirects | join(' ') }}; 141 | 142 | {% if item.value.ssl is not defined or not item.value.ssl.enabled | default(false) -%} 143 | include acme-challenge-location.conf; 144 | 145 | location / { 146 | return 301 $scheme://{{ host.canonical }}$request_uri; 147 | } 148 | {% else %} 149 | return 301 $scheme://{{ host.canonical }}$request_uri; 150 | {% endif %} 151 | } 152 | {% endfor %} 153 | {% endblock %} 154 | 155 | {% block after_server %}{% endblock %} 156 | -------------------------------------------------------------------------------- /roles/nginx/templates/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | # nginx Configuration File 4 | # http://wiki.nginx.org/Configuration 5 | 6 | # Run as a less privileged user for security reasons. 7 | user {{ nginx_user }} {{ nginx_group }}; 8 | 9 | # How many worker threads to run; 10 | # "auto" sets it to the number of CPU cores available in the system, and 11 | # offers the best performance. Don't set it higher than the number of CPU 12 | # cores if changing this parameter. 13 | 14 | # The maximum number of connections for Nginx is calculated by: 15 | # max_clients = worker_processes * worker_connections 16 | worker_processes auto; 17 | 18 | # Maximum open file descriptors per process; 19 | # should be > worker_connections. 20 | worker_rlimit_nofile 8192; 21 | 22 | events { 23 | # When you need > 8000 * cpu_cores connections, you start optimizing your OS, 24 | # and this is probably the point at which you hire people who are smarter than 25 | # you, as this is *a lot* of requests. 26 | worker_connections 8000; 27 | } 28 | 29 | # Default error log file 30 | # (this is only used when you don't override error_log on a server{} level) 31 | error_log {{ nginx_logs_root }}/error.log warn; 32 | pid /run/nginx.pid; 33 | 34 | http { 35 | # Hide nginx version information. 36 | server_tokens off; 37 | 38 | # Setup the fastcgi cache. 39 | fastcgi_buffers {{ nginx_fastcgi_buffers }}; 40 | fastcgi_buffer_size {{ nginx_fastcgi_buffer_size }}; 41 | fastcgi_cache_path {{ nginx_cache_path }} levels=1:2 keys_zone=laravan:{{ nginx_cache_key_storage_size }} max_size={{ nginx_cache_size }} inactive={{ nginx_cache_inactive }}; 42 | fastcgi_cache_use_stale updating error timeout invalid_header http_500; 43 | fastcgi_cache_lock on; 44 | fastcgi_cache_key $realpath_root$scheme$host$request_uri$request_method; 45 | fastcgi_ignore_headers Cache-Control Expires Set-Cookie; 46 | fastcgi_pass_header Set-Cookie; 47 | fastcgi_pass_header Cookie; 48 | 49 | # Define the MIME types for files. 50 | include h5bp-server-configs/mime.types; 51 | default_type application/octet-stream; 52 | 53 | # Update charset_types due to updated mime.types 54 | charset_types text/css text/plain text/vnd.wap.wml application/javascript application/json application/rss+xml application/xml; 55 | 56 | # Format to use in log files 57 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 58 | '$status $body_bytes_sent "$http_referer" ' 59 | '"$http_user_agent" "$http_x_forwarded_for"'; 60 | 61 | # Default log file 62 | # (this is only used when you don't override access_log on a server{} level) 63 | access_log {{ nginx_logs_root }}/access.log main; 64 | 65 | # How long to allow each connection to stay idle; longer values are better 66 | # for each individual client, particularly for SSL, but means that worker 67 | # connections are tied up longer. (Default: 65) 68 | keepalive_timeout 20; 69 | 70 | # Speed up file transfers by using sendfile() to copy directly 71 | # between descriptors rather than using read()/write(). 72 | sendfile on; 73 | 74 | # Tell Nginx not to send out partial frames; this increases throughput 75 | # since TCP frames are filled up before being sent out. (adds TCP_CORK) 76 | tcp_nopush on; 77 | 78 | # Compression 79 | 80 | # Enable Gzip compressed. 81 | gzip on; 82 | 83 | # Compression level (1-9). 84 | # 5 is a perfect compromise between size and cpu usage, offering about 85 | # 75% reduction for most ascii files (almost identical to level 9). 86 | gzip_comp_level 5; 87 | 88 | # Don't compress anything that's already small and unlikely to shrink much 89 | # if at all (the default is 20 bytes, which is bad as that usually leads to 90 | # larger files after gzipping). 91 | gzip_min_length 256; 92 | 93 | # Compress data even for clients that are connecting to us via proxies, 94 | # identified by the "Via" header (required for CloudFront). 95 | gzip_proxied any; 96 | 97 | # Tell proxies to cache both the gzipped and regular version of a resource 98 | # whenever the client's Accept-Encoding capabilities header varies; 99 | # Avoids the issue where a non-gzip capable client (which is extremely rare 100 | # today) would display gibberish if their proxy gave them the gzipped version. 101 | gzip_vary on; 102 | 103 | # Compress all output labeled with one of the following MIME-types. 104 | gzip_types 105 | application/atom+xml 106 | application/javascript 107 | application/json 108 | application/ld+json 109 | application/manifest+json 110 | application/rss+xml 111 | application/vnd.geo+json 112 | application/vnd.ms-fontobject 113 | application/x-font-ttf 114 | application/x-web-app-manifest+json 115 | application/xhtml+xml 116 | application/xml 117 | font/opentype 118 | image/bmp 119 | image/svg+xml 120 | image/x-icon 121 | text/cache-manifest 122 | text/css 123 | text/plain 124 | text/vcard 125 | text/vnd.rim.location.xloc 126 | text/vtt 127 | text/x-component 128 | text/x-cross-domain-policy; 129 | # text/html is always compressed by HttpGzipModule 130 | 131 | # This should be turned on if you are going to have pre-compressed copies (.gz) of 132 | # static files available. If not it should be left off as it will cause extra I/O 133 | # for the check. It is best if you enable this in a location{} block for 134 | # a specific directory, or on an individual server{} level. 135 | # gzip_static on; 136 | 137 | # Workaround 138 | server_names_hash_bucket_size {{ server_names_hash_bucket_size }}; 139 | 140 | include sites-enabled/*; 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravan 2 | 3 | **Ansible Playbooks for Laravel - machine provisioning and app deployment** 4 | 5 | Laravan is capable of preparing a fresh Ubuntu 16.04 / 18.04 machine for running laravel apps and docker containers by setting up the following components using Ansible: 6 | 7 | - Nginx 8 | - PHP 7.2, 7.3 & 7.4 9 | - MariaDB 10 10 | - PostgreSQL 11 11 | - Beanstalkd 12 | - Redis 13 | 14 | It also brings some neat, optional stuff: 15 | 16 | - Run services in docker containers and expose them via https through a reverse proxy 17 | - Single-command deployment of Laravel applications with rollback capability 18 | - Automatic database backups 19 | - Free TLS certificates via letsencrypt 20 | - Supervisor for reliably running queue workers 21 | - Hosting of static pages and Single Page Applications 22 | 23 | This makes Laravan a viable alternative to paid tools like Forge or Envoyer. It can also be used to implement Continuous Integration/Deployment by running the playbooks from your CI server. 24 | 25 | ## Table of Contents 26 | - [Getting Started](#getting-started) 27 | - [1. Install Ansible](#1-install-ansible) 28 | - [2. Prepare Server](#2-prepare-server) 29 | - [3. Install Laravan](#3-install-laravan) 30 | - [4. Link Machines](#4-link-machines) 31 | - [5. Configure Applications](#5-configure-applications) 32 | - [6. Configure Databases](#6-configure-databases) 33 | - [7. Encrypt vault](#7-encrypt-vault) 34 | - [8. Set ssh keys for web user](#8-set-ssh-keys-for-web-user) 35 | - [9. Provision](#9-provision) 36 | - [10. Deploy](#10-deploy) 37 | - [Using laravan in your CI/CD pipeline](#using-laravan-in-your-cicd-pipeline) 38 | - [Using laravan with Gitlab Pipelines](#using-laravan-with-gitlab-pipelines) 39 | - [Credits](#credits) 40 | - [License](#license) 41 | 42 | ## Getting Started 43 | ### 1. Install Ansible 44 | Ansible needs to be installed on your **local machine** from which you're going to provision your servers and deploy your Laravel apps. Install instructions: [http://docs.ansible.com/ansible/latest/intro_installation.html](http://docs.ansible.com/ansible/latest/intro_installation.html) 45 | 46 | ### 2. Prepare Server 47 | Boot up a fresh Ubuntu 18.04 machine. Set up ssh keys for the root user and make sure you can log in from your local machine. 48 | 49 | The target machine needs to have `python` (v2) installed in order to be provisioned by Ansible. 50 | 51 | #### Vagrant (optional) 52 | A Vagrantfile is provided with laravan, so you can fire up a fresh local vm by just stating `vagrant up` from your laravan directory. You will still need to set up your ssh keys for the root user of the Vagrant box. 53 | 54 | ### 3. Install Laravan 55 | On your local machine, run the following commands to prepare Laravan: 56 | 57 | ```bash 58 | git clone https://github.com/jsphpl/laravan 59 | cd laravan 60 | ./setup.sh 61 | ``` 62 | 63 | ### 4. Link Machines 64 | 1. Enter the IP addresses of your hosts to the `hosts` file, each under their respective environment. If you created a vagrant machine using `vagrant up` from the laravan directory, its IP address will be `10.10.0.42`. 65 | 66 | ### 5. Configure Applications 67 | Open up `group_vars/{env}/apps.yml` and provide the necessary information for your Laravel apps. There is an example under `group_vars/production/apps.yml` to get you started. 68 | 69 | **Note:** in `apps.yml`, you will need to specify some "sensitive" variables that should not be entered into your VCS in plain text, eg. `env.APP_KEY`. In the example, the values reference variables in the vault, eg. `{{ vault.app_key }}`. These variables need to be assigned concrete values in the `group_vars/{env}/vault.yml`. Please refer to section *7. "Encrypt vault"* for instructions on how to encrypt your vault files. 70 | 71 | - `myapp` (key of the dictionary items) must be replaced with a unique name for the app 72 | - `canonical:` holds the primary domain name under which your app will be available 73 | - `env:` those values will eventually be written into an `.env` file on the server 74 | 75 | ### 6. Configure Databases 76 | Open up `group_vars/{env}/databases.yml` and configure the databases required for your apps. There is an example under `group_vars/production/databases.yml` to get you started. 77 | 78 | ### 7. Encrypt vault 79 | In order to prevent your production secrets from ending up as plain text in your git repositories, use the [ansible vault](http://docs.ansible.com/ansible/latest/vault.html). 80 | 81 | 1. Create a `.vault_pass` file containing a strong password in the project root (eg. `openssl rand -base64 64 > .vault_pass`). This file is gitignored, which you should leave at all cost. Your coworkers who need to be able to provision/deploy as well, will need a copy of the file. *Send it to them by other (encrypted) means, but don't add it to your VCS!* 82 | 2. Encrypt your vault: `ansible-vault encrypt group_vars/production/vault.yml` 83 | 3. **Never again decrypt the vault!**. Use `ansible-vault view ` or `ansible-vault edit ` to open the vault file. This reduces the risk of the vault ending up plain text in your version control. 84 | 85 | ### 8. Set ssh keys for `web` user 86 | In `group_vars/all/users.yml`, make sure to add a valid ssh public key for the `web` user. Either use the `lookup()` syntax to read a local file, or specify a http(s) url for remotely hosted public keys (eg. on github). 87 | 88 | ### 9. Provision 89 | **Note: When using "letsencrypt" as TLS certificate provider, all domains listed under `canonical` or `redirects` must be mapped to your IP address (resolvable via public DNS) before you can successfully provision your server.** 90 | 91 | On your local machine, run the following command, replacing `production` with the environment you want to provision (most likely `development`, `staging` or `production`). 92 | 93 | ```bash 94 | ansible-playbook provision.yml -e env=production 95 | ``` 96 | 97 | ### 10. Deploy 98 | Again, on the local machine, run the following command to fetch your app from the git repository, install composer dependencies and run the migrations. The `app=myapp` variable refers to a key in the `group_vars/{env}/apps.yml` dictionary. 99 | 100 | ```bash 101 | ansible-playbook deploy.yml -e env=production -e app=myapp 102 | ``` 103 | 104 | *Note: Replace `myapp` with the key that your app is listed under in the `apps` object in `group_vars/{env}/apps.yml`. Replace `production` with the environment you want to deploy to* 105 | 106 | In case deployment fails with an error message indicating a lack of access right to the git repository, make sure that a local ssh key is authorized with the git remote of the app. There could also be a problem with with ssh agent-forwarding, which you can troubleshoot using this guide: [https://developer.github.com/v3/guides/using-ssh-agent-forwarding/](https://developer.github.com/v3/guides/using-ssh-agent-forwarding/). 107 | 108 | ## Using laravan in your CI/CD pipeline 109 | In order to use laravan in your CI/CD pipeline, you have to: 110 | 1. Give your CI server access to your laravan repository (with all its configuration in it) 111 | 2. Make the `.vault_pass` available to your CI server 112 | 3. Run the `deploy.yml` playbook in your pipeline in order to deploy the application 113 | 114 | ### Using laravan with Gitlab Pipelines 115 | Take a look at [.gitlab-ci.yml.example](.gitlab-ci.yml.example) for an example configuration for Gitlab Pipelines. Make sure to replace all YOUR_xyz_GOES_HERE strings with your actual values. 116 | 117 | In order to make it work, configure the following environment variables in gitlab for the project you're deploying: 118 | - STAGING_DEPLOY_KEY: *an ssh private key that authorizes the `web` user on your staging system* 119 | - PRODUCTION_DEPLOY_KEY: *an ssh private key that authorizes the `web` user on your production system* 120 | - VAULT_PASS: *The vault password to decrypt the ansible vault* 121 | 122 | Besides that, make sure: 123 | - the respective "DEPLOY_KEYs" are allowed read access on your application repository and the laravan repository (set them up as deploy keys in the respective Gitlab project) 124 | - in your `apps.yml` configuration under `source`, set `version: "{{ lookup('env', 'VERSION') }}"` in order to deploy the git revision for which the pipeline is running. Otherwise it would default to master and possibly deploy the wrong version. 125 | 126 | ## Credits 127 | Credits to the awesome [Trellis](https://github.com/roots/trellis) project, which heavily inspired me to create Laravan and from which i copied some code and concepts. 128 | 129 | 130 | ## License 131 | This software is released "as it is" without any warranty under the MIT licence. See [LICENSE](LICENSE) for details. 132 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7c12340abaf5f91a953b7a105aa5aeb8212a6316d35f0f3797def9bc36b53d15" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "ansible": { 20 | "hashes": [ 21 | "sha256:16cfb99d7f321cec408afcd3ead538337ebc3247c7a77080e5cabb58054e2a0b" 22 | ], 23 | "index": "pypi", 24 | "version": "==2.6.20" 25 | }, 26 | "bcrypt": { 27 | "hashes": [ 28 | "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", 29 | "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", 30 | "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", 31 | "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", 32 | "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", 33 | "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", 34 | "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", 35 | "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", 36 | "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", 37 | "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", 38 | "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", 39 | "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", 40 | "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", 41 | "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", 42 | "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", 43 | "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", 44 | "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", 45 | "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" 46 | ], 47 | "version": "==3.1.7" 48 | }, 49 | "cffi": { 50 | "hashes": [ 51 | "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", 52 | "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", 53 | "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", 54 | "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", 55 | "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", 56 | "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", 57 | "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", 58 | "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", 59 | "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", 60 | "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", 61 | "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", 62 | "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", 63 | "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", 64 | "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", 65 | "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", 66 | "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", 67 | "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", 68 | "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", 69 | "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", 70 | "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", 71 | "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", 72 | "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", 73 | "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", 74 | "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", 75 | "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", 76 | "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", 77 | "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", 78 | "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" 79 | ], 80 | "version": "==1.14.0" 81 | }, 82 | "cryptography": { 83 | "hashes": [ 84 | "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", 85 | "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", 86 | "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", 87 | "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", 88 | "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", 89 | "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", 90 | "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", 91 | "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", 92 | "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", 93 | "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", 94 | "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", 95 | "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", 96 | "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", 97 | "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", 98 | "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", 99 | "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", 100 | "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", 101 | "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", 102 | "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", 103 | "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", 104 | "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" 105 | ], 106 | "version": "==2.8" 107 | }, 108 | "jinja2": { 109 | "hashes": [ 110 | "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", 111 | "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" 112 | ], 113 | "version": "==2.11.1" 114 | }, 115 | "jmespath": { 116 | "hashes": [ 117 | "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec", 118 | "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9" 119 | ], 120 | "index": "pypi", 121 | "version": "==0.9.5" 122 | }, 123 | "markupsafe": { 124 | "hashes": [ 125 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 126 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 127 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 128 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 129 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 130 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 131 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 132 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 133 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 134 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 135 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 136 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 137 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 138 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 139 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 140 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 141 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 142 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 143 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 144 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 145 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 146 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 147 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 148 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 149 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 150 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 151 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 152 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 153 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 154 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 155 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 156 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 157 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 158 | ], 159 | "version": "==1.1.1" 160 | }, 161 | "paramiko": { 162 | "hashes": [ 163 | "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f", 164 | "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f" 165 | ], 166 | "version": "==2.7.1" 167 | }, 168 | "pycparser": { 169 | "hashes": [ 170 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 171 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 172 | ], 173 | "version": "==2.20" 174 | }, 175 | "pynacl": { 176 | "hashes": [ 177 | "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", 178 | "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", 179 | "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", 180 | "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", 181 | "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", 182 | "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", 183 | "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", 184 | "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", 185 | "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", 186 | "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", 187 | "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", 188 | "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", 189 | "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", 190 | "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", 191 | "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", 192 | "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", 193 | "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", 194 | "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", 195 | "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", 196 | "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", 197 | "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" 198 | ], 199 | "version": "==1.3.0" 200 | }, 201 | "pyyaml": { 202 | "hashes": [ 203 | "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", 204 | "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", 205 | "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", 206 | "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", 207 | "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", 208 | "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", 209 | "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", 210 | "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", 211 | "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", 212 | "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", 213 | "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" 214 | ], 215 | "version": "==5.3" 216 | }, 217 | "six": { 218 | "hashes": [ 219 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 220 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 221 | ], 222 | "version": "==1.14.0" 223 | } 224 | }, 225 | "develop": {} 226 | } 227 | --------------------------------------------------------------------------------