Terraform: modules, Outputs, and Variables

By | 12/11/2022

Eventually, I got to the modules in Terraform.

Namely, I had to figure out how to transfer the values ​​of the variables between two modules.

So in this post, the most basic and simple examples of working with modules and their values ​​&& outputs.

See more in the documentation – Modules.

The Root module

First, let’s create a root module, which simply creates a local file, and where later we will add the modules.

Create a testing directory:

[simterm]

$ mkdir modules_example
$ cd modules_example/

[/simterm]

In this directory add a file main.tf with the resource of the local_file type that will create a file.txt with the “file content” text:

resource "local_file" "file" {
  content  = "file content"
  filename = "file.txt"
}

Run terraform init to pull up the necessary modules of Terraform itself:

[simterm]

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/local...
- Installing hashicorp/local v2.2.3...
- Installed hashicorp/local v2.2.3 (signed by HashiCorp)
...
Terraform has been successfully initialized!

[/simterm]

Then, run terraform plan to check whether it will work at all, and what exactly it will do:

[simterm]

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.file will be created
  + resource "local_file" "file" {
      + content              = "file content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

[/simterm]

If it looks OK, run terraform apply:

[simterm]

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.file will be created
  + resource "local_file" "file" {
      + content              = "file content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

local_file.file: Creating...
local_file.file: Creation complete after 0s [id=87758871f598e1a3b4679953589ae2f57a0bb43c]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

[/simterm]

And check the contents of the directory where we created the main.tf file and launched the commands:

[simterm]

$ ls -l
total 12
-rwxr-xr-x 1 setevoy setevoy  12 Nov 28 13:14 file.txt
-rw-r--r-- 1 setevoy setevoy  90 Nov 28 13:09 main.tf
-rw-r--r-- 1 setevoy setevoy 854 Nov 28 13:14 terraform.tfstate

[/simterm]

And the file.txt content:

[simterm]

$ cat file.txt 
file content

[/simterm]

It works! let’s go further.

Terraform Modules

Next, let’s go to the modules.

Create two directories for two modules:

[simterm]

$ mkdir -p modules/file_1
$ mkdir -p modules/file_2

[/simterm]

In each of them, create their own main.tf files – the modules/file_1/main.tf and modules/file_2/main.tf.

In the modules/file_1/main.tf use the same local_file resource to create a file_1.txt file :

resource "local_file" "file_1" {
  content  = "file_1 content"
  filename = "file_1.txt"
}

Similarly in the modules/file_2/main.tf for the file_2.txt:

resource "local_file" "file_2" {
  content  = "file_2 content"
  filename = "file_2.txt"
}

Update the root module, that is, modules_example/main.tfdelete the resource "local_file", and instead of it describe the two modules with the paths to the directories of both modules:

module "file_1" {
  source = "./modules/file_1"
}

module "file_2" {
  source = "./modules/file_2"
}

Run init again so that Terraform creates its modules structure:

[simterm]

$ terraform init
Initializing modules...
- file_1 in modules/file_1
- file_2 in modules/file_2

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/local from the dependency lock file
- Using previously-installed hashicorp/local v2.2.3

Terraform has been successfully initialized!

[/simterm]

Check the .terraform/modules/:

[simterm]

$ cat .terraform/modules/modules.json | jq 
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "file_1",
      "Source": "./modules/file_1",
      "Dir": "modules/file_1"
    },
    {
      "Key": "file_2",
      "Source": "./modules/file_2",
      "Dir": "modules/file_2"
    }
  ]
}

[/simterm]

Run plan:

[simterm]

$ terraform plan
local_file.file: Refreshing state... [id=87758871f598e1a3b4679953589ae2f57a0bb43c]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # local_file.file will be destroyed
  # (because local_file.file is not in configuration)
  - resource "local_file" "file" {
      - content              = "file content" -> null
      - directory_permission = "0777" -> null
      - file_permission      = "0777" -> null
      - filename             = "file.txt" -> null
      - id                   = "87758871f598e1a3b4679953589ae2f57a0bb43c" -> null
    }

  # module.file_1.local_file.file_1 will be created
  + resource "local_file" "file_1" {
      + content              = "file_1 content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file_1.txt"
      + id                   = (known after apply)
    }

  # module.file_2.local_file.file_2 will be created
  + resource "local_file" "file_2" {
      + content              = "file_2 content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file_2.txt"
      + id                   = (known after apply)
    }

Plan: 2 to add, 0 to change, 1 to destroy.

[/simterm]

And now we can perform the apply.

In order not to enter “yes” every time, we can use the -auto-approve argument:

[simterm]

