├── .gitignore ├── sample_data ├── .gitignore ├── 0.11.x │ ├── count-basic │ │ ├── main.tf │ │ ├── expect.json │ │ └── terraform.tfstate │ ├── count-advanced │ │ ├── expect.json │ │ ├── main.tf │ │ └── terraform.tfstate │ ├── basic │ │ ├── main.tf │ │ ├── expect.json │ │ └── terraform.tfstate │ └── individual-vars │ │ ├── expect.json │ │ ├── main.tf │ │ └── terraform.tfstate └── 0.12.x │ ├── count-basic │ ├── main.tf │ ├── expect.json │ └── terraform.tfstate │ ├── count-advanced │ ├── expect.json │ ├── main.tf │ └── terraform.tfstate │ ├── basic │ ├── main.tf │ ├── expect.json │ └── terraform.tfstate │ └── individual-vars │ ├── expect.json │ ├── main.tf │ └── terraform.tfstate ├── bin ├── fake-terraform ├── run-tests ├── update-expects ├── debug └── test-inventory-script ├── LICENSE ├── README.md ├── CHANGELOG.md └── terraform.py /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /sample_data/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | terraform.tfstate.backup 3 | -------------------------------------------------------------------------------- /bin/fake-terraform: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | if [ "$*" == "state pull" ]; then 6 | cat ./terraform.tfstate 2>/dev/null 7 | fi 8 | -------------------------------------------------------------------------------- /bin/run-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | PROJECT_ROOT="$(git rev-parse --show-toplevel)" 5 | 6 | find "${PROJECT_ROOT}/sample_data" -type d -depth 2 -print0 \ 7 | | xargs -0 -n1 ${PROJECT_ROOT}/bin/test-inventory-script 8 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.11.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 0.0.5" 7 | } 8 | 9 | resource "ansible_host" "count_sample" { 10 | count = 5 11 | inventory_hostname = "count-sample-${count.index}.example.com" 12 | } 13 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 1.0.1" 7 | } 8 | 9 | resource "ansible_host" "count_sample" { 10 | count = 5 11 | inventory_hostname = "count-sample-${count.index}.example.com" 12 | } 13 | -------------------------------------------------------------------------------- /bin/update-expects: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROJECT_ROOT="$(git rev-parse --show-toplevel)" 4 | export ANSIBLE_TF_BIN="${PROJECT_ROOT}/bin/fake-terraform" 5 | 6 | find "${PROJECT_ROOT}/sample_data" -type d -depth 2 \ 7 | -exec bash -c "ANSIBLE_TF_DIR=\"{}\" \"${PROJECT_ROOT}/terraform.py\" | jq -S > {}/expect.json" \; 8 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-advanced/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "broad-union.example.com": {}, 5 | "lively-tree.example.com": {}, 6 | "mute-fog.example.com": {}, 7 | "rough-bread.example.com": {}, 8 | "young-violet.example.com": {} 9 | } 10 | }, 11 | "all": { 12 | "children": [], 13 | "hosts": [ 14 | "broad-union.example.com", 15 | "lively-tree.example.com", 16 | "mute-fog.example.com", 17 | "rough-bread.example.com", 18 | "young-violet.example.com" 19 | ], 20 | "vars": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-advanced/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "broad-union.example.com": {}, 5 | "lively-tree.example.com": {}, 6 | "mute-fog.example.com": {}, 7 | "rough-bread.example.com": {}, 8 | "young-violet.example.com": {} 9 | } 10 | }, 11 | "all": { 12 | "children": [], 13 | "hosts": [ 14 | "broad-union.example.com", 15 | "lively-tree.example.com", 16 | "mute-fog.example.com", 17 | "rough-bread.example.com", 18 | "young-violet.example.com" 19 | ], 20 | "vars": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-advanced/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.11.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 0.0.5" 7 | } 8 | 9 | variable "hostnames" { 10 | type = "list" 11 | 12 | default = [ 13 | "broad-union", 14 | "young-violet", 15 | "lively-tree", 16 | "mute-fog", 17 | "rough-bread", 18 | ] 19 | } 20 | 21 | variable "domain" { 22 | default = "example.com" 23 | } 24 | 25 | resource "ansible_host" "count_advanced" { 26 | count = "${length(var.hostnames)}" 27 | inventory_hostname = "${element(var.hostnames, count.index)}.${var.domain}" 28 | } 29 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-basic/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "count-sample-0.example.com": {}, 5 | "count-sample-1.example.com": {}, 6 | "count-sample-2.example.com": {}, 7 | "count-sample-3.example.com": {}, 8 | "count-sample-4.example.com": {} 9 | } 10 | }, 11 | "all": { 12 | "children": [], 13 | "hosts": [ 14 | "count-sample-0.example.com", 15 | "count-sample-1.example.com", 16 | "count-sample-2.example.com", 17 | "count-sample-3.example.com", 18 | "count-sample-4.example.com" 19 | ], 20 | "vars": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-advanced/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 1.0.1" 7 | } 8 | 9 | variable "hostnames" { 10 | type = "list" 11 | 12 | default = [ 13 | "broad-union", 14 | "young-violet", 15 | "lively-tree", 16 | "mute-fog", 17 | "rough-bread", 18 | ] 19 | } 20 | 21 | variable "domain" { 22 | default = "example.com" 23 | } 24 | 25 | resource "ansible_host" "count_advanced" { 26 | count = "${length(var.hostnames)}" 27 | inventory_hostname = "${element(var.hostnames, count.index)}.${var.domain}" 28 | } 29 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-basic/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "count-sample-0.example.com": {}, 5 | "count-sample-1.example.com": {}, 6 | "count-sample-2.example.com": {}, 7 | "count-sample-3.example.com": {}, 8 | "count-sample-4.example.com": {} 9 | } 10 | }, 11 | "all": { 12 | "children": [], 13 | "hosts": [ 14 | "count-sample-0.example.com", 15 | "count-sample-1.example.com", 16 | "count-sample-2.example.com", 17 | "count-sample-3.example.com", 18 | "count-sample-4.example.com" 19 | ], 20 | "vars": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/debug: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u 4 | set -o pipefail 5 | 6 | # Assert test case argument received. 7 | if [ ! "$#" -eq "1" ]; then 8 | echo "ERROR: Expected exactly 1 argument, got $#." 9 | echo "Script expects path to test case." 10 | exit 1 11 | fi 12 | 13 | # Assert expect file exists. 14 | if [ ! -f "$1/expect.json" ]; then 15 | echo "ERROR: Missing expectation file: $1/expect.json" 16 | exit 1 17 | fi 18 | 19 | PROJECT_ROOT="$(git rev-parse --show-toplevel)" 20 | 21 | export ANSIBLE_TF_BIN="${PROJECT_ROOT}/bin/fake-terraform" 22 | export ANSIBLE_TF_DIR="$1" 23 | 24 | set -e 25 | 26 | "${PROJECT_ROOT}/terraform.py" | jq -S | diff -y "${ANSIBLE_TF_DIR}/expect.json" - 27 | -------------------------------------------------------------------------------- /bin/test-inventory-script: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u 4 | set -o pipefail 5 | 6 | # Assert test case argument received. 7 | if [ ! "$#" -eq "1" ]; then 8 | echo "ERROR: Expected exactly 1 argument, got $#." 9 | echo "Script expects path to test case." 10 | exit 1 11 | fi 12 | 13 | # Assert expect file exists. 14 | if [ ! -f "$1/expect.json" ]; then 15 | echo "ERROR: Missing expectation file: $1/expect.json" 16 | exit 1 17 | fi 18 | 19 | PROJECT_ROOT="$(git rev-parse --show-toplevel)" 20 | 21 | export ANSIBLE_TF_BIN="${PROJECT_ROOT}/bin/fake-terraform" 22 | export ANSIBLE_TF_DIR="$1" 23 | 24 | "${PROJECT_ROOT}/terraform.py" | jq -S | diff "${ANSIBLE_TF_DIR}/expect.json" - \ 25 | > /dev/null 2> /dev/null 26 | 27 | if [ ! "$?" -eq 0 ]; then 28 | echo "TEST FAILED: $1" 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /sample_data/0.11.x/basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.11.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 0.0.5" 7 | } 8 | 9 | resource "ansible_host" "www" { 10 | inventory_hostname = "www.example.com" 11 | groups = ["example", "web"] 12 | 13 | vars { 14 | fooo = "aaa" 15 | bar = "bbb" 16 | } 17 | } 18 | 19 | resource "ansible_host" "db" { 20 | inventory_hostname = "db.example.com" 21 | groups = ["example", "db"] 22 | 23 | vars { 24 | fooo = "ccc" 25 | bar = "ddd" 26 | } 27 | } 28 | 29 | resource "ansible_group" "web" { 30 | inventory_group_name = "web" 31 | children = ["foo", "bar", "baz"] 32 | 33 | vars { 34 | foo = "bar" 35 | bar = 2 36 | } 37 | } 38 | 39 | # A host with no optional properties. 40 | resource "ansible_host" "base" { 41 | inventory_hostname = "base.example.com" 42 | } 43 | 44 | # A group with no optional properties. 45 | resource "ansible_group" "base" { 46 | inventory_group_name = "base" 47 | } 48 | -------------------------------------------------------------------------------- /sample_data/0.12.x/basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 1.0.1" 7 | } 8 | 9 | resource "ansible_host" "www" { 10 | inventory_hostname = "www.example.com" 11 | groups = ["example", "web"] 12 | 13 | vars = { 14 | fooo = "aaa" 15 | bar = "bbb" 16 | } 17 | } 18 | 19 | resource "ansible_host" "db" { 20 | inventory_hostname = "db.example.com" 21 | groups = ["example", "db"] 22 | 23 | vars = { 24 | fooo = "ccc" 25 | bar = "ddd" 26 | } 27 | } 28 | 29 | resource "ansible_group" "web" { 30 | inventory_group_name = "web" 31 | children = ["foo", "bar", "baz"] 32 | 33 | vars = { 34 | foo = "bar" 35 | bar = 2 36 | } 37 | } 38 | 39 | # A host with no optional properties. 40 | resource "ansible_host" "base" { 41 | inventory_hostname = "base.example.com" 42 | } 43 | 44 | # A group with no optional properties. 45 | resource "ansible_group" "base" { 46 | inventory_group_name = "base" 47 | } 48 | -------------------------------------------------------------------------------- /sample_data/0.11.x/individual-vars/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "db.example.com": { 5 | "bar": "ddd", 6 | "foo": "ccc" 7 | }, 8 | "www.example.com": { 9 | "bar": "bbb", 10 | "db_host": "db.example.com", 11 | "foo": "eee" 12 | } 13 | } 14 | }, 15 | "all": { 16 | "children": [], 17 | "hosts": [ 18 | "db.example.com", 19 | "www.example.com" 20 | ], 21 | "vars": {} 22 | }, 23 | "db": { 24 | "children": [], 25 | "hosts": [ 26 | "db.example.com" 27 | ], 28 | "vars": { 29 | "ansible_user": "postgres" 30 | } 31 | }, 32 | "example": { 33 | "children": [], 34 | "hosts": [ 35 | "db.example.com", 36 | "www.example.com" 37 | ], 38 | "vars": {} 39 | }, 40 | "web": { 41 | "children": [ 42 | "bar", 43 | "baz", 44 | "foo" 45 | ], 46 | "hosts": [ 47 | "www.example.com" 48 | ], 49 | "vars": { 50 | "bar": "2", 51 | "foo": "fff" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sample_data/0.12.x/individual-vars/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "db.example.com": { 5 | "bar": "ddd", 6 | "foo": "ccc" 7 | }, 8 | "www.example.com": { 9 | "bar": "bbb", 10 | "db_host": "db.example.com", 11 | "foo": "eee" 12 | } 13 | } 14 | }, 15 | "all": { 16 | "children": [], 17 | "hosts": [ 18 | "db.example.com", 19 | "www.example.com" 20 | ], 21 | "vars": {} 22 | }, 23 | "db": { 24 | "children": [], 25 | "hosts": [ 26 | "db.example.com" 27 | ], 28 | "vars": { 29 | "ansible_user": "postgres" 30 | } 31 | }, 32 | "example": { 33 | "children": [], 34 | "hosts": [ 35 | "db.example.com", 36 | "www.example.com" 37 | ], 38 | "vars": {} 39 | }, 40 | "web": { 41 | "children": [ 42 | "bar", 43 | "baz", 44 | "foo" 45 | ], 46 | "hosts": [ 47 | "www.example.com" 48 | ], 49 | "vars": { 50 | "bar": "2", 51 | "foo": "fff" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sample_data/0.11.x/basic/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "base.example.com": {}, 5 | "db.example.com": { 6 | "bar": "ddd", 7 | "fooo": "ccc" 8 | }, 9 | "www.example.com": { 10 | "bar": "bbb", 11 | "fooo": "aaa" 12 | } 13 | } 14 | }, 15 | "all": { 16 | "children": [], 17 | "hosts": [ 18 | "base.example.com", 19 | "db.example.com", 20 | "www.example.com" 21 | ], 22 | "vars": {} 23 | }, 24 | "base": { 25 | "children": [], 26 | "hosts": [], 27 | "vars": {} 28 | }, 29 | "db": { 30 | "children": [], 31 | "hosts": [ 32 | "db.example.com" 33 | ], 34 | "vars": {} 35 | }, 36 | "example": { 37 | "children": [], 38 | "hosts": [ 39 | "db.example.com", 40 | "www.example.com" 41 | ], 42 | "vars": {} 43 | }, 44 | "web": { 45 | "children": [ 46 | "bar", 47 | "baz", 48 | "foo" 49 | ], 50 | "hosts": [ 51 | "www.example.com" 52 | ], 53 | "vars": { 54 | "bar": "2", 55 | "foo": "bar" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sample_data/0.12.x/basic/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hostvars": { 4 | "base.example.com": {}, 5 | "db.example.com": { 6 | "bar": "ddd", 7 | "fooo": "ccc" 8 | }, 9 | "www.example.com": { 10 | "bar": "bbb", 11 | "fooo": "aaa" 12 | } 13 | } 14 | }, 15 | "all": { 16 | "children": [], 17 | "hosts": [ 18 | "base.example.com", 19 | "db.example.com", 20 | "www.example.com" 21 | ], 22 | "vars": {} 23 | }, 24 | "base": { 25 | "children": [], 26 | "hosts": [], 27 | "vars": {} 28 | }, 29 | "db": { 30 | "children": [], 31 | "hosts": [ 32 | "db.example.com" 33 | ], 34 | "vars": {} 35 | }, 36 | "example": { 37 | "children": [], 38 | "hosts": [ 39 | "db.example.com", 40 | "www.example.com" 41 | ], 42 | "vars": {} 43 | }, 44 | "web": { 45 | "children": [ 46 | "bar", 47 | "baz", 48 | "foo" 49 | ], 50 | "hosts": [ 51 | "www.example.com" 52 | ], 53 | "vars": { 54 | "bar": "2", 55 | "foo": "bar" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicholas Bering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample_data/0.11.x/individual-vars/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.11.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 0.0.6" 7 | } 8 | 9 | resource "ansible_host" "www" { 10 | inventory_hostname = "www.example.com" 11 | groups = ["example", "web"] 12 | 13 | vars = { 14 | foo = "aaa" 15 | bar = "bbb" 16 | } 17 | } 18 | 19 | resource "ansible_host" "db" { 20 | inventory_hostname = "db.example.com" 21 | groups = ["example", "db"] 22 | 23 | vars = { 24 | foo = "ccc" 25 | bar = "ddd" 26 | } 27 | } 28 | 29 | resource "ansible_host_var" "extra" { 30 | inventory_hostname = "www.example.com" 31 | key = "db_host" 32 | value = "${ansible_host.db.inventory_hostname}" 33 | } 34 | 35 | resource "ansible_host_var" "override" { 36 | inventory_hostname = "www.example.com" 37 | key = "foo" 38 | value = "eee" 39 | } 40 | 41 | resource "ansible_host_var" "underride" { 42 | inventory_hostname = "www.example.com" 43 | variable_priority = 10 44 | key = "bar" 45 | value = "ggg" 46 | } 47 | 48 | resource "ansible_group" "web" { 49 | inventory_group_name = "web" 50 | children = ["foo", "bar", "baz"] 51 | 52 | vars = { 53 | foo = "bar" 54 | bar = 2 55 | } 56 | } 57 | 58 | resource "ansible_group_var" "override" { 59 | inventory_group_name = "web" 60 | key = "foo" 61 | value = "fff" 62 | } 63 | 64 | resource "ansible_group_var" "underride" { 65 | inventory_group_name = "web" 66 | variable_priority = 10 67 | key = "bar" 68 | value = "hhh" 69 | } 70 | 71 | resource "ansible_group_var" "extra" { 72 | inventory_group_name = "db" 73 | key = "ansible_user" 74 | value = "postgres" 75 | } 76 | -------------------------------------------------------------------------------- /sample_data/0.12.x/individual-vars/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "ansible" { 6 | version = "~> 1.0.2" 7 | } 8 | 9 | resource "ansible_host" "www" { 10 | inventory_hostname = "www.example.com" 11 | groups = ["example", "web"] 12 | 13 | vars = { 14 | foo = "aaa" 15 | bar = "bbb" 16 | } 17 | } 18 | 19 | resource "ansible_host" "db" { 20 | inventory_hostname = "db.example.com" 21 | groups = ["example", "db"] 22 | 23 | vars = { 24 | foo = "ccc" 25 | bar = "ddd" 26 | } 27 | } 28 | 29 | resource "ansible_host_var" "extra" { 30 | inventory_hostname = "www.example.com" 31 | key = "db_host" 32 | value = "${ansible_host.db.inventory_hostname}" 33 | } 34 | 35 | resource "ansible_host_var" "override" { 36 | inventory_hostname = "www.example.com" 37 | key = "foo" 38 | value = "eee" 39 | } 40 | 41 | resource "ansible_host_var" "underride" { 42 | inventory_hostname = "www.example.com" 43 | variable_priority = 10 44 | key = "bar" 45 | value = "ggg" 46 | } 47 | 48 | resource "ansible_group" "web" { 49 | inventory_group_name = "web" 50 | children = ["foo", "bar", "baz"] 51 | 52 | vars = { 53 | foo = "bar" 54 | bar = 2 55 | } 56 | } 57 | 58 | resource "ansible_group_var" "override" { 59 | inventory_group_name = "web" 60 | key = "foo" 61 | value = "fff" 62 | } 63 | 64 | resource "ansible_group_var" "underride" { 65 | inventory_group_name = "web" 66 | variable_priority = 10 67 | key = "bar" 68 | value = "hhh" 69 | } 70 | 71 | resource "ansible_group_var" "extra" { 72 | inventory_group_name = "db" 73 | key = "ansible_user" 74 | value = "postgres" 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Inventory 2 | 3 | An Ansible [dynamic inventory][2] script to process Terraform state and return 4 | Ansible host data from [Terraform Provider for Ansible][1] host resources. See the 5 | Terraform Provider for it's own installation and use. 6 | 7 | ## Usage 8 | 9 | Copy the [terraform.py](./terraform.py) script file to a location on your system. Ansible's own documentation suggests the location `/etc/ansible/terraform.py`, but the particular location does not matter to the script. Ensure it has executable permissions (`chmod +x /etc/ansible/terraform.py`). 10 | 11 | With your Ansible playbook and Terraform configuration in the same directory, run Ansible with the `-i` flag to set the inventory source. 12 | 13 | ``` 14 | $ ansible-playbook -i /etc/ansible/terraform.py playbook.yml 15 | ``` 16 | 17 | ## Environment Variables 18 | ### ANSIBLE\_TF\_BIN 19 | 20 | Override the path to the Terraform command executable. This is useful if you have multiple copies or versions installed and need to specify a specific binary. The inventory script runs the `terraform state pull` command to fetch the Terraform state, so that remote state will be fetched seemlessly regardless of the backend configuration. 21 | 22 | ### ANSIBLE\_TF\_DIR 23 | 24 | Set the working directory for the `terraform` command when the scripts shells out to it. This is useful if you keep your terraform and ansible configuration in separate directories. Defaults to using the current working directory. 25 | 26 | ### ANSIBLE\_TF\_WS\_NAME 27 | 28 | Sets the workspace for the `terraform` command when the scripts shells out to it, defaults to `default` workspace - if you don't use workspaces this is the one you'll be using. 29 | 30 | 31 | ## License 32 | 33 | Licensed for use under the [MIT License](./LICENSE). 34 | 35 | [1]: https://github.com/nbering/terraform-provider-ansible/ 36 | [2]: http://docs.ansible.com/ansible/latest/intro_dynamic_inventory.html 37 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-advanced/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 5, 5 | "lineage": "c4a1bf15-040c-aa5a-533a-28396b3ccec0", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "ansible_host", 11 | "name": "count_advanced", 12 | "each": "list", 13 | "provider": "provider.ansible", 14 | "instances": [ 15 | { 16 | "index_key": 0, 17 | "schema_version": 0, 18 | "attributes": { 19 | "groups": null, 20 | "id": "broad-union.example.com", 21 | "inventory_hostname": "broad-union.example.com", 22 | "vars": null 23 | } 24 | }, 25 | { 26 | "index_key": 1, 27 | "schema_version": 0, 28 | "attributes": { 29 | "groups": null, 30 | "id": "young-violet.example.com", 31 | "inventory_hostname": "young-violet.example.com", 32 | "vars": null 33 | } 34 | }, 35 | { 36 | "index_key": 2, 37 | "schema_version": 0, 38 | "attributes": { 39 | "groups": null, 40 | "id": "lively-tree.example.com", 41 | "inventory_hostname": "lively-tree.example.com", 42 | "vars": null 43 | } 44 | }, 45 | { 46 | "index_key": 3, 47 | "schema_version": 0, 48 | "attributes": { 49 | "groups": null, 50 | "id": "mute-fog.example.com", 51 | "inventory_hostname": "mute-fog.example.com", 52 | "vars": null 53 | } 54 | }, 55 | { 56 | "index_key": 4, 57 | "schema_version": 0, 58 | "attributes": { 59 | "groups": null, 60 | "id": "rough-bread.example.com", 61 | "inventory_hostname": "rough-bread.example.com", 62 | "vars": null 63 | } 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /sample_data/0.12.x/count-basic/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 6, 5 | "lineage": "5705137a-fe0c-dda2-8336-e650212920d3", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "ansible_host", 11 | "name": "count_sample", 12 | "each": "list", 13 | "provider": "provider.ansible", 14 | "instances": [ 15 | { 16 | "index_key": 0, 17 | "schema_version": 0, 18 | "attributes": { 19 | "groups": null, 20 | "id": "count-sample-0.example.com", 21 | "inventory_hostname": "count-sample-0.example.com", 22 | "vars": null 23 | } 24 | }, 25 | { 26 | "index_key": 1, 27 | "schema_version": 0, 28 | "attributes": { 29 | "groups": null, 30 | "id": "count-sample-1.example.com", 31 | "inventory_hostname": "count-sample-1.example.com", 32 | "vars": null 33 | } 34 | }, 35 | { 36 | "index_key": 2, 37 | "schema_version": 0, 38 | "attributes": { 39 | "groups": null, 40 | "id": "count-sample-2.example.com", 41 | "inventory_hostname": "count-sample-2.example.com", 42 | "vars": null 43 | } 44 | }, 45 | { 46 | "index_key": 3, 47 | "schema_version": 0, 48 | "attributes": { 49 | "groups": null, 50 | "id": "count-sample-3.example.com", 51 | "inventory_hostname": "count-sample-3.example.com", 52 | "vars": null 53 | } 54 | }, 55 | { 56 | "index_key": 4, 57 | "schema_version": 0, 58 | "attributes": { 59 | "groups": null, 60 | "id": "count-sample-4.example.com", 61 | "inventory_hostname": "count-sample-4.example.com", 62 | "vars": null 63 | } 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /sample_data/0.12.x/basic/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 6, 5 | "lineage": "69c3a57e-dfdf-2a58-0156-240b42234c4b", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "ansible_group", 11 | "name": "base", 12 | "provider": "provider.ansible", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "children": null, 18 | "id": "base", 19 | "inventory_group_name": "base", 20 | "vars": null 21 | } 22 | } 23 | ] 24 | }, 25 | { 26 | "mode": "managed", 27 | "type": "ansible_group", 28 | "name": "web", 29 | "provider": "provider.ansible", 30 | "instances": [ 31 | { 32 | "schema_version": 0, 33 | "attributes": { 34 | "children": [ 35 | "foo", 36 | "bar", 37 | "baz" 38 | ], 39 | "id": "web", 40 | "inventory_group_name": "web", 41 | "vars": { 42 | "bar": "2", 43 | "foo": "bar" 44 | } 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | "mode": "managed", 51 | "type": "ansible_host", 52 | "name": "base", 53 | "provider": "provider.ansible", 54 | "instances": [ 55 | { 56 | "schema_version": 0, 57 | "attributes": { 58 | "groups": null, 59 | "id": "base.example.com", 60 | "inventory_hostname": "base.example.com", 61 | "vars": null 62 | } 63 | } 64 | ] 65 | }, 66 | { 67 | "mode": "managed", 68 | "type": "ansible_host", 69 | "name": "db", 70 | "provider": "provider.ansible", 71 | "instances": [ 72 | { 73 | "schema_version": 0, 74 | "attributes": { 75 | "groups": [ 76 | "example", 77 | "db" 78 | ], 79 | "id": "db.example.com", 80 | "inventory_hostname": "db.example.com", 81 | "vars": { 82 | "bar": "ddd", 83 | "fooo": "ccc" 84 | } 85 | } 86 | } 87 | ] 88 | }, 89 | { 90 | "mode": "managed", 91 | "type": "ansible_host", 92 | "name": "www", 93 | "provider": "provider.ansible", 94 | "instances": [ 95 | { 96 | "schema_version": 0, 97 | "attributes": { 98 | "groups": [ 99 | "example", 100 | "web" 101 | ], 102 | "id": "www.example.com", 103 | "inventory_hostname": "www.example.com", 104 | "vars": { 105 | "bar": "bbb", 106 | "fooo": "aaa" 107 | } 108 | } 109 | } 110 | ] 111 | } 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [2.2.0] - 2019-05-26 10 | ### Added 11 | - Respect `variable_priority`, added in providers releases [0.0.6](https://github.com/nbering/terraform-provider-ansible/releases/tag/v0.0.6)/[1.0.2](https://github.com/nbering/terraform-provider-ansible/releases/tag/v1.0.2) 12 | 13 | ### Changed 14 | - List values in output are now sorted for consistency, and to make regression testing easier 15 | - With state files where `variable_priority` is not set, the default values of 50 (for `ansible_host` and `ansible_group`) and 60 (for `ansible_host_var` and `ansible_group_var`) will be inferred, changing variable merging behaviour 16 | 17 | ## [2.1.0] - 2019-05-20 18 | ### Added 19 | - Support for `ansible_host_var` resource type 20 | - Support for `ansible_group_var` resource type 21 | 22 | ### Fixed 23 | - Corrected a minor issue where `ansible_host` or `ansible_host_var` resources with the same `inventory_hostname` would result in multiple copies of the hostname in any groups they shared in common (including the "all" group) 24 | 25 | ## [2.0.0] - 2019-05-05 26 | ### Added 27 | - Support for Terraform 0.12's new state file structure 28 | - Simple regression testing with Bash scripts 29 | 30 | ### Changed 31 | - Removed version-specific python shebang, as `terraform.py` seems to work fine with Python 2.7 and 3.x 32 | 33 | ### Removed 34 | - `terraform state pull` no longer uses `-input=false` as this argument is not recognized by Terraform 0.12 35 | 36 | ## [1.1.0] - 2019-01-09 37 | ### Added 38 | - Support for Terraform workspaces via `ANSIBLE_TF_WS_NAME` environment variable. Thanks [@dnitsch]! 39 | 40 | ## [1.0.1] - 2018-02-25 41 | ### Added 42 | - Support for `ansible_group` resource, added in [nbering/terraform-provider-ansible#8](https://github.com/nbering/terraform-provider-ansible/pull/8) 43 | 44 | ## [1.0.0] - 2018-01-07 45 | ### Added 46 | - Added `ANSIBLE_TF_DIR` environment variable to set Terraform configuration directory. 47 | 48 | ### Changed 49 | - Ported the script to Python to be more compatible with the Ansible ecosystem. 50 | - Changed `TERRAFORM_PATH` environment variable to `ANSIBLE_TF_PATH`. 51 | 52 | ### Deprecated 53 | - The earlier NodeJS implementation will not be supported going forward. 54 | 55 | ## [0.0.2] - 2018-01-06 56 | ### Changed 57 | - Update docs and package info for move back to GitHub. 58 | 59 | [Unreleased]: https://github.com/nbering/terraform-inventory/compare/v2.2.0...HEAD 60 | [2.2.0]: https://github.com/nbering/terraform-inventory/compare/v2.1.0...v2.2.0 61 | [2.1.0]: https://github.com/nbering/terraform-inventory/compare/v2.0.0...v2.1.0 62 | [2.0.0]: https://github.com/nbering/terraform-inventory/compare/v1.1.0...v2.0.0 63 | [1.1.0]: https://github.com/nbering/terraform-inventory/compare/v1.0.1...v1.1.0 64 | [1.0.1]: https://github.com/nbering/terraform-inventory/compare/v1.0.0...v1.0.1 65 | [1.0.0]: https://github.com/nbering/terraform-inventory/compare/v0.0.2...v1.0.0 66 | [0.0.2]: https://github.com/nbering/terraform-inventory/compare/v0.0.1...v0.0.2 67 | 68 | [@dnitsch]:https://github.com/dnitsch 69 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-advanced/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "terraform_version": "0.11.3", 4 | "serial": 1, 5 | "lineage": "04a519a3-4443-47ab-8a2c-4a0b63de0f80", 6 | "modules": [ 7 | { 8 | "path": [ 9 | "root" 10 | ], 11 | "outputs": {}, 12 | "resources": { 13 | "ansible_host.count_advanced.0": { 14 | "type": "ansible_host", 15 | "depends_on": [], 16 | "primary": { 17 | "id": "broad-union.example.com", 18 | "attributes": { 19 | "id": "broad-union.example.com", 20 | "inventory_hostname": "broad-union.example.com" 21 | }, 22 | "meta": {}, 23 | "tainted": false 24 | }, 25 | "deposed": [], 26 | "provider": "provider.ansible" 27 | }, 28 | "ansible_host.count_advanced.1": { 29 | "type": "ansible_host", 30 | "depends_on": [], 31 | "primary": { 32 | "id": "young-violet.example.com", 33 | "attributes": { 34 | "id": "young-violet.example.com", 35 | "inventory_hostname": "young-violet.example.com" 36 | }, 37 | "meta": {}, 38 | "tainted": false 39 | }, 40 | "deposed": [], 41 | "provider": "provider.ansible" 42 | }, 43 | "ansible_host.count_advanced.2": { 44 | "type": "ansible_host", 45 | "depends_on": [], 46 | "primary": { 47 | "id": "lively-tree.example.com", 48 | "attributes": { 49 | "id": "lively-tree.example.com", 50 | "inventory_hostname": "lively-tree.example.com" 51 | }, 52 | "meta": {}, 53 | "tainted": false 54 | }, 55 | "deposed": [], 56 | "provider": "provider.ansible" 57 | }, 58 | "ansible_host.count_advanced.3": { 59 | "type": "ansible_host", 60 | "depends_on": [], 61 | "primary": { 62 | "id": "mute-fog.example.com", 63 | "attributes": { 64 | "id": "mute-fog.example.com", 65 | "inventory_hostname": "mute-fog.example.com" 66 | }, 67 | "meta": {}, 68 | "tainted": false 69 | }, 70 | "deposed": [], 71 | "provider": "provider.ansible" 72 | }, 73 | "ansible_host.count_advanced.4": { 74 | "type": "ansible_host", 75 | "depends_on": [], 76 | "primary": { 77 | "id": "rough-bread.example.com", 78 | "attributes": { 79 | "id": "rough-bread.example.com", 80 | "inventory_hostname": "rough-bread.example.com" 81 | }, 82 | "meta": {}, 83 | "tainted": false 84 | }, 85 | "deposed": [], 86 | "provider": "provider.ansible" 87 | } 88 | }, 89 | "depends_on": [] 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /sample_data/0.11.x/count-basic/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "terraform_version": "0.11.3", 4 | "serial": 2, 5 | "lineage": "a8860428-0695-4150-b785-5761b4fdeea0", 6 | "modules": [ 7 | { 8 | "path": [ 9 | "root" 10 | ], 11 | "outputs": {}, 12 | "resources": { 13 | "ansible_host.count_sample.0": { 14 | "type": "ansible_host", 15 | "depends_on": [], 16 | "primary": { 17 | "id": "count-sample-0.example.com", 18 | "attributes": { 19 | "id": "count-sample-0.example.com", 20 | "inventory_hostname": "count-sample-0.example.com" 21 | }, 22 | "meta": {}, 23 | "tainted": false 24 | }, 25 | "deposed": [], 26 | "provider": "provider.ansible" 27 | }, 28 | "ansible_host.count_sample.1": { 29 | "type": "ansible_host", 30 | "depends_on": [], 31 | "primary": { 32 | "id": "count-sample-1.example.com", 33 | "attributes": { 34 | "id": "count-sample-1.example.com", 35 | "inventory_hostname": "count-sample-1.example.com" 36 | }, 37 | "meta": {}, 38 | "tainted": false 39 | }, 40 | "deposed": [], 41 | "provider": "provider.ansible" 42 | }, 43 | "ansible_host.count_sample.2": { 44 | "type": "ansible_host", 45 | "depends_on": [], 46 | "primary": { 47 | "id": "count-sample-2.example.com", 48 | "attributes": { 49 | "id": "count-sample-2.example.com", 50 | "inventory_hostname": "count-sample-2.example.com" 51 | }, 52 | "meta": {}, 53 | "tainted": false 54 | }, 55 | "deposed": [], 56 | "provider": "provider.ansible" 57 | }, 58 | "ansible_host.count_sample.3": { 59 | "type": "ansible_host", 60 | "depends_on": [], 61 | "primary": { 62 | "id": "count-sample-3.example.com", 63 | "attributes": { 64 | "id": "count-sample-3.example.com", 65 | "inventory_hostname": "count-sample-3.example.com" 66 | }, 67 | "meta": {}, 68 | "tainted": false 69 | }, 70 | "deposed": [], 71 | "provider": "provider.ansible" 72 | }, 73 | "ansible_host.count_sample.4": { 74 | "type": "ansible_host", 75 | "depends_on": [], 76 | "primary": { 77 | "id": "count-sample-4.example.com", 78 | "attributes": { 79 | "id": "count-sample-4.example.com", 80 | "inventory_hostname": "count-sample-4.example.com" 81 | }, 82 | "meta": {}, 83 | "tainted": false 84 | }, 85 | "deposed": [], 86 | "provider": "provider.ansible" 87 | } 88 | }, 89 | "depends_on": [] 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /sample_data/0.11.x/basic/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "terraform_version": "0.11.3", 4 | "serial": 7, 5 | "lineage": "493f46cf-d49e-4e5e-a6eb-9d21744a27a6", 6 | "modules": [ 7 | { 8 | "path": [ 9 | "root" 10 | ], 11 | "outputs": {}, 12 | "resources": { 13 | "ansible_group.base": { 14 | "type": "ansible_group", 15 | "depends_on": [], 16 | "primary": { 17 | "id": "base", 18 | "attributes": { 19 | "id": "base", 20 | "inventory_group_name": "base" 21 | }, 22 | "meta": {}, 23 | "tainted": false 24 | }, 25 | "deposed": [], 26 | "provider": "provider.ansible" 27 | }, 28 | "ansible_group.web": { 29 | "type": "ansible_group", 30 | "depends_on": [], 31 | "primary": { 32 | "id": "web", 33 | "attributes": { 34 | "children.#": "3", 35 | "children.0": "foo", 36 | "children.1": "bar", 37 | "children.2": "baz", 38 | "id": "web", 39 | "inventory_group_name": "web", 40 | "vars.%": "2", 41 | "vars.bar": "2", 42 | "vars.foo": "bar" 43 | }, 44 | "meta": {}, 45 | "tainted": false 46 | }, 47 | "deposed": [], 48 | "provider": "provider.ansible" 49 | }, 50 | "ansible_host.base": { 51 | "type": "ansible_host", 52 | "depends_on": [], 53 | "primary": { 54 | "id": "base.example.com", 55 | "attributes": { 56 | "id": "base.example.com", 57 | "inventory_hostname": "base.example.com" 58 | }, 59 | "meta": {}, 60 | "tainted": false 61 | }, 62 | "deposed": [], 63 | "provider": "provider.ansible" 64 | }, 65 | "ansible_host.db": { 66 | "type": "ansible_host", 67 | "depends_on": [], 68 | "primary": { 69 | "id": "db.example.com", 70 | "attributes": { 71 | "groups.#": "2", 72 | "groups.0": "example", 73 | "groups.1": "db", 74 | "id": "db.example.com", 75 | "inventory_hostname": "db.example.com", 76 | "vars.%": "2", 77 | "vars.bar": "ddd", 78 | "vars.fooo": "ccc" 79 | }, 80 | "meta": {}, 81 | "tainted": false 82 | }, 83 | "deposed": [], 84 | "provider": "provider.ansible" 85 | }, 86 | "ansible_host.www": { 87 | "type": "ansible_host", 88 | "depends_on": [], 89 | "primary": { 90 | "id": "www.example.com", 91 | "attributes": { 92 | "groups.#": "2", 93 | "groups.0": "example", 94 | "groups.1": "web", 95 | "id": "www.example.com", 96 | "inventory_hostname": "www.example.com", 97 | "vars.%": "2", 98 | "vars.bar": "bbb", 99 | "vars.fooo": "aaa" 100 | }, 101 | "meta": {}, 102 | "tainted": false 103 | }, 104 | "deposed": [], 105 | "provider": "provider.ansible" 106 | } 107 | }, 108 | "depends_on": [] 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /sample_data/0.12.x/individual-vars/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 20, 5 | "lineage": "40cc4be1-f5ea-02d6-b06a-26813e05cd13", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "ansible_group", 11 | "name": "web", 12 | "provider": "provider.ansible", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "children": [ 18 | "foo", 19 | "bar", 20 | "baz" 21 | ], 22 | "id": "web", 23 | "inventory_group_name": "web", 24 | "variable_priority": 50, 25 | "vars": { 26 | "bar": "2", 27 | "foo": "bar" 28 | } 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | "mode": "managed", 35 | "type": "ansible_group_var", 36 | "name": "extra", 37 | "provider": "provider.ansible", 38 | "instances": [ 39 | { 40 | "schema_version": 0, 41 | "attributes": { 42 | "id": "db/ansible_user", 43 | "inventory_group_name": "db", 44 | "key": "ansible_user", 45 | "value": "postgres", 46 | "variable_priority": 60 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | "mode": "managed", 53 | "type": "ansible_group_var", 54 | "name": "override", 55 | "provider": "provider.ansible", 56 | "instances": [ 57 | { 58 | "schema_version": 0, 59 | "attributes": { 60 | "id": "web/foo", 61 | "inventory_group_name": "web", 62 | "key": "foo", 63 | "value": "fff", 64 | "variable_priority": 60 65 | } 66 | } 67 | ] 68 | }, 69 | { 70 | "mode": "managed", 71 | "type": "ansible_group_var", 72 | "name": "underride", 73 | "provider": "provider.ansible", 74 | "instances": [ 75 | { 76 | "schema_version": 0, 77 | "attributes": { 78 | "id": "web/bar", 79 | "inventory_group_name": "web", 80 | "key": "bar", 81 | "value": "hhh", 82 | "variable_priority": 10 83 | } 84 | } 85 | ] 86 | }, 87 | { 88 | "mode": "managed", 89 | "type": "ansible_host", 90 | "name": "db", 91 | "provider": "provider.ansible", 92 | "instances": [ 93 | { 94 | "schema_version": 0, 95 | "attributes": { 96 | "groups": [ 97 | "example", 98 | "db" 99 | ], 100 | "id": "db.example.com", 101 | "inventory_hostname": "db.example.com", 102 | "variable_priority": 50, 103 | "vars": { 104 | "bar": "ddd", 105 | "foo": "ccc" 106 | } 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | "mode": "managed", 113 | "type": "ansible_host", 114 | "name": "www", 115 | "provider": "provider.ansible", 116 | "instances": [ 117 | { 118 | "schema_version": 0, 119 | "attributes": { 120 | "groups": [ 121 | "example", 122 | "web" 123 | ], 124 | "id": "www.example.com", 125 | "inventory_hostname": "www.example.com", 126 | "variable_priority": 50, 127 | "vars": { 128 | "bar": "bbb", 129 | "foo": "aaa" 130 | } 131 | } 132 | } 133 | ] 134 | }, 135 | { 136 | "mode": "managed", 137 | "type": "ansible_host_var", 138 | "name": "extra", 139 | "provider": "provider.ansible", 140 | "instances": [ 141 | { 142 | "schema_version": 0, 143 | "attributes": { 144 | "id": "www.example.com/db_host", 145 | "inventory_hostname": "www.example.com", 146 | "key": "db_host", 147 | "value": "db.example.com", 148 | "variable_priority": 60 149 | }, 150 | "depends_on": [ 151 | "ansible_host.db" 152 | ] 153 | } 154 | ] 155 | }, 156 | { 157 | "mode": "managed", 158 | "type": "ansible_host_var", 159 | "name": "override", 160 | "provider": "provider.ansible", 161 | "instances": [ 162 | { 163 | "schema_version": 0, 164 | "attributes": { 165 | "id": "www.example.com/foo", 166 | "inventory_hostname": "www.example.com", 167 | "key": "foo", 168 | "value": "eee", 169 | "variable_priority": 60 170 | } 171 | } 172 | ] 173 | }, 174 | { 175 | "mode": "managed", 176 | "type": "ansible_host_var", 177 | "name": "underride", 178 | "provider": "provider.ansible", 179 | "instances": [ 180 | { 181 | "schema_version": 0, 182 | "attributes": { 183 | "id": "www.example.com/bar", 184 | "inventory_hostname": "www.example.com", 185 | "key": "bar", 186 | "value": "ggg", 187 | "variable_priority": 10 188 | } 189 | } 190 | ] 191 | } 192 | ] 193 | } 194 | -------------------------------------------------------------------------------- /sample_data/0.11.x/individual-vars/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "terraform_version": "0.11.14", 4 | "serial": 4, 5 | "lineage": "fd5627d8-41cc-2c58-f463-66453b5f2978", 6 | "modules": [ 7 | { 8 | "path": [ 9 | "root" 10 | ], 11 | "outputs": {}, 12 | "resources": { 13 | "ansible_group.web": { 14 | "type": "ansible_group", 15 | "depends_on": [], 16 | "primary": { 17 | "id": "web", 18 | "attributes": { 19 | "children.#": "3", 20 | "children.0": "foo", 21 | "children.1": "bar", 22 | "children.2": "baz", 23 | "id": "web", 24 | "inventory_group_name": "web", 25 | "variable_priority": "50", 26 | "vars.%": "2", 27 | "vars.bar": "2", 28 | "vars.foo": "bar" 29 | }, 30 | "meta": {}, 31 | "tainted": false 32 | }, 33 | "deposed": [], 34 | "provider": "provider.ansible" 35 | }, 36 | "ansible_group_var.extra": { 37 | "type": "ansible_group_var", 38 | "depends_on": [], 39 | "primary": { 40 | "id": "db/ansible_user", 41 | "attributes": { 42 | "id": "db/ansible_user", 43 | "inventory_group_name": "db", 44 | "key": "ansible_user", 45 | "value": "postgres", 46 | "variable_priority": "60" 47 | }, 48 | "meta": {}, 49 | "tainted": false 50 | }, 51 | "deposed": [], 52 | "provider": "provider.ansible" 53 | }, 54 | "ansible_group_var.override": { 55 | "type": "ansible_group_var", 56 | "depends_on": [], 57 | "primary": { 58 | "id": "web/foo", 59 | "attributes": { 60 | "id": "web/foo", 61 | "inventory_group_name": "web", 62 | "key": "foo", 63 | "value": "fff", 64 | "variable_priority": "60" 65 | }, 66 | "meta": {}, 67 | "tainted": false 68 | }, 69 | "deposed": [], 70 | "provider": "provider.ansible" 71 | }, 72 | "ansible_group_var.underride": { 73 | "type": "ansible_group_var", 74 | "depends_on": [], 75 | "primary": { 76 | "id": "web/bar", 77 | "attributes": { 78 | "id": "web/bar", 79 | "inventory_group_name": "web", 80 | "key": "bar", 81 | "value": "hhh", 82 | "variable_priority": "10" 83 | }, 84 | "meta": {}, 85 | "tainted": false 86 | }, 87 | "deposed": [], 88 | "provider": "provider.ansible" 89 | }, 90 | "ansible_host.db": { 91 | "type": "ansible_host", 92 | "depends_on": [], 93 | "primary": { 94 | "id": "db.example.com", 95 | "attributes": { 96 | "groups.#": "2", 97 | "groups.0": "example", 98 | "groups.1": "db", 99 | "id": "db.example.com", 100 | "inventory_hostname": "db.example.com", 101 | "variable_priority": "50", 102 | "vars.%": "2", 103 | "vars.bar": "ddd", 104 | "vars.foo": "ccc" 105 | }, 106 | "meta": {}, 107 | "tainted": false 108 | }, 109 | "deposed": [], 110 | "provider": "provider.ansible" 111 | }, 112 | "ansible_host.www": { 113 | "type": "ansible_host", 114 | "depends_on": [], 115 | "primary": { 116 | "id": "www.example.com", 117 | "attributes": { 118 | "groups.#": "2", 119 | "groups.0": "example", 120 | "groups.1": "web", 121 | "id": "www.example.com", 122 | "inventory_hostname": "www.example.com", 123 | "variable_priority": "50", 124 | "vars.%": "2", 125 | "vars.bar": "bbb", 126 | "vars.foo": "aaa" 127 | }, 128 | "meta": {}, 129 | "tainted": false 130 | }, 131 | "deposed": [], 132 | "provider": "provider.ansible" 133 | }, 134 | "ansible_host_var.extra": { 135 | "type": "ansible_host_var", 136 | "depends_on": [ 137 | "ansible_host.db" 138 | ], 139 | "primary": { 140 | "id": "www.example.com/db_host", 141 | "attributes": { 142 | "id": "www.example.com/db_host", 143 | "inventory_hostname": "www.example.com", 144 | "key": "db_host", 145 | "value": "db.example.com", 146 | "variable_priority": "60" 147 | }, 148 | "meta": {}, 149 | "tainted": false 150 | }, 151 | "deposed": [], 152 | "provider": "provider.ansible" 153 | }, 154 | "ansible_host_var.override": { 155 | "type": "ansible_host_var", 156 | "depends_on": [], 157 | "primary": { 158 | "id": "www.example.com/foo", 159 | "attributes": { 160 | "id": "www.example.com/foo", 161 | "inventory_hostname": "www.example.com", 162 | "key": "foo", 163 | "value": "eee", 164 | "variable_priority": "60" 165 | }, 166 | "meta": {}, 167 | "tainted": false 168 | }, 169 | "deposed": [], 170 | "provider": "provider.ansible" 171 | }, 172 | "ansible_host_var.underride": { 173 | "type": "ansible_host_var", 174 | "depends_on": [], 175 | "primary": { 176 | "id": "www.example.com/bar", 177 | "attributes": { 178 | "id": "www.example.com/bar", 179 | "inventory_hostname": "www.example.com", 180 | "key": "bar", 181 | "value": "ggg", 182 | "variable_priority": "10" 183 | }, 184 | "meta": {}, 185 | "tainted": false 186 | }, 187 | "deposed": [], 188 | "provider": "provider.ansible" 189 | } 190 | }, 191 | "depends_on": [] 192 | } 193 | ] 194 | } 195 | -------------------------------------------------------------------------------- /terraform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Terraform Inventory Script 5 | ========================== 6 | This inventory script generates dynamic inventory by reading Terraform state 7 | contents. Servers and groups a defined inside the Terraform state using special 8 | resources defined by the Terraform Provider for Ansible. 9 | 10 | Configuration 11 | ============= 12 | 13 | State is fetched using the "terraform state pull" subcommand. The behaviour of 14 | this action can be configured using some environment variables. 15 | 16 | Environment Variables: 17 | ...................... 18 | 19 | ANSIBLE_TF_BIN 20 | Override the path to the Terraform command executable. This is useful if 21 | you have multiple copies or versions installed and need to specify a 22 | specific binary. The inventory script runs the `terraform state pull` 23 | command to fetch the Terraform state, so that remote state will be 24 | fetched seemlessly regardless of the backend configuration. 25 | 26 | ANSIBLE_TF_DIR 27 | Set the working directory for the `terraform` command when the scripts 28 | shells out to it. This is useful if you keep your terraform and ansible 29 | configuration in separate directories. Defaults to using the current 30 | working directory. 31 | 32 | ANSIBLE_TF_WS_NAME 33 | Sets the workspace for the `terraform` command when the scripts shells 34 | out to it, defaults to `default` workspace - if you don't use workspaces 35 | this is the one you'll be using. 36 | ''' 37 | 38 | import sys 39 | import json 40 | import os 41 | import re 42 | import traceback 43 | from subprocess import Popen, PIPE 44 | 45 | TERRAFORM_PATH = os.environ.get('ANSIBLE_TF_BIN', 'terraform') 46 | TERRAFORM_DIR = os.environ.get('ANSIBLE_TF_DIR', os.getcwd()) 47 | TERRAFORM_WS_NAME = os.environ.get('ANSIBLE_TF_WS_NAME', 'default') 48 | 49 | 50 | class TerraformState(object): 51 | ''' 52 | TerraformState wraps the state content to provide some helpers for iterating 53 | over resources. 54 | ''' 55 | 56 | def __init__(self, state_json): 57 | self.state_json = state_json 58 | 59 | if "modules" in state_json: 60 | # uses pre-0.12 61 | self.flat_attrs = True 62 | else: 63 | # state format for 0.12+ 64 | self.flat_attrs = False 65 | 66 | def resources(self): 67 | '''Generator method to iterate over resources in the state file.''' 68 | if self.flat_attrs: 69 | modules = self.state_json["modules"] 70 | for module in modules: 71 | for resource in module["resources"].values(): 72 | yield TerraformResource(resource, flat_attrs=True) 73 | else: 74 | resources = self.state_json["resources"] 75 | for resource in resources: 76 | for instance in resource["instances"]: 77 | yield TerraformResource(instance, resource_type=resource["type"]) 78 | 79 | 80 | class TerraformResource(object): 81 | ''' 82 | TerraformResource wraps individual resource content and provide some helper 83 | methods for reading older-style dictionary and list values from attributes 84 | defined as a single-level map. 85 | ''' 86 | DEFAULT_PRIORITIES = { 87 | 'ansible_host': 50, 88 | 'ansible_group': 50, 89 | 'ansible_host_var': 60, 90 | 'ansible_group_var': 60 91 | } 92 | 93 | def __init__(self, source_json, flat_attrs=False, resource_type=None): 94 | self.flat_attrs = flat_attrs 95 | self._type = resource_type 96 | self._priority = None 97 | self.source_json = source_json 98 | 99 | def is_ansible(self): 100 | '''Check if the resource is provided by the ansible provider.''' 101 | return self.type().startswith("ansible_") 102 | 103 | def priority(self): 104 | '''Get the merge priority of the resource.''' 105 | if self._priority is not None: 106 | return self._priority 107 | 108 | priority = 0 109 | 110 | if self.read_int_attr("variable_priority") is not None: 111 | priority = self.read_int_attr("variable_priority") 112 | elif self.type() in TerraformResource.DEFAULT_PRIORITIES: 113 | priority = TerraformResource.DEFAULT_PRIORITIES[self.type()] 114 | 115 | self._priority = priority 116 | 117 | return self._priority 118 | 119 | def type(self): 120 | '''Returns the Terraform resource type identifier.''' 121 | if self._type: 122 | return self._type 123 | return self.source_json["type"] 124 | 125 | def read_dict_attr(self, key): 126 | ''' 127 | Read a dictionary attribute from the resource, handling old-style 128 | Terraform state where maps are stored as multiple keys in the resource's 129 | attributes. 130 | ''' 131 | attrs = self._raw_attributes() 132 | 133 | if self.flat_attrs: 134 | out = {} 135 | for k in attrs.keys(): 136 | match = re.match(r"^" + key + r"\.(.*)", k) 137 | if not match or match.group(1) == "%": 138 | continue 139 | 140 | out[match.group(1)] = attrs[k] 141 | return out 142 | return attrs.get(key, {}) 143 | 144 | def read_list_attr(self, key): 145 | ''' 146 | Read a list attribute from the resource, handling old-style Terraform 147 | state where lists are stored as multiple keys in the resource's 148 | attributes. 149 | ''' 150 | attrs = self._raw_attributes() 151 | 152 | if self.flat_attrs: 153 | out = [] 154 | 155 | length_key = key + ".#" 156 | if length_key not in attrs.keys(): 157 | return [] 158 | 159 | length = int(attrs[length_key]) 160 | if length < 1: 161 | return [] 162 | 163 | for i in range(0, length): 164 | out.append(attrs["{}.{}".format(key, i)]) 165 | 166 | return out 167 | return attrs.get(key, None) 168 | 169 | def read_int_attr(self, key): 170 | ''' 171 | Read an attribute from state an convert it to type Int. 172 | ''' 173 | val = self.read_attr(key) 174 | 175 | if val is not None: 176 | val = int(val) 177 | 178 | return val 179 | 180 | def read_attr(self, key): 181 | ''' 182 | Read an attribute from the underlaying state content. 183 | ''' 184 | return self._raw_attributes().get(key, None) 185 | 186 | def _raw_attributes(self): 187 | if self.flat_attrs: 188 | return self.source_json["primary"]["attributes"] 189 | return self.source_json["attributes"] 190 | 191 | 192 | class AnsibleInventory(object): 193 | ''' 194 | AnsibleInventory handles conversion from Terraform resource content to 195 | Ansible inventory entities, and building of the final inventory json. 196 | ''' 197 | 198 | def __init__(self): 199 | self.groups = {} 200 | self.hosts = {} 201 | self.inner_json = {} 202 | 203 | def add_host_resource(self, resource): 204 | '''Upsert type action for host resources.''' 205 | hostname = resource.read_attr("inventory_hostname") 206 | 207 | if hostname in self.hosts: 208 | host = self.hosts[hostname] 209 | host.add_source(resource) 210 | else: 211 | host = AnsibleHost(hostname, source=resource) 212 | self.hosts[hostname] = host 213 | 214 | def add_group_resource(self, resource): 215 | '''Upsert type action for group resources.''' 216 | groupname = resource.read_attr("inventory_group_name") 217 | 218 | if groupname in self.groups: 219 | group = self.groups[groupname] 220 | group.add_source(resource) 221 | else: 222 | group = AnsibleGroup(groupname, source=resource) 223 | self.groups[groupname] = group 224 | 225 | def update_groups(self, groupname, children=None, hosts=None, group_vars=None): 226 | '''Upsert type action for group resources''' 227 | if groupname in self.groups: 228 | group = self.groups[groupname] 229 | group.update(children=children, hosts=hosts, group_vars=group_vars) 230 | else: 231 | group = AnsibleGroup(groupname) 232 | group.update(children, hosts, group_vars) 233 | self.groups[groupname] = group 234 | 235 | def add_resource(self, resource): 236 | ''' 237 | Process a Terraform resource, passing to the correct handler function 238 | by type. 239 | ''' 240 | if resource.type().startswith("ansible_host"): 241 | self.add_host_resource(resource) 242 | elif resource.type().startswith("ansible_group"): 243 | self.add_group_resource(resource) 244 | 245 | def to_dict(self): 246 | ''' 247 | Generate the file Ansible inventory structure to be serialized into JSON 248 | for consumption by Ansible proper. 249 | ''' 250 | out = { 251 | "_meta": { 252 | "hostvars": {} 253 | } 254 | } 255 | 256 | for hostname, host in self.hosts.items(): 257 | host.build() 258 | for group in host.groups: 259 | self.update_groups(group, hosts=[host.hostname]) 260 | out["_meta"]["hostvars"][hostname] = host.get_vars() 261 | 262 | for groupname, group in self.groups.items(): 263 | group.build() 264 | out[groupname] = group.to_dict() 265 | 266 | return out 267 | 268 | 269 | class AnsibleHost(object): 270 | ''' 271 | AnsibleHost represents a host for the Ansible inventory. 272 | ''' 273 | 274 | def __init__(self, hostname, source=None): 275 | self.sources = [] 276 | self.hostname = hostname 277 | self.groups = set(["all"]) 278 | self.host_vars = {} 279 | 280 | if source: 281 | self.add_source(source) 282 | 283 | def update(self, groups=None, host_vars=None): 284 | '''Update host resource with additional groups and vars.''' 285 | if host_vars: 286 | self.host_vars.update(host_vars) 287 | if groups: 288 | self.groups.update(groups) 289 | 290 | def add_source(self, source): 291 | '''Add a Terraform resource to the sources list.''' 292 | self.sources.append(source) 293 | 294 | def build(self): 295 | '''Assemble host details from registered sources.''' 296 | self.sources.sort(key=lambda source: source.priority()) 297 | for source in self.sources: 298 | if source.type() == "ansible_host": 299 | groups = source.read_list_attr("groups") 300 | host_vars = source.read_dict_attr("vars") 301 | 302 | self.update(groups=groups, host_vars=host_vars) 303 | elif source.type() == "ansible_host_var": 304 | host_vars = {source.read_attr( 305 | "key"): source.read_attr("value")} 306 | 307 | self.update(host_vars=host_vars) 308 | self.groups = sorted(self.groups) 309 | 310 | def get_vars(self): 311 | '''Get the host's variable dictionary.''' 312 | return dict(self.host_vars) 313 | 314 | 315 | class AnsibleGroup(object): 316 | ''' 317 | AnsibleGroup represents a group for the Ansible inventory. 318 | ''' 319 | 320 | def __init__(self, groupname, source=None): 321 | self.groupname = groupname 322 | self.sources = [] 323 | self.hosts = set() 324 | self.children = set() 325 | self.group_vars = {} 326 | 327 | if source: 328 | self.add_source(source) 329 | 330 | def update(self, children=None, hosts=None, group_vars=None): 331 | ''' 332 | Update host resource with additional children, hosts, or group variables. 333 | ''' 334 | if hosts: 335 | self.hosts.update(hosts) 336 | if children: 337 | self.children.update(children) 338 | if group_vars: 339 | self.group_vars.update(group_vars) 340 | 341 | def add_source(self, source): 342 | '''Add a Terraform resource to the sources list.''' 343 | self.sources.append(source) 344 | 345 | def build(self): 346 | '''Assemble group details from registered sources.''' 347 | self.sources.sort(key=lambda source: source.priority()) 348 | for source in self.sources: 349 | if source.type() == "ansible_group": 350 | children = source.read_list_attr("children") 351 | group_vars = source.read_dict_attr("vars") 352 | 353 | self.update(children=children, group_vars=group_vars) 354 | elif source.type() == "ansible_group_var": 355 | group_vars = {source.read_attr( 356 | "key"): source.read_attr("value")} 357 | 358 | self.update(group_vars=group_vars) 359 | 360 | self.hosts = sorted(self.hosts) 361 | self.children = sorted(self.children) 362 | 363 | def to_dict(self): 364 | '''Prepare structure for final Ansible inventory JSON.''' 365 | return { 366 | "children": list(self.children), 367 | "hosts": list(self.hosts), 368 | "vars": dict(self.group_vars) 369 | } 370 | 371 | 372 | def _execute_shell(): 373 | encoding = 'utf-8' 374 | tf_workspace = [TERRAFORM_PATH, 'workspace', 'select', TERRAFORM_WS_NAME] 375 | proc_ws = Popen(tf_workspace, cwd=TERRAFORM_DIR, stdout=PIPE, 376 | stderr=PIPE, universal_newlines=True) 377 | _, err_ws = proc_ws.communicate() 378 | if err_ws != '': 379 | sys.stderr.write(str(err_ws)+'\n') 380 | sys.exit(1) 381 | else: 382 | tf_command = [TERRAFORM_PATH, 'state', 'pull'] 383 | proc_tf_cmd = Popen(tf_command, cwd=TERRAFORM_DIR, 384 | stdout=PIPE, stderr=PIPE, universal_newlines=True) 385 | out_cmd, err_cmd = proc_tf_cmd.communicate() 386 | if err_cmd != '': 387 | sys.stderr.write(str(err_cmd)+'\n') 388 | sys.exit(1) 389 | else: 390 | return json.loads(out_cmd, encoding=encoding) 391 | 392 | 393 | def _main(): 394 | try: 395 | tfstate = TerraformState(_execute_shell()) 396 | inventory = AnsibleInventory() 397 | 398 | for resource in tfstate.resources(): 399 | if resource.is_ansible(): 400 | inventory.add_resource(resource) 401 | 402 | sys.stdout.write(json.dumps(inventory.to_dict(), indent=2)) 403 | except Exception: 404 | traceback.print_exc(file=sys.stderr) 405 | sys.exit(1) 406 | 407 | 408 | if __name__ == '__main__': 409 | _main() 410 | --------------------------------------------------------------------------------