ansible / terraform-provider-ansible

community terraform provider for ansible
https://registry.terraform.io/providers/ansible/ansible/latest
GNU General Public License v3.0
183 stars 42 forks source link

ansible_host: add to inventory without using private key files #34

Open sean-freeman opened 1 year ago

sean-freeman commented 1 year ago

ansible_host: add to inventory without using private key files

TL;DR = The Terraform Provider for Ansible, is not using similar developer best practice measures as available with Terraform Provisioners. See SSH Connection documentation > https://developer.hashicorp.com/terraform/language/resources/provisioners/connection


Problem

Using this Terraform Provider for Ansible, still requires an SSH Private Key on the Terraform execution host's filesystem (e.g. in /tmp); the private key contents should be ephemeral to Terraform / in-memory during the Terraform execution for security purposes.

When defining the ansible_host, it should have the correct args to generate an Ansible Inventory - similar to ansible.builtin.add_host module

See code snippets below for:


Standard Ansible Inventory file

---

group1:
  hosts:
    host1:
      ansible_host: IP_GOES_HERE
      ansible_port: 22
      ansible_user: root
      ansible_ssh_private_key_file: "{{ playbook_dir }}/ssh/hosts_rsa"
      ansible_ssh_common_args: -o ConnectTimeout=180 -o ControlMaster=auto -o ControlPersist=3600s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ForwardX11=no -o ProxyCommand='ssh -W %h:%p bastionuser@IP_GOES_HERE -p 50222 -i {{ playbook_dir }}/ssh/bastion_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'

Sample file of Problem


### Create SSH Keys, load to AWS

# Create private SSH key only for Bastion/Jump Host usage
resource "tls_private_key" "bastion_ssh" {
  algorithm = "RSA"
}

# Create private SSH key for ssh connection of the host
resource "tls_private_key" "host_ssh" {
  algorithm = "RSA"
}

resource "aws_key_pair" "bastion_ssh" {
  key_name   = "test-ssh-keypair-bastion"
  public_key = trimspace(tls_private_key.bastion_ssh.public_key_openssh)
}

resource "aws_key_pair" "host_ssh" {
  key_name   = "test-ssh-keypair-host"
  public_key = trimspace(tls_private_key.host_ssh.public_key_openssh)
}

### Create AWS EC2 virtual server, use AWS Key Pair containing the SSH Public Key

resource "aws_instance" "bastion" {
# add many args here
}

resource "aws_instance" "host" {
# add many args here
}

### Download Ansible Playbook to Terraform execution directory

resource "null_resource" "ansible_playbooks_repo_download" {
  provisioner "local-exec" {
    command = <<EOT
    curl -L https://github.com/gh-org/repo/archive/refs/heads/main.tar.gz -o ${path.root}/tmp/ansible_playbooks_repo-main.tar.gz
    mkdir -p ${path.root}/tmp/ansible_playbooks_repo
    tar xvf ${path.root}/tmp/ansible_playbooks_repo-main.tar.gz --strip-components=1 -C ${path.root}/tmp/ansible_playbooks_repo
    EOT
  }
}

### Create Ansible Inventory

resource "ansible_group" "group1" {
  name     = "group1"
}

