Terraform dynamic variable - terraform

I'm creating subnets as part of a seperate terraform template and exporting the IDs as follows.
output "subnet-aza-dev" {
value = "${aws_subnet.subnet-aza-dev.id}"
}
output "subnet-azb-dev" {
value = "${aws_subnet.subnet-azb-dev.id}"
}
output "subnet-aza-test" {
value = "${aws_subnet.subnet-aza-test.id}"
}
output "subnet-azb-test" {
value = "${aws_subnet.subnet-azb-test.id}"
}
...
I'm then intending to lookup these IDs in another template which is reused to provision multiple environments. Example below shows my second template is calling a module to provision an EC2 instance and is passing through the subnet_id.
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-dev}"
}
What I'd like to do is pass the environment variable as part of the lookup for the subnet_id e.g.
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-${var.environment}"
However I'm aware that variable interpolation isn't supported. I've tried using a map inside of the first terraform template to export them all to a 'subnet' which I could then use to lookup from the second template. This didn't work as I was unable to output variables inside of the map.
This sort of design pattern is something I've used previously with CloudFormation, however I'm much newer to terraform. Am I missing something obvious here?

Worked out a way to do this using data sources
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.aws_subnet.subnet-aza.id}"
}
data "aws_subnet" "subnet-aza" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-aza"]
}
}
data "aws_subnet" "subnet-azb" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-azb"]
}
}
Whilst this works and fulfils my original need, I'd like to improve on this by moving the data blocks to within the module, so that there's less repetition. Still working on that one though...

Related

Creating a dynamic secret variable block within Terraform for Cloud Run

I'm trying to create the following block dynamically based on a list of strings
env {
name = "SECRET_ENV_VAR"
value_from {
secret_key_ref {
name = google_secret_manager_secret.secret.secret_id
key = "1"
}
}
}
Based off documentation: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service#example-usage---cloud-run-service-secret-environment-variables
I would like to dynamically add Secrets, and have defined the following dynamic block:
dynamic "env" {
for_each = toset(var.secrets)
content {
name = each.value
value_from {
secret_key_ref {
name = each.value
key = "1"
}
}
}
}
Where secrets is a variable of type list(string)
However, this throws an error: Blocks of type "value_from" are not expected here.
I'm not sure what I'm missing, or where I have incorrectly specified the value_from block.
Could someone point me in the right direction for fixing this up?
UPDATE;
I have also tried to implement this variable as a map, as per the suggestion in the comments on this post. (https://www.terraform.io/docs/language/expressions/dynamic-blocks.html#multi-level-nested-block-structures)
dynamic "env" {
for_each = var.secrets
content {
name = each.key
dynamic "value_from" {
for_each = env.value.name
secret_key_ref {
name = value_from.value.name
key = value_from.value.version
}
}
}
}
However, this also gives the same error. Blocks of type "value_from" are not expected here.
In this example, the secrets variable is defined as a list(any) with this value:
secrets = [
{
name = "SECRET"
version = "1"
}
]
You have to upgrade your gcp provider. Support for secrets in google_cloud_run_service was added in v3.67.0. Current version is v4.1.0, which means that you must be using very old gcp provider.
In the end, I solved this by changing the variable type to a map(any):
secrets = {
"SECRET" = "1"
}
This allowed me to create the "dynamic" env block, without needing to implement the nested dynamic block.

How to render terraform data when using a count

I was using a count for creating multiple AWS task_definitions that should be executed by an AWS step function.
The task_definition required a data "template_file" "task_definition" { section to be able to fill the template data.
Then I needed to render the template data for multiple definitions at a time and I was blocked by an error looking like this:
The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
Here's the initial code:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = data.template_file.task_definition.rendered
}
The point here is that I can't render task_definition because these are not known by terraform yet before the apply. I wasn't able to use the -target argument either because I wanted to make the change in code and not in my deployment pipeline. Meaning when you try to do a terraform plan on the definition_file, the error will pop up.
Solution is below.
What worked was to decouple the use of the count from the .rendered argument by doing this:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
resource "local_file" "foo" {
count = length(var.task_container_command)
content = element(data.template_file.task_definition.*.rendered, count.index)
filename = "task-definition-${count.index}"
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = local_file.foo[count.index].filename
}
Now your data is rendered in the resource called "foo" here and then passed to the step_function module so the terraform plan already knows what's inside your variable. The content element of foo acts like a loop to render each task_definition that I've created using a different filename to avoid duplicates.
Hope this helped :)

Create multiple aws_cloudformation_stack based on parametrized name with Terraform

