Terraform: Pass blocks to modules

Created on 2 Oct 2015  ยท  20Comments  ยท  Source: hashicorp/terraform

It might be great if modules could accept blocks. For example the AWS ELB resource accepts a listener block e.g.:

resource "aws_elb" "service" {
  listener = {
    lb_protocol = "HTTP"
    lb_port = 80
    instance_protocol = "HTTP"
    instance_port = 8000
  }
  listener = {
    lb_protocol = "HTTPS"
    lb_port = 443
    ssl_certificate_id = "${var.domain_certificate}"
    instance_protocol = "HTTP"
    instance_port = 8000
  }
  ...
}

Sometimes I want to e.g. wrap an AWS ELB with some default subnets / tags / security_groups just leaving the user to be required to define some listener blocks. This is currently impossible, e.g.:

module "load_balancer" {
  listener = {
    lb_protocol = "HTTP"
    lb_port = 80
    instance_protocol = "HTTP"
    instance_port = 8000
  }
  listener = {
    lb_protocol = "HTTPS"
    lb_port = 443
    ssl_certificate_id = "${var.domain_certificate}"
    instance_protocol = "HTTP"
    instance_port = 8000
  }
}

I realize that module/resource is not a 1:1 mapping (modules can encapsulate whole graphs) which means how to actually accomplish feature in a way that is flexible would need some consideration indeed. I don't have any special thoughts about that yet.

config enhancement

Most helpful comment

On Terraform v0.12-alpha1 I just tried the following example load balancer module based on the example given in the original comment on this issue:

variable "listeners" {
  type = list(object({
    lb_protocol        = string
    lb_port            = number
    instance_protocol  = string
    instance_port      = number
    ssl_certificate_id = string
  }))
}

resource "aws_elb" "example" {
  dynamic "listener" {
    for_each = var.listeners
    content {
      lb_protocol        = listener.value.lb_protocol
      lb_port            = listener.value.lb_port
      instance_protocol  = listener.value.instance_protocol
      instance_port      = listener.value.instance_port
      ssl_certificate_id = listener.value.ssl_certificate_id
    }
  }
}

I called this module with the following root module:

provider "aws" {
  region = "us-west-2"
}

variable "domain_certificate" {
  type    = string
  default = null
}

module "load_balancer" {
  source = "./load-balancer"

  listeners = [
    {
      lb_protocol        = "HTTP"
      lb_port            = 80
      instance_protocol  = "HTTP"
      instance_port      = 8000
      ssl_certificate_id = null
    },
    {
      lb_protocol        = "HTTPS"
      lb_port            = 443
      instance_protocol  = "HTTP"
      instance_port      = 8000
      ssl_certificate_id = var.domain_certificate
    },
  ]
}

Applying this runs into #19152 for the moment, so I can't actually illustrate applying this, but I can see it generating the expected plan.

I don't have a certificate ARN to pass in there but I tried it with a bogus one and verified that I got a validation error about it, showing that it was populated into the resource as expected.

This is also subject to known issue #19142 which leads to a confusing error message if the caller doesn't provide all of the required attributes inside the listener blocks. That'll be addressed separately.

But with all of that said, I think the two features I indicated previously do address the use-case that this feature request was about, albeit in a different way than originally suggested, so I'm going to close this. We'll get those other issues addressed before v0.12.0 final so that a configuration like my example above is fully applyable.

Thanks for your patience on this, everyone!

All 20 comments

Another use case - say you have a module for setting up rds with an associated parameter group, one has no way to customize the parameters in that group from outside the module

resource "aws_db_parameter_group" "app" {
  name = "${var.name}"
  family = "${var.family}"

 //  parameter {
 //    name = "log_statement"
 //    value = "none"
 //    apply_method = "immediate"
 //  }
}

resource "aws_db_subnet_group" "app" {
  name = "${var.name}"
  subnet_ids = ["${split(",", var.subnet_ids_csv)}"]
}

resource "aws_db_instance" "app" {
  identifier = "${var.name}"
  engine = "${var.engine}"
  engine_version = "${var.engine}"
  instance_class = "${var.db_instance_type}"
  allocated_storage = "${var.db_instance_storage}"
  storage_type = "${var.db_instance_storage_type}"

  username = "${var.db_username}"
  password = "${var.db_password}"
  name = "${var.db_name}"

  db_subnet_group_name = "${aws_db_subnet_group.app.name}"
  parameter_group_name = "${aws_db_parameter_group.app.name}"

  vpc_security_group_ids = ["${compact(split(",", var.security_groups_csv))}"]
}

Something like this is now possible with the built-in map support!

@mitchellh can you give an example? My use case is passing parameters blocks to the aws_db_parameter_group resource inside a module.

@phoolish Oooooh I misread this then, actually. That isn't possible. I'll reopen.

However, you can get a lot more effecient with this by passing through maps and using the map elements.

@mitchellh Yeah, that feature has been extremely helpful.

Would blocks accepting the count argument be a solution?

+1 on this feature. Are there any work-arounds?

