terraform: data.aws_subnet, value of 'count' cannot be computed - terraform

terraform version 0.11.13
Error: Error refreshing state: 1 error(s) occurred:
data.aws_subnet.private_subnet: data.aws_subnet.private_subnet: value of 'count' cannot be computed
VPC code generated the error above:
resources.tf
data "aws_subnet_ids" "private_subnet_ids" {
vpc_id = "${module.vpc.vpc_id}"
}
data "aws_subnet" "private_subnet" {
count = "${length(data.aws_subnet_ids.private_subnet_ids.ids)}"
#count = "${length(var.private-subnet-mapping)}"
id = "${data.aws_subnet_ids.private_subnet_ids.ids[count.index]}"
}
Change the above code to use count = "${length(var.private-subnet-mapping)}", I successfully provisioned the VPC. But, the output of vpc_private_subnets_ids is empty.
vpc_private_subnets_ids = []
Code provisioned VPC, but got empty list of vpc_private_subnets_ids:
resources.tf
data "aws_subnet_ids" "private_subnet_ids" {
vpc_id = "${module.vpc.vpc_id}"
}
data "aws_subnet" "private_subnet" {
#count = "${length(data.aws_subnet_ids.private_subnet_ids.ids)}"
count = "${length(var.private-subnet-mapping)}"
id = "${data.aws_subnet_ids.private_subnet_ids.ids[count.index]}"
}
outputs.tf
output "vpc_private_subnets_ids" {
value = ["${data.aws_subnet.private_subnet.*.id}"]
}
The output of vpc_private_subnets_ids:
vpc_private_subnets_ids = []
I need the values of vpc_private_subnets_ids. After successfully provisioned VPC use the line, count = "${length(var.private-subnet-mapping)}", I changed code back to count = "${length(data.aws_subnet_ids.private_subnet_ids.ids)}". terraform apply, I got values of the list vpc_private_subnets_ids without above error.
vpc_private_subnets_ids = [
subnet-03199b39c60111111,
subnet-068a3a3e76de66666,
subnet-04b86aa9dbf333333,
subnet-02e1d8baa8c222222
......
]
I cannot use count = "${length(data.aws_subnet_ids.private_subnet_ids.ids)}" when I provision VPC. But, I can use it after VPC provisioned. Any clue?

The problem here seems to be that your VPC isn't created yet and so the data "aws_subnet_ids" "private_subnet_ids" data source read must wait until the apply step, which in turn means that the number of subnets isn't known, and thus the number of data "aws_subnet" "private_subnet" instances isn't predictable and Terraform returns this error.
If this configuration is also the one responsible for creating those subnets then the better design would be to refer to the subnet objects directly. If your module.vpc is also the module creating the subnets then I would suggest to export the subnet ids as an output from that module. For example:
output "subnet_ids" {
value = "${aws_subnet.example.*.id}"
}
Your calling module can then just get those ids directly from module.vpc.subnet_ids, without the need for a redundant extra API call to look them up:
output "vpc_private_subnets_ids" {
value = ["${module.vpc.subnet_ids}"]
}
Aside from the error about count, the configuration you showed also has a race condition because the data "aws_subnet_ids" "private_subnet_ids" block depends only on the VPC itself, and not on the individual VPCs, and so Terraform can potentially read that data source before the subnets have been created. Exporting the subnet ids through module output means that any reference to module.vpc.subnet_ids indirectly depends on all of the subnets and so those downstream actions will wait until all of the subnets have been created.
As a general rule, a particular Terraform configuration should either be managing an object or reading that object via a data source, and not both together. If you do both together then it may sometimes work but it's easy to inadvertently introduce race conditions like this, where Terraform can't tell that the data resource is attempting to consume the result of another resource block that's participating in the same plan.

Related

Terraform: loop over directory to create a single resource

