Custom Plugin Support

Need to expand Runway to wrap other tools? Yes - you can do that with custom plugin support.

Overview

Runway can import Python modules that can perform custom deployments with your own set of Runway modules. Let’s say for example you want to have Runway execute an Ansible playbook to create an EC2 security group as one of the steps in the middle of your Runway deployment list - this is possible with your own plugin. The custom plugin support allows you to mix-and-match natively supported modules (e.g. CloudFormation, Terraform) with plugins you write providing additional support for non-native modules. Although written in Python, these plugins can natively execute non Python binaries.

RunwayModule Class

Runway provides a Python Class named RunwayModule that can be imported into your custom plugin/Python module. This base class will give you the ability to write your own module that can be added to your runway.yml deployment list (More info on runway.yml below). There are three required functions:

- plan - This code block gets called when ``runway taxi`` executes
- deploy - This code block gets called when ``runway takeoff`` executes
- destroy - This code block gets called when ``runway destroy`` executes

All of these functions are required, but are permitted to be empty no-op/pass statements if applicable.

Context Object

self.context includes many helpful resources for use in your Python module. Some notable examples are:

- self.context.env_name - name of the environment
- self.context.env_region - region in which the module is being executed
- self.context.env_vars - OS environment variables provided to the module
- self.path - path to your Runway module folder

runway.yml Example

After you have written your plugin, you need to add the module class_path to your module’s configuration. Below is an example runway.yml containing a single module that looks for an Ansible playbook in a folder at the root of your Runway environment (i.e. repo) named “security_group.ansible”.

Setting class_path tells Runway to import the DeployToAWS Python class, from a file named Ansible.py in a folder named “local_runway_extensions” (Standard Python import conventions apply). Runway will execute the deploy function in your class when you perform a runway deploy (AKA takeoff).

deployments:
  - modules:
      - path: security_group.ansible
        class_path: local_runway_extensions.Ansible.DeployToAWS
    regions:
      - us-east-1

Below is the Ansible.py module referenced above that wraps the ansible-playbook command. It will be responsible for deploying an EC2 Security Group from the playbook with a naming convention of <env>-<region>.yaml within a fictional security_group.ansible Runway module folder. In this example, the ansible-playbook binary would already have been installed prior to a Runway deploy, but this example does check to see if it is installed before execution and logs an error if not. The Runway plugin will only execute the ansible-playbook against a yaml file associated with the environment and set for the Runway execution and region defined in the runway.yml.

Using the above runway.yml and the plugin/playbook below saved to the Runway module folder you will only have a deployment occur in the dev environment in us-east-1. If you decide to perform a runway deployment in the prod environment, or in a different region, the ansible-playbook deployment will be skipped. This matches the behavior of the Runway’s native modules.

"""Ansible Plugin example for Runway."""

import logging
import subprocess
import sys
import os

from runway.module import RunwayModule
from runway.util import which

LOGGER = logging.getLogger('runway')


def check_for_playbook(playbook_path):
    """Determine if environment/region playbook exists."""
    if os.path.isfile(playbook_path):
        LOGGER.info("Processing playbook: %s", playbook_path)
        return {'skipped_configs': False}
    else:
        LOGGER.error("No playbook for this environment/region found -- "
                     "looking for %s", playbook_path)
        return {'skipped_configs': True}


class DeployToAWS(RunwayModule):
    """Ansible Runway Module."""

    def plan(self):
        """Skip plan"""
        LOGGER.info('plan not currently supported for Ansible')
        pass

    def deploy(self):
        """Run ansible-playbook."""
        if not which('ansible-playbook'):
            LOGGER.error('"ansible-playbook" not found in path or is not '
                         'executable; please ensure it is installed'
                         'correctly.')
            sys.exit(1)
        playbook_path = (self.path + "-" + self.context.env_name + self.context.env_region)
        response = check_for_playbook(playbook_path)
        if response['skipped_configs']:
            return response
        else:
            subprocess.check_output(
                ['ansible-playbook', playbook_path])
            return response

    def destroy(self):
        """Skip destroy."""
        LOGGER.info('Destroy not currently supported for Ansible')
        pass

And below is the example Ansible playbook itself, saved as dev-us-east-1.yaml in the security_group.ansible folder:

- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
      - name: create a security group in us-east-1
        ec2_group:
          name: dmz
          description: Dev example ec2 group
          region: us-east-1
          rules:
            - proto: tcp
              from_port: 80
              to_port: 80
              cidr_ip: 0.0.0.0/0
        register: security_group

The above would be deployed if runway deploy was executed in the dev environment to us-east-1.