declare a variable using `execute` Interpolation in Terraform - terraform

I want to declare a a sub-string of a variable to another variable. I tested taking a sub-string using terraform console.
> echo 'element(split (".", "10.250.3.0/24"), 2)' | terraform console
> 3
my subnet is 10.250.3.0/24 and I want my virtual machine to get private IP address within this subnet mask 10.250.3.6. I want this to get automatically assign by looking at subnet address. What I've tried;
test.tf
variable subnet {
type = "string"
default = "10.250.3.0/24"
description = "subnet mask myTestVM will live"
}
variable myTestVM_subnet {
type = "string"
default = "10.250." ${element(split(".", var.trusted_zone_onpremises_subnet), 2)} ".6"
}
And then I test it by
terraform console
>Failed to load root config module: Error parsing /home/anum/test/test.tf: At 9:25: illegal char
I guess its just simple syntax issue. but couldn't figure out what!

As you've seen, you can't interpolate the values of variables in Terraform.
You can, however, interpolate locals instead and use those if you want to avoid repeating yourself anywhere.
So you could do something like this:
variable "subnet" {
type = "string"
default = "10.250.3.0/24"
description = "subnet mask myTestVM will live"
}
locals {
myTestVM_subnet = "10.250.${element(split(".", var.trusted_zone_onpremises_subnet), 2)}.6"
}
resource "aws_instance" "instance" {
...
private_ip = "${local.myTestVM_subnet}"
}
Where the aws_instance is just for demonstration and could be any resource that requires/takes an IP address.
As a better option in this specific use case though you could use the cidrhost function to generate the host address in a given subnet.
So in your case you would instead have something like this:
resource "aws_instance" "instance" {
...
private_ip = "${cidrhost(var.subnet, 6)}"
}
Which would create an AWS instance with a private IP address of 10.250.3.6. This can then make it much easier to create a whole series of machines that increment the IP address used by using the count meta parameter.

Terraform doesn't allows interpolations declaration of variables in default. So I get ;
Error: variable "myTestVM_subnet": default may not contain interpolations
and the syntax error really got fixed after banging my head, so here is what Terraform likes;
private_ip_address = "10.250.${element(split(".", "${var.subnet}"), 2)}.5"

Related

Terraform . How to pass multiple values in command line using list (string) in variable.tf file?

