hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.47k stars 9.51k forks source link

Reusable configuration blocks, ie. provisioner connection #8616

Open hydroxide opened 8 years ago

hydroxide commented 8 years ago

Currently, it is not possible to reuse blocks of configuration information, which gives way to large amounts of repetition.

As an example, I have several differing EC2 instances with remote-exec provisioners. The connection information is the same between them (bastions, private keys, etc). Is there anyway to reuse this information?

This would be solved with a global connection block, but it crops up again in other places, ie. identical ingress and egress rules for security groups.

mengesb commented 8 years ago

@hydroxide could you not make a module out of this?

https://www.terraform.io/intro/getting-started/modules.html https://www.terraform.io/docs/modules/usage.html

hydroxide commented 8 years ago

@mengesb Modules don't solve this problem particularly well. The instances share a connection block, but the other arguments are all unique and must be parametrized. This leads to more excess than it removes.

Additionally, this problem crops up in other places, such as security groups (mentioned above) which may share some set of ingress and egress rules but differ in others. Modules don't solve that problem at all.

I am looking for something akin to storing a block of configuration in a variable and using it where needed.

kasperisager commented 8 years ago

I'm looking for this as well. My use case is a provisioner block that is used in several different resources for setting up some base configuration (made up VPN example):

resource "foo" "master" {
  provisioner "file" {
    content = <<EOF
Address = ${self.ipv4_address}
Connect = ${foo.master.0.ipv4_address}
EOF
    destination = "/etc/vpn.d/conf"
  }
}

resource "foo" "minion" {
  provisioner "file" {
    content = <<EOF
Address = ${self.ipv4_address}
Connect = ${foo.master.0.ipv4_address}
EOF
    destination = "/etc/vpn.d/conf"
  }
}

Using a template resource would make this somewhat nicer if not for #2167.

guydavis commented 7 years ago

Just wondering if there was any solution found for this need for a global SSH connection block. Currently, we have about 6 different resources, each with a duplicated connection block.

Sometimes our Terraform users are outside the VPC and need to SSH connect to self.public_ip, sometimes they are inside the VPC and need to connect to self.private_ip. Currently, we are search/replacing the main.tf in multiple places for this public vs. private issue.

Is there anyway to specify the SSH connection block once to remove the need for this search and replace (or writing a Terraform pre-processor)? Any tips would be most appreciated.

mengesb commented 7 years ago

@guydavis You can likely use inline ternary operators to swap on a var between public/private ips

koenighotze commented 7 years ago

I totally agree with @hydroxide. Whenever we try to introduce Terraform at our projects, this is one of the major issues we have to fight with and which ultimately leads to using other tools :(

xtimon commented 7 years ago

Try to use locals: https://www.terraform.io/docs/configuration/locals.html

lautarodragan commented 6 years ago

Locals does solve my issue (the example they provide with tags was perfect).

It'd be nice if there was a less verbose way to do the merging, though.

Something like this (using JS spread operator):

    tags {
      ...local.common_tags
      Name = "awesome-app-server"
      Role = "server"
    }

Instead of:

  tags = "${merge(
    local.common_tags,
    map(
      "Name", "awesome-app-server",
      "Role", "server"
    )
  )}"
gavvvr commented 6 years ago

Still do not understand how can I use locals for connection.

Tried:

locals {
  connect = {
    type = "ssh"
    user = "user"
    password = "${var.admin_password["plain"]}"
    bastion_host = "${var.bastion_host}"
    bastion_user = "bastionuser"
    bastion_password = "${var.admin_password["plain"]}"
  }
}

resource "null_resource" "worker_provisioner" {
  count = "${var.workers_number}"

  connection = "${merge(local.connect, map(
  "host", "${openstack_networking_port_v2.private_network_worker_port.*.fixed_ip.0.ip_address[count.index]}"
  ))}"
}

And got an error:

