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.83k stars 9.57k forks source link

Use output variables inside the module that defines them #17144

Open Lasering opened 6 years ago

Lasering commented 6 years ago

Terraform Version

Terraform v0.11.2

Terraform Configuration Files

variable "security-group-id" {
  description = "The security group id to which the rules should be added"
}

output "network-ipv4-cidr" {
  value = "169.254.216.0/24"
}

resource "openstack_networking_secgroup_rule_v2" "ssh-network-ipv4" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "${output.network-ipv4-cidr}"
  security_group_id = "${var.security-group-id}"
}

Crash Output

Error reading config for openstack_networking_secgroup_rule_v2[ssh-network-ipv4]: output.network-ipv4-cidr: resource variables must be three parts: TYPE.NAME.ATTR in:

${output.network-ipv4-cidr}

Expected Behavior

Be able to use output variables inside the module they are defined in. I suggest using the syntax ${output.output-var-name} because using ${var.output-var-name} will colide with a variable defined with the same name.

Additional Context

This piece of code is defined in a very simple module so it can be reused on the modules that actually instantiate a service. By defining an output variable with an hardcoded value we hoped to use it inside the module and at the same time output it because it is also useful for the module that will use this module to have access to it.

I know I could have define a variable and an output variable both with the same name. Then just assign the output variable value to the value of the variable. However that would convey the message to the users of my module that they can parameterize it, which I not something I want to allow!

jbardin commented 6 years ago

Hi @Lasering,

If I understand your use case correctly, this touches on one of the reasons for Local Values. I would suggest the following alternative configuration:

variable "security-group-id" {
  description = "The security group id to which the rules should be added"
}

locals {
  network-ipv4-cidr = "169.254.216.0/24"
}

output "network-ipv4-cidr" {
  value = "${local.network-ipv4-cidr}"
}

resource "openstack_networking_secgroup_rule_v2" "ssh-network-ipv4" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "${local.network-ipv4-cidr}"
  security_group_id = "${var.security-group-id}"
}
Lasering commented 6 years ago

That solves the problem. However I still think it would be simpler to allow using the output variable directly.

apparentlymart commented 6 years ago

Hi @Lasering,

Thanks for that feedback! At the moment we do not plan to make outputs directly interpolatable, since there is already a working approach as James noted and so we're currently focusing our efforts elsewhere, but once the current configuration language revamp work is stable (which includes a lot of changes already) we can revisit this and figure out the tradeoffs for this feature: increased convenience of not needing to separately define a local value vs. potential increase in user-facing complexity of having another possible way to say the same thing and confusing the idea that outputs are for passing data out of a module.

For the moment, I'm going to tag this as "thinking" to remind us to revisit it once other work has settled down. Thanks again!

zappan commented 6 years ago

Just to chime in, I have the same need, but with a computed property (resource ID) - in such case, the local approach doesn't work, as that's not a user-settable value.

grimm26 commented 6 years ago

If you want to use a computed property, just use the computed property. If you need to output it as well, output it.

nkbt commented 5 years ago

You can solve it in a very odd, a bit funny, but totally working way.

Here we declare something output in module x

# modules/x/something.tf

output "something" {
  value = "nothing"
}

In this place we will use output something using null_resource trigger (could be any other resource/locals you ever need)

# modules/x/need_something.tf

variable "something" {
}

resource "null_resource" "debug_stuff" {
  triggers {
    something = "${var.something}"
  }
}

And now the best part of it, how to wire all these up in root.tf

# root.tf

module "x" {
  source = "./modules/x"
  something = "${module.x.something}"
}

Voila!

image

image

apparentlymart commented 5 years ago

Hi @nkbt! Thanks for sharing that interesting discovery.

What you've found there is an interesting consequence of an intentional design feature of Terraform modules: each input variable and output value participates separately in the dependency graph. Terraform is designed this way to allow better composability of modules that may have dependencies between one another, and to achieve better concurrency, but as you've seen here it also means that you can have an input variable that directly depends on an output value, as long as there are no graph cycles otherwise.

Of course, this is more code than just defining a local value to use for both the output and the other references:

locals {
  something = "nothing"
}

resource "null_resource" "debug_stuff" {
  triggers = {
    something = "${local.something}"
  }
}

output "something" {
  value = "${local.something}"
}

...so it seems like there's little reason to do this in practice, but it's definitely an interesting unintended outcome of the design of modules!

nkbt commented 5 years ago