Found myself needing this feature as well +1

it can be very useful!

Do you need help? Because I really want it!

+1 on this feature. It'd be nice to be able to pass a block of dimension to an aws_cloudwatch_metric_alarm metric. its impossible to parameterize all possible dimensions using map keys

Another use case is placement strategies for ecs tasks

Yeah looking at this as a way to pass metadata to a wrapper module for handling ECS task_definition with LoadBalancer and not wanting to just create a myriad of parameters to pull it off

Looking for this same functionality +1

Going to have to nudge this. +1

+1

+1

Posting +1 comments does nothing but annoy the people who have subscribed to this issue for updates. Please instead vote for the issue by leaving a :+1: reaction on the original opening comment, which we can actually aggregate as an input to prioritization.

With that said, the next major release is already planned to have two features that I think together meet the use-case described here:

Since module inputs are still defined as single arguments rather than blocks, a different syntax is required:

# Feature planned for Terraform 0.12; details may change before release

module "load_balancer" {
  listeners = [
    {
      lb_protocol = "HTTP"
      lb_port = 80
      instance_protocol = "HTTP"
      instance_port = 8000
    },
    {
      lb_protocol = "HTTPS"
      lb_port = 443
      ssl_certificate_id = "${var.domain_certificate}"
      instance_protocol = "HTTP"
      instance_port = 8000
    },
  ]
}

...but some difference in punctuation aside, this sort of complex structure could then be passed into a module and used to dynamically-generate nested blocks inside resource configurations.

I have this method

resource "aws_api_gateway_method" "method_request" {
  rest_api_id   = "${var.rest_api_id}"
  resource_id   = "${var.resource_id}"
  http_method   = "${var.http_method}"
  authorization = "${var.authorization}"
  authorizer_id = "${var.authorizer_id}"
}

Has any way to send request_parameters as a var too?

Hi @guidiego,

request_methods is a map attribute rather than a child block, so it can already be passed through the module boundary and assigned as a variable:

variable "request_parameters" {
  type = "map"
}

resource "aws_api_gateway_method" "method_request" {
  rest_api_id   = "${var.rest_api_id}"
  resource_id   = "${var.resource_id}"
  http_method   = "${var.http_method}"
  authorization = "${var.authorization}"
  authorizer_id = "${var.authorizer_id}"

  request_parameters = "${var.request_parameters}"
}

In the new release the above will still work but some new syntax will allow removing the interpolation markers and specifying explicitly that the map keys are boolean, so Terraform can type-check that at the module interface and return better error messages if the module is not used correctly.

# In forthcoming 0.12 release (some details may change before release)

variable "request_parameters" {
  type = map(bool)
}

resource "aws_api_gateway_method" "method_request" {
  rest_api_id   = var.rest_api_id
  resource_id   = var.resource_id
  http_method   = var.http_method
  authorization = var.authorization
  authorizer_id = var.authorizer_id

  request_parameters = var.request_parameters
}

On Terraform v0.12-alpha1 I just tried the following example load balancer module based on the example given in the original comment on this issue:

variable "listeners" {
  type = list(object({
    lb_protocol        = string
    lb_port            = number
    instance_protocol  = string
    instance_port      = number
    ssl_certificate_id = string
  }))
}

resource "aws_elb" "example" {
  dynamic "listener" {
    for_each = var.listeners
    content {
      lb_protocol        = listener.value.lb_protocol
      lb_port            = listener.value.lb_port
      instance_protocol  = listener.value.instance_protocol
      instance_port      = listener.value.instance_port
      ssl_certificate_id = listener.value.ssl_certificate_id
    }
  }
}

I called this module with the following root module:

provider "aws" {
  region = "us-west-2"
}

variable "domain_certificate" {
  type    = string
  default = null
}

module "load_balancer" {
  source = "./load-balancer"

  listeners = [
    {
      lb_protocol        = "HTTP"
      lb_port            = 80
      instance_protocol  = "HTTP"
      instance_port      = 8000
      ssl_certificate_id = null
    },
    {
      lb_protocol        = "HTTPS"
      lb_port            = 443
      instance_protocol  = "HTTP"
      instance_port      = 8000
      ssl_certificate_id = var.domain_certificate
    },
  ]
}

Applying this runs into #19152 for the moment, so I can't actually illustrate applying this, but I can see it generating the expected plan.

I don't have a certificate ARN to pass in there but I tried it with a bogus one and verified that I got a validation error about it, showing that it was populated into the resource as expected.

This is also subject to known issue #19142 which leads to a confusing error message if the caller doesn't provide all of the required attributes inside the listener blocks. That'll be addressed separately.

But with all of that said, I think the two features I indicated previously do address the use-case that this feature request was about, albeit in a different way than originally suggested, so I'm going to close this. We'll get those other issues addressed before v0.12.0 final so that a configuration like my example above is fully applyable.

Thanks for your patience on this, everyone!

I'm going to lock this issue because it has been closed for _30 days_ โณ. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

Was this page helpful?
0 / 5 - 0 ratings