Variable - dynamic object

Hi,

Trying to create a module for our company to use ECS Fargate and pass in a task definition where each service may have a different set of containers and each container has a different set of configs.

Is there a way to be able to pass something like that in without needing to jsonencode it on the way in and then use jsondecode in the module again.
Doing this results in the plan being (known after apply).

An example would be:

locals {
  container_definitions = [
    {
      name = "nginx"
      image = "nginx"
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
          protocol      = "tcp"
        }
      ]
      healthCheck = {
        command = [
          "CMD-SHELL",
          "curl -f http://localhost || exit 1",
        ]
        interval = 30
        retries  = 3
        timeout  = 5
      }
    },
    {
      name = "php-fpm"
      image = "php:8-fpm-alpine"
      environmentFiles = [
        {
          type  = "s3",
          value = "<some-s3-arn>",
        }
      ]
      secrets = [
        {
          name = "<some-name>"
          valueFrom = "<some-value-from>"
        }
      ]
    },
    {
      name  = "redis"
      image = "redis:alpine"
    }
  ]
}

Thanks,
Gary

Ok, I’ve figured out a way to do it, not sure if it’s ideal though.

In my module I can do the following:

locals {
  ecs_task_container_definitions = [
    for container in var.ecs_task_container_definitions : {
      name  = container.name
      image = container.image
      portMappings = [
        for port in container.ports : {
          containerPort = port
          hostPort      = port
          protocol      = "tcp"
        }
      ]
      healthCheck = length(container.healthCheck) > 0 ? {
        command = container.healthCheck
        interval = 30
        retries  = 3
        timeout  = 5
      } : null
      environmentFiles = [
        for value in container.environmentFiles : {
          type = "s3"
          value = value
        }
      ]
      environment = [
        for name, value in container.environment : {
          name  = name
          value = value
        }
      ]
      secrets = [
        for name, valueFrom in container.secrets : {
          name  = name
          valueFrom = valueFrom
        }
      ]
    }
  ]
}

variable "ecs_task_container_definitions" {
  type = list(object({
    name = string
    image = string
    ports = list(number)
    healthCheck = list(string)
    environmentFiles = list(string)
    environment = map(string)
    secrets = map(string)
  }))
}

And where the module is being called from I can do this:

locals {
  container_definitions = [
    {
      name = "nginx"
      image = "nginx:1-alpine"
      port = [80]
      healthCheck = [
        "CMD-SHELL",
        "curl -f http://localhost || exit 1",
      ]
      environmentFiles = []
      environment = {
        FASTCGI_PASS = "localhost"
      }
      secrets = {}
    },
    {
      name = "php-fpm"
      image = "php:8-fpm-alpine"
      port = []
      healthCheck = []
      environmentFiles = [
        <some-s3-arn>
      ]
      environment = {}
      secrets = {
        <some-name>: "<some-value-from>"
      }
    },
    {
      name = "redis"
      image= "redis:3.2.4-alpine"
      port = []
      healthCheck = []
      environmentFiles = []
      environment = {}
      secrets = {}
    }
  ]
}

Also, the (known after apply) may have been the result of waiting for an S3 object resource to happen first.

If anyone has a better solution though, I’m open to suggestions.

Hi @garyrutland,

What you did here – defining a variable as being a collection of objects – is a pretty typical answer to this sort of problem, and so I’m not really sure what else to suggest without knowing what you found lacking about it.

If the issue here is that this is an open-ended schema defined by the remote system and that you would therefore just pass data through verbatim to that system and let it worry about validating, you could achieve that by declaring the variable as being of type any and then just pass it through verbatim (possibly via jsonencode) to a resource argument, although that would then not allow you to do the kind of preprocessing you seem to be doing in local.ecs_task_container_definitions, if I’m understanding correctly the flow… if you want to interpret the data within Terraform then working with Terraform’s type system is often a good idea in order to get things validated and coerced into the shape you’re expecting.

I think what I’ve ended up with is good enough for now, perhaps if/when the optional() functionality in objects comes out of experimental I might be able to be a bit more explicit, dynamic and forgiving at the same time.