I am calling a terraform module from my main.tf as follows:
module "vpc" {
source = "./modules/vpc"
match_pub_ip_env_vpc_sn = true
env_vpc_sg_name = "My name"
env_vpc_sg_desc = "My description"
}
My question is this: Can I use a file to populate the call to the above module with the values I want for the above variables without explicitly adding them in the module statement (as above). If so is there a file naming convention that terraform uses for such a purpose? (i.e. without me needing to pass -vars-file in the cmd?)
Unfortunately not. It's like a method/function call. The module needs to declare what variables it takes (eg the signature or parameter list in a programming language, with some being defaulted if not set) and then the caller of the module needs to pass any required variables when it declares the module.
Related
When writing terraform modules, one is commonly writing pass through variables/inputs for dependent objects.
How can I write the variable so that the description/type just references the dependent description?
I imagine something like
variable "foo" {
type = dependant.resource.foo.var.type
description = dependant.resource.foo.var.description
default = "module default"
}
Variable descriptions are metadata used by Terraform itself (specifically: by documentation mechanisms like Terraform Registry) and are not data visible to your module code.
Each module's input variables and output values must be entirely self-contained. Mechanisms like the Terraform Registry rely on this so that they are able to generate the documentation for a module only by reference to that module, without any need to fetch and analyze any other modules or other dependencies.
If you do intend to have a variable just "pass through" to a child module or to a resource configuration then you will need to redeclare its type and description in your module.
I would also suggest considering the advice in the documentation section When to write a module; directly passing through a variable to a child object isn't necessarily a wrong thing to do, but it can result from a module not raising the level of abstraction and therefore not passing the test described there.
In such cases, it can be better to use module composition instead of nested modules.
In this case would mean that the caller of your module would themselves call the other module you are currently wrapping. Instead of your module taking that other module's input variables as its own, it would be declared to accept the other module's output values as its input, so that the caller can pass the object representing the first module result into the second module:
module "first" {
# ...
}
module "second" {
# ...
first = module.first
}
Inside this "second" module, the input variable first would be declared as requiring an object type with attributes matching whatever subset of the output values of "first" that the second module depends on.
Suppose I had a module (or resource, or locals block, or whatever)
module "example" {
foo = "foo"
bar = "${foo}" # why wont' this work??
}
It's frustrating to me that the terraform "language" doesn't support this. How am I supposed to write simple DRY code without being able to reference variables?
Edit: I think that example is too contrived. Here's a better one:
module "example" {
domain = "example.com"
uri = "https://${domain}" # why wont' this work??
}
It is supported, but it depends on your module. Your module must output foo first. Then you can do:
module "exmaple" {
source = "./mymodule1"
foo = "dfsaf"
bar = module.exmaple.foo
}
The simplest example of mymodule1 would be:
variable "foo" {
default = 1
}
variable "bar" {
default = 2
}
output "foo" {
value = var.foo
}
But in your example it really does not make sense doing that, as bar is always same as foo, thus it shouldn't even be exposed.
The better and more natrual way would be:
locals {
foo = "foo"
}
module "exmaple" {
source = "./mymodule1"
foo = local.foo
bar = local.foo
}
Your question seems to be two different questions: how can you achieve the goal you described, and also why didn't the thing you tried first work.
Others already answered how you can factor out values to use in multiple locations, and so I wanted to just try to address the second question about why this didn't work, in case it's useful for your onward journey with Terraform.
The Terraform language is designed to appear as a heirarchical data structure rather than as linear code to be executed, which of course has some significant tradeoffs. In return for hopefully making a well-written Terraform configuration read like a manifest of what should exist rather than a program for creating it, the Terraform language does somewhat obscure the real control flow and symbol scoping that you might be accustomed to in other languages.
For your question in particular, I think it's important to recognize that the arguments inside a module block are not like declarations of variables, but are instead more like arguments passed to a function.
For your second example then, it might help to see it reworked into a function-call-like syntax:
module "example" {
source = "./mymodule1"
domain = "example.com"
uri = "https://${domain}"
}
# NOTE: pseudocode
example(domain: "example.com", uri: "https://#{domain}")
In most imperative-style languages, function arguments bind to symbols inside the module rather than outside of it, and so defining domain as "example.com" here doesn't make that symbol visible to other subsequent arguments in the same call.
Now taking Marcin's final example, adapted to resemble your second example:
locals {
domain = "example.com"
}
module "example" {
source = "./mymodule1"
domain = local.domain
url = "https://${local.domain}"
}
# NOTE: pseudocode
domain = "example.com"
example(domain: domain, uri: "https://#{domain}")
Of course this is also exposing another difference with Terraform compared to general-purpose languages, which is that it doesn't have a shared variable scope with everything assigned into it and instead places local variables inside a namespace local. But despite the difference in syntax, they are local values scoped to the module they are defined with, and so you can refer to local.domain anywhere else in that same module. (Note: this is a whole-module scope, and not a lexical scope as you might be accustomed to in other languages.)
A similar principle applies to resource, data, and provider blocks; those two are more like arguments to a function than they are variable declarations, but just shaped in a different syntax with the goal of it appearing as a series of declarations rather than as sequential code.
I have a module a in terraform which creates a text file , i need to use that text file in another module b, i am using locals to pull the content of that text file like below in module b
locals {
ports = split("\n", file("ports.txt") )
}
But the terraform expects this file to be present at the start itself, throws error as below
Invalid value for "path" parameter: no file exists at
path/ports.txt; this function works only with files
that are distributed as part of the configuration source code, so if this file
will be created by a resource in this configuration you must instead obtain
this result from an attribute of that resource.
What am i missing here? Any help on this would be appreciated. Is there any depends_on for locals, how can i make this work
Modules are called from within other modules using module blocks. Most arguments correspond to input variables defined by the module. To reference the value from one module, you need to declare the output in that module, then you can call the output value from other modules.
For example, I suppose you have a text file in module a.
.tf file in module a
output "textfile" {
value = file("D:\\Terraform\\modules\\a\\ports.txt")
}
.tf file in module b
variable "externalFile" {
}
locals {
ports = split("\n", var.externalFile)
}
# output "b_test" {
# value = local.ports
# }
.tf file in the root module
module "a" {
source = "./modules/a"
}
module "b" {
source = "./modules/b"
externalFile = module.a.textfile
depends_on = [module.a]
}
# output "module_b_output" {
# value = module.b.b_test
# }
For more reference, you could read https://www.terraform.io/docs/language/modules/syntax.html#accessing-module-output-values
As the error message reports, the file function is only for files that are included on disk as part of your configuration, not for files generated dynamically during the apply phase.
I would typically suggest avoiding writing files to local disk as part of a Terraform configuration, because one of Terraform's main assumptions is that any objects you manage with Terraform will persist from one run to the next, but that could only be true for a local file if you always run Terraform in the same directory on the same computer, or if you use some other more complex approach such as a network filesystem. However, since you didn't mention why you are writing a file to disk I'll assume that this is a hard requirement and make a suggestion about how to do it, even though I would consider it a last resort.
The hashicorp/local provider includes a data source called local_file which will read a file from disk in a similar way to how a more typical data source might read from a remote API endpoint. In particular, it will respect any dependencies reflected in its configuration and defer reading the file until the apply step if needed.
You could coordinate this between modules then by making the output value which returns the filename also depend on whichever resource is responsible for creating the file. For example, if the file were created using a provisioner attached to an aws_instance resource then you could write something like this inside the module:
output "filename" {
value = "D:\\Terraform\\modules\\a\\ports.txt"
depends_on = [aws_instance.example]
}
Then you can pass that value from one module to the other, which will carry with it the implicit dependency on aws_instance.example to make sure the file is actually created first:
module "a" {
source = "./modules/a"
}
module "b" {
source = "./modules/b"
filename = module.a.filename
}
Then finally, inside the module, declare that input variable and use it as part of the configuration for a local_file data resource:
variable "filename" {
type = string
}
data "local_file" "example" {
filename = var.filename
}
Elsewhere in your second module you can then use data.local_file.example.content to get the contents of that file.
Notice that dependencies propagate automatically aside from the explicit depends_on in the output "filename" block. It's a good practice for a module to encapsulate its own behaviors so that everything needed for an output value to be useful has already happened by the time a caller uses it, because then the rest of your configuration will just get the correct behavior by default without needing any additional depends_on annotations.
But if there is any way you can return the data inside that ports.txt file directly from the first module instead, without writing it to disk at all, I would recommend doing that as a more robust and less complex approach.
I have a terraform module that looks like below
module "newserver" {
source = "./newserver"
// passing the variables here
}
Instead of passing the variables there, can i pass them as a file in the command like below
terraform apply -var-file="/path/variables.tfvars"
When i do that, i am getting errors like missing required argument, what am i missing here, any help on this would be appreciated.
Your module has variables defined inside it with variable blocks such as
variable "example_var" {}
When you instantiate the module, you'll need to pass the required variable into the module. If you want to pass variables into the module when you instantiate it, you'll also have to define those variables as well
module "newserver" {
source = "./newserver"
example_var = var.example_var
}
variable "example_var" {}
Terraform is scoped so that variables are scoped inside of modules, you'll have to make a new variable if you instantiate a module and want to pass variables into it
If I create a varaible definition like this:
variable "aws_ecs_config" {
type = object({
cpu = number
memory = number
ecs_image_address = string
})
logs = {
type = object({
group = string
region = string
stream_prefix = string
})
}
}
How can I reuse that definition in multiple places without copy-pasting?
It is not possible to re-use variable declarations in Terraform. If variables in different modules will have the same type, that type must be repeated in each module.
Terraform has a structural type system rather than a nominal type system, so types themselves are not named and instead are matched/constrained by their form. When passing values between modules, we can use type constraints to create conventions between a family of related modules, but there is no mechanism to define a type (or declare a variable) in one place and reuse it in other places.
Terraform's type constraint mechanism considers any object with at least the attributes in the type constraint to be valid, so it's not necessary to define an exhaustive object type every time.
For example, if you define a variable with the following type:
object({
name = string
})
The following object value is acceptable for that type constraint, because it has a name attribute of the correct type regardless of any other attributes it defines:
{
name = "foo"
other = "bar"
}
For that reason, it can be better to limit the variable declaration in each module to only the subset of attributes that particular module actually requires, which reduces the coupling between the modules: they only need to be compatible to the extent that their attribute names overlap, and don't need to directly bind to one another.
Workaround
This is possible without explicit Terraform support. (There are some discussions about types. Weigh in if you think it would be handy)
Terraform allows variables to be declared over multiple files, and if the modules live on a Linux filesystem, reuse is vastly simplified. (Windows folks... well, it is possible, just not pretty and may not play well with version control.)
How?
Move the variable definition to a file named variable.aws_ecs_config.tf
Place it in a well known place. Say, a directory called variables that lives next to modules.
Whenever the variable is needed in a module, create a symlink to the variable file: ln -s <path/to/variables>/variable.aws_ecs_config.tf