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 RunwayModule
to use as the base class of all module handler classes.
This base class will give you the ability to write your own module handler class that can be added to your runway.yml deployment list (More info on runway.yml below).
There are four methods that need to be defined for the class:
- deploy
This method is called when
runway deploy
is run.- destroy
This method is called when
runway destroy
is run.- init
This method is called when
runway init
is run.- plan
This method is called when
runway plan
is run.
Context Object
self.ctx
includes many helpful resources for use in your Python module.
Some notable examples are:
self.ctx.env.name
- name of the environmentself.ctx.env.aws_region
- region in which the module is being executedself.ctx.env.vars
- OS environment variables provided to the moduleself.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."""
from __future__ import annotations
import logging
import subprocess
import sys
from typing import TYPE_CHECKING, Dict
from runway.module.base import RunwayModule
from runway.utils import which
if TYPE_CHECKING:
from pathlib import Path
LOGGER = logging.getLogger("runway")
def check_for_playbook(playbook_path: Path) -> Dict[str, bool]:
"""Determine if environment/region playbook exists."""
if playbook_path.is_file():
LOGGER.info("Processing playbook: %s", playbook_path)
return {"skipped_configs": False}
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 deploy(self) -> None:
"""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 / f"{self.ctx.env.name}-{self.ctx.env.aws_region}"
response = check_for_playbook(playbook_path)
if response["skipped_configs"]:
return
subprocess.check_output(["ansible-playbook", str(playbook_path)])
def destroy(self) -> None:
"""Skip destroy."""
LOGGER.info("destroy not currently supported for Ansible")
def init(self) -> None:
"""Skip init."""
LOGGER.info("init not currently supported for Ansible")
def plan(self) -> None:
"""Skip plan."""
LOGGER.info("plan not currently supported for Ansible")
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
.