In short, I have a resource and a dynamic block both use for_each for a string value which is nullable by default. In a dynamic block, the handling is different from the resources. Can someone explain why and what's behind it?
Now the long details:
That's the input
variable "kms_arn" {
description = "ARN of KMS fetched"
type = string
nullable = true
default = null
}
variable "policy" {
description = "Bucket policy which is assigned to bucket and allows access to it"
type = string
nullable = true
default = null
}
This is the dynamic block which uses != null ? [0] : []
resource "aws_s3_bucket_server_side_encryption_configuration" "enc" {
bucket = aws_s3_bucket.bucket.id
rule {
dynamic "apply_server_side_encryption_by_default" {
for_each = var.kms_arn != null ? [0] : []
content {
kms_master_key_id = var.kms_arn
sse_algorithm = "aws:kms"
}
}
dynamic "apply_server_side_encryption_by_default" {
for_each = var.kms_arn == null ? [0] : []
content {
sse_algorithm = "AES256"
}
}
}
}
This is the resources block and here I have to use != null ? toset(["1"]) : []
as without toset
and a string, it is not working and complains.
resource "aws_s3_bucket_policy" "policy" {
for_each = var.policy != null ? toset(["1"]) : []
bucket = aws_s3_bucket.bucket.id
policy = var.policy
}
If I use the same way I use for dynamic blocks for the resource as well, I get the following error.
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Invalid for_each argument
│
│ on modules/ressources/s3/main.tf line 64, in resource "aws_s3_bucket_policy" "policy":
│ 64: for_each = var.policy != null ? [0] : []
│ ├────────────────
│ │ var.policy is null
│
│ The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type
│ list of number.
The question essentially is asking "why is a list
type allowed as a value for the meta-parameter argument for_each
within a dynamic
block, but not within a resource
block?" If we check the Terraform DSL collection type documentation:
list(...): a sequence of values identified by consecutive whole numbers starting with zero.
set(...): a collection of unique values that do not have any secondary identifiers or ordering.
The important differentiation here is that a set
type is unordered whereas a list
type is ordered. Therefore the question can rephrased as "why must the value be unordered within a resource
block and not within a dynamic
block?"
This is basically for two reasons:
dynamic
block is within the attribute schema of a resource, and plan differentiation, and state setting and getting, for attributes does not need to be unordered.struct
that is not cognizant of ordering. Accurate Terraform state setting and getting requires unordered resources.Therefore the list
is allowed for the dynamic block for_each
metaparameter, but not within the resource
.
There is also a side question of simplifying the values. This can be accomplished most easily be modifying the variable declarations as such:
variable "kms_arn" {
description = "ARN of KMS fetched"
type = set(string)
default = []
}
variable "policy" {
description = "Bucket policy which is assigned to bucket and allows access to it"
type = set(string)
default = []
}
and the config:
dynamic "apply_server_side_encryption_by_default" {
for_each = var.kms_arn
content {
kms_master_key_id = each.value
sse_algorithm = "aws:kms"
}
}
resource "aws_s3_bucket_policy" "policy" {
for_each = var.policy
bucket = aws_s3_bucket.bucket.id
policy = each.value
}
where the standard lazily evaluated collection will short-circuit when the value is empty i.e. constructor with no values: []
.