How to set conditional with a default value of variable have type object

I posted this issuse yesterday in here: Cannot use `count` with a default value of variables have type `object` or `list of objects` · Issue #32689 · hashicorp/terraform · GitHub .
And I knew how to do with list of object but with object I’m trying like below:

  • module/lambda.tf
resource "aws_lambda_permission" "lambda_permission_cloudwatch_log" {
  for_each = var.lambda_function_cloudwatch_log
# I use count also but it doesn't work:
# count         = length(var.lambda_function_cloudwatch_log) > 0 ? 1 : 0

  statement_id  = "AllowExecutionFromCloudWatchLogs"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda_function.arn
  principal     = "logs.${var.region}.amazonaws.com"
}
  • module/_variables.tf
variable "lambda_function_cloudwatch_log" {
  description = "Provide the Name and ARN of the CloudWatch log group you want to trigger to the Lambda Function"
  default     = {}
  type = object(
    {
      log_subscription_filter_name    = optional(string, null)
      log_group_name                  = optional(string, null)
      log_subscription_filter_pattern = optional(string, null)
    }
  )
}
  • env/lambda.tf
module "lambda_codepipeline_notification" {
  source = "xxx"
  #basic
  project = var.project
  env     = var.env
  region  = var.region
  service = "codepipeline"

  #lambda-zip
  lambda_zip_python = {
    code_path     = "../../../../terraform-dependencies/lambda-function/codepipeline-notification"
    code_zip_name = "codepipeline_notification"
    code_zip_path = "../../../../terraform-dependencies/lambda-function/codepipeline-notification"
  }
  #lambda
  lambda_function = {
    name    = "codepipeline"
    role    = module.iam_role_lambda_codepipeline_notification.iam_role_arn
    runtime = "python3.8"
    environment_variables = {
      ENV              = var.env
    }
  }
  #sns
  lambda_function_sns = [
    {
      topic_name = module.sns_codepipeline_notification.sns_topic_name
      topic_arn  = module.sns_codepipeline_notification.sns_topic_arn
    }
  ]
}

Expected Behavior

Don’t create resource"aws_lambda_permission" “lambda_permission_cloudwatch_log” {}

Actual Behavior

  # module.lambda_codepipeline_notification.aws_lambda_permission.lambda_permission_cloudwatch_log["log_group_name"] will be created
  + resource "aws_lambda_permission" "lambda_permission_cloudwatch_log" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "arn:aws:lambda:ap-northeast-1:xxx:function:xxx-dev-codepipeline-lambda"
      + id                  = (known after apply)
      + principal           = "logs.ap-northeast-1.amazonaws.com"
      + statement_id        = "AllowExecutionFromCloudWatchLogs"
      + statement_id_prefix = (known after apply)
    }

  # module.lambda_codepipeline_notification.aws_lambda_permission.lambda_permission_cloudwatch_log["log_subscription_filter_name"] will be created
  + resource "aws_lambda_permission" "lambda_permission_cloudwatch_log" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "arn:aws:lambda:ap-northeast-1:xxx:function:xxx-dev-codepipeline-lambda"
      + id                  = (known after apply)
      + principal           = "logs.ap-northeast-1.amazonaws.com"
      + statement_id        = "AllowExecutionFromCloudWatchLogs"
      + statement_id_prefix = (known after apply)
    }

  # module.lambda_codepipeline_notification.aws_lambda_permission.lambda_permission_cloudwatch_log["log_subscription_filter_pattern"] will be created
  + resource "aws_lambda_permission" "lambda_permission_cloudwatch_log" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "arn:aws:lambda:ap-northeast-1:xxx:function:xxx-dev-codepipeline-lambda"
      + id                  = (known after apply)
      + principal           = "logs.ap-northeast-1.amazonaws.com"
      + statement_id        = "AllowExecutionFromCloudWatchLogs"
      + statement_id_prefix = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Hi @quansang,

Using length with a single object value is not the right approach here because for an object that function returns the number of attributes defined in the object type.

Since a single object represents only a single item rather than a collection of items, the typical way to represent the absence of it would be null, which is how Terraform represents a single item being absent.

variable "lambda_function_cloudwatch_log" {
  description = "Provide the Name and ARN of the CloudWatch log group you want to trigger to the Lambda Function"
  default     = null

  type = object({
    log_subscription_filter_name    = optional(string, null)
    log_group_name                  = optional(string, null)
    log_subscription_filter_pattern = optional(string, null)
  })
}

