Update slides

This commit is contained in:
Oliver Davies 2020-01-30 22:26:34 +00:00
parent 9ab929a7c0
commit fce230f798
4 changed files with 434 additions and 175 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -3,51 +3,16 @@ build-lists: true
code: line-height(1.2) code: line-height(1.2)
header-emphasis: #53B0EB header-emphasis: #53B0EB
text: alignment(left) text: alignment(left)
theme: plain jane, 8 theme: simple, 8
# [fit] **Deploying PHP applications** <br>using Ansible, Ansible Vault <br>and Ansistrano # [fit] **Deploying PHP applications** <br>using Ansible, Ansible Vault <br>and Ansistrano
^ Ansible crash course ^ I work primarily with PHP, and there will be some PHP-isms in this talk (LAMP stack, Composer).
Basic deployment Will be using a Drupal 8 application as the example, but the tools are tool and language agnostic.
Better deployment
--- ---
[.build-lists: false] # [fit] **Deploying ~~PHP~~ applications** <br>using Ansible, Ansible Vault <br>and Ansistrano
[.header: #111111]
![right 500%](../images/me-phpnw-inviqa.jpg)
- Full Stack Web Developer & System Administrator
- **Senior Software Engineer** at **Inviqa**
- Acquia certified **Drupal 8 Grand Master**
- Open sourcer
- Drupal 7 & 8 **core contributor**
- @opdavies
- www.oliverdavies.uk
^ Maintain Drupal modules, PHP CLI tools and libraries
Blog on my website
I work for Inviqa, but this based on my personal and side projects, though I've implemented similar solutions in the past.
---
![350% inline](images/logo-platformsh.png)
![150% inline](images/logo-acquia.png)
![130% inline](images/logo-pantheon.png)
^ Large, well-known managed hosting companies
Optimised servers for PHP/Drupal applications
Include some sort of deployment system
---
![100%](images/logo-digital-ocean.png)
![30%](images/logo-linode.png)
![30%](https://www.vultr.com/media/media_card_1200x630.png)
^ More applicable to virtual or dedicated servers with no existing deployment process
Not enough budget for fully-managed, or using internal infrastructure
--- ---
@ -59,14 +24,58 @@ Not enough budget for fully-managed, or using internal infrastructure
--- ---
[.build-lists: false]
[.header: #111111]
![right 500%](../images/me-phpnw-inviqa.jpg)
- Full Stack Software Developer & System Administrator
- **Senior Software Engineer** at **Inviqa**
- Acquia certified **Drupal 8 Grand Master** and **Cloud Pro**
- Open sourcer
- Drupal 7 & 8 **core contributor**
- @opdavies
- www.oliverdavies.uk
^ Maintain Drupal modules, PHP CLI tools and libraries, Ansible roles
Blog on my website
I work primarily with Drupal and Symfony
I work for Inviqa, but this based on my personal and side projects.
I've been using Ansible for a number of years, initially only for provisioning and setting up my laptop, and later for application deployments
---
![350% inline](images/logo-platformsh.png)
![150% inline](images/logo-acquia.png)
![130% inline](images/logo-pantheon.png)
^ Large, well-known managed hosting companies
Optimised servers for PHP/Drupal applications
Include some sort of deployment system
This workflow doesn't apply to this scenario
---
![100%](images/logo-digital-ocean.png)
![30%](images/logo-linode.png)
![30%](images/logo-vultr.png)
^ More applicable to virtual or dedicated servers with no existing deployment process
Not enough budget for fully-managed, or using internal infrastructure
This is where the this workflow would be useful
---
# **What is Ansible?** # **What is Ansible?**
--- ---
## Ansible is **open source software** that automates **software provisioning**, <br>**configuration management**, <br>and **application deployment**. ## Ansible is an open-source **software provisioning, configuration management, and application-deployment** tool.
![10% right](images/ansible.png) ![10% right](images/ansible.png)
[.footer: https://en.wikipedia.org/wiki/Ansible_(software)]
--- ---
![10% right](images/ansible.png) ![10% right](images/ansible.png)
@ -76,12 +85,14 @@ Not enough budget for fully-managed, or using internal infrastructure
* CLI tool * CLI tool
* Written in Python * Written in Python
* Configured with YAML * Configured with YAML
* Executes remote commands * Executes ad-hoc remote commands
* Installs software packages * Installs software packages
* Performs deployment steps * Performs deployment steps
* Batteries included * Batteries included
^ Written in Python but you don't need to write or know Python to use it ^ Written in Python but you don't need to write or know Python to use it
Drupal, Symfony and a lot of other projects use YAML
First-party modules (SSH keys, file and directory management, package repositories, stopping/starting/restarting services, DO/Linode/AWS integration)
--- ---
@ -95,6 +106,10 @@ Not enough budget for fully-managed, or using internal infrastructure
* Tasks * Tasks
* Roles * Roles
^ Hosts: where your managed nodes/hosts are. Can be static or dynamic.
Commands: run from a control node onto managed nodes
Playbooks and Tasks: YAML representation of a series of commands/steps
--- ---
![10% right](images/ansible.png) ![10% right](images/ansible.png)
@ -106,10 +121,12 @@ Not enough budget for fully-managed, or using internal infrastructure
* No server dependencies * No server dependencies
* Easy to add to an existing project * Easy to add to an existing project
* Includes relevant modules (e.g. Composer) * Includes relevant modules (e.g. Composer)
* Idempotency
^ Drupal 8, Symfony, Ansible all use YAML ^ Drupal 8, Symfony, Ansible all use YAML
Runs on any server with Python Runs on any server with Python
Plugins into Drupal via CLI apps like Drush and Drupal Console Plugins into Drupal via CLI apps like Drush and Drupal Console
Changes are only made when needed (once)
--- ---
@ -124,6 +141,8 @@ Plugins into Drupal via CLI apps like Drush and Drupal Console
192.168.33.10 192.168.33.10
``` ```
^ Supports wildcards and ranges.
--- ---
```yaml ```yaml
@ -140,11 +159,11 @@ webservers:
--- ---
# `ansible all -m ping` # `ansible all -i hosts.yml -m ping`
--- ---
``` ```json
webservers | SUCCESS => { webservers | SUCCESS => {
"ansible_facts": { "ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python" "discovered_interpreter_python": "/usr/bin/python"
@ -156,11 +175,11 @@ webservers | SUCCESS => {
--- ---
# `ansible all `<br>`-m command `<br>`-a 'git pull `<br>`--chdir=/app'` # `ansible all `<br>`-i hosts.yml `<br>`-m command `<br>`-a 'git pull `<br>`--chdir=/app'`
--- ---
# `ansible all `<br>`-m git -a 'repo=https://github.com/opdavies/dransible dest=/app`' # `ansible all -i hosts.yml`<br>`-m git -a 'repo=https://github.com/opdavies/dransible dest=/app`'
--- ---
@ -170,9 +189,8 @@ webservers | SUCCESS => {
```yaml ```yaml
# playbook.yml # playbook.yml
--- ---
- hosts: all - hosts: webservers # or 'all'
vars: vars:
git_repo: https://github.com/opdavies/dransible git_repo: https://github.com/opdavies/dransible
@ -188,11 +206,11 @@ webservers | SUCCESS => {
--- ---
# `ansible-playbook `<br>`ansible/playbook.yml `<br>`-i hosts.yml` # `ansible-playbook `<br>`playbook.yml -i hosts.yml`
--- ---
# **Roles** # **Roles: <br>configuring a LAMP stack**
^ Collections of tasks, variables and handlers ^ Collections of tasks, variables and handlers
@ -200,7 +218,6 @@ webservers | SUCCESS => {
```yaml ```yaml
# requirements.yml # requirements.yml
--- ---
- src: geerlingguy.apache - src: geerlingguy.apache
- src: geerlingguy.composer - src: geerlingguy.composer
@ -213,15 +230,14 @@ webservers | SUCCESS => {
--- ---
# `ansible-galaxy -r`<br>`ansible/requirements.yml install` # `ansible-galaxy -r`<br>`requirements.yml install`
--- ---
```yaml ```yaml
# playbook.yml # provision.yml
--- ---
- hosts: all - hosts: webservers
roles: roles:
- geerlingguy.apache - geerlingguy.apache
@ -236,72 +252,171 @@ webservers | SUCCESS => {
--- ---
```yaml ```yaml
# ansible/provision.yml # provision.yml
---
- hosts: webservers
# ...
tasks: vars:
- name: Create a database apache_vhosts:
mysql_db: - servername: dransible.wip
name: mydatabase documentroot: /app/web
state: present
- name: Add the database user
mysql_user:
name: drupal
password: secret
priv: 'mydatabase.*:ALL'
state: present
``` ```
--- ---
# **Basic deployment**
---
```yaml ```yaml
# ansible/deploy.yml # provision.yml
---
- hosts: webservers
# ...
tasks: vars:
- name: Creating project directory # ...
file: php_version: '7.4'
path: /app php_packages_extra:
state: directory - libapache2-mod-php{{ php_version }}
- libpcre3-dev
- name: Uploading application
synchronize:
src: "{{ playbook_dir }}/../"
dest: /app
- name: Installing Composer dependencies
composer:
command: install
working_dir: /app
``` ```
--- ---
# Disadvantages ```yaml
# provision.yml
---
- hosts: webservers
# ...
* Single point of failure vars:
* No ability to roll back # ...
* Sensitive data stored in plain text mysql_databases:
- name: main
mysql_users:
- name: user
password: secret
priv: main.*:ALL
```
--- ---
# **Keeping secrets: <br>Ansible Vault** # `ansible-playbook provision.yml -i hosts.yml`
--- ---
# `ansible-vault create` <br>`ansible/vault.yml` ```
PLAY [Provision the webserver machines] ********************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [webservers]
TASK [geerlingguy.apache : Include OS-specific variables.] *************************************************************
ok: [webservers]
TASK [geerlingguy.apache : Include variables for Amazon Linux.]
skipping: [webservers]
TASK [geerlingguy.apache : Define apache_packages.] ********************************************************************
ok: [webservers]
TASK [geerlingguy.apache : include_tasks] ******************************************************************************
included: /Users/opdavies/.ansible/roles/geerlingguy.apache/tasks/setup-Debian.yml for webservers
TASK [geerlingguy.apache : Update apt cache.] **************************************************************************
changed: [webservers]
```
---
```
TASK [geerlingguy.composer : Ensure composer directory exists.] ********************************************************
ok: [webservers]
TASK [geerlingguy.composer : include_tasks] ****************************************************************************
skipping: [webservers]
TASK [geerlingguy.composer : include_tasks] ****************************************************************************
skipping: [webservers]
RUNNING HANDLER [geerlingguy.apache : restart apache] ******************************************************************
changed: [webservers]
RUNNING HANDLER [geerlingguy.mysql : restart mysql] ********************************************************************
changed: [webservers]
RUNNING HANDLER [geerlingguy.php : restart webserver] ******************************************************************
changed: [webservers]
RUNNING HANDLER [geerlingguy.php : restart php-fpm] ********************************************************************
skipping: [webservers]
PLAY RECAP *************************************************************************************************************
webservers : ok=111 changed=32 unreachable=0 failed=0 skipped=78 rescued=0 ignored=0
```
---
![70%](images/after-provision-1.png)
---
![70%](images/after-provision-2.png)
---
# **Keeping secrets with <br>Ansible Vault**
--- ---
```yaml ```yaml
# ansible/vars/vault.yml # provision.yml
---
- hosts: webservers
# ...
vars:
# ...
mysql_databases:
- name: main
mysql_users:
- name: user
password: secret
priv: main.*:ALL
```
--- ---
vault_database_name: mydatabase
vault_database_user: drupal [.code-highlight: 11-14]
```yaml
# provision.yml
---
- hosts: webservers
# ...
vars:
# ...
mysql_databases:
- name: main
mysql_users:
- name: user
password: secret
priv: main.*:ALL
```
---
# `ansible-vault create` <br>`vault.yml`
---
```yaml
# vars/vault.yml
---
vault_database_name: main
vault_database_user: user
vault_database_password: secret vault_database_password: secret
``` ```
@ -327,8 +442,7 @@ $ANSIBLE_VAULT;1.1;AES256
--- ---
```yaml ```yaml
# ansible/vars/vars.yml # vars/vars.yml
--- ---
database_name: "{{ vault_database_name }}" database_name: "{{ vault_database_name }}"
database_user: "{{ vault_database_user }}" database_user: "{{ vault_database_user }}"
@ -338,22 +452,58 @@ database_password: "{{ vault_database_password }}"
--- ---
```yaml ```yaml
# ansible/provision.yml # provision.yml
---
mysql_databases:
- '{{ database_name }}'
tasks: mysql_users:
- name: Create a database - name: '{{ database_user }}'
mysql_db: password: '{{ database_password }}'
name: '{{ database_name }}' priv: '{{ database_name }}.*:ALL'
state: present
``` ```
--- ---
# `ansible-vault edit ansible/vault.yml` # `ansible-vault edit vault.yml`
--- ---
# `ansible-playbook` <br>`-i hosts.yml` <br>`ansible/deploy.yml`<br>`--ask-vault-pass` # `ansible-playbook` <br>`-i hosts.yml` <br>`deploy.yml`<br>`--ask-vault-pass`
---
# **Basic deployment**
---
```yaml
# deploy.yml
tasks:
- name: Creating project directory
file:
path: /app
state: directory
- name: Uploading application
synchronize:
src: "{{ playbook_dir }}/../"
dest: /app
- name: Installing Composer dependencies
composer:
command: install
working_dir: /app
```
---
# Disadvantages
* Single point of failure
* No ability to roll back
* Sensitive data stored in plain text
--- ---
@ -383,10 +533,9 @@ Ansible port of Capistrano
--- ---
```yaml ```yaml
# ansible/requirements.yml # requirements.yml
--- ---
... # ...
- ansistrano.deploy - ansistrano.deploy
- ansistrano.rollback - ansistrano.rollback
``` ```
@ -394,7 +543,7 @@ Ansible port of Capistrano
--- ---
```yaml ```yaml
# ansible/deploy.yml # deploy.yml
--- ---
- hosts: all - hosts: all
@ -406,12 +555,11 @@ Ansible port of Capistrano
--- ---
```yaml ```yaml
# ansible/deploy.yml # deploy.yml
--- ---
... # ...
vars: vars:
project_deploy_dir: /var/www project_deploy_dir: /app
ansistrano_deploy_to: '{{ project_deploy_dir }}' ansistrano_deploy_to: '{{ project_deploy_dir }}'
ansistrano_deploy_via: git ansistrano_deploy_via: git
@ -421,7 +569,59 @@ Ansible port of Capistrano
--- ---
# `ansible-playbook` <br>`-i hosts.yml` <br>`ansible/deploy.yml` # `ansible-playbook` <br>`-i hosts.yml` <br>`deploy.yml`
---
```
PLAY [webservers] ******************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [webservers]
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
included: /Users/opdavies/.ansible/roles/ansistrano.deploy/tasks/setup.yml for webservers
TASK [ansistrano.deploy : ANSISTRANO | Ensure deployment base path exists] *********************************************
ok: [webservers]
TASK [ansistrano.deploy : ANSISTRANO | Ensure releases folder exists] **************************************************
ok: [webservers]
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared elements folder exists] *******************************************
ok: [webservers]
TASK [ansistrano.deploy : ANSISTRANO | Ensure shared paths exists] *****************************************************
ok: [webservers] => (item=web/sites/default/files)
```
---
```
TASK [ansistrano.deploy : Update file permissions] *********************************************************************
changed: [webservers]
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
included: /Users/opdavies/.ansible/roles/ansistrano.deploy/tasks/cleanup.yml for webservers
TASK [ansistrano.deploy : ANSISTRANO | Clean up releases] **************************************************************
changed: [webservers]
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
TASK [ansistrano.deploy : include_tasks] *******************************************************************************
included: /Users/opdavies/.ansible/roles/ansistrano.deploy/tasks/anon-stats.yml for webservers
TASK [ansistrano.deploy : ANSISTRANO | Send anonymous stats] ***********************************************************
skipping: [webservers]
PLAY RECAP *************************************************************************************************************
webservers : ok=33 changed=14 unreachable=0 failed=0 skipped=7 rescued=0 ignored=0
```
--- ---
@ -449,9 +649,12 @@ drwxr-xr-x 9 4096 Jul 22 20:30 20190722203038Z
--- ---
```yaml # `ansible-playbook` <br>`-i hosts.yml` <br>`rollback.yml`
# ansible/rollback.yml
---
```yaml
# rollback.yml
--- ---
- hosts: all - hosts: all
@ -464,10 +667,6 @@ drwxr-xr-x 9 4096 Jul 22 20:30 20190722203038Z
--- ---
# `ansible-playbook` <br>`-i hosts.yml` <br>`ansible/rollback.yml`
---
# **Customising Ansistrano: <br>Build Hooks** # **Customising Ansistrano: <br>Build Hooks**
--- ---
@ -482,23 +681,25 @@ Clean up = remove node_modules, database export, sqlite testing DB
--- ---
```yaml ```yaml
# ansible/deploy.yml # deploy.yml
---
# ...
vars: ansistrano_after_symlink_shared_tasks_file: '{{ playbook_dir }}/deploy/after-symlink-shared.yml'
... ansistrano_after_symlink_tasks_file: '{{ playbook_dir }}/deploy/after-symlink.yml'
ansistrano_after_symlink_shared_tasks_file: "{{ playbook_dir }}/deploy/after-symlink-shared.yml" ansistrano_after_update_code_tasks_file: '{{ playbook_dir }}/deploy/after-update-code.yml'
ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/deploy/after-symlink.yml"
ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/deploy/after-update-code.yml"
release_web_path: "{{ ansistrano_release_path.stdout }}/web" release_web_path: '{{ ansistrano_release_path.stdout }}/web'
release_drush_path: "{{ ansistrano_release_path.stdout }}/vendor/bin/drush" release_drush_path: '{{ ansistrano_release_path.stdout }}/vendor/bin/drush'
``` ```
^ Each step has a 'before' and 'after' step
Ansistrano allows us to add more things by providing a path to a playbook and adding additional steps.
--- ---
```yaml ```yaml
# ansible/deploy/after-update-code.yml # deploy/after-update-code.yml
--- ---
- name: Install Composer dependencies - name: Install Composer dependencies
composer: composer:
@ -509,8 +710,7 @@ vars:
--- ---
```yaml ```yaml
# ansible/deploy/after-symlink-shared.yml # deploy/after-symlink-shared.yml
--- ---
- name: Run database updates - name: Run database updates
command: '{{ release_drush_path }} --root {{ release_web_path }} updatedb' command: '{{ release_drush_path }} --root {{ release_web_path }} updatedb'
@ -519,8 +719,7 @@ vars:
--- ---
```yaml ```yaml
# ansible/deploy/after-symlink.yml # deploy/after-symlink.yml
--- ---
- name: Clear Drupal cache - name: Clear Drupal cache
command: '{{ release_drush_path }} --root {{ release_web_path }} cache-rebuild' command: '{{ release_drush_path }} --root {{ release_web_path }} cache-rebuild'
@ -532,30 +731,12 @@ vars:
--- ---
# **Demo**
---
# **Questions?**
---
![](images/drupalcon/contribution.jpg)
---
![](images/drupalcon/feedback.jpg)
^ Please leave feedback via the DrupalCon app, or via Twitter
---
# **Managing data <br>across deployments** # **Managing data <br>across deployments**
--- ---
```yaml ```yaml
# ansible/deploy.yml # deploy.yml
vars: vars:
# ... # ...
@ -588,29 +769,27 @@ drwxr-xr-x 3 4096 Jan 22 17:30 ..
-rw-r--r-- 1 6762 Jul 19 00:14 default.services.yml -rw-r--r-- 1 6762 Jul 19 00:14 default.services.yml
-rw-r--r-- 1 31342 Jul 19 00:14 default.settings.php -rw-r--r-- 1 31342 Jul 19 00:14 default.settings.php
lrwxrwxrwx 1 45 Jul 19 00:14 files -> ../../../../../shared/web/sites/default/files lrwxrwxrwx 1 45 Jul 19 00:14 files -> ../../../../../shared/web/sites/default/files
lrwxrwxrwx 1 35 Jul 19 00:12 settings.php -> /tmp/app/sites/default/settings.php -rw-r--r-- 1 35 Jul 19 00:12 settings.php
``` ```
--- ---
# **Generating Drupal settings <br>files per deployment** # **Generating settings <br>files per deployment**
--- ---
```yaml ```yaml
# ansible/vars/vault.yml # vars/vault.yml
--- ---
vault_database_name: drupal vault_database_name: main
vault_database_user: drupal vault_database_user: user
vault_database_password: drupal vault_database_password: secret
vault_hash_salt: dfgiy$fd2!34gsf2*34g74 vault_hash_salt: dfgiy$fd2!34gsf2*34g74
``` ```
--- ---
```yaml ```yaml
# ansible/vars/vars.yml # vars/vars.yml
--- ---
database_name: "{{ vault_database_name }}" database_name: "{{ vault_database_name }}"
database_password: "{{ vault_database_password }}" database_password: "{{ vault_database_password }}"
@ -621,11 +800,10 @@ hash_salt: "{{ vault_hash_salt }}"
--- ---
```yaml ```yaml
# ansible/vars/vars.yml # vars/vars.yml
--- ---
drupal_settings: drupal_settings:
- drupal_root: /tmp/app - drupal_root: /app/web
sites: sites:
- name: default - name: default
settings: settings:
@ -642,11 +820,11 @@ drupal_settings:
sync: ../config/sync sync: ../config/sync
``` ```
--- ---
```php ```php
// templates/settings.php.j2 // templates/settings.php.j2
// {{ ansible_managed }} // {{ ansible_managed }}
{% for key, values in item.1.settings.databases.items() %} {% for key, values in item.1.settings.databases.items() %}
@ -665,25 +843,106 @@ $databases['{{ key }}']['{{ target }}'] = array(
{% if item.1.settings.base_url is defined %} {% if item.1.settings.base_url is defined %}
$base_url = '{{ item.1.settings.base_url }}'; $base_url = '{{ item.1.settings.base_url }}';
{% endif %} {% endif %}
{# ... #}
``` ```
--- ---
```yaml ```yaml
- name: Remove settings.php # tasks/main.yml
---
- name: Ensure directory exists
file: file:
path: '{{ ansistrano_release_path.stdout }}/web/sites/{{ item.1.name|default("default")}}/settings.php' state: directory
state: absent path: '{{ item.0.drupal_root }}/sites/{{ item.1.name|default("default") }}'
with_subelements: with_subelements:
- '{{ drupal_settings }}' - '{{ drupal_settings }}'
- sites - sites
no_log: true
- name: Link settings.php - name: Create settings files
file: template:
src: '/tmp/app/sites/{{ item.1.name|default("default")}}/settings.php' src: settings.php.j2
dest: '{{ ansistrano_release_path.stdout }}/web/sites/{{ item.1.name|default("default")}}/settings.php' dest: '{{ item.0.drupal_root }}/sites/{{ item.1.name|default("default") }}/{{ item.1.filename|default("settings.php") }}'
state: link
with_subelements: with_subelements:
- '{{ drupal_settings }}' - '{{ drupal_settings }}'
- sites - sites
no_log: true
``` ```
---
# **Multiple environments**
## Dev, test, production
---
```yaml
mysql_databases:
- name: production
- name: staging
mysql_users:
- name: production
password: '{{ live_db_password }}'
priv: '{{ live_db_name }}.*:ALL'
- name: staging
password: '{{ staging_db_password }}'
priv: staging.*:ALL
```
---
```yaml
# inventories/live.yml
---
all:
hosts:
webservers:
ansible_ssh_host: 192.168.33.10
ansible_ssh_port: 22
project_deploy_path: /app
git_branch: master
drupal_hash_salt: "{{ vault_drupal_hash_salt }}"
drupal_install: true
drupal_settings:
# ...
```
---
```yaml
# inventories/staging.yml
---
all:
hosts:
webservers:
ansible_ssh_host: 192.168.33.10
ansible_ssh_port: 22
project_deploy_path: /app-test
git_branch: develop
drupal_hash_salt: "{{ vault_drupal_hash_salt }}"
drupal_install: true
drupal_settings:
# ...
```
---
# `ansible-playbook deploy.yml -i inventories/staging.yml`
---
# `ansible-playbook deploy.yml -i inventories/live.yml`
---
# **Questions?**