$ terraform apply -auto-approve
local_file.file: Refreshing state... [id=87758871f598e1a3b4679953589ae2f57a0bb43c]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # local_file.file will be destroyed
  # (because local_file.file is not in configuration)
  - resource "local_file" "file" {
      - content              = "file content" -> null
      - directory_permission = "0777" -> null
      - file_permission      = "0777" -> null
      - filename             = "file.txt" -> null
      - id                   = "87758871f598e1a3b4679953589ae2f57a0bb43c" -> null
    }

  # module.file_1.local_file.file_1 will be created
  + resource "local_file" "file_1" {
      + content              = "file_1 content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file_1.txt"
      + id                   = (known after apply)
    }

  # module.file_2.local_file.file_2 will be created
  + resource "local_file" "file_2" {
      + content              = "file_2 content"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "file_2.txt"
      + id                   = (known after apply)
    }

Plan: 2 to add, 0 to change, 1 to destroy.
local_file.file: Destroying... [id=87758871f598e1a3b4679953589ae2f57a0bb43c]
module.file_2.local_file.file_2: Creating...
module.file_1.local_file.file_1: Creating...
local_file.file: Destruction complete after 0s
module.file_2.local_file.file_2: Creation complete after 0s [id=7225b36c22072cd558c23529d0d992c29cb873be]
module.file_1.local_file.file_1: Creation complete after 0s [id=82888a6ec6b05fc219759bd241ca5f0d6cba0e23]

Apply complete! Resources: 2 added, 0 changed, 1 destroyed.

[/simterm]

Check whether the files have appeared:

[simterm]

$ ll
total 24
-rwxr-xr-x 1 setevoy setevoy   14 Nov 28 13:21 file_1.txt
-rwxr-xr-x 1 setevoy setevoy   14 Nov 28 13:21 file_2.txt
-rw-r--r-- 1 setevoy setevoy  104 Nov 28 13:18 main.tf
drwxr-xr-x 4 setevoy setevoy 4096 Nov 28 13:15 modules

[/simterm]

And their content:

[simterm]

$ cat file_1.txt 
file_1 content

$ cat file_2.txt 
file_2 content

[/simterm]

Works? Move on.

Variables in Terraform modules

There is nothing special here – everything is the same as with common Terraform variables. See the documentationInput Variables.

In the modules_example/modules/file_1/main.tf declare a variable user_name:

variable "user_name" {
  type = string
}    

resource "local_file" "file_1" {
  content  = "file_1 content from ${var.user_name}"
  filename = "file_1.txt"
}

The same in the second module:

variable "user_name" {
  type = string
}    

resource "local_file" "file_2" {
  content  = "file_2 content from ${var.user_name}"
  filename = "file_2.txt"
}

Update the root module – pass the values for the user_name​ variable ​for both modules:

module "file_1" {
  user_name = "user1"
  source = "./modules/file_1"
}

module "file_2" {
  user_name = "user2"
  source = "./modules/file_2"
}

Apply the changes:

[simterm]

