Terraform : How to avoid code duplication of variables? - terraform

I have two modules and one root module that pretty much use the same "blueprint" of a variable, namely to instantiate projects within my infrastructure. As I'm in a test and learn process, I have to go back to all the occurences of variables.tf where they are declared. Therefore, I must change everything every time I want to add an attribute or remove it from the modules. It's a pain and waste of time.
Either within module defined inputs
Or within root-module variables
How I wish to use this variable :
Global blueprint declaration (sort of how you would declare a class in python or java)
"blueprint" project" {
type = object({
name = string
id_suffix = string
auto_create_network = bool
apis_to_enable = list(string)
attached_principals_info = object({
service_account_principals = map(object({
name = string
description = string
disabled = bool
roles = list(string)
impersonators = list(string)
}))
user_principals = map(object({
email = string
description = string
full_name = string
roles = list(string)
}))
})
})
}
"blueprint" "projects" {
type = map(project)
}
Usage within the module input .tfs:
variable "project" {
type = blueprint.project
}
variable "projects" {
type = blueprint.projects
}
How I currently use it:
The variable project and variable projects are explicitely detailed within each .tf of each module, similarly to what I declared above...
Side note : I've been following the credo write as little code as possible, be as lazy-efficient as possible within all my coding
endeavors, it's quite hard to do it in Terraform.

Related

Terraform : removal of identity block does not remove identity assigned from resource azure logic app

I have this in my main.tf and
dynamic "identity" {
for_each = var.identity == [] ? [] : [1]
content {
type = lookup(var.identity, "type", null)
#identity_ids = lookup(var.identity, "identity_ids", null)
}
}
I have defined variable as below.
variable "identity" {
description = "creates the identity for Logic App."
type = any
default = []
}
Removing identity block from input does not remove assigned identity. Terraform does not detect the change. Can some1 help ?
Also Logic App standard only supports SystemAssigned but doc says something else :
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/logic_app_standard
There seems to be some type confusion in your configuration here, but Terraform isn't able to detect and report it because you didn't give a specific type constraint for your variable.
Specifically, it's not clear whether you intended var.identity to be a list of objects or a single object. You declared the default as [], suggesting you meant a list, but the content of the dynamic "identity" block treats var.identity as if it's just a single object.
I'm going to write this out both ways, so you can choose which one meets your actual requirement.
For a list of "identities" with one identity block each:
variable "identities" {
type = list(object({
type = string
identity_ids = set(string)
}))
default = []
}
resource "example" "example" {
dynamic "identity" {
for_each = var.identities
content {
type = each.value.type
identity_ids = each.value.identity_ids
}
}
}
For a single "identity" object that is optional:
variable "identities" {
type = object({
type = string
identity_ids = set(string)
})
default = null
}
resource "example" "example" {
dynamic "identity" {
for_each = var.identities[*]
content {
type = each.value.type
identity_ids = each.value.identity_ids
}
}
}
In this second example, notice that:
The type constraint for variable "identities" is now just for an object type directly, without the list(...) from the first example.
The default value for that variable is now null, which is the typical way to represent the absence of a single value.
The dynamic "identity" block's for_each expression uses the [*] operator, called the "splat operator", which has a special behavior where it'll convert a null value into an empty list and a non-null value into a single-element list, thus producing a suitable collection value for the for_each argument.
I would recommend always writing type constraints for your input variables, because then Terraform can give you better feedback in situations like yours where you were not consistent in the types you were using. If you use any in a type constraint then Terraform will have less insight into what you are intending and so its error messages will typically be less specific and possibly even misleading, if it makes an incorrect assumption about what your goals were.

Reference a field within same module

Lets say I have a module block like this:
resource "aws_instance" "server" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = var.subnet_ids
tags = {
Name = format("ami:%s", ami) # << **How to do this?**
}
}
And I have to use a module field such as ami in this example as a value to another field? Is there a way i can do this.
Above is an example in really, I am working with custom module and one value gets used multiple times so I find it non-efficient to write/change same thing at multiple places. I also want to avoid creating a separate external variable.
Is there a way above can be achieved with some sort of internal referencing of fields within same module?
TIA!
Use variables to reuse values in the same module. If you want to access other modules resourcesLets use the data attribute.
Example: start with using a module:
module "dev-server" {
source = "./modules/server"
ami = var.dev_ami_name
instance_id = var.instance_id
}
Also add a module var file with the correct variables you're passing. Then your module:
resource "aws_instance" "server" {
ami = var.ami
instance_type = var.instance_id
tags = {
Name = format("ami:%s", var.ami) # << **How to do this?**
}
}
A general answer to this is to factor out the common value into a separate named value that you can refer to elsewhere.
54m's answer shows one way to do that with input variables. That's a good choice if it's a value that should be chosen by the user of your module. If it's instead something that would make sense to have "hard-coded", but still used in multiple locations, then you can use a local value:
locals {
ami_id = "ami-a1b2c3d4"
}
resource "aws_instance" "server" {
ami = local.ami
instance_type = "t2.micro"
subnet_id = var.subnet_ids
tags = {
Name = format("ami:%s", local.ami)
}
}
A local value is private to the module that defined it, so this doesn't change the public interface of the module at all. In particular, it would not be possible for the user of this module to change the AMI ID, just like before.
If you have experience with general-purpose programming languages then it might be helpful to think of input variables as being similar to function parameters, while local values are somewhat like local variables inside a function. In this analogy, the module itself corresponds with the function.

terraform variable default value interpolation from locals

