.. 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