Error reading connection info for null_resource[worker_provisioner]: At 86:16: root: not an object type for map (*ast.LiteralType)

harmanbirdi commented 6 years ago

I am just in the process of introducing terraform at my company, and am facing the same issue. It would be nice to have a global connection block, which if needed, can be over-ridden in the provisioner. If, however, a provisioner does not specify a connection for file or remote-exec, then it can check and use the global connection block, or throw an error if one is not configured.

Right now, I find that I am duplicating connection blocks. Will look into @mengesb's suggestion of using a module, but IMO it would be best if this can be provided out-of-the-box.

rrevol commented 6 years ago

@harmanbirdi Isn't null_resource providing what you're looking for ?

https://www.terraform.io/docs/provisioners/null_resource.html

g1ps commented 6 years ago

I can't see how. Example?

mengesb commented 5 years ago

@hydroxide -- with the pending v0.12 release does this look to be solved there?

yves-vogl commented 5 years ago

I'm interested in a solution, too. Using locals seem not work:

locals {
  connection = {
    type     = "winrm"
    user     = "Administrator"
    password = "${aws_secretsmanager_secret_version.example.secret_string}"
    timeout  = "10m"
  }
}

resource "aws_instance" "example" {
  connection              = "${local.connection}"
}

Same error like @gavvvr

isaachui commented 5 years ago

I was hoping to accomplish something similar to this to avoid retyping the same thing over and over, but alas:

This is addressed here https://github.com/hashicorp/terraform/issues/17402

The best way I see this happening would be

locals {
  connection = {
    host        = "${module.test.public_ip}"
    user        = "${var.user}"
    private_key = "${file("${var.private_key_location}")}"
  }
}

resource "null_resource" "example" {
  provisioner "file" {
  # [ ... ] 
    connection {
      host        = "${local.connection["host"]}"
      user        = "${local.connection["user"]}"
      private_key = "${local.connection["private_key"]}"
   }
  }
}
remoe commented 5 years ago

Does anyone know a workaround to create a conditional connection without duplicating code?:

resource "null_resource" "example" {
  provisioner "file" {
    connection {
      host        = "${local.connection["host"]}"
      user        = "${local.connection["user"]}"
      private_key = "${local.connection["private_key"]}"

      // Want to add the following only when a condition is true
      bastion_host        = "example.local"
      bastion_user        = "core"
      bastion_private_key = "${var.ssh_private_key}"
   }
  }
}
mengesb commented 5 years ago

@remoe you should be able to use inline ternaries

bastion_host = "${var.is_bastion ? var.bastion["host"] : ""}"

or

bastion_host = "${length(var.bastion["host"]) > 0 ? var.bastion["host"] : ""}"
davidjeddy commented 5 years ago

Ran into this use case today as well. Looking at the syntax used to define connection indicated it is not an assignable property; similar to inline or file. IE there is not assignment operator, =, between connection and the desired configuration. As such assigning locals or null_resource will not work.

Sadly I am not that familiar with Golang so my search through the code base was fruitless. My guess is that the solution will involved converting the data type of the connection logic from it's current to an assignable type.

f91og commented 4 years ago

I can use local to pass argument, but cannot pass an reference to a block, which can greatly help me to reduce code redundancy。The error always be 'Unsupported argument', seems that it is not possible to assign one reference variable to a block though '='. The terraform dynamic block seems only be able to use referenced inside a block. is there a better solution?

jcogilvie commented 4 years ago

Adding details to @f91og's comment, I'm running into that exact issue with condition blocks on AWS IAM policies.

One can expect in the context of a large set of IAM policies that common conditions can and do arise. Things like, "this access is only granted in the context of a certain VPC" or "this resource can only be created if the creator provides values for a certain required tag(s)".

A simple piece of syntactic sugar that would allow us to express repeatable blocks as lists would solve this nicely by naturally allowing for a simple amount of indirection:

For anything that supports

