DrupalVM and CircleCI: Deploying for the wins

Published onThursday 27, April 2017

DrupalVM is a tool created by Jeff Geerling that “makes building local Drupal development environments quick and easy” for Drupal. It is built using Vagrant and provisioned with Ansible. Since it uses Ansible, it also provides a means to support a production environment deployment. This allows for a repeatable and determinable environment when developing and deploying to remote servers.

In fact, I currently use DrupalVM to power a Drupal Commerce 2 environment that I run locally and in production. The production environment is updated using continuous deployment via CircleCI. The wonderful roles created by Jeff Geerling and his playbook in DrupalVM handle my production deployment.

Want the tl;dr, skip to bottom for the example.

Setting up DrupalVM within your project

First things first, you’ll need to use DrupalVM. In this example we will add DrupalVM as a project dependency, using Composer. This is important. It ensures that any developer using this project, the CI/CD environment, and the final destination all have the same version of files. It also makes it easier to manage and receive upstream fixes.

For my project I followed Jeff’s blog post Soup to Nuts to configure DrupalVM and setup my DigitalOcean droplet to be my production target. You might want to read that over if you have yet to see how DrupalVM can go to production. I won’t copy those steps here, I’ll show how you can get CircleCI to deploy your DrupalVM configuration to a remote host.

See the article for full details, or my example linked later. For now I’ll do a quick review of some basic config.

The inventory file:

[drupalvm] ansible_ssh_user=drupalvm

The config.yml file:

drupal_domain: "example.com"
vagrant_hostname: "{{ drupal_domain }}"

  - servername: "{{ drupal_domain }}"
    documentroot: "{{ drupal_core_path }}"
    extra_parameters: "{{ apache_vhost_php_fpm_parameters }}"

The vagrant.config.yml file:

# Note, {{ drupal_domain }} overridden for Vagrant to use local.* prefix.
drupal_domain: "local.example.com"
The prod.config.yml file:

drupal_deploy: true
drupal_deploy_repo: "[email protected]:organization/repo.git"
drupal_deploy_dir: "/var/www/drupal"

Those are the only tidbits in my production configuration. I treat the config.yml as primary with specific non-production overrides in vagrant.config.yml.

Adding CircleCI

NOTE! You’ll need to be able to deploy from CircleCI to a remote server, which means adding SSH permissions. With CircleCI you must create a key on your server and give the private key to project configuration.

Okay! So DrupalVM is running, you have accessed your site. Now, let’s run this sucker with CircleCI to do testing and deployment. Create a circle.yml file and let us walk through writing it.

First, we need to specify what language we’re using. In this case, I am using a PHP 7.0 image.

    version: 7.0.7

Defining dependencies

Next we want to set up our dependencies. Dependencies are things that we will need to actually run our tests, and they can be cached to speed up future runs. I’ll annotate specific steps using comments within the YAML.

  # Cache the caches for Composer and PIP, because package download is always a PITA and time eater.
    - ~/.composer/cache
    - ~/.cache/pip
    # Install Ansible
    - pip install ansible
    - pip install --upgrade setuptools
    - echo $ANSIBLE_VAULT_PASSWORD > ~/.vault.txt
    # Disable xdebug (performance) and set timezzone.
    - echo "date.timezone = 'America/Chicago'"  > /opt/circleci/php/7.0.7/etc/conf.d/xdebug.ini
    # I save a GitHub personal OAuth token for when Composer beats down GitHub's API limits
    - git config --global github.accesstoken $GITHUB_OAUTH_TOKEN
    - composer config -g github-oauth.github.com $GITHUB_OAUTH_TOKEN
    - composer install --prefer-dist --no-interaction

Running tests

Before deploying anything to production or staging, we should make sure it does not deploy broken code.

    # Containers come with PhantomJS, so let's use it for JavaScript testing.
    # We specify it to run in background so that CircleCI saves output as build artifacts for later review.
    - phantomjs --webdriver=4444:
        background: true
    # Sometimes the test script can get cranky when this directory is missing, so make it.
    - mkdir web/sites/simpletest
    # User the PHP built-in webserver for tests, also run in background for log artifacts.
    - php -S localhost:8080 -t web:
        background: true
    # Run some tests
    - ./bin/phpunit --testsuite unit --group commerce
    - ./bin/phpunit --testsuite kernel --group commerce
    - ./bin/behat -f junit -o $CIRCLE_TEST_REPORTS -f pretty -o std

Saving artifacts

In the event that our tests do fail, it would be great to see additional artifacts. Any process that had background: true defined will have its logs available. Adding the following lines will let us access HTML output from the PHPUnit Functional tests and any Behat tests.

  # Expose test output folders as artifacts so we can review when failures happen.
    # Folder where Functional/FunctionalJavascript dump HTML output
    - "web/sites/simpletest/browser_output"
    # Folder where Behat generates HTML/PNG output for step failures
    - "tests/failures"

Setting up deployment

Now, it is time to tell CircleCI about our deployment process. Nothing too magical here. We are simplify defining that when the master branch passes to run a specific command. This command will load our production config and run the DrupalVM provisioning playbook on our production server. We specify the drupal tags to limit the scope of deployment.

    branch: master
    # Specify DrupalVM environment and fire off provisioning.
    # DRUPALVM_ENV specifies which *.config.yml to load.
    - DRUPALVM_ENV=prod ansible-playbook \
      -i vm/inventory \ 
      vendor/geerlingguy/drupal-vm/provisioning/playbook.yml \
      # Points to directory containing your config.yml files.
      -e "config_dir=$(pwd)/vm" \
      # I exported my Ansible user sudo password to environment variables to make provisioning work.
      --extra-vars "ansible_become_pass=${ANSIBLE_VAULT_PASSWORD}"

If your tests fail then the deployment will not run.


Want to see an example of the full config? See my Drupal Commerce 2 composer project template