Is it possible to create multiple CloutFormation stacks with one aws_cloudformation_stack resource definition in terraform, based on parametrized name ?
I have the following resources defined and I would like to have a stack per app_name, app_env build_name combo:
resource "aws_s3_bucket_object" "sam_deploy_object" {
bucket = var.sam_bucket
key = "${var.app_env}/${var.build_name}/sam_template_${timestamp()}.yaml"
source = "../.aws-sam/sam_template_output.yaml"
etag = filemd5("../.aws-sam/sam_template_output.yaml")
}
resource "aws_cloudformation_stack" "subscriptions_sam_stack" {
name = "${var.app_name}---${var.app_env}--${var.build_name}"
capabilities = ["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
template_url = "https://${var.sam_bucket}.s3-${data.aws_region.current.name}.amazonaws.com/${aws_s3_bucket_object.sam_deploy_object.id}"
}
When I run terraform apply when build_name name changes, the old stack gets deleted and a new one created, however I would like to keep the old stack and create a new one
One way would be to define your variable build_name as a list. Then, when you create new build, you just append them to the list, and create stacks with the help of for_each to iterate over the build names.
For example, if you have the following:
variable "app_name" {
default = "test1"
}
variable "app_env" {
default = "test2"
}
variable "build_name" {
default = ["test3"]
}
resource "aws_cloudformation_stack" "subscriptions_sam_stack" {
for_each = toset(var.build_name)
name = "${var.app_name}---${var.app_env}--${each.value}"
capabilities = ["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
template_url = "https://${var.sam_bucket}.s3-${data.aws_region.current.name}.amazonaws.com/${aws_s3_bucket_object.sam_deploy_object.id}"
}
Then if you want second build for the stack, you just extend variable "build_name":
variable "build_name" {
default = ["test3", "new_build"]
}

Iterate Through Map of Maps in Terraform 0.12

I need to build a list of templatefile's like this:
templatefile("${path.module}/assets/files_eth0.nmconnection.yaml", {
interface-name = "eth0",
addresses = element(values(var.virtual_machines), count.index),
gateway = element(var.gateway, count.index % length(var.gateway)),
dns = join(";", var.dns_servers),
dns-search = var.domain,
}),
templatefile("${path.module}/assets/files_etc_hostname.yaml", {
hostname = element(keys(var.virtual_machines), count.index),
}),
by iterating over a map of maps like the following:
variable templatefiles {
default = {
"files_eth0.nmconnection.yaml" = {
"interface-name" = "eth0",
"addresses" = "element(values(var.virtual_machines), count.index)",
"gateway" = "element(var.gateway, count.index % length(var.gateway))",
"dns" = "join(";", var.dns_servers)",
"dns-search" = "var.domain",
},
"files_etc_hostname.yaml" = {
"hostname" = "host1"
}
}
}
I've done something similar with a list of files:
file("${path.module}/assets/files_90-disable-console-logs.yaml"),
file("${path.module}/assets/files_90-disable-auto-updates.yaml"),
...but would like to expand this to templatefiles (above).
Here's the code I've done for the list of files:
main.tf
variable files {
default = [
"files_90-disable-auto-updates.yaml",
"files_90-disable-console-logs.yaml",
]
}
output "snippets" {
value = flatten(module.ingition_snippets.files)
}
modules/main.tf
variable files {}
resource "null_resource" "files" {
for_each = toset(var.files)
triggers = {
snippet = file("${path.module}/assets/${each.value}")
}
}
output "files" {
value = [for s in null_resource.files: s.triggers.*.snippet]
}
Appreciate any help!
Both of these use-cases can be met without using any resource blocks at all, because the necessary features are built in to the Terraform language.
Here is a shorter way to write the example with static files:
variable "files" {
type = set(string)
}
output "files" {
value = tomap({
for fn in var.files : fn => file("${path.module}/assets/${fn}")
})
}
The above would produce a map from filenames to file contents, so the calling module can more easily access the individual file contents.
We can adapt that for templatefile like this:
variable "template_files" {
# We can't write down a type constraint for this case
# because each file might have a different set of
# template variables, but our later code will expect
# this to be a mapping type, like the default value
# you shared in your comment, and will fail if not.
type = any
}
output "files" {
value = tomap({
for fn, vars in var.template_files : fn => templatefile("${path.module}/assets/${fn}", vars)
})
}
Again, the result will be a map from filename to the result of rendering the template with the given variables.
If your goal is to build a module for rendering templates from a source directory to publish somewhere, you might find the module hashicorp/dir/template useful. It combines fileset, file, and templatefile in a way that is hopefully convenient for static website publishing and similar use-cases. (At the time I write this the module is transitioning from being in my personal GitHub account to being in the HashiCorp organization, so if you look at it soon after you may see some turbulence as the docs get updated, etc.)

Using count in nested block

Is there a way in Terraform to use the count param within a nested block? I don't want to create multiple instances of a resource, I want to generate a dynamic number of nested blocks with a resource. As an example:
variable "envNames" {
type = "list"
}
variable "envValues" {
type = "list"
}
resource "test_resource" "example" {
# If length(var.envNames) == 5, I would want 5 env blocks
env {
count = "${length(var.envNames)}"
name = "${element(var.envNames, count.index)}"
value = "${element(var.envValues, count.index)}"
}
}
It looks like in terraform v0.12 I would be able to use the dynamic keyword on the block along with the foreach declaration and a map variable, but is there a way to do this in v0.11?
If it helps, this is for a Kubernetes Deployment resource.

Resources