@apparentlymart I totally get the locals are meant to be used for this. In our specific case we have hundreds of modules that were created before locals existed. We also interop tf with some other tooling by examining tf state after it completed plan/apply cycle. The problem with recent tf is that it omits outputs that are not used from the state (not used in tf's opinion), but we do rely on them to pull the data out to be used for other tooling. So after upgrading to the most recent tf we needed some automated solution to force those output values to be preserved in the state (hence null_resource) but at the same time we do not want to rewrite hundreds of existing modules to use locals.

Since al out tf templates are basically erb templates, on processing step I inject a pair of variable/null_resource trigger to ensure it stays in state. And then add reference to it root.tf.json for the module that needs it.

I guess our own context is quite irrelevant for most of tf users, but we went that erb preprocessing, tf state parsing and post-tf tooling route because we were using terraform since v0.4 (or smth like that) and most of the existing functionality did not exist at the time (hell, it was rough 4 years ago). But we still run all the stuff in prod and do not want to add more changes to the state/*.tf then it is absolutely essential.

CodeSwimBikeRun commented 5 years ago

one of my modules depends on a computed property of the current module. How do I do this?

nkbt commented 5 years ago

@CodeSwimBikeRun store computed property as one of locals and re-use it within module

afernandezody commented 5 years ago

Hello, I'm trying to use a couple of the suggestions but still unable to make it work (my issue is probably very similar to the one posed by CodeSwimBikeRun). The variable of interest is the MAC address -for a second network card- that is computed via an output variable called MYMAC_ADDRESS. The first thing is that I have to declare MYMAC_ADDRESS among the variables or get the message: "Error: resource 'null_resource.copy_in_setup_data_mgmt' provisioner remote-exec (#8): unknown variable referenced: 'MYMAC_ADDRESS'; define it with a 'variable' block." Once this is done, the first part of the TF script seems to work correctly as the apply command returns the right MAC value.

What is not working is the second part of the script where inline commands do not recognize MYMAC_ADDRESS correctly . To sum things up: -If no locals are defined and the inline command calls $(var.MYMAC_ADDRESS) --> The deployment completes without error messages but the inline commands have used the 'default' value for MYMAC_ADDRESS rather than the computed MAC address. This makes me wonder if some how TF is still seeing them as different variables. -When I define MYMAC_ADDRESS as a local variable and tried following nkbt's suggestion using the example from jbardin, I get the following error message: "* null_resource.copy_in_setup_data_mgmt[1]: At column 1, line 1: output of an HIL expression must be a string, or a single list (argument 2 is TypeList) in:"

Thanks. P.S. Just for clarification, I haven't defined any module and would expect Terraform to treat everything as a single one.

YugandharaP commented 4 years ago

Hi,

I want to use s3-bucket-arn in lambda module. s3 and lambda having different modules.

structure like below -main.tf -variable.tf -values.tfvars -/modules

How this would be possible?

can anyone help me. Thanks

apparentlymart commented 4 years ago

Hi all,

If you have usage questions about Terraform as it exists today, rather than discussion about this feature request, please create a topic in the community forum. We use GitHub issues only for tracking potential future work, and asking questions here creates notification noise for those who are watching this issue to see development updates relating to it.

rollendxavier commented 2 years ago

any updates on this issue, can we use outputs in same module? like below

resource "azurerm_servicebus_namespace" "sb_namespace" {
  name                = "${local.prefix}-${local.environment_tag}-sbnamespace"
  location            = local.resource_location
  resource_group_name = local.resource_group_name
  sku                 = "Standard"
  tags = {
    environment = local.environment_tag
  }
}

output "sb_namespace_name" {
  value           = azurerm_servicebus_namespace.sb_namespace.name
  description = "The name of the azure servicebus namespace being deployed"
}

resource "azurerm_servicebus_namespace_authorization_rule" "sb_authrule" {
  name                = "${local.prefix}-sbnauth"
  namespace_name      = var.sb_namespace_name
  resource_group_name = output.resource_group_name
  send                = true
  listen              = true
  manage              = true
  depends_on = [
    output.sb_namespace_name,
  ]
}

Error: A managed resource "output" "sb_namespace_name" has not been declared in the root module.

apparentlymart commented 2 years ago

The recommended answer is still to declare a named local value if you need to use a complex expression in more than one location in a module.

That is the Terraform equivalent of what in a general-purpose language might be a local variable used both in a call to some other function and in the return value:

let ret = 2;
doSomething(ret);
return ret;
locals {
  ret = 2
}

resource "something" "example" {
  value = local.ret
}

output "example" {
  value = local.ret
}
rollendxavier commented 2 years ago

does that mean "output" is only meant to be used when u want something out from another module?

terryannis commented 1 year ago

In my scenario of using the preferred approach of a local variable, the value is an object which contains a key:value dependent on an attribute of resource in the root module. After the resource creation, this local variable would then be passed into another module for subsequent use.

Since local variables are instantiated before resources, the above won't work as it is a chicken/egg situation. I.e., How do I declare an attribute of a resource that has yet to be created?

Since output variables aren't recognized by the root module, it is therefore not possible to pass around an attribute of a resource within a local variable. However, the attribute is accessible when referring to the resource directly!

I recognize that it's possible that I'm working with an anti-pattern or I'm failing to understand something obvious, but not being able to use attributes of resources in a local variable seems like a fairly large constraint. Is there a built-in that I'm not aware of that I can use to overcome this? If not, having a feature that allows for this capability seems reasonable to me! ☺️

gavenkoa commented 9 months ago

So there is no way to reference output in the module itself. One needs to define locals and duplicated name in outputs if want "same" entity...

ketzacoatl commented 9 months ago

So there is no way to reference output in the module itself. One needs to define locals and duplicated name in outputs if want "same" entity...

Yes. Some would consider this unfortunate, and that's understandable.

IMHO, to keep clean/readable code, I am putting a lot of things in locals {}, including all outputs, and then defining outputs and other attributes from local values.