resource "ansible_host" "host" {
  name      = var.hostname
  groups    = ["group1"]

  variables = {
    ansible_user                 = "root"
    ansible_host                 = aws_instance.host.private_ip
    ansible_ssh_private_key_file = var.ssh_host_private_key_path
    ansible_python_interpreter   = "/usr/bin/python3"
    ansible_ssh_common_args      =  yamldecode(<<-EOT
                                      string_input: >-
                                      -o ConnectTimeout=180 -o ControlMaster=auto
                                      -o ControlPersist=3600s -o StrictHostKeyChecking=no
                                      -o UserKnownHostsFile=/dev/null -o ForwardX11=no
                                      -o ProxyCommand='ssh -W %h:%p ${var.bastion_os_user}@${var.bastion_public_ip}
                                      -p ${var.bastion_port} -i ${var.ssh_bastion_private_key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
                                    EOT
                                    ).string_input
  }
}

### Run Ansible Playbook using the created Ansible Inventory

resource "ansible_playbook" "playbook" {
  playbook   = "${path.root}/tmp/ansible_playbooks_repo/playbook.yml"
  name       = "localhost"
  replayable = false

  extra_vars = {
    ansible_connection = "local"
    var_a              = "Some variable"
    var_b              = "Another variable"
  }
}

Suggestion for Terraform Resource structure of ansible_host

resource "ansible_host" "host" {
  name                = var.hostname
  inventory_groups    = ["group1"]

  # Follow syntax of Terraform Prosivioners, generate the ansible_ssh_common_args
  connection {
    user                = "root"                                       ## ansible_user
    host                = aws_instance.host.private_ip                 ## ansible_host
    private_key         = tls_private_key.host_ssh.private_key_pem     ## ansible_ssh_private_key_file
    bastion_host        = aws_instance.bastion.public_ip               ## generate string for ansible_ssh_common_args
    bastion_port        = 22                                           ## generate string for ansible_ssh_common_args
    bastion_user        = "ec2-user"                                   ## generate string for ansible_ssh_common_args
    bastion_private_key = tls_private_key.bastion_ssh.private_key_pem  ## generate string for ansible_ssh_common_args
  }
}
anazobec commented 1 year ago

Hi, in Ansible it is possible to define almost any Ansible setting using (magic) variables, which is why we chose to use this 1-to-1 mapping implementation and why variables (in ansible_host and ansible_group resources) is named variables, since you don't necessarily have to pass in/define only Ansible connection settings. With this explanation aside, it wouldn't be possible to rename variables to connection since the word connection is a reserved word used by Terraform provisioners and ansible_host and ansible_group are not provisioners but resources. At most, we could rename it to something like connection_settings or connection_setup instead.

sean-freeman commented 1 year ago

Hi @anazobec

I understand the principle behind variables implementation for both Terraform Resources ansible_host and ansible_group. This is a flexible approach that allows the 1-to-1 mapping, that makes sense.

However, I would encourage further design research for _(connection_settings or connection_setup as you suggested)_ dynamically generating the Ansible connection settings.

To be verbose about the intent behind this GH Issue....

Terraform Resource "null_resource" with provisioner "remote-exec".... connection:

  • Target Host
  • OS User
  • IP
  • Private Key PEM as string... [NOT using Private Key PEM file path as string]
  • Bastion Host
  • OS User
  • IP
  • Private Key PEM as string... [NOT using Private Key PEM file path as string]

Terraform Resource ansible_host or ansible_group.... variables:

  • Target Host
  • OS User
  • IP
  • Private Key file path as string [UNSECURE, requires PEM file on Terraform execution host]
  • Bastion Host
  • OS User
  • IP
  • Private Key file path as string [UNSECURE, requires PEM file on Terraform execution host]

The private key contents should be ephemeral / in-memory during the Terraform execution for security purposes. The user should not have to output the SSH Private Key PEM file contents to the Terraform execution host, before any Ansible Inventory can be built for an Ansible Playbook execution.

sean-freeman commented 11 months ago

@anazobec any further update please?

65156 commented 5 months ago

@tima seems like a critical functional hole with security implications, is anybody looking at this?

sean-freeman commented 5 months ago

@65156 It has been ignored, and it breaks all potential usage of this Terraform Provider with any 'runner' implementations (e.g. Terraform Cloud/Enterprise, Spacelift, Env0, Azure DevOps Pipelines, IBM Cloud Schematics etc etc) for calling Terraform > Ansible. Only @mandar242 is committing code for the past 12 months.

jillr commented 5 months ago

Hi @sean-freeman . Thank you for the detailed information about the use case in this ticket, the examples are super helpful. You're right, running this provider in another pipeline is not a deployment scenario that is currently optimized for in this provider.

We are continuing to expand our support of Terraform based workflows with Ansible, so while contributions in this particular repo have been slower in the last few months we are still investing in this area overall. @tima is at configmgmt camp this week (where he presented on orchestrating Terraform with Ansible earlier today https://cfp.cfgmgmtcamp.org/2024/talk/KQHVVK/), I'd like him to chime in on this one when he gets back.