Skip to content

Commit

Permalink
Merge pull request #4 from jonmorehouse/first-pass
Browse files Browse the repository at this point in the history
first-release: setting up a first plugin release
  • Loading branch information
jonmorehouse committed Feb 22, 2016
2 parents 1faebba + d080f21 commit ccfae46
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 75 deletions.
64 changes: 57 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,63 @@
# terraform-provisioner-ansible
An attempt at provisioning terraform instances with ansible
> Provision terraform resources with ansible
Still a WIP but this `terraform` plugin provides a basic provisioner for
bootstrapping `terraform` instance resources with ansible.
## Overview

As a first pass, this uses a `local-ansible.py` script for running ansible
locally on the host. Ansible code is shipped to the resource and run locally.
This is to avoid having to introduce ansible as a dependency at the `terraform`
layer and to simplify the responsibilities of this plugin.
**[Terraform](https://github.com/hashicorp/terraform)** is a tool for automating infrastructure. Terraform includes the ability to provision resources at creation time through a plugin api. Currently, some builtin [provisioners](https://www.terraform.io/docs/provisioners/) such as **chef** and standard scripts are provided; this provisioner introduces the ability to provision an instance at creation time with **ansible**.

This provisioner provides the ability to apply **host-groups**, **plays** or **roles** against a host at provision time. Ansible is run on the host itself and this provisioner configures a dynamic inventory on the fly as resources are created.

**terraform-provisioner-ansible** is shipped as a **Terraform** [module](https://www.terraform.io/docs/modules/create.html). To include it, simply download the binary and enable it as a terraform module in your **terraformrc**.

## Installation

**terraform-provisioner-ansible** ships as a single binary and is compatible with **terraform**'s plugin interface. Behind the scenes, terraform plugins use https://github.com/hashicorp/go-plugin and communicate with the parent terraform process via RPC.

To install, download and un-archive the binary and place it on your path.

```bash
$ https://github.com/jonmorehouse/terraform-provisioner-ansible/releases/download/0.0.1-terraform-provisioner-ansible.tar.gz

$ tar -xvf 0.0.1-terraform-provisioner-ansible.tar.gz /usr/local/bin
```

Once installed, a `~/.terraformrc` file is used to _enable_ the plugin.

```bash
providers {
ansible = "/usr/local/bin/terraform-provisioner-ansible"
}
```

## Usage

Once installed, you can provision resources by including an `ansible` provisioner block.

The following example demonstrates a configuration block to apply a host group's plays to new instances. You can specify a list of hostgroups and a list of plays to specify which ansible tasks to perform on the host.

Additionally, `groups` and `extra_vars` are accessible to resolve variables and group the new host in ansible.

```
{
resource "aws_instance" "terraform-provisioner-ansible-example" {
ami = "ami-408c7f28"
instance_type = "t1.micro"
provisioner "ansible" {
connection {
user = "ubuntu"
}
playbook = "ansible/playbook.yml"
groups = ["all"]
hosts = ["terraform"]
extra_vars = {
"env": "terraform"
}
}
}
}
```

Check out [example](example/) for a more detailed walkthrough of the provisioner and how to provision resources with **ansible**.

73 changes: 51 additions & 22 deletions ansible-local.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import argparse
import collections
import json
import os
import os.path
import socket
import subprocess
Expand All @@ -23,48 +24,77 @@
'become', 'become_method', 'become_user', 'verbosity', 'check'))


def build_inventory(loader, variable_manager, group_names):
def build_inventory(loader, variable_manager, group_names, playbook_basedir):
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=['localhost'])
host = inventory.get_hosts()[0]


# because we just pass in a "host list" which isn't a real inventory file,
# we explicitly have to add all of the desired groups to the inventory. By
# default an "all" group is created whenever a new inventory is created
for group_name in group_names:
if not inventory.get_group(group_name):
group = Group(group_name)
inventory.add_group(group)
inventory.add_group(Group(group_name))

# because we are explicitly adding groups, we also need to make sure that a
# playbook basedir is set so that `group_vars` can be loaded from the
# correct directory.
inventory.set_playbook_basedir(playbook_basedir)
inventory_hosts = inventory.get_hosts()

inventory.get_group(group_name).add_host(host)
# for each group specified, ensure that the inventory's host (localhost) is
# explicitly in the group.
for group_name in group_names:
group = inventory.get_group(group_name)
if group.get_hosts():
continue

