Accessing variables declared and defined in parent from child module

Hi,

I am quite new in using Terraform, please bear with me if my questions are silly.
My code structure is as follows:
Root:-----------
main.tf
variables.tf
main_values.tfvars
nva-------(/* child module folder */)
create_firewall.tf
variables.tf
firewall_values.tfvars

I have tried executing just the child module as a separate standalone one, it worked successfully.
As a next step I wanted to try out module configuration.
Root: ---- consists of only creation of vnet and subnets.
Child Module ----- creates AzureFirewall subnet in vnet created in parent .
and Azure Firewall in that subnet.
My issues:

  1. How to refer to the vnet that is created in parent.
  2. Although all values are provided in firewall_values.tfvars, it is still asking the values from the main.tf in parent where child module is getting called.

I know as per the module configuration I need to pass all the values at the time of calling the module. Is there any way I can point to values defined in firewall_values.tfvars file. If this is possible, then child module variables definitions can be segregated.

Thanks in advance.

Hi,

If you want use the resources get created in child module you need to expose them with ids or arns in output.tf of child module. then you can call those values in root module.

For your first question, the answer is to declare in the child module an input variable that accepts the parts of the vnet object you need. I’m assuming that when you say “vnet” you mean azurerm_virtual_network, in which case I expect that the attributes you need will be the id, the name, the location, the resource group name, and the id and name of each of the subnets, which suggests a variable declaration like this:

variable "virtual_network" {
  type = object({
    id                  = string
    name                = string
    location            = string
    resource_group_name = string
    subnet   = set(object({
      id   = string
      name = string
    }))
  })
}

The type argument gives a type constraint, which in this case is an object type constraint, specifying that this variable expects to be given an object with at least the attributes listed in the constraint. Here I made the constraint compatible with objects produced by the azurerm_virtual_network resource type.

You can then use the attributes from this variable to populate your firewall resource:

resource "azurerm_firewall" "example" {
  for_each = {
    for subnet in var.virtual_network.subnet : subnet.name => subnet
  }

  name                = "example"
  location            = var.virtual_network.location
  resource_group_name = var.virtual_network.resource_group_name

  ip_configuration {
    name                 = "example"
    subnet_id            = each.value.id
    public_ip_address_id = azurerm_public_ip.example.id
  }
}

The above is using some more advanced Terraform features: resource for_each and for expressions. When writing a reusable module it’s often necessary to use these more advanced features to map from the user’s input onto what the underlying provider actually needs, and in this case the idea is to create one firewall per subnet declared in the virtual network. (I’m not an Azure expert by any means, so I’m not sure that one firewall per subnet is a normal thing to do, but my intent here is only to show how to use these Terraform language features; please do adapt it if you have a different goal in mind!)

With all of that in place, you can then call this module and pass in the virtual network object that the root module declared:

resource "azurerm_virtual_network" "example" {
  # (insert network configuration here)
}

module "nva" {
  source = "./nva"

  virtual_network = azurerm_virtual_network.example
}

Because the type constraint for this virtual_network variable was designed to match the azurerm_virtual_network resource type, we should be able to pass in the network object directly to the variable, avoiding the need to pass all of those separate arguments individually.

I think the above may have indirectly answered your second question too: .tfvars files are only for setting variables in the root module. If you want to pass values into variables of child modules, you do that inside the module block that called the module, as in my most recent example above.

There’s some broader background information on this sort of design in the Terraform guide Module Composition.

Thank you for replying. Actually I just want the other way round. Accessing parent variable from child module.

Hi,

Thank you for explaining it beautifully in detail.
I have just tried to create that virtual network variable and use it in child module. However when I am trying to execute it, it is asking to enter value for the created variable. Although I have assigned its value in calling module section as you have mentioned.
Am I missing something?

I have figured out that I need to declare variables in both the variables.tf file, one under root and another one under module ( here under nva/variables.tf).

Please correct me if I am wrong:
It seems like bit of redundancy here.
If I have 5 modules then all its variables will be declared under its module/variables.tf file and also in root/variables.tf file. Eventually root/variables.tf will grow very large and unmanageable.

Thanks in advance.

@shareKnowledge

See the following example for AWS that passes variables from the parent to the child and from the child to the parent and other modules.

Parent calls module aws_network_vpc and passes values aws_vpc_block and aws_vpc_tag_name:

module "aws_network_vpc" {
  source           = "./modules/aws/network/vpc"
  aws_vpc_block    = var.aws_vpc_block
  aws_vpc_tag_name = var.aws_vpc_tag_name
}

Module accepts values from calling parent

variable "aws_vpc_tag_name" {
  description = "Name of the VPC"
}