I have a simple main and variable files for deploying webapp for containers in Azure.
But I would like that terraform plan uses variables from the command line to choose names like follows:
terraform plan -var resource_group_name=my-rg
This worked perfectly commenting the name of the default value for the RG like this.
main.tf
data "azurerm_resource_group" "my-rg" {
name = var.resource_group_name
}
variable.tf
variable "resource_group_name" {
# default = "Search-API"
}
But if I want to do the same for a list string I don´t know how to do it. I want to be able to do something that If I put 2 names 2 webapps are going to be created, if I put 3, 3 webapps and so.
I tried with this (also commenting default value) :
main.tf
resource "azurerm_app_service" "azure-webapp" {
count = length(var.webapp_server_name)
name = var.webapp_server_name[count.index]
variable.tf
variable "webapp_server_name" {
description = "Create Webapp with following names"
type = list(string)
#default = ["webapp-a", "webapp-b", "webapp-c"]
But I´m getting:
terraform plan -var webapp_server_name=webapp-a
Error: Variables not allowed
on <value for var.webapp_server_name> line 1:
(source code not available)
Variables may not be used here.
I also tried with empty string like:
variable "webapp_server_name" {
description = "Create Webapp with following names"
type = list(string)
default = []
}
Is there a way to do such a thing with terraform? to define an empty list and pass values (one, two, or more) from command?
thanks
UPDATE
Tried like this, following this post but now is asking to put the value even though I´m passing it through command line
terraform plan -var 'listvar=["webapp-a"]'
var.webapp_server_name
Create Webapp with following names
Enter a value:
If there is a variable declaration:
variable "webapp_server_name" {
description = "Create Webapp with following names"
type = list(string)
#default = ["webapp-a", "webapp-b", "webapp-c"]
}
You could use it like this with \ to escape the quotes".
terraform plan -var 'webapp_server_name=[\"webapp-a\", \"webapp-b\", \"webapp-c\"]'
For example, it worked with using the latest terraform provider version Terraform v0.13.4.
When you pass in the variable from the command line with -var webapp_server_name=webapp-a you are passing it in as a string.
But you've defined the variable as a list. So based on the docs you'll want the command line to look something like:
terraform plan -var='webapp_server_name=["webapp-a"]'

Get a list of possible outbound ip addresses in terraform

I'm trying to use the export from a azure function app in terraform to get the possible outbound ip addresses that I can add to a whitelist for a firewall
The parameter returned is a string of ips comma separated.
I have tried using the split function within terraform but it doesn't give a list, it gives an interface which can't be used as a list. I've tried using local scopes to add square brackets around it but still the same.
Let me just add this is terraform 11 not 12.
resource "azurerm_key_vault" "keyvault" {
name = "${var.project_name}-${var.environment}-kv"
location = "${azurerm_resource_group.environment.location}"
resource_group_name = "${azurerm_resource_group.environment.name}"
enabled_for_disk_encryption = true
tenant_id = "${var.tenant_id}"
sku_name = "standard"
network_acls {
bypass = "AzureServices"
default_action = "Deny"
ip_rules = "${split(",", azurerm_function_app.function.possible_outbound_ip_addresses)}"
}
tags = {
asset-code = "${var.storage_tags["asset_code"]}"
module-code = "${var.storage_tags["module_code"]}"
environment = "${var.environment}"
instance-code = "${var.storage_tags["instance_code"]}"
source = "terraform"
}
}
This comes back with the error "ip_rules must be a list".
Thanks
I think what you are seeing here is a classic Terraform 0.11 design flaw: when a value is unknown at plan time (because it will be decided only during apply), Terraform 0.11 can't properly track the type information for it.
Because possible_outbound_ip_addresses is an unknown value at planning time, the result of split with that string is also unknown. Because Terraform doesn't track type information for that result, the provider SDK code rejects that unknown value because it isn't a list.
To address this in Terraform 0.11 requires doing your initial run with the -target argument so that Terraform can focus on creating the function (and thus allocating its outbound IP addresses) first, and then deal with the processing of that string separately once it's known:
terraform apply -target=azurerm_function_app.function
terraform apply # to complete the rest of the work that -target excluded
Terraform 0.12 addressed this limitation by tracking type information for both known and unknown values, so in Terraform 0.12 the split function would see that you gave it an unknown string and accept that as being correctly typed, and then it would return an unknown list of strings to serve as a placeholder for the result that will be finally determined during the apply phase.
If is var.string is 1.2.3.4,5.6.7.8-
split(',', var.string)[0] should give you back 1.2.3.4 as a string. Your questions is difficult without an example.
Here is an example of how I can get a list of possible IPs
create a data source and then a locals var
app_services = [ "app1", "app2", "app3" ]
data "azurerm_app_service" "outbound_ips" {
count = length(var.app_services)
name = var.app_services[count.index]
resource_group_name = var.server_resource_group_name
}
locals {
apps_outbound_ips = distinct(flatten(concat(data.azurerm_app_service.outbound_ips.*.possible_outbound_ip_address_list)))
}
You don't have to use a data source either, if you are building the resource just use the outputs instead of a data source, in my case I use a data source as I build my apps separately.
Works flawlessly for me and produces a list of strings (Strings being each unique outbound IP of the set of app services / function apps) in the form of local.apps_outbound_ips
Enjoy :)

terraform route53 resolver setup

Just been trying to use the new terraform aws_route53_resolver_endpoint resource. It takes the subnet ids as a block type list. Unfortunately there appears to be no way to populate this from a list of subnets read from an output variable from the previous step.
Basically I have a set of subnets created using the count on the subnet resources in a previous step. Im trying to use these and setup aws_route53_resolver_endpoint in each of these subnets:
resource "null_resource" "management_subnet_list" {
count = "${length(var.subnet_ids)}"
triggers {
subnet_id = "${element(data.terraform_remote_state.app_network.management_subnet_ids, count.index)}"
}
}
resource "aws_route53_resolver_endpoint" "dns_endpoint" {
name = "${var.environment_name}-${var.network_env}-dns"
direction = "OUTBOUND"
security_group_ids = ["${var.security_groups}"]
ip_address = "${null_resource.management_subnet_list.*.triggers}"
}
The above when run, results in an error: ip_address: should be a list
If I modify the code as follow:
ip_address = ["${null_resource.management_subnet_list.*.triggers}"]
I get the error: ip_address: attribute supports 2 item as a minimum, config has 1 declared
I can't seem to figure out any other way to create the resource list dynamically from a list of subnets.
Any help will be appreciated.
Per the resource reference for aws_route53_resolver_endpoint, the subnet_id in the ip_address block is a single string value.
To specify multiple subnets, you need to have multiple ip_address blocks.
Since you state that you're creating subnets with a count argument, you could potentially reference each individually with the index like: aws_subnet.main[0].id, aws_subnet.main[1].id and so on, each in it's own ip_address block. (or for Terraform 0.11, I think it was "${aws_subnet.main.0.id}".)
However, a better way would be to use the Dynamic Blocks available in Terraform 0.12 +
Dynamic Blocks allow you to create repeatable nested blocks within top-level blocks.(resource, data, provider, and provisioner blocks currently support dynamic blocks).
A dynamic ip_address block within the aws_route53_resolver_endpoint resource could look like:
dynamic "ip_address" {
for_each = aws_subnet.main[*].id
iterator = subnet
content {
subnet_id = subnet.value
}
}
Which would result in a separate ip_address nested block for each subnet created in the aws_subnet.main resource.
The for_each argument is the complex value to iterate over. It accepts accepts any collection or structural value, typically a list or map with one element per desired nested block.
For complete info on the dynamic nested block expression, see the Terraform documentation at: https://www.terraform.io/docs/language/expressions/dynamic-blocks.html

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}"
}