I am trying to create a single GCP Workflows using Terraform (Terraform Workflows documentation here). To create a workflow, I have defined the desired steps and order of execution using the Workflows syntax in YAML (can also be JSON).
I have around 20 different jobs and each of theses jobs are on different .yaml files under the same folder, workflows/. I just want to loop over the /workflows folder and have a single .yaml file to be able to create my resource. What would be the best way to achieve this using Terraform? I read about for_each but it was primarily used to loop over something to create multiple resources rather than a single resource.
workflows/job-1.yaml
- getCurrentTime:
call: http.get
args:
url: https://us-central1-workflowsample.cloudfunctions.net/datetime
result: currentDateTime
workflows/job-2.yaml
- readWikipedia:
call: http.get
args:
url: https://en.wikipedia.org/w/api.php
query:
action: opensearch
search: ${currentDateTime.body.dayOfTheWeek}
result: wikiResult
main.tf
resource "google_workflows_workflow" "example" {
name = "workflow"
region = "us-central1"
description = "Magic"
service_account = google_service_account.test_account.id
source_contents = YAML FILE HERE
Terraform has a function fileset which allows a configuration to react to files available on disk alongside its definition. You can use this as a starting point for constructing a suitable expression for for_each:
locals {
workflow_files = fileset("${path.module}/workflows", "*.yaml")
}
It looks like you'd also need to specify a separate name for each workflow, due to the design of the remote system, and so perhaps you'd decide to set the name to be the same as the filename but with the .yaml suffix removed, like this:
locals {
workflows = tomap({
for fn in local.workflow_files :
substr(fn, 0, length(fn)-5) => "${path.module}/workflows/${fn}"
})
}
This uses a for expression to project the set of filenames into a map from workflow name (trimmed filename) to the path to the specific file. The result then would look something like this:
{
job-1 = "./module/workflows/job-1.yaml"
job-2 = "./module/workflows/job-2.yaml"
}
This now meets the requirements for for_each, so you can refer to it directly as the for_each expression:
resource "google_workflows_workflow" "example" {
for_each = local.workflows
name = each.key
region = "us-central1"
description = "Magic"
service_account = google_service_account.test_account.id
source_contents = file(each.value)
}
Your question didn't include any definition for how to populate the description argument, so I've left it set to hard-coded "Magic" as in your example. In order to populate that with something reasonable you'd need to have an additional data source for that, since what I wrote above is already making full use of the information we get just from scanning the content of the directory.
resource "google_workflows_workflow" "example" {
# count for total iterations
count = 20
name = "workflow"
region = "us-central1"
description = "Magic"
service_account = google_service_account.test_account.id
# refer to file using index, index starts from 0
source_contents = file("${path.module}/workflows/job-${each.index}.yaml")
}

Terraform - Create or not create resources based on conditions

I need my resources to be created on specified environments. For example, if I have a AWS Lambda that is not ready for production, I need it to only exist in development environment. Is there a nice way to do this? I know that it's possible to set count to 0, but I'm not sure how to cascade this decision to other resources.
For example, I have a resource for an AWS Lambda and the count is set to 0.
resource "aws_lambda_function" "example_lambda" {
count ? local.is_production ? 0 : 1
}
How do I cascade this decision to other resources that depends on the AWS Lambda above?
And let's say I have a S3 Bucket which will invoke the Lambda function.
resource "aws_s3_bucket" "example_bucket" {
bucket = "bucket_name"
}
resource "aws_lambda_permission" "example_bucket_etl" {
statement_id = "AllowExecutionFromS3Bucket"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.example_lambda.arn
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.example_bucket.arn
}
resource "aws_s3_bucket_notification" "bucket_notification" {
bucket = aws_s3_bucket.example_bucket.id
lambda_function {
lambda_function_arn = aws_lambda_function.example_lambda.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "example_bucket/"
filter_suffix = ".txt"
lambda_function {
lambda_function_arn = aws_lambda_function.another_lambda_function.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "another_example_bucket/"
filter_suffix = ".txt"
}
}
You can use the same count variable on multiple resources. A nicer and clear way would be to add all resources into a module, if that is possible in your code. https://www.terraform.io/docs/language/meta-arguments/count.html
When you use count in a resource block, that makes Terraform treat references to that resource elsewhere as producing a list of objects representing each of the instances of that resource.
Since that value is just a normal list value, you can take its length in order to concisely write down what is essentially the statement "there should be one Y for each X", or in your specific case "there should be one lambda permission for each lambda function".
For example:
resource "aws_lambda_function" "example" {
count = local.is_production ? 0 : 1
# ...
}
resource "aws_lambda_permission" "example_bucket_etl" {
count = length(aws_lambda_function.example)
function_name = aws_lambda_function.example[count.index].name
# ...
}
Inside the aws_lambda_permission configuration we first set the count to be whatever is the count of the aws_lambda_function.example, which tells Terraform that we intend for the counts of these to always match. That connection helps Terraform understand how to resolve situations where you increase or reduce the count, by hinting that the resulting create/destroy actions will need to happen in a particular order in order to be valid. We then use count.index to refer to indices of the other resource, which in this case will only ever be zero but again helps Terraform understand our intent during validation.
The lambda_function nested block inside aws_s3_bucket_notification requires a slightly different strategy, since in that case we're not creating a separate resource instance per lambda function but instead just generating some dynamic configuration blocks inside a single resource instance. For that situation, we can use dynamic blocks which serve as a sort of macro for generating multiple blocks based on elements of a collection:
resource "aws_s3_bucket_notification" "bucket_notification" {
bucket = aws_s3_bucket.example_bucket.id
dynamic "lambda_function" {
for_each = aws_lambda_function.example
content {
# "lambda_function" in this block is the iterator
# symbol, so lambda_function.value refers to the
# current element of aws_lambda_function.example.
lambda_function_arn = lambda_function.value.arn
# ...
}
}
}
Again this is relying on the fact that aws_lambda_function.example is a list of objects, but in a different way: we ask Terraform to generate a lambda_function block for each element of aws_lambda_function.example, setting lambda_function.value to the whole aws_lambda_function object corresponding to each block. We can therefore access the .arn attribute from that object to get the corresponding ARN that we need to populate the lambda_function_arn argument inside the block.
Again, for this case there will only ever be zero or one lambda function objects and therefore only zero or one lambda_function blocks, but in both cases this pattern generalizes to other values of count, ensuring that all of these will stay aligned as your configuration evolves.

how to pass list input to aws vpc elb in terraform

here i'm trying to provision a aws classic ELB in a VPC where i have 2 public subnets. These subnets are also provisioned by terraform and i'm trying to pass both the subnets ids to elb module.SO the problem is i'm not able to give list input to elb subnets field
public_subnet variable works fine as i have used it for route table association it's just that i'm not able to handle the list and give it as input to vpc.
it works if i use subnets = [var.public_subnet.0,var.public_subnet.1]
here's my code
resource "aws_elb" "webelb" {
name = "foobar-terraform-elb"
#availability_zones = [var.public_subnet]
subnets = [var.public_subnet]
#
#
#
}
variable "public_subnet" {
type = list
}
subnet.tf
output "public_subnet" {
value = aws_subnet.public.*.id
}```
Error:
```Error: Incorrect attribute value type
on elb/elb.tf line 4, in resource "aws_elb" "webelb":
4: availability_zones = [var.public_subnet]
Inappropriate value for attribute "availability_zones": element 0: string
required.```
Since var.public_subnet is already a list. [var.public_subnet] is equivalent to [["192.168.0.0/32"]] instead of the expected, un-nested input ["102.168.0.0/32"]
ie...just use var.public_subnet

Cannot get egress attribute from terraform data source aws_security_group

I have a tf data resource defined.
data "aws_security_group" "this_sg" {
filter {
name = "group-name"
values = ["this_sg"]
}
vpc_id = "vpc-12345678"
}
I can pull out common arguments listed from here fine.
https://www.terraform.io/docs/providers/aws/r/security_group.html#name
${data.aws_security_group.this_sg.name}
${data.aws_security_group.this_sg.id}
but i can't pull the egress map
${data.aws_security_group.this_sg.egress}
my intention is to grab the list of cidr_blocks from within the egress map, likely through some horrendous mix of interpolations. But the egress argument can't seem to be pulled at all.
(i've checked the aws console egress cidr values are there)
Error: Error running plan: 1 error(s) occurred:
* Resource 'data.aws_security_group.this_sg' does not have attribute 'egress' for variable 'data.aws_security_group.this_sg.egress'
some ideas or work arounds would be much appreciated.

Terraform target aws_volume_attachment with only its corresponding aws_instance resource from a list

I am not able to target a single aws_volume_attachment with its corresponding aws_instance via -target.
The problem is that the aws_instance is taken from a list by using count.index, which forces terraform to refresh all aws_instance resources from that list.
In my concrete case I am trying to manage a consul cluster with terraform.
The goal is to be able to reinit a single aws_instance resource via the -target flag, so I can upgrade/change the whole cluster node by node without downtime.
I have the following tf code:
### IP suffixes
variable "subnet_cidr" { "10.10.0.0/16" }
// I want nodes with addresses 10.10.1.100, 10.10.1.101, 10.10.1.102
variable "consul_private_ips_suffix" {
default = {
"0" = "100"
"1" = "101"
"2" = "102"
}
}
###########
# EBS
#
// Get existing data EBS via Name Tag
data "aws_ebs_volume" "consul-data" {
count = "${length(keys(var.consul_private_ips_suffix))}"
filter {
name = "volume-type"
values = ["gp2"]
}
filter {
name = "tag:Name"
values = ["${var.platform_type}.${var.platform_id}.consul.data.${count.index}"]
}
}
#########
# EC2
#
resource "aws_instance" "consul" {
count = "${length(keys(var.consul_private_ips_suffix))}"
...
private_ip = "${cidrhost(aws_subnet.private-b.cidr_block, lookup(var.consul_private_ips_suffix, count.index))}"
}
resource "aws_volume_attachment" "consul-data" {
count = "${length(keys(var.consul_private_ips_suffix))}"
device_name = "/dev/sdh"
volume_id = "${element(data.aws_ebs_volume.consul-data.*.id, count.index)}"
instance_id = "${element(aws_instance.consul.*.id, count.index)}"
}
This works perfectly fine for initializing the cluster.
Now I make a change in my user_data init script of the consul nodes and want to rollout node by node.
I run terraform plan -target=aws_volume_attachment.consul_data[0] to reinit node 0.
This is when I run into the above mentioned problem, that terraform renders all aws_instance resources because of instance_id = "${element(aws_instance.consul.*.id, count.index)}".
Is there a way to "force" tf to target a single aws_volume_attachment with only its corresponding aws_instance resource?
At the time of writing this sort of usage is not possible due to the fact that, as you've seen, an expression like aws_instance.consul.*.id creates a dependency on all the instances, before the element function is applied.
The -target option is not intended for routine use and is instead provided only for exceptional circumstances such as recovering carefully from an unintended change.
For this specific situation it may work better to use the ignore_changes lifecycle setting to prevent automatic replacement of the instances when user_data changes, like this:
resource "aws_instance" "consul" {
count = "${length(keys(var.consul_private_ips_suffix))}"
...
private_ip = "${cidrhost(aws_subnet.private-b.cidr_block, lookup(var.consul_private_ips_suffix, count.index))}"
lifecycle {
ignore_changes = ["user_data"]
}
}
With this set, Terraform will detect but ignore changes to the user_data attribute. You can then get the gradual replacement behavior you want by manually tainting the resources one at a time:
$ terraform taint aws_instance.consul[0]
On the next plan, Terraform will then see that this resource instance is tainted and produce a plan to replace it. This gives you direct control over when the resources are replaced, so you can therefore ensure that e.g. the consul leave step gets a chance to run first, or whatever other cleanup you need to do.
This workflow is recommended over -target because it makes the replacement step explicit. -target can be confusing in a collaborative environment because there is no evidence of its use, and thus no clear explanation of how the current state was reached. taint, on the other hand, explicitly marks your intention in the state where other team members can see it, and then replaces the resource via the normal plan/apply steps.

Resources