Skip to content
5 changes: 4 additions & 1 deletion examples/ecs_fargate/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ module "datadog_ecs_fargate_task" {
# Configure Datadog
dd_api_key = var.dd_api_key
dd_site = var.dd_site
dd_service = var.dd_service
dd_tags = "team:cont-p, owner:container-monitoring"
dd_essential = true
dd_is_datadog_dependency_enabled = true

dd_service = var.dd_service
dd_env = var.dd_env
dd_version = var.dd_version

dd_environment = [
{
name = "DD_CUSTOM_FEATURE",
Expand Down
14 changes: 13 additions & 1 deletion examples/ecs_fargate/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ variable "dd_api_key_secret_arn" {
}

variable "dd_service" {
description = "Service name for resource filtering in Datadog"
description = "The service name for resource filtering and UST tagging in Datadog"
type = string
default = null
}

variable "dd_env" {
description = "The environment for resource filtering and UST tagging in Datadog"
type = string
default = null
}

variable "dd_version" {
description = "The version for resource filtering and UST tagging in Datadog"
type = string
default = null
}
Expand Down
1 change: 1 addition & 0 deletions modules/ecs_fargate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ No modules.
| <a name="input_dd_cluster_name"></a> [dd\_cluster\_name](#input\_dd\_cluster\_name) | Datadog cluster name | `string` | `null` | no |
| <a name="input_dd_cpu"></a> [dd\_cpu](#input\_dd\_cpu) | Datadog Agent container CPU units | `number` | `null` | no |
| <a name="input_dd_cws"></a> [dd\_cws](#input\_dd\_cws) | Configuration for Datadog Cloud Workload Security (CWS) | <pre>object({<br/> enabled = optional(bool, false)<br/> cpu = optional(number)<br/> memory_limit_mib = optional(number)<br/> })</pre> | <pre>{<br/> "enabled": false<br/>}</pre> | no |
| <a name="input_dd_docker_labels"></a> [dd\_docker\_labels](#input\_dd\_docker\_labels) | Datadog Agent container docker labels | `map(string)` | `{}` | no |
| <a name="input_dd_dogstatsd"></a> [dd\_dogstatsd](#input\_dd\_dogstatsd) | Configuration for Datadog DogStatsD | <pre>object({<br/> enabled = optional(bool, true)<br/> origin_detection_enabled = optional(bool, true)<br/> dogstatsd_cardinality = optional(string, "orchestrator")<br/> socket_enabled = optional(bool, true)<br/> })</pre> | <pre>{<br/> "dogstatsd_cardinality": "orchestrator",<br/> "enabled": true,<br/> "origin_detection_enabled": true,<br/> "socket_enabled": true<br/>}</pre> | no |
| <a name="input_dd_env"></a> [dd\_env](#input\_dd\_env) | The task environment name. Used for tagging (UST) | `string` | `null` | no |
| <a name="input_dd_environment"></a> [dd\_environment](#input\_dd\_environment) | Datadog Agent container environment variables. Highest precedence and overwrites other environment variables defined by the module. For example, `dd_environment = [ { name = 'DD_VAR', value = 'DD_VAL' } ]` | `list(map(string))` | <pre>[<br/> {}<br/>]</pre> | no |
Expand Down
41 changes: 27 additions & 14 deletions modules/ecs_fargate/datadog.tf
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ locals {
] : [],
)

ust_docker_labels = merge(
var.dd_env != null ? {
"com.datadoghq.tags.env" = var.dd_env
} : {},
var.dd_service != null ? {
"com.datadoghq.tags.service" = var.dd_service
} : {},
var.dd_version != null ? {
"com.datadoghq.tags.version" = var.dd_version
} : {},
)

application_env_vars = concat(
var.dd_apm.profiling != null ? [
{
Expand Down Expand Up @@ -169,6 +181,11 @@ locals {
local.ust_env_vars,
local.application_env_vars,
),
# Merge UST docker labels with any existing docker labels.
dockerLabels = merge(
lookup(container, "dockerLabels", {}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other application container configured options like environment variables, mount points, and container dependencies give more precedence to the values defined by the Datadog Terraform module.

I think we should be fine to maintain that level of precedence here and assume that if a user defined the UST values for env, service, and version then those values would take precedence as well.

variable "dd_service" {
description = "The task service name. Used for tagging (UST)"
type = string
default = null
}
variable "dd_env" {
description = "The task environment name. Used for tagging (UST)"
type = string
default = null
}
variable "dd_version" {
description = "The task version name. Used for tagging (UST)"
type = string
default = null
}

We can leave a bugfix note in the changelog for the next release to notify users that the UST tagging has been fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable. I guess if they wanted unique UST values, then they shouldn't set the global env,service,and version. They should then manually set it per container definition.

I initially wanted to prioritize the user's docker labels over ours because the customer's problem description implied the need for different UST values.

local.ust_docker_labels,
),
# Append new volume mounts to any existing mountPoints.
mountPoints = concat(
lookup(container, "mountPoints", []),
Expand Down Expand Up @@ -288,20 +305,20 @@ locals {
local.dynamic_env,
local.origin_detection_vars,
local.cws_vars,
local.ust_env_vars,
local.dd_environment,
)

# Datadog Agent container definition
dd_agent_container = [
merge(
{
name = "datadog-agent"
image = "${var.dd_registry}:${var.dd_image_version}"
essential = var.dd_essential
environment = local.dd_agent_env
cpu = var.dd_cpu
memory = var.dd_memory_limit_mib
name = "datadog-agent"
image = "${var.dd_registry}:${var.dd_image_version}"
essential = var.dd_essential
environment = local.dd_agent_env
dockerLabels = var.dd_docker_labels
cpu = var.dd_cpu
memory = var.dd_memory_limit_mib
secrets = var.dd_api_key_secret != null ? [
{
name = "DD_API_KEY"
Expand Down Expand Up @@ -340,11 +357,6 @@ locals {

dd_log_environment = var.dd_log_collection.fluentbit_config.environment != null ? var.dd_log_collection.fluentbit_config.environment : []

dd_log_agent_env = concat(
local.ust_env_vars,
local.dd_log_environment
)

# Datadog log router container definition
dd_log_container = local.is_fluentbit_supported ? [
merge(
Expand All @@ -366,7 +378,8 @@ locals {
memory_limit_mib = var.dd_log_collection.fluentbit_config.memory_limit_mib
user = "0"
mountPoints = var.dd_log_collection.fluentbit_config.mountPoints
environment = local.dd_log_agent_env
environment = local.dd_log_environment
dockerLabels = var.dd_docker_labels
portMappings = []
systemControls = []
volumesFrom = []
Expand Down Expand Up @@ -396,7 +409,7 @@ locals {
entryPoint = []
command = ["/cws-instrumentation", "setup", "--cws-volume-mount", "/cws-instrumentation-volume"]
mountPoints = local.cws_mount
environment = local.ust_env_vars
dockerLabels = var.dd_docker_labels
portMappings = []
systemControls = []
volumesFrom = []
Expand Down
6 changes: 6 additions & 0 deletions modules/ecs_fargate/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ variable "dd_environment" {
nullable = false
}

variable "dd_docker_labels" {
description = "Datadog Agent container docker labels"
type = map(string)
default = {}
}

variable "dd_tags" {
description = "Datadog Agent global tags (eg. `key1:value1, key2:value2`)"
type = string
Expand Down
4 changes: 4 additions & 0 deletions smoke_tests/ecs_fargate/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ output "role-parsing-with-path" {
output "role-parsing-without-path" {
value = module.dd_task_role_parsing_without_path
}

output "ust-docker-labels" {
value = module.dd_task_ust_docker_labels
}
49 changes: 49 additions & 0 deletions smoke_tests/ecs_fargate/ust-docker-labels.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Unless explicitly stated otherwise all files in this repository are licensed
# under the Apache License Version 2.0.
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2025-present Datadog, Inc.

################################################################################
# Task Definition: UST Docker Labels Test
################################################################################

module "dd_task_ust_docker_labels" {
source = "../../modules/ecs_fargate"

# Configure Datadog with UST tags
dd_api_key = var.dd_api_key
dd_site = var.dd_site
dd_service = "ust-test-service"
dd_env = "ust-test-env"
dd_version = "1.2.3"
dd_tags = "team:test"
dd_essential = true

dd_is_datadog_dependency_enabled = true

dd_log_collection = {
enabled = true,
}

dd_cws = {
enabled = true,
}

dd_docker_labels = {
"com.datadoghq.tags.service" : "docker-agent-service",
"com.datadoghq.tags.env" : "agent-dev",
"com.datadoghq.tags.version" : "v1.2.3"
}

# Configure Task Definition with multiple containers
family = "${var.test_prefix}-ust-docker-labels"
container_definitions = jsonencode([
{
name = "dummy-app",
image = "nginx:latest",
essential = true,
},
])

requires_compatibilities = ["FARGATE"]
}
1 change: 0 additions & 1 deletion tests/all_dd_disabled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func (s *ECSFargateSuite) TestAllDDDisabled() {
expectedAgentEnvVars := map[string]string{
"DD_API_KEY": "test-api-key",
"DD_SITE": "datadoghq.com",
"DD_SERVICE": "test-service",
"DD_TAGS": "team:cont-p, owner:container-monitoring",
"DD_DOGSTATSD_TAG_CARDINALITY": "orchestrator",
"DD_ECS_TASK_COLLECTION_ENABLED": "true",
Expand Down
2 changes: 0 additions & 2 deletions tests/all_dd_inputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ func (s *ECSFargateSuite) TestAllDDInputs() {
"DD_API_KEY": "test-api-key",
"DD_SITE": "datadoghq.com",
"ECS_FARGATE": "true",
"DD_SERVICE": "test-service",
"DD_RUNTIME_SECURITY_CONFIG_EBPFLESS_ENABLED": "true",
"DD_INSTALL_INFO_TOOL": "terraform",
// "DD_INSTALL_INFO_INSTALLER_VERSION": "0.0.0",
Expand All @@ -61,7 +60,6 @@ func (s *ECSFargateSuite) TestAllDDInputs() {
expectedLogOptions := map[string]string{
"apikey": "test-api-key",
"provider": "ecs",
"dd_service": "dd-test",
"Host": "http-intake.logs.datadoghq.com",
"TLS": "on",
"dd_source": "dd-test",
Expand Down
1 change: 0 additions & 1 deletion tests/apm_dsd_tcp_udp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func (s *ECSFargateSuite) TestApmDsdTcpUdp() {
expectedAgentEnvVars := map[string]string{
"DD_API_KEY": "test-api-key",
"DD_SITE": "datadoghq.com",
"DD_SERVICE": "test-service",
"DD_TAGS": "team:cont-p, owner:container-monitoring",
"DD_DOGSTATSD_TAG_CARDINALITY": "orchestrator",
"DD_ECS_TASK_COLLECTION_ENABLED": "true",
Expand Down
2 changes: 0 additions & 2 deletions tests/logging_only_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func (s *ECSFargateSuite) TestLoggingOnly() {
expectedAgentEnvVars := map[string]string{
"DD_API_KEY": "test-api-key",
"DD_SITE": "datadoghq.com",
"DD_SERVICE": "test-service",
"DD_DOGSTATSD_TAG_CARDINALITY": "orchestrator",
"DD_ECS_TASK_COLLECTION_ENABLED": "true",
"ECS_FARGATE": "true",
Expand Down Expand Up @@ -87,7 +86,6 @@ func (s *ECSFargateSuite) TestLoggingOnly() {

// Verify log router environment variables
expectedLogRouterEnvVars := map[string]string{
"DD_SERVICE": "test-service",
"DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL": "true",
}
AssertEnvVars(s.T(), logRouterContainer, expectedLogRouterEnvVars)
Expand Down
54 changes: 54 additions & 0 deletions tests/ust_docker_labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-present Datadog, Inc.

package test

import (
"encoding/json"
"log"

"github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/gruntwork-io/terratest/modules/terraform"
)

// TestUSTDockerLabels tests that UST docker labels are propagated to all container definitions
// when dd_service, dd_env, and dd_version are set
func (s *ECSFargateSuite) TestUSTDockerLabels() {
log.Println("TestUSTDockerLabels: Running test...")

// Retrieve the task output for the "ust-docker-labels" module
var containers []types.ContainerDefinition
task := terraform.OutputMap(s.T(), s.terraformOptions, "ust-docker-labels")
s.Equal(s.testPrefix+"-ust-docker-labels", task["family"], "Unexpected task family name")

err := json.Unmarshal([]byte(task["container_definitions"]), &containers)
s.NoError(err, "Failed to parse container definitions")
s.Equal(4, len(containers), "Expected 4 containers in the task definition (1 app container + 3 agent sidecar)")

// Expected UST docker labels that should be present on all application containers
expectedUSTLabels := map[string]string{
"com.datadoghq.tags.service": "ust-test-service",
"com.datadoghq.tags.env": "ust-test-env",
"com.datadoghq.tags.version": "1.2.3",
}

dummyApp, found := GetContainer(containers, "dummy-app")
s.True(found, "Container dummy-app not found in definitions")
AssertDockerLabels(s.T(), dummyApp, expectedUSTLabels)

// Expect UST docker labels to be present on all Datadog containers with
// overwritten labels when UST docker labels are specified.
datadogContainers := []string{"datadog-agent", "datadog-log-router", "cws-instrumentation-init"}
expectedAgentUSTLabels := map[string]string{
"com.datadoghq.tags.service": "docker-agent-service",
"com.datadoghq.tags.env": "agent-dev",
"com.datadoghq.tags.version": "v1.2.3",
}
for _, containerName := range datadogContainers {
container, found := GetContainer(containers, containerName)
s.True(found, "Container %s not found in definitions", containerName)
AssertDockerLabels(s.T(), container, expectedAgentUSTLabels)
}
}
11 changes: 11 additions & 0 deletions tests/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,14 @@ func AssertContainerDependency(t *testing.T, container types.ContainerDefinition
assert.True(t, found, "Expected dependency (container:%s, condition:%s) not found in %s container",
*expectedDependency.ContainerName, expectedDependency.Condition, *container.Name)
}

// AssertDockerLabels checks if the expected docker labels are all present in the container
func AssertDockerLabels(t *testing.T, container types.ContainerDefinition, expectedLabels map[string]string) {
assert.NotNil(t, container.Name, "Container name cannot be nil")

for key, expectedValue := range expectedLabels {
value, found := container.DockerLabels[key]
assert.True(t, found, "Docker label %s not found in %s container", key, *container.Name)
assert.Equal(t, expectedValue, value, "Docker label %s value does not match expected in %s container", key, *container.Name)
}
}