variable "aws_vpc_block" {
  description = "Private IP block for the VPC in CIDR format"
}

Actual module uses vars:

resource "aws_vpc" "default" {
  cidr_block           = var.aws_vpc_block
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = var.aws_vpc_tag_name
  }
}

Actual module exports values to calling parent:

output "id" {
  value = aws_vpc.default.id
}

output "vpc_main_route_table_id" {
  value = aws_vpc.default.main_route_table_id
}

Exported values are acceded with construction module.name_of_module.variable, example for our previous module aws_network_vpc we need the exported ID of the VPC to create an Internet Gateway.

Parent uses values exported from module:

#Create an Internet GW
module "aws_internet_gw" {
  source = "./modules/aws/network/internet_gateway"
  vpc_id = module.aws_network_vpc.id
  name   = var.aws_internet_gw_name
}

Full example and code at:

1 Like

Hi @shareKnowledge,

Each module has its own set of variables. Think of a module as being like a function in a general-purpose programming language: the variables are like the function’s arguments. We don’t generally expect a function to be able to access the arguments of the function that called it. The same principle applies to Terraform modules, in that each module should ideally be self-contained and define its own interface (input variables and output values) and not depend directly on anything outside of its own configuration.

I tried this, but it did not work. While executing am getting :
please enter value for
virtual_network
Am I missing something?

Hi @shareKnowledge,

It’s hard to know what is going on here without seeing your current code in full, but it sounds like you have a variable "virtual_network" block in your root module. The virtual network is declared in the root module, so there shouldn’t be a variable "virtual_network" block in the root, only in the child module.

If that doesn’t help, please re-share your full configuration containing any updates you’ve made in response to answers so far.

Yes, you were correct.
It worked fine after removing the variable declaration from the root.
It also cleared my confusion.
Thank you .

I am attaching a piece of my working code, it is a resource definition of application rule collection in the firewall.
I have used dynamic block for looping properties under resource, as there are multiple rule types associated with the firewall, NAT, N/W, and Application rule collection.
I was able to make this work for NAT rule and N/W rule collection in the firewall, as they did not contain a nested property of block type.
Under the application rule collection, there can be more than one protocol blocks. I am unable to make the protocol block dynamic. Hence, repeated in the below code.

Could you please advise me on making this protocol block dynamic.

##Section of code from create_firewall.tf file
#Create App Rules

resource “azurerm_firewall_application_rule_collection” “example” {
count = local.is_enabled == 1 ? var.app_rule_coll_cnt : 0
name = var.app_rule_coll_name[count.index]
azure_firewall_name = azurerm_firewall.firewall[0].name
resource_group_name = data.azurerm_resource_group.existing-rg.name
priority = var.app_rule_priority[count.index]
action = var.app_rule_action[count.index]
dynamic rule {
for_each = [for s in var.app_rules[count.index]: {
name = s.name
fqdn_tags = s.fqdn_tags
source_addresses = s.source_addresses
protocol = {
port = s.protocol.port
type = s.protocol.type
}
protocol = {
port = s.protocol.port
type = s.protocol.type
}
target_fqdns = s.target_fqdns

    }]
    content {
        name = rule.value.name
        fqdn_tags = rule.value.fqdn_tags
        source_addresses = rule.value.source_addresses
        protocol {
            port = rule.value.protocol.port
            type = rule.value.protocol.type 
        }  
        protocol {
            port = rule.value.protocol.port
            type = rule.value.protocol.type 
        }       
        target_fqdns = rule.value.target_fqdns       
    }
}

}

##Rules defined in firewall_values.tfvars file

app_rule_coll_cnt = 1
app_rule_coll_name = [“AllowInternet”]
app_rule_priority = [“1000”]
app_rule_action = [“Allow”]
app_rules = {
0 = [
{
name= “AllowMicrosoft”
fqdn_tags = [“”,]
source_addresses = [““,]
protocol = {
port = “443”
type = “Https”
}
protocol = {
port = “80”
type = “Http”
}
target_fqdns = [”
.microsoft.com”]
},
{
name= “AllowOracle”
fqdn_tags = [“”,]
source_addresses = [““,]
protocol = {
port = “443”
type = “Https”
}
protocol = {
port = “80”
type = “Http”
}
target_fqdns = [”
.oracle.com”]
},
{
name= “AllowApache”
fqdn_tags = [“”,]
source_addresses = [“*”,]
protocol = {
port = “443”
type = “Https”
}
protocol = {
port = “80”
type = “Http”
}
target_fqdns = [“apachemirror.wuchna.com”]
}
]
}

Excellent and a very simple solution