condition {
   ...
}
condition {
   ...
}

a natural extension would be

conditions = [ 
   { ... },
   { ... }
]

With this simple improvement I could reference and reuse local variables for the contents of conditions, like so:

locals {
  conditions = {
    has_created_by_tag = {
      test = "StringEquals"
      values = ["created_by"]
      variable = "aws:TagKeys"
    }
    is_in_target_vpc = {
      test = "StringEquals"
      values = [data.aws_vpc.target_vpc.arn]
      variable = "ec2:vpc"
    }
  }
}

And reference them later:

  statement {
    sid = "..."
    effect = "..."
    actions = [
        ...
    ]
    resources = [...]

    # note that the module in question presently takes 0..n condition{ } blocks.
    conditions = [
      local.conditions.is_in_target_vpc,
      local.has_created_by_tag 
    ]

  }

This would save me a bunch of code and allow me to stop repeating myself, which would in turn allow fewer opportunities for copy/paste errors and a central place to view and manage my conditions should they change.

N.B.: Dynamic blocks are way overkill for this use case, and actually increase the amount of code and repetition with respect to baseline.

JCMais commented 3 years ago

Did anyone find a better solution to handle this? Or do we still need to duplicate the blocks?

Ghost---Shadow commented 3 years ago

bump

voltechs commented 2 years ago

Seems like it would be a relatively straightforward feature to implement if (and I haven't seen the codebase) Terraform is built with any semblance of reusability and common-sense software development patterns (which I believe it is). It's already got reusable resources, they'd "just" need to implement connection as it's own first class citizen that could be passed in something like this.

resource "connection" "ec2" {
  type        = "ssh"
  private_key = var.private_key
  host = aws_instance.ec2.public_ip
  user = "ec2-user"
}

# This...
resource "provisioner" "setup-packages" {
  triggers = { }
  connection = connection.ec2
  provisioner "remote-exec" {
    inline = []
  }
}

# Instead of...
resource "provisioner" "setup-packages" {
  triggers = { }
  connection = {
    type        = "ssh"
    private_key = var.private_key
    host = aws_instance.ec2.public_ip
    user = "ec2-user"
  }
  provisioner "remote-exec" {
    inline = []
  }
}

note: just boilerplate—I'm not condoning anything other than the structure here.

mengesb commented 2 years ago

I think that manipulating connection to be of a similar class as a resource or data_source is interesting, however that might break the Terraform dog walk of the DAG... at least that'd be my speculation. This is an inventive and interesting way to solve for the problem though.

If connection blocks were assignable, then we could do something like connection = map() or connection = object({}) ... or perhaps we need a new function such as block() which would allow it to be generated such as connection = block(var.connection). This however is fundamentally flawed, because then it wouldn't be a block anymore - it would be an attribute, so my own suggestion is self defeating.

Presently, I think the only DIY way would be to use dynamic syntax. While designed for supplying multiples of a block of syntax, it can be used to supply 0 or greater blocks, and thus works for connection as well.

variable "connection" {
  type = map(string)
  description = "default connection block"
  sensitive = true
  default = {
    type = "ssh"
    user = "root"
    password = "default_super_secret_password"
    host = "192.168.1.2"
  }
}

# Copies the file as the root user using SSH
provisioner "file" {
  source      = "conf/myapp.conf"
  destination = "/etc/myapp.conf"

  dynamic "connection" {
    for_each = var.connection

    content {
      type     = connection.value["type"]
      user     = connection.value["user"]
      password = connection.value["password"]
      host     = connection.value["host"]
    }
  }
}
aschleifer commented 1 year ago

Is this last example suppose to work? I'm trying to write a null_resource that uses a local-exec provisioner to execute an ansible-playbook, but I would like to use the connect block to verify the hosts are up.

But when I try to use a dynamic "connection" block in the null_resource I get the error that 'Blocks of type "connection" are not expected here.'