My goal is to define a container definition which has a “main” container like nginx or apache, and it optionally has multiple sidecar containers. The sidecar containers are re-used by a bunch of different services, so I’m defining those as a local variable which I can reuse as many times as needed. Also, the sidecar containers need to be conditionally enabled, such that we can use sidecars in prod but not use them in dev, or vice versa, etc.
Here is a super minimal reproducable example using apache as a “main” container and conditionally concatting the sidecars into the definition:
variable "use_generic_sidecars" {
default = true
}
locals {
# These generic sidecars are conditionally reused by a bunch
# of different services, so we define them here as their own
# variable and we re-use that variable in various other
# container definitions.
generic_sidecars = [
{
name = "sidecar1"
image = "sidecar:latest"
essential = false
},
{
name = "sidecar2"
image = "sidecar:latest"
essential = false
}
]
apache = concat([
{
name = "apache"
image = "httpd:latest"
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
}
],
var.use_generic_sidecars ? local.generic_sidecars : [],
)
}
resource "aws_ecs_task_definition" "generic_service" {
family = "service"
container_definitions = jsonencode(local.apache)
}
I can use terraform plan
and this plans as expected. It will build a task definition which has 3 total containers. 1 apache container plus two sidecars which are brought it by way of concat()
. This suggests to me that the general method I’m using for composing this resource is valid.
# aws_ecs_task_definition.generic_service will be created
+ resource "aws_ecs_task_definition" "generic_service" {
+ arn = (known after apply)
+ arn_without_revision = (known after apply)
+ container_definitions = jsonencode(
[
+ {
+ essential = true
+ image = "httpd:latest"
+ name = "apache"
+ portMappings = [
+ {
+ containerPort = 80
+ hostPort = 80
},
]
},
+ {
+ essential = false
+ image = "sidecar:latest"
+ name = "sidecar1"
},
+ {
+ essential = false
+ image = "sidecar:latest"
+ name = "sidecar2"
},
]
)
+ family = "service"
+ id = (known after apply)
+ network_mode = (known after apply)
+ revision = (known after apply)
+ skip_destroy = false
+ tags_all = (known after apply)
+ track_latest = false
}
Plan: 1 to add, 0 to change, 0 to destroy.
But when I try the same method with a more real-world example like using datadog sidecar containers, it doesn’t work. Here is a reproducable example using nginx as the “main” container, plus two datadog containers with the same conditional concat method:
variable "use_datadog" {
default = true
}
locals {
# These datadog sidecars are conditionally reused by a bunch
# of different services, so we define them here as their own
# variable and we re-use that variable in various other
# container definitions.
datadog_containers = [
{
# https://docs.datadoghq.com/integrations/ecs_fargate/?tab=cloudformation#aws-cloudformation-task-definition
name = "datadog-agent"
image = "public.ecr.aws/datadog/agent:latest"
essential = false
cpu = null
memory = null
environment = [
{
name = "ECS_FARGATE"
value = "true"
},
# https://docs.datadoghq.com/security/cloud_security_management/setup/csm_pro/agent/ecs_ec2/
{
name = "DD_CONTAINER_IMAGE_ENABLED"
value = "true"
},
{
name = "DD_SBOM_ENABLED"
value = "true"
},
{
name = "DD_SBOM_CONTAINER_IMAGE_ENABLED"
value = "true"
},
]
},
{
name = "cws-instrumentation-init"
image = "datadog/cws-instrumentation:latest"
essential = false
user = "0"
command = [
"/cws-instrumentation",
"setup",
"--cws-volume-mount",
"/cws-instrumentation-volume"
]
mountPoints = [
{
sourceVolume = "cws-instrumentation-volume"
containerPath = "/cws-instrumentation-volume"
readOnly = false
}
]
},
]
nginx = concat([
{
name = "nginx"
image = "nginx:latest"
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
}
],
var.use_datadog ? local.datadog_containers : [],
)
}
resource "aws_ecs_task_definition" "service_with_datadog" {
family = "service"
container_definitions = jsonencode(local.nginx)
}
Error message from terraform plan
:
│ Error: Inconsistent conditional result types
│
│ on main.tf line 71, in locals:
│ 71: var.use_datadog ? local.datadog_containers : [],
│ ├────────────────
│ │ local.datadog_containers is tuple with 2 elements
│
│ The true and false result expressions must have consistent types. The 'true' tuple has length 2, but the 'false' tuple
│ has length 0.
In the nginx/datadog example, if I comment out the cws-instrumentation-init
container, then I can get a valid plan with only one sidecar. But I do need that other sidecar.
Anybody know how I can unblock this inconsistent usage of conditional concatting?
And in case it is helpful, both of the above HCL examples are reproducable by pasting the HCL seen above into a main.tf
file in an empty directory, run an init
and then run a plan
. The apache example will plan cleanly and the nginx example will break with the same error seen above.
$ terraform version
Terraform v1.8.0
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v5.46.0
Thanks.