Terraform: pass a list of security groups - terraform

I try to pass a list of security groups to create ec2 instance.
variables.tf
variable "parameters" {
type = map(any)
}
terraform.tfvars.json
{
"parameters": {
"ami": "ami1234",
"vpc_security_group_ids": "sg-1234,sg-wxyz"
}
}
Note, teffaform does not allow to use list for security groups as it requires all element type of map be the same. So I have to use comma-separated string.
resource "aws_instance" "worker" {
...
vpc_security_group_ids = ["${split(",",var.parameters.vpc_security_group_ids)}"]
}
I copy some online code to split the string, but terraform complains that the variable is only known after apply.

As I think you've already understood, the any in map(any) represents asking Terraform to automatically infer the element type of the map, and so Terraform will study the given value and automatically choose a single concrete type to replace any. In the case of your example here, the result would be map(string) because all of the values in your map are strings.
However, the example you've described here appears to me to more suited to be an object type rather than a map. Maps are intended for arbitrary key/value pairs where all of the elements represent the same kind of thing and can therefore be of the same type. Object types are for describing a single thing that has multiple different properties of different types.
Therefore I would suggest rewriting your type constraint to properly describe the data structure you're expecting, which seems to be the following based on context:
variable "parameters" {
type = object({
ami = string
vpc_security_group_ids = set(string)
})
}
set(string) matches the provider's type constraint for vpc_security_group_ids in aws_instance, since security groups don't have any meaningful ordering when associated with an EC2 instance and so it wouldn't make sense to use a list.
With this data type in place then you should be able to just assign the variable values directly, because they will already be of the expected types:
resource "aws_instance" "worker" {
ami = var.parameters.ami
vpc_security_group_ids = var.parameters.vpc_security_group_ids
}

Related

Terraform resource as a module input variable

When developing a terraform module, I sometimes find myself in the need to define different input variables for the same resources. For example, right now I need the NAME and ARN of the same AWS/ECS cluster for my module, so, I defined two variables in my module: ecs_cluster_arn and ecs_cluster_name.
For the sake of DRY, it would be very nice if I could just define the input variable ecs_cluster of type aws_ecs_cluster and the just use whatever I need inside my module.
I can't seem to find a way to do this. Does anyone know if it's possible?
You can define an input variable whose type constraint is compatible with the schema of the aws_ecs_cluster resource type. Typically you'd write a subset type constraint that contains only the attributes the module actually needs. For example:
variable "ecs_cluster" {
type = object({
name = string
arn = string
})
}
Elsewhere in the module, you can use var.ecs_cluster.name and var.ecs_cluster.arn to refer to those attributes. The caller of the module can pass in anything that's compatible with that type constraint, which includes a whole instance of the aws_ecs_cluster resource type, but would also include a literal object containing just those two attributes:
module "example" {
# ...
ecs_cluster = aws_ecs_cluster.example
}
module "example" {
# ...
ecs_cluster = {
name = "blah"
arn = "arn:aws:yada-yada:blah"
}
}
In many cases this would also allow passing the result of the corresponding data source instead of the managed resource type. Unfortunately for this pairing in particular the data source for some reason uses the different attribute name cluster_name and therefore isn't compatible. That's unfortunate, and not the typical design convention for pairs of managed resource type and data source with the same name; I assume it was a design oversight.
module "example" {
# ...
# This doesn't actually work for the aws_ecs_cluster
# data source because of a design quirk, but this would
# be possible for most other pairings such as
# the aws_subnet managed resource type and data source.
ecs_cluster = data.aws_ecs_cluster.example
}

Get type of a variable in Terraform

Is there a way to detect the type of a variable in Terraform? Say, I have a module input variable of type any, can I do some kind of switch, depending on the type?
variable "details" {
type = any
}
local {
name = var.details.type == map ? var.details["name"] : var.details
}
What I want to archive is, to be able to pass either a string as shorthand or a complex object with additional keys.
module "foo" {
details = "my-name"
}
or
module "foo" {
details = {
name = "my-name"
age = "40"
}
}
I know this example doesn't make much sense and you would like to suggest to instead use two input vars with defaults. This example is just reduced to the minimal (non)working example. The end goal is to have a list of IAM policy statements, so it is going to be a list of lists of objects.
Terraform v0.12.20 introduced a new function try which can be used to concisely select between different ways of retrieving a value, taking the first one that wouldn't produce an error.
variable "person" {
type = any
# Optional: add a validation rule to catch invalid types,
# though this feature remains experimental in Terraform v0.12.20.
# (Since this is experimental at the time of writing, it might
# see breaking changes before final release.)
validation {
# If var.person.name succeeds then var.person is an object
# which has at least the "name" attribute.
condition = can(var.person.name) || can(tostring(var.person))
error_message = "The \"person\" argument must either be a person object or a string giving a person's name."
}
}
locals {
person = try(
# The value of the first successful expression will be taken.
{name = tostring(var.person)}, # If the value is just a string
var.person, # If the value is not a string (directly an object)
)
}
Elsewhere in the configuration you can then write local.person.name to obtain the name, regardless of whether the caller passed an object or a string.
The remainder of this answer is an earlier response that now applies only to Terraform versions between v0.12.0 and v0.12.20.
There is no mechanism for switching behavior based on types in Terraform. Generally Terraform favors selecting specific types so that module callers are always consistent and Terraform can fully validate the given values, even if that means a little extra verbosity in simpler cases.
I would recommend just defining details as an object and having the caller explicitly write out the object with the name attribute, in order to be more explicit and consistent:
variable "details" {
type = object({
name = string
})
}
module "example" {
source = "./modules/example"
details = { name = "example" }
}
If you need to support two different types, the closest thing in the Terraform language would be to define two variables and detect which one is null:
variable "details" {
type = object({
name = string
})
default = null
}
variable "name" {
type = string
default = null
}
local {
name = var.name != null ? var.name : var.details.name
}
However since there is not currently a way to express that exactly one of those two must be specified, the module configuration you write must be ready to deal with the possibility that both will be set (in the above example, var.name takes priority) or that neither will be set (in the above example, the expression would produce an error, but not a very caller-friendly one).
terraform v1.0+ introduces a new function type() for this purpose. See https://www.terraform.io/language/functions/type