With the default set to null you can use var.lambda_function_cloudwatch_log != null to test if this variable was set.

An empty collection can be a good default for a variable of a collection type (list, set, or map), representing “zero of these”, but an object isn’t a collection so there isn’t an equivalent sense of “object with zero elements”. We use null to represent the case where there can be either zero or one of something and there are currently zero of it.

Thanks for replying back to me @apparentlymart. I see and in this case, I used null with no problem. But in another case, can you help me define why this error occurs?

  • module/s3.tf
resource "aws_s3_bucket_server_side_encryption_configuration" "s3_bucket_sse" {
  count = var.s3_bucket.sse != null ? 1 : 0

  bucket = aws_s3_bucket.s3_bucket.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = var.s3_bucket.sse.sse_algorithm
      kms_master_key_id = var.s3_bucket.sse.kms_master_key_id
    }
  }
}
  • module/_variables.tf
variable "s3_bucket" {
  description = "All configurations to Provides a S3 bucket resource."
  type = object({
    name = string
    sse = optional(object({
      kms_master_key_id = optional(string, null)
      sse_algorithm     = string  #Valid values: AES256 & aws:kms
    }), null)
    acl        = optional(string, "private")
    versioning = optional(string, "Suspended")
    logging = optional(object({
      target_bucket_id = string
    }), null)
  })
}
  • env/s3.tf
module "s3_standard" {
  source = "../../modules/s3"
  #s3-bucket 
  s3_bucket = {
    name = "${var.project}-${var.env}-standard"
  }
}

Expected Behavior

Don’t create resource “aws_s3_bucket_server_side_encryption_configuration” “s3_bucket_sse” {}

Actual Behavior

│ The given value is not suitable for module.s3_standard.var.s3_bucket declared at ../../modules/s3/_variables.tf:3,1-21: attribute "sse": attribute "sse_algorithm" is required.

In my opinion, the “sse_algorithm” attribute should not be required in this case. The count won’t create this resource because the variable var.s3_bucket.sse has received a null value from optional. This logic has been defined with the variable var.lambda_function_cloudwatch_log

Hi @quansang,

I agree with you that I’d expect what you showed to work. I tried to recreate your error but I wasn’t able to: for me, it worked as we both expected.

Here’s what I tried:

  1. I created a module with just your variable "s3_bucket" block, because that’s what your error message was talking about and the resource block isn’t relevant to the problem. Here’s what’s in my .tf file:

    variable "s3_bucket" {
      description = "All configurations to Provides a S3 bucket resource."
      type = object({
        name = string
        sse = optional(object({
          kms_master_key_id = optional(string, null)
          sse_algorithm     = string  #Valid values: AES256 & aws:kms
        }), null)
        acl        = optional(string, "private")
        versioning = optional(string, "Suspended")
        logging = optional(object({
          target_bucket_id = string
        }), null)
      })
    }
    
  2. Because my module is just a root module, unlike yours, I created a terraform.tfvars file instead of a module block, with the following contents that should be equivalent to your module block:

    s3_bucket = {
      name = "example"
    }
    

When I ran terraform plan with this minimal configuration I saw no errors:

$ terraform plan

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against
your configuration and found no differences, so no changes
are needed.

To recreate your error I added a sse attribute with an empty object to my terraform.tfvars file, thereby making it invalid because sse_algorithm is required for this object:

s3_bucket = {
  name = "example"
  sse  = {}
}
$ terraform plan

Planning failed. Terraform encountered an error while generating
this plan.

╷
│ Error: Invalid value for input variable
│ 
│   on terraform.tfvars line 1:
│    1: s3_bucket = {
│    2:   name = "example"
│    3:   sse  = {}
│    4: }
│ 
│ The given value is not suitable for var.s3_bucket declared at
│ var-optional-attr-nested.tf:1,1-21: attribute "sse": attribute
│ "sse_algorithm" is required.
╵

I would expect to see the error you showed only if you have an sse attribute defined in your s3_bucket object. Are you sure that the configuration you shared here is what you used when you saw that error?

Hi @apparentlymart ,
I just found out the exact reason. That’s because I’m using Terraform version 1.3.3 and when I try the new version (1.3.9), it works perfectly (I don’t know new version fixed this bug)… Are you using the latest version too?