$ terraform apply -auto-approve
module.file_1.local_file.file_1: Refreshing state... [id=82888a6ec6b05fc219759bd241ca5f0d6cba0e23]
module.file_2.local_file.file_2: Refreshing state... [id=7225b36c22072cd558c23529d0d992c29cb873be]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.file_1.local_file.file_1 must be replaced
-/+ resource "local_file" "file_1" {
      ~ content              = "file_1 content" -> "file_1 content from user1" # forces replacement
      ~ id                   = "82888a6ec6b05fc219759bd241ca5f0d6cba0e23" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

  # module.file_2.local_file.file_2 must be replaced
-/+ resource "local_file" "file_2" {
      ~ content              = "file_2 content" -> "file_2 content from user2" # forces replacement
      ~ id                   = "7225b36c22072cd558c23529d0d992c29cb873be" -> (known after apply)
        # (3 unchanged attributes hidden)
    }
...
Apply complete! Resources: 2 added, 0 changed, 2 destroyed.

[/simterm]

And the result check:

[simterm]

$ cat file_1.txt 
file_1 content from user1

[/simterm]

Modules and Output values ​​of the variables

How can we pass the value of the variable from the child module to the root module? Use the outputs. See Output Values ​​documentation.

Update the first module – add the outputwith the name “file_content“:

variable "user_name" {
  type = string
}    

resource "local_file" "file_1" {
  content  = "file_1 content from ${var.user_name}"
  filename = "file_1.txt"
} 

output "file_content" {
  value = file("file_1.txt")
}

The same in the second:

variable "user_name" {
  type = string
}    

resource "local_file" "file_2" {
  content  = "file_2 content from ${var.user_name}"
  filename = "file_2.txt"
} 

output "file_content" {
  value = file("file_2.txt")
} 

And then in the root module, use the values ​​obtained using with the module.<MODULE_NAME>.<OUTPUD_NAME> to create a file concat_file.txt :

module "file_1" {
  user_name = "user1"
  source = "./modules/file_1"
}

module "file_2" {
  user_name = "user2"
  source = "./modules/file_2"
}

resource "local_file" "concat_file" {
  content  = "${module.file_1.file_content}\n${module.file_2.file_content}\n"
  filename = "concat_file.txt"
}

Run apply:

[simterm]

$ terraform apply -auto-approve
module.file_2.local_file.file_2: Refreshing state... [id=5771aa8b1046de4f4342d492faee20e3c289365d]
module.file_1.local_file.file_1: Refreshing state... [id=8a3552bfa2e46f311e7b718f8eed71b69bb8115f]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.concat_file will be created
  + resource "local_file" "concat_file" {
      + content              = <<-EOT
            file_1 content from user1
            file_2 content from user2
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "concat_file.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
local_file.concat_file: Creating...
local_file.concat_file: Creation complete after 0s [id=a4e1240fb9cdc96fffc1b0984392f6218422d194]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

[/simterm]

Check the result:

[simterm]

$ cat concat_file.txt 
file_1 content from user1
file_2 content from user2

[/simterm]

Transfer variable values ​​between modules

And finally, what we started with – how to transfer values ​​from one module to another?

In the file main.tfof the first module, add a variable with the name module_1_value and the default value “module 1 value“:

...
variable "module_1_value" {
  type = string
  default = "module 1 value"
}
...

In the same place, create a second variable module_2_value, into which we will then pass the value from the second module:

...
variable "module_2_value" {
  type = string
}
...

Add the output to return the value of the variable module_1_value to the root module for further use in the second module:

...
output "module_1_value" {
  value = var.module_1_value
}
...

The complete file now looks like this:

variable "user_name" {
  type = string
} 
  
variable "module_1_value" {
  type = string
  default = "module 1 value"
} 
  
variable "module_2_value" {
  type = string
}

resource "local_file" "file_1" {
  content  = "file_1 content from ${var.user_name} with value from file_2: ${var.module_2_value}"
  filename = "file_1.txt"
}

output "file_content" {
  value = file("file_1.txt")
}

output "module_1_value" {
  value = var.module_1_value
}

Here in the resource local_file we will use the value transferred from the second module.

Repeat the same for the module file_2:

variable "user_name" {
  type = string
} 
  
variable "module_2_value" {
  type = string
  default = "module 2 value"
} 
  
variable "module_1_value" {
  type = string
}

resource "local_file" "file_2" {
  content  = "file_2 content from ${var.user_name} with value from file_1: ${var.module_1_value}"
  filename = "file_2.txt"
}

output "file_content" {
  value = file("file_2.txt")
}

output "module_2_value" {
  value = var.module_2_value
}

Return to the root module and add the transfer of the variable module_2_value to the first module, and the variable module_1_value to the second module:

module "file_1" {
  user_name = "user1"
  module_2_value = module.file_2.module_2_value
  source = "./modules/file_1"
}   
  
module "file_2" {
  user_name = "user2"
  module_1_value = module.file_1.module_1_value
  source = "./modules/file_2"
} 

resource "local_file" "concat_file" {
  content  = "${module.file_1.file_content}\n${module.file_2.file_content}\n"
  filename = "concat_file.txt"
}

Now in the file file_1.txt we have to get the value from the module "file_2", and vice versa.

Apply:

[simterm]

$ terraform apply -auto-approve
module.file_1.local_file.file_1: Refreshing state... [id=d74b0e0b3296ab02e7b4791942d983b175d071b4]
local_file.concat_file: Refreshing state... [id=e0e88293b53693a484db146b15bd2ab3d8f4d250]
module.file_2.local_file.file_2: Refreshing state... [id=12faf027dc96d886c6022b54c878324a21d3d112]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.concat_file must be replaced
-/+ resource "local_file" "concat_file" {
      ~ content              = <<-EOT # forces replacement
          - file_1 content from user1 with value from file_2: variable 2 value
          - file_2 content from user2 with value from file_1: variable 1 value
          + file_1 content from user1 with value from file_2: module 2 value
          + file_2 content from user2 with value from file_1: module 1 value
        EOT
      ~ id                   = "e0e88293b53693a484db146b15bd2ab3d8f4d250" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
local_file.concat_file: Destroying... [id=e0e88293b53693a484db146b15bd2ab3d8f4d250]
local_file.concat_file: Destruction complete after 0s
local_file.concat_file: Creating...
local_file.concat_file: Creation complete after 0s [id=648854841581b273d26646fff776b33fdf060a19]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

[/simterm]

Check the result:

[simterm]

$ cat concat_file.txt 
file_1 content from user1 with value from file_2: module 2 value
file_2 content from user2 with value from file_1: module 1 value

[/simterm]

Done.