terraform icon indicating copy to clipboard operation
terraform copied to clipboard

Reusable configuration blocks, ie. provisioner connection

Open hydroxide opened this issue 9 years ago • 27 comments

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.

hydroxide avatar Sep 01 '16 23:09 hydroxide

@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

mengesb avatar Sep 02 '16 20:09 mengesb

@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.

hydroxide avatar Sep 06 '16 20:09 hydroxide

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.

kasperisager avatar Sep 07 '16 07:09 kasperisager

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.

guydavis avatar Mar 11 '17 00:03 guydavis

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

mengesb avatar Apr 20 '17 04:04 mengesb

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 :(

koenighotze avatar Jul 01 '17 06:07 koenighotze

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

xtimon avatar Sep 14 '17 15:09 xtimon

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"
    )
  )}"

lautarodragan avatar Jan 23 '18 19:01 lautarodragan

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)

gavvvr avatar Mar 18 '18 14:03 gavvvr

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.

harmanbirdi avatar Mar 21 '18 19:03 harmanbirdi

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

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

rrevol avatar May 28 '18 07:05 rrevol

I can't see how. Example?

g1ps avatar Jul 23 '18 10:07 g1ps

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

mengesb avatar Dec 05 '18 19:12 mengesb

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

yves-vogl avatar Dec 21 '18 10:12 yves-vogl

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"]}"
   }
  }
}

isaachui avatar Jan 07 '19 08:01 isaachui

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}"
   }
  }
}

remoe avatar Jan 26 '19 22:01 remoe

@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"] : ""}"

mengesb avatar Feb 01 '19 10:02 mengesb

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.

davidjeddy avatar Aug 21 '19 17:08 davidjeddy

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?

f91og avatar Dec 26 '19 06:12 f91og

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.

jcogilvie avatar Sep 23 '20 16:09 jcogilvie

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

JCMais avatar Aug 01 '21 15:08 JCMais

bump

Ghost---Shadow avatar Sep 22 '21 06:09 Ghost---Shadow

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.

voltechs avatar Jan 06 '22 19:01 voltechs

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"]
    }
  }
}

mengesb avatar May 16 '22 02:05 mengesb

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.'

aschleifer avatar Mar 20 '23 18:03 aschleifer

Hello,

No news on this ticket?? This is crazy we still can not share "block" between resources! In our case, we need to create "databricks_job" resources (databricks provider). The module approach is not pertinent in our case since our jobs are quite different. The only thing in common, is the block "job_cluster" that in our case would be 50lines long..

loic-git avatar Oct 23 '24 15:10 loic-git

Hello,

No news on this ticket??

This is crazy we still can not share "block" between resources!

In our case, we need to create "databricks_job" resources (databricks provider).

The module approach is not pertinent in our case since our jobs are quite different.

The only thing in common, is the block "job_cluster" that in our case would be 50lines long..

I have exactly the same issue. Unfortunately I think it's basically bad design on the terraform provider since this block sharing is not possible. That being said, it's surprising this isn't possible given that dynamic blocks is a thing.

hejfelix avatar Feb 05 '25 17:02 hejfelix