How to approach repeatable items in Terraform

Say that I need to provision a large number of vpc subnets in terraform. Each subnet has a cidr, a name and a availability zone. So in other config management tools I'd do something like:
[
{
"name":"subnet1",
"cidr":"10.0.0.1/24",
"az":"us-west-1a"
},
{
"name":"subnet2",
"cidr":"10.0.0.2/24",
"az":"us-west-1b"
}
]
And then iterate over that array.
Terraform doesn't have a notion of arrays/objects as far as I can see. So, for arrays of single attributes I would just use a list item:
subnets: ["10.0.0.1/24","10.0.0.2/24"]
But that doesn't allow me to name or place the subnets where I want.
I know that I can also use multiple lists in Terraform, something like:
subnet_names: ["subnet1", "subnet2"]
subnets: ["10.0.0.1/24","10.0.0.2/24"]
subnet_az: ["us-west-1a", "us-west-1b"]
But that strikes me as messy and counter-intuitive. The last option I see is to mash everything togehter into an ugly list of strings, and then split them apart in Terraform:
things: ["subnet1__10.0.0.1/24__us-west-1a","subnet2__10.0.0.2/24__us-west-2a"]
But thats just ugly.
How can I deal with array/object-type of repeats in Terraform? For now I've just explicitly defined all my things, which caused a simple vpc definition to be 300 lines long :-(
As you've seen, at present Terraform doesn't support lists of structured data like you're trying to create here.
Having multiple flat lists of strings as you showed in your question is one common solution to this problem. It works, but as you've seen it's somewhat counter-intuitive to keep track of which values belong together that way.
An alternative approach that is likely to produce a more readable and maintainable result is to factor your aws_subnet resource out into a module that takes care of the elements that are always the same for all subnets. Then you can instantiate the module once per subnet, providing only the values that vary:
module "subnet1" {
source = "./subnet"
name = "subnet1"
cidr = "10.0.0.1/24"
az = "us-west-1a"
}
module "subnet2" {
source = "./subnet"
name = "subnet2"
cidr = "10.0.0.2/24"
az = "us-west-1b"
}
In many cases there's some sort of systematic relationship between AZs and CIDR blocks. If that's true for you then you can also use your module to encode these numbering rules. For example, in your subnet module:
variable "region_network_numbers" {
default = {
"us-west-1" = 0
"us-east-1" = 1
"us-west-2" = 2
}
}
variable "az_network_numbers" {
default = {
a = 1
b = 2
}
}
variable "base_cidr_block" {
default = "10.0.0.0/8"
}
variable "az" {
}
data "aws_availability_zone" "selected" {
name = "${var.az}"
}
resource "aws_subnet" "main" {
cidr_block = "${cidrsubnet(cidrsubnet(var.base_cidr_block, 8, var.region_network_numbers[data.aws_availability_zone.selected.region]), 4, var.az_network_numbers[data.aws_availability_zone.selected.name_suffix])}"
# ...
}
With this it's sufficient to provide just the az argument to the module, with the cidr and name produced systematically from the AZ name. This is the same general idea as shown in the example for the aws_availability_zone data source, and there's a more complete, elaborate example of this in the Terraform repository itself.

Resources