Search code examples
dynamicforeachterraform

for_each for resource vs dynamic


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.

Solution

  • 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:

    1. The 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.
    2. The resource block is within the top level namespace of the state, and the resource data model is set and assigned as a 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: [].