I have a use case where I need two AWS providers for different resources. The default aws provider is configured in the main module which uses another module that defines the additional aws provider.
By default, I'd like both providers to use the same AWS credentials unless explicitly overridden.
I figured I could do something like this. In the main module:
locals {
foo_cloud_access_key = aws.access_key
foo_cloud_secret_key = aws.secret_key
}
variable "foo_cloud_access_key" {
type = string
default = local.foo_cloud_access_key
}
variable "foo_cloud_secret_key" {
type = string
default = local.foo_cloud_secret_key
}
where variables foo_cloud_secret_key and foo_cloud_access_key would then be passed down to the child module like this:
module foobar {
...
foobar_access_key = var.foo_cloud_access_key
foobar_secret_key = var.foo_cloud_secret_key
...
}
Where module foobar would then configure its additional was provide with these variables:
provider "aws" {
alias = "foobar_aws"
access_key = var.foobar_access_key
secret_key = var.foobar_secret_key
}
When I run the init terraform spits out this error (for both variables):
Error: Variables not allowed
on variables.tf line 66, in variable "foo_cloud_access_key":
66: default = local.foo_cloud_access_key
Variables may not be used here.
Is it possible to achieve something like this in terraform or is there any other way to go about this?
Having complex, computed default values of variables is possible, but only with a workaround:
define a dummy default value for the variable, e.g. null
define a local variable, its value is either the value of the variable or the actual default value
variable "something" {
default = null
}
locals {
some_computation = ... # based on whatever data you want
something = var.something == null ? local.some_computation : var.something
}
And then only only use local.something instead of var.something in the rest of the terraform files.

Terraform variable to assign using function

variable "cidr" {
type = map(string)
default = {
development = "x.1.0.0/16"
qa = "x.1.0.0/16"
default = "x.1.0.0/16"
}
}
variable "network_address_space" {
default = lookup(var.cidr, var.environment_name,"default")
}
Am getting error that "Error: Function calls not allowed"
variable "subnet_address_space": cidr_subnet2_address_space = cidrsubnet(var.network_address_space,8,1)
A Terraform Input Variable is analogous to a function argument in a general-purpose programming language: its value comes from an expression in the calling module, not from the current module.
The default mechanism allows us to substitute a value for when the caller doesn't specify one, but because variables are intended for getting data into a module from the outside, it doesn't make sense to set the default to something from inside that module: that would cause the result to potentially be something the caller of the module could never actually specify, because they don't have access to the necessary data.
Terraform has another concept Local Values which are roughly analogous to a local variable within a function in a general-purpose programming language. These can draw from function results and other objects in the current module to produce their value, and so we can use input variables and local values together to provide fallback behaviors like you've illustrated in your question:
var "environment_name" {
type = string
}
var "environment_default_cidr_blocks" {
type = map(string)
default = {
development = "10.1.0.0/16"
qa = "10.2.0.0/16"
}
}
var "override_network_range" {
type = string
default = null # If not set by caller, will be null
}
locals {
subnet_cidr_block = (
var.override_network_range != null ?
var.override_network_range :
var.environment_default_cidr_blocks[var.environment_name]
)
}
Elsewhere in the module you can use local.subnet_cidr_block to refer to the final CIDR block selection, regardless of whether it was set explicitly by the caller or by lookup into the table of defaults.
When a module uses computation to make a decision like this, it is sometimes useful for the module to export its result as an Output Value so that the calling module can make use of it too, similar to how Terraform resources also export additional attributes recording decisions made by the provider or by the remote API:
output "subnet_cidr_block" {
value = local.subnet_cidr_block
}
As stated in Interpolate variables inside .tfvars to define another variable by the Hashicorp person, it is intended to be constant by design.
Input variables are constant values passed into the root module, and so they cannot contain interpolations or other expressions that do not yield a constant value.
We cannot use variables in backend either as in Using variables in terraform backend config block.
These are the things we Terraform users tripped on at some point, I suppose.

Pass complex, non-primitive data types to Terraform template provider

Having a more complex list object like this
variable "proxy" {
type = list(object({
enabled = bool
host = string
port = number
user = string
password = string
}))
default = [
{
enabled = false
host = ""
port = 0
user = ""
password = ""
}
]
}
I want to use this in a external template (cloudinit in my case). The template_file directive allows passing variables to a template. Sadly, not for more complex types:
Note that variables must all be primitives. Direct references to lists or maps will cause a validation error.
So something like this
data "template_file" "cloudinit_data" {
template = file("cloudinit.cfg")
vars = {
proxy = var.proxy
}
}
cause the error
Inappropriate value for attribute "vars": element "proxy": string required.
This leads me to two questions:
How can I pass the variable to the template? I assume that I need to convert it to a primitive type like this:
vars = {
proxy_host = var.proxy.host
}
This doesn't work:
This value does not have any attributes.
Is there an alternative way to pass this object directly to the template?
I'm using v0.12.17.
The template_file data source continues to exist only for compatibility with configurations written for Terraform 0.11. Since you are using Terraform 0.12, you should use the templatefile function instead, which is a built-in part of the language and supports all value types.
Because templatefile is a function, you can call it from anywhere expressions are expected. If you want to use the rendered result multiple times then you could define it as a named local value, for example:
locals {
cloudinit_data = templatefile("${path.module}/cloudinit.cfg", {
proxy = var.proxy
})
}
If you only need this result once -- for example, if you're using it just to populate the user_data of a single aws_instance resource -- then you can just write this expression inline in the resource block, to keep everything together and make the configuration (subjectively) easier to read:
resource "aws_instance" "example" {
# ...
user_data = templatefile("${path.module}/cloudinit.cfg", {
proxy = var.proxy
})
}

Resources