How to create unstructured data in terraform

I'm trying to create configurations in terraform that I can later pass to modules (I'm doing this to work around the lack of "count" in modules).
The closest thing I got was using a null_data_source but the problem with that is that it only supports a single level of properties in inputs:
data "null_data_source" "my_data" {
count = var.my_data_count
inputs = {
settings = { ... } //this doesn't work
}
}
Then I looked at the docs of how to create a custom provider but couldn't work around the types that terraform supports - TypeMap will automatically turned into map[string]string unless I pass in the Elem property but that also only accepts terraform defined types (it doesn't accept standard golang types e.g.: map[string]interface{} or interface{}).
Does anyone know a way to get unstructured data as config like this?
There is no such thing as "unstructured data" in Terraform: every value has an associated type. However, in Terraform 0.12 introduced two structural types that allow for different element/attribute types to be mixed together inside a single value, which is not possible for the collection types.
You can use Local Values if you need to factor out the expressions for these structural values for use in multiple locations:
locals {
your_data = {
settings = {
foo = "bar"
baz = []
}
}
}
Although the details of this often don't matter, Terraform will see the above as being of the following type:
object({
settings = object({
foo = string
baz = tuple([])
})
})
As the author of a module, you can associated with each variable a type constraint that can both check that the given value has the appropriate type and give Terraform some hints to interpret such a value differently. For example, if baz in the above example were a list of strings whose length isn't fixed by the module (often the case) then you can specify it as such in your type constraint:
variable "example" {
type = object({
settings = object({
foo = string
baz = list(string)
})
})
}
Then the caller can pass in the local value we constructed earlier:
module "example" {
source = "./modules/example"
example = local.your_data
}
Terraform will then take the tuple([]) value from the local value and convert it automatically to list(string), in this case creating an empty list of strings.
For Terraform 0.11 your options are more limited, because it does not have structural types. In that case, the usual approach is to flatten the structure into many separate variables and set them separately, but then it's not possible to conveniently construct them all in one place and pass them as a single value.

What is the iterator feature for in Terraform's for_each?

I am trying to understand the iterator feature of the for_each in Terraform 0.12. The docs say:
Iterator:
The iterator argument (optional) sets the name of a temporary variable that represents the current element of the complex value. If omitted, the name of the variable defaults to the label of the dynamic block ...
But I can't find any code examples that uses this feature and I can't get my head around what it is for. I have read the Terraform 0.12 preview but it is not mentioned there, and I found some GitHub issues (e.g. this one) but can't find clues there either.
Is it just for improving readability? I would really appreciate a code example and explanation that goes beyond what I can find in the docs.
Basically in various languages like Python, Ruby, C++, Javascript, Groovy, etc. you can establish a temporary variable within a lambda (especially if it is iterative) that stores the temporary value per iteration within the lambda. In some languages (e.g. Groovy), there is a default name for this variable, or you can set one yourself (i.e. default variable name in Groovy is it). For example, in Groovy we have:
strings.each() {
print it
}
would print the content of the string variable assignment (assuming it can be cast to String). The following code has the exact same functionality:
strings.each() { a_string ->
print a_string
}
where we have explicitly named the temporary variable as a_string. This is analogous to the iterator argument in your question. So in Terraform, we see an example in the documentation:
resource "aws_security_group" "example" {
name = "example" # can use expressions here
dynamic "ingress" {
for_each = var.service_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
}
According to the documentation:
If omitted, the name of the variable defaults to the label of the dynamic block
and the name above is ingress (notice it is the label specified adjacent to the dynamic block). Sure enough, we see the name of the temporary variable above is ingress and it is being accessed via ingress.value. To utilize the functionality of iterator to rename this temporary variable, we can do something like the below.
resource "aws_security_group" "example" {
name = "example" # can use expressions here
dynamic "ingress" {
for_each = var.service_ports
iterator = "service_port"
content {
from_port = service_port.value
to_port = service_port.value
protocol = "tcp"
}
}
}
thus renaming the temporary variable storing the element of var.service_ports in each iteration within the lambda from default name ingress to service_port. The primary added value I see in this (and likewise when I use it in Groovy for Jenkins Pipeline libraries) is to provide a more clear name for the temporary variable storing the value to improve readability.

Need to understand terraform resource structure

I am reading terraform and found below code:
resource "aws_vpc" "vpc_main" {
cidr_block = "10.0.0.0/16"
tags {
Name = "Main VPC"
}
}
Here I could not understand what vpc_main stands for in the resource definition. Could somebody explain?
It's a user-defined name for the resource - without this you wouldn't be able to distinguish multiple instances of the same resource type.
See the docs for more details.
Variable types and names in other programming languages are a good analogy. For example, in Java, you can declare a variable as follows:
String foo
The type of the variable is String and the name of the variable is foo. You must specify a name so you can (a) distinguish it from other variables of the same type and (b) refer to it later.
In Terraform, it's essentially the same idea:
resource "aws_instance" "foo" { ... }
Here, the type of the resource is aws_instance and the name is foo. You must specify a name so you can (a) distinguish it from other resources of the same type and (b) refer to it later. For example, to output the public IP address of that Instance, you could do the following:
output "public_ip" {
value = "${aws_instance.foo.public_ip}"
}

Resources