for host in inventory.get_hosts():
group.add_host(host)

return inventory


def build_plays(loader, variable_manager, playbook_path, play_ids):
def build_plays(loader, variable_manager, playbook_path, plays=[], hosts=[]):
playbook = Playbook.load(playbook_path, variable_manager, loader)
plays = []

for play in playbook.get_plays():
# a play can correspond to a play name or the host group
for play_id in play_ids:
if play.get_name() == play_id:
plays.append(play)
elif play._ds['hosts'] == play_id:
if play.get_name() in plays:
plays.append(play)
continue

if play._ds['hosts'] in hosts:
plays.append(play)
continue

for piece in play._ds['hosts']:
if piece in hosts:
plays.append(play)
break

return plays


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='ansible-local script for running ansible against localhost')

parser.add_argument('--module-path', help='path to ansible source', required=True)
parser.add_argument('--playbook', help='path to playbook file', required=True)
parser.add_argument('--plays', help='list of plays to apply to host', required=True, type=lambda x: x.split(","))
parser.add_argument('--groups', help='list of groups to apply to host', required=True, type=lambda x: x.split(","))
parser.add_argument('--playbook', help='full filepath of the playbook', required=True)
parser.add_argument('--extra-vars', help='json encoded string with extra-var information', required=True, type=lambda x: json.loads(x))

parser.add_argument('--groups', help='list of groups to apply to the localhost', required=False, type=lambda x: x.split(','))
parser.add_argument('--plays', help='list of explicitly named plays to run', required=True, type=lambda x: x.split(','))
parser.add_argument('--hosts', help='list of host groups based plays to run ', required=False, type=lambda x: x.split(','))
args = parser.parse_args()

# clean up arguments that were optional or semioptional
assert args.plays or args.hosts, "either a list of host groups or a list of plays are required"
if not args.plays:
args.plays = []
if not args.hosts:
args.hosts = []

playbook_basedir = os.path.dirname(os.path.abspath(args.playbook))

options = OptionsClass(connection='local',
module_path=args.module_path,
module_path=playbook_basedir,
forks=100,
remote_user=None,
private_key_file=None,
Expand All @@ -85,12 +115,11 @@ def build_plays(loader, variable_manager, playbook_path, play_ids):
'roles': 'all',
}
variable_manager.extra_vars.update(args.extra_vars)

inventory = build_inventory(loader, variable_manager, args.groups + args.plays)
inventory = build_inventory(loader, variable_manager, args.groups, playbook_basedir)

variable_manager.set_inventory(inventory)
plays = build_plays(loader, variable_manager, args.playbook, args.plays)

plays = build_plays(loader, variable_manager, args.playbook, plays=args.plays, hosts=args.hosts)
tqm = TaskQueueManager(
inventory=inventory,
variable_manager=variable_manager,
Expand Down
23 changes: 23 additions & 0 deletions example/example.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
provider "aws" {
region = "us-east-1"
}

resource "aws_instance" "ansible-test" {
ami = "ami-408c7f28"
instance_type = "t1.micro"

provisioner "ansible" {
connection {
user = "ubuntu"
}

playbook = "playbook.yml"
plays = ["terraform"]
hosts = ["all"]
groups = ["terraform"]
extra_vars = {
"extra_var": "terraform"
}
}
}

2 changes: 2 additions & 0 deletions example/group_vars/terraform
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
env: terraform
8 changes: 8 additions & 0 deletions example/playbook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
- hosts: all
roles:
- base

- name: terraform
roles:
- terraform
6 changes: 6 additions & 0 deletions example/roles/base/roles/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: base role applied
debug:
msg: 'applying the base role'


4 changes: 4 additions & 0 deletions example/roles/terraform/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
- name: install some dependencies
debug:
msg: "This is run on terraform provisioned hosts"
5 changes: 0 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import (
"github.com/hashicorp/terraform/terraform"
)

//const AnsibleLocal = "https://github.com/jonmorehouse/terraform-provi

// this implements the tfrpc.ProvisionerFunc type and returns an instance of a
// resourceProvisioner when called
func ResourceProvisionerBuilder() terraform.ResourceProvisioner {

return &ResourceProvisioner{}
}

Expand Down
Loading

0 comments on commit ccfae46

Please sign in to comment.