talks/deploying-php-ansible-ansistrano/slides.rst

1041 lines
22 KiB
ReStructuredText

.. footer:: @opdavies
Deploying PHP with Ansible, Ansible Vault, and Ansistrano
#########################################################
|
.. class:: titleslideinfo
Oliver Davies, Inviqa
.. raw:: pdf
TextAnnotation "Full stack Developer and Systems Administrator"
TextAnnotation "Organiser of PHP South Wales"
.. page:: imagePage
.. image:: images/techs.png
:width: 14cm
.. page:: standardPage
Things we'll be looking at
==========================
- **Ansible** crash course
- Keeping secrets with **Ansible Vault**
- Deployments with **Ansistrano**
.. page:: imagePage
.. image:: images/logo-acquia.png
:width: 12cm
|
.. image:: images/logo-platformsh.png
:width: 12cm
|
.. image:: images/logo-pantheon.png
:width: 12cm
.. page::
.. image:: images/logo-digital-ocean.png
:width: 6cm
|
.. image:: images/logo-linode.png
:width: 6cm
|
.. image:: images/logo-vultr.png
:width: 8cm
.. page:: standardPage
What is Ansible?
================
.. class:: text-lg
Ansible is an open-source **software provisioning**, **configuration management**, and **application-deployment** tool.
|
https://en.wikipedia.org/wiki/Ansible_(software)
.. page::
What is Ansible?
================
- CLI tool
- Configured with YAML
- Agentless, connects via SSH
- Jinja2 for templating
- Executes ad-hoc remote commands
- Installs software packages
- Performs deployment steps
- Batteries included
.. raw:: pdf
TextAnnotation "- Written in Python but configured with Yaml."
TextAnnotation "Drupal, Symfony and a lot of other projects use YAML."
TextAnnotation "Nothing needed on the server, other than Python."
TextAnnotation "First-party modules (SSH keys, file and directory management, package repositories, stopping/starting/restarting services, DO/Linode/AWS integration)."
.. page::
Why Ansible?
============
- Familiar syntax (Drupal 8, Symfony, Sculpin)
- Easily readable
- No server dependencies
- Easy to add to an existing project
- Includes relevant modules (Git, Composer)
- Idempotency, resulting in cleaner scripts
.. page:: titlePage
.. class:: centredtitle
Hosts / Inventories
.. page:: standardPage
hosts.ini
=========
.. code:: ini
[webservers]
192.168.33.10
[webservers:vars]
ansible_ssh_port=22
ansible_ssh_user=opdavies
.. raw:: pdf
TextAnnotation "Vagrant IP address."
TextAnnotation "Supports wildcards and ranges"
hosts.yml
=========
.. code-block:: yaml
---
all:
children:
webservers:
hosts:
192.168.33.10:
vars:
ansible_ssh_port: 22
ansible_ssh_user: opdavies
.. raw:: pdf
TextAnnotation "My prefered format."
TextAnnotation "More consistency across the project, easier to copy variables from other places such as playbooks."
.. page:: titlePage
.. class:: centredtitle
Ad-hoc Commands
.. page::
.. class:: centredtitle
``ansible all -i hosts.yml
-m ping``
.. raw:: pdf
TextAnnotation "Single ad-hoc command."
TextAnnotation "-i = inventory"
TextAnnotation "-m = module"
.. page:: standardPage
.. code:: json
webservers | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
.. page:: titlePage
.. class:: centredtitle
``ansible all -i hosts.yml
-m command
-a "git pull --chdir=/app"``
.. raw:: pdf
TextAnnotation "Update a codebase using "git pull""
TextAnnotation "-a = (additional) arguments"
TextAnnotation "--chdir = change directory"
.. page::
.. class:: centredtitle
``ansible all -i hosts.yml
-m git
-a "repo=https://github.com
/opdavies/dransible
--chdir=/app"``
.. raw:: pdf
TextAnnotation "Same example, but using the core "Git" module"
.. page:: titlePage
.. class:: centredtitle
Playbooks
.. page:: standardPage
.. code-block:: yaml
---
- hosts: webservers
vars:
git_repo: https://github.com/opdavies/dransible
project_root_dir: /app
tasks:
- name: Update the code
git:
repo: '{{ git_repo }}'
dest: '{{ project_root_dir }}'
.. raw:: pdf
TextAnnotation "YAML file"
TextAnnotation "Collection of multiple tasks"
TextAnnotation "Can add and use variables"
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook main.yml
-i hosts.yml``
.. raw:: pdf
TextAnnotation "How do we run a playbook?"
TextAnnotation "Use the ansible-playbook command and specify the name of the playbook."
.. page:: titlePage
.. class:: centredtitle
Roles: configuring a LAMP stack
.. page:: standardPage
requirements.yml
================
.. code-block:: yaml
---
- src: geerlingguy.apache
- src: geerlingguy.composer
- src: geerlingguy.mysql
- src: geerlingguy.php
- src: geerlingguy.php-mysql
.. raw:: pdf
TextAnnotation "Requirements file for Ansible roles"
TextAnnotation "Typically requirements.yml"
TextAnnotation "Pulled from Ansible Galaxy"
TextAnnotation "Equivilent to composer.json/Packagist in PHP"
.. page:: titlePage
.. class:: centredtitle
``ansible-galaxy install
-r requirements.yml``
.. page:: standardPage
.. code-block:: yaml
# playbook.yml
---
- hosts: webservers
roles:
- geerlingguy.apache
- geerlingguy.mysql
- geerlingguy.php
- geerlingguy.php-mysql
- geerlingguy.composer
.. raw:: pdf
TextAnnotation "How do we use them?"
TextAnnotation "Add them to the playbook under 'roles'."
TextAnnotation "Ordering matters here!"
TextAnnotation "If these were ordered alphabetically then Composer install would fail because it would run before PHP is installed."
.. page::
.. code-block:: yaml
# playbook.yml
---
vars:
apache_vhosts:
- servername: dransible
documentroot: /app/web
.. raw:: pdf
TextAnnotation "configuring the Apache role to install virtual hosts."
.. page::
.. code-block:: yaml
# playbook.yml
---
vars:
php_version: 7.4
php_packages_extra:
- libapache2-mod-php{{ php_version }}
- libpcre3-dev
.. raw:: pdf
TextAnnotation "configuring PHP."
.. page::
.. code-block:: yaml
# playbook.yml
---
vars:
mysql_databases:
- name: main
mysql_users:
- name: user
password: secret
priv: main.*:ALL
.. raw:: pdf
TextAnnotation "configuring MySQL databases and users."
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook provision.yml
-i hosts.yml``
.. page:: standardPage
.. code-block::
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]
.. page::
.. code-block::
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
.. page::
.. image:: images/after-provision-1.png
:width: 24cm
.. raw:: pdf
TextAnnotation "IP address of server, Apache is installed and running."
.. page::
.. image:: images/after-provision-2.png
:width: 24cm
.. raw:: pdf
TextAnnotation "No application code on the server yet."
.. page:: titlePage
.. class:: centredtitle
Basic deployment
.. page:: standardPage
.. class:: small
.. code-block:: 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
.. raw:: pdf
TextAnnotation "Using file module to create the directory"
TextAnnotation "Using synchronize module/rsync to upload the files"
TextAnnotation "Using Composer module to install dependencies. There are other possible values."
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook deploy.yml
-i hosts.yml``
.. page:: standardPage
.. image:: images/after-deploy-1.png
:width: 24cm
.. page:: standardPage
Disadvantages
=============
- Sensitive data stored in plain text
- Single point of failure
- No ability to roll back
.. page:: titlePage
.. class:: centredtitle
Keeping secrets with Ansible Vault
.. page:: standardPage
.. code-block:: yaml
---
vars:
mysql_databases:
- name: main
mysql_users:
- name: user
password: secret
priv: main.*:ALL
.. page::
.. code-block:: yaml
# provision_vault.yml
---
vault_database_name: main
vault_database_user: user
vault_database_password: secret
.. page:: titlePage
.. class:: centredtitle
``ansible-vault encrypt
provision_vault.yml``
.. class:: centredtitle
``New Vault password:
Confirm New Vault password:
Encryption successful``
.. page:: standardPage
.. code-block::
$ANSIBLE_VAULT;1.1;AES256
63656632326165643137646334343537396533656565313032363262623962393861666438393539
6366336638316133373061306332303761383565343035330a373637373830356430353630356161
32313831663039343733343539636365386333303862363635323138346137666166356639323338
3264636538356634390a343766353661386666376362376439386630363664616166643364366335
62373530393933373830306338386539626565313364643133666131613138383431353638636334
39376437633462373934313236363662633832643138386433646230313465383337373031373137
61353963623364393134386335373731356337366464633531656435383161656435313530363234
37373865393839616534353165656463313961333532363537383263343364646534333032336337
3235
.. page::
.. code-block:: yaml
# provision_vars.yml
---
database_name: '{{ vault_database_name }}'
database_user: '{{ vault_database_user }}'
database_password: '{{ vault_database_password }}'
.. page::
.. code-block:: yaml
# provision.yml
---
vars_files:
- vars/provision_vault.yml
- vars/provision_vars.yml
vars:
mysql_databases:
- '{{ database_name }}'
mysql_users:
- name: '{{ database_user }}'
password: '{{ database_password }}'
priv: '{{ database_name }}.*:ALL'
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook deploy.yml
-i hosts.yml
--ask-vault-pass``
.. page::
.. class:: centredtitle
``ansible-playbook deploy.yml
-i hosts.yml
--vault-password-file secret.txt``
.. page::
.. class:: centredtitle
Better deployments with Ansistrano
.. page:: standardPage
.. image:: images/ansistrano.png
:width: 24cm
.. page::
Features
========
- Multiple release directories
- Shared paths and files
- Customisable
- Multiple deployment strategies
- Multi-stage environments
- Prune old releases
- Rollbacks
.. page::
.. code-block:: yaml
# requirements.yml
---
- src: ansistrano.deploy
- src: ansistrano.rollback
.. raw:: pdf
TextAnnotation "to install Ansistrano, add the additional roles to the requirements.yml file"
.. page::
.. code-block:: yaml
# deploy.yml
---
- hosts: all
roles:
- ansistrano.deploy
.. raw:: pdf
TextAnnotation "add to roles within the playbook"
.. page::
.. code-block:: yaml
# deploy.yml
---
vars:
project_deploy_dir: /app
ansistrano_deploy_to: '{{ project_deploy_dir }}'
ansistrano_deploy_via: git
ansistrano_git_branch: master
ansistrano_git_repo: 'git@github.com:opdavies/dransible'
.. page::
.. code-block::
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)
.. page::
.. code-block::
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
.. page::
.. code-block::
vagrant@dransible:/app$ ls -l
total 8
lrwxrwxrwx 1 26 Jul 19 00:15 current -> ./releases/20190719001241Z
drwxr-xr-x 5 4096 Jul 22 20:30 releases
drwxr-xr-x 4 4096 Jul 19 00:00 shared
.. page::
.. code-block::
vagrant@dransible:/app/releases$ ls -l
total 20
drwxr-xr-x 5 4096 Jul 22 20:30 .
drwxr-xr-x 4 4096 Jul 19 00:15 ..
drwxr-xr-x 10 4096 Jul 19 00:02 20190719000013Z
drwxr-xr-x 10 4096 Jul 19 00:14 20190719001241Z
drwxr-xr-x 9 4096 Jul 22 20:30 20190722203038Z
.. page::
.. code-block:: yaml
# rollback.yml
---
- hosts: all
roles:
- ansistrano.rollback
vars:
ansistrano_deploy_to: '{{ project_deploy_dir }}'
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook rollback.yml
-i hosts.yml``
.. page::
.. class:: centredtitle
Customising Ansistrano:
Build Hooks
.. page:: imagePage
.. image:: images/ansistrano-flow.png
:width: 18cm
.. raw:: pdf
TextAnnotation "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."
.. page:: standardPage
.. code-block:: yaml
# 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_update_code_tasks_file: >
'{{ playbook_dir }}/deploy/after-update-code.yml'
release_web_path: '{{ ansistrano_release_path.stdout }}/web'
release_drush_path: '{{ ansistrano_release_path.stdout }}/bin/drush'
.. page::
.. code-block:: yaml
# deploy/after-update-code.yml
---
- name: Install Composer dependencies
composer:
command: install
working_dir: '{{ ansistrano_release_path.stdout }}'
.. page::
.. code-block:: yaml
# deploy/after-symlink-shared.yml
---
- name: Run database updates
command: >
{{ release_drush_path }}
--root {{ release_web_path }}
updatedb
.. page::
.. code-block:: yaml
# deploy/after-symlink.yml
---
- name: Rebuild Drupal cache
command: >
{{ release_drush_path }}
--root {{ release_web_path }}
cache-rebuild
.. page:: titlePage
.. class:: centredtitle
Demo
.. page::
.. class:: centredtitle
Generating settings files per deployment
.. page:: standardPage
.. code-block:: yaml
# deploy_vars.yml
---
drupal_settings:
- drupal_root: /app/web
sites:
- name: default
settings:
databases:
default:
default:
driver: mysql
host: localhost
database: '{{ database_name }}'
username: '{{ database_user }}'
password: '{{ database_password }}'
hash_salt: '{{ hash_salt }}'
config_directories:
sync: ../config/sync
.. page::
.. code-block:: jinja
{# templates/settings.php.j2 #}
{% for key, values in item.1.settings.databases.items() %}
{% for target, values in values.items() %}
$databases['{{ key }}']['{{ target }}'] = array(
'driver' => '{{ values.driver|default('mysql') }}',
'host' => '{{ values.host|default('localhost') }}',
'database' => '{{ values.database }}',
'username' => '{{ values.username }}',
'password' => '{{ values.password }}',
);
{% endfor %}
{% endfor %}
{% if item.1.settings.base_url is defined %}
$base_url = '{{ item.1.settings.base_url }}';
{% endif %}
.. page::
.. code-block:: yaml
# tasks/main.yml
---
- name: Ensure directory exists
file:
state: directory
path: '{{ item.0.drupal_root }}/sites/{{ item.1.name|default("default") }}'
with_subelements:
- '{{ drupal_settings }}'
- sites
no_log: true
- name: Create settings files
template:
src: settings.php.j2
dest:
'{{ item.0.drupal_root }}/sites/{{ item.1.name|default("default") }}/{{
item.1.filename|default("settings.php") }}'
with_subelements:
- '{{ drupal_settings }}'
- sites
no_log: true
.. page:: titlePage
.. class:: centredtitle
Multiple environments development, test, production
.. page:: standardPage
.. code-block:: yaml
# vars.yml
---
vars:
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
.. page::
.. code-block:: yaml
# hosts.yml
---
production:
children:
hosts:
webservers:
ansible_ssh_host: 192.168.33.10
ansible_ssh_port: 22
project_deploy_path: /app
git_branch: production
drupal_hash_salt: '{{ vault_drupal_hash_salt }}'
drupal_install: false
drupal_settings:
# ...
.. page::
.. code-block:: yaml
# hosts.yml
---
staging:
children:
hosts:
webservers:
ansible_ssh_host: 192.168.33.10
ansible_ssh_port: 22
project_deploy_path: /app-staging
git_branch: staging
drupal_hash_salt: '{{ vault_drupal_hash_salt }}'
drupal_install: true
drupal_settings:
# ...
.. page:: titlePage
.. class:: centredtitle
``ansible-playbook deploy.yml
-i hosts.yml
--limit staging``
.. page::
.. class:: centredtitle
``ansible-playbook deploy.yml
-i hosts.yml
--limit production``
.. page:: standardPage
Thanks!
=======
References:
- https://oliverdavies.link/ansible-repos
- https://docs.ansible.com
- https://www.ansistrano.com
- https://symfonycasts.com/screencast/ansistrano
|
Me:
* https://www.oliverdavies.uk