Skip to content

container-definition module not usable on its own #147

@giom-l

Description

@giom-l

Description

This is kinda a reopening of #122.
I also hit the same issues and will try to explain as best as I can.

First thing first, the module is working perfectly fine.
For example (a very simplified example)

variable "subnet_ids" { type = list(string) } locals { name = "test-ecs-module" tags = { Env = "test" Project = "ecs-module" } } module "cluster" { source = "terraform-aws-modules/ecs/aws//modules/cluster" cluster_name = local.name fargate_capacity_providers = { FARGATE = { default_capacity_provider_strategy = { weight = 100 } } } tags = local.tags } module "nginx" { source = "terraform-aws-modules/ecs/aws//modules/container-definition" version = "5.7.3" name = local.name service = local.name essential = true readonly_root_filesystem = false image = "public.ecr.aws/nginx/nginx:1.25.3" mount_points = [ { containerPath = "/conf/" sourceVolume = "conf" readOnly = true } ] port_mappings = [ { containerPort = 80 hostPort = 80 protocol = "tcp" } ] enable_cloudwatch_logging = true create_cloudwatch_log_group = false } module "service" { source = "terraform-aws-modules/ecs/aws//modules/service" version = "5.7.3" name = local.name cluster_arn = module.cluster.arn cpu = 256 memory = 512 desired_count = 1 launch_type = "FARGATE" create_task_exec_iam_role = true create_tasks_iam_role = true create_security_group = true security_group_rules = [ { description = "Allow egress" type = "egress" protocol = "all" from_port = 0 to_port = 65535 cidr_blocks = ["0.0.0.0/0"] } ] subnet_ids = var.subnet_ids network_mode = "awsvpc" assign_public_ip = false container_definitions = { (local.name) = { essential = true readonly_root_filesystem = false image = "public.ecr.aws/nginx/nginx:1.25.3" mount_points = [ { containerPath = "/conf/" sourceVolume = "conf" readOnly = true } ] port_mappings = [ { containerPort = 80 hostPort = 80 protocol = "tcp" } ] enable_cloudwatch_logging = true } } volume = [ { name : "conf" } ] enable_autoscaling = false ignore_task_definition_changes = false tags = local.tags propagate_tags = "TASK_DEFINITION" } 

This works.
However, in some of our services, we have up to 5 containers.
Each one of them may have its own port mappings, own volumes, own commands and so.
Having that within only one resource is extremely hard to read and maintain.

On our current stack, we split each container definition in its own module, and the service only refers to all container definitions.
This way, one can better identify the resource, its properties and so on.

Using the current module, it would give something like this :

variable "subnet_ids" { type = list(string) } locals { name = "test-ecs-module" tags = { Env = "test" Project = "ecs-module" } } module "cluster" { source = "terraform-aws-modules/ecs/aws//modules/cluster" cluster_name = local.name fargate_capacity_providers = { FARGATE = { default_capacity_provider_strategy = { weight = 100 } } } tags = local.tags } module "nginx" { source = "terraform-aws-modules/ecs/aws//modules/container-definition" version = "5.7.3" name = local.name service = local.name essential = true readonly_root_filesystem = false image = "public.ecr.aws/nginx/nginx:1.25.3" mount_points = [ { containerPath = "/conf/" sourceVolume = "conf" readOnly = true } ] port_mappings = [ { containerPort = 80 hostPort = 80 protocol = "tcp" } ] enable_cloudwatch_logging = true create_cloudwatch_log_group = false } module "service" { source = "terraform-aws-modules/ecs/aws//modules/service" version = "5.7.3" name = local.name cluster_arn = module.cluster.arn cpu = 256 memory = 512 desired_count = 1 launch_type = "FARGATE" create_task_exec_iam_role = true create_tasks_iam_role = true create_security_group = true security_group_rules = [ { description = "Allow egress" type = "egress" protocol = "all" from_port = 0 to_port = 65535 cidr_blocks = ["0.0.0.0/0"] } ] subnet_ids = var.subnet_ids network_mode = "awsvpc" assign_public_ip = false container_definitions = { (local.name) = module.nginx.container_definition } volume = [ { name : "conf" } ] enable_autoscaling = false ignore_task_definition_changes = false tags = local.tags propagate_tags = "TASK_DEFINITION" } 

However, this does not work because of the case used in container-definition outputs for fields composed by multiple words.
Hence, instead of having portMappings, it should return the proper property name port_mappings

I made it work by modifying the local module
from

locals { is_not_windows = contains(["LINUX"], var.operating_system_family) log_group_name = "/aws/ecs/${var.service}/${var.name}" log_configuration = merge( { for k, v in { logDriver = "awslogs", options = { awslogs-region = data.aws_region.current.name, awslogs-group = try(aws_cloudwatch_log_group.this[0].name, ""), awslogs-stream-prefix = "ecs" }, } : k => v if var.enable_cloudwatch_logging }, var.log_configuration ) linux_parameters = var.enable_execute_command ? merge({ "initProcessEnabled" : true }, var.linux_parameters) : merge({ "initProcessEnabled" : false }, var.linux_parameters) definition = { command = length(var.command) > 0 ? var.command : null cpu = var.cpu dependsOn = length(var.dependencies) > 0 ? var.dependencies : null # depends_on is a reserved word disableNetworking = local.is_not_windows ? var.disable_networking : null dnsSearchDomains = local.is_not_windows && length(var.dns_search_domains) > 0 ? var.dns_search_domains : null dnsServers = local.is_not_windows && length(var.dns_servers) > 0 ? var.dns_servers : null dockerLabels = length(var.docker_labels) > 0 ? var.docker_labels : null dockerSecurityOptions = length(var.docker_security_options) > 0 ? var.docker_security_options : null entrypoint = length(var.entrypoint) > 0 ? var.entrypoint : null environment = var.environment environmentFiles = length(var.environment_files) > 0 ? var.environment_files : null essential = var.essential extraHosts = local.is_not_windows && length(var.extra_hosts) > 0 ? var.extra_hosts : null firelensConfiguration = length(var.firelens_configuration) > 0 ? var.firelens_configuration : null healthCheck = length(var.health_check) > 0 ? var.health_check : null hostname = var.hostname image = var.image interactive = var.interactive links = local.is_not_windows && length(var.links) > 0 ? var.links : null linuxParameters = local.is_not_windows && length(local.linux_parameters) > 0 ? local.linux_parameters : null logConfiguration = length(local.log_configuration) > 0 ? local.log_configuration : null memory = var.memory memoryReservation = var.memory_reservation mountPoints = var.mount_points name = var.name portMappings = var.port_mappings privileged = local.is_not_windows ? var.privileged : null pseudoTerminal = var.pseudo_terminal readonlyRootFilesystem = local.is_not_windows ? var.readonly_root_filesystem : null repositoryCredentials = length(var.repository_credentials) > 0 ? var.repository_credentials : null resourceRequirements = length(var.resource_requirements) > 0 ? var.resource_requirements : null secrets = length(var.secrets) > 0 ? var.secrets : null startTimeout = var.start_timeout stopTimeout = var.stop_timeout systemControls = length(var.system_controls) > 0 ? var.system_controls : null ulimits = local.is_not_windows && length(var.ulimits) > 0 ? var.ulimits : null user = local.is_not_windows ? var.user : null volumesFrom = var.volumes_from workingDirectory = var.working_directory } # Strip out all null values, ECS API will provide defaults in place of null/empty values container_definition = { for k, v in local.definition : k => v if v != null } }

to

locals { is_not_windows = contains(["LINUX"], var.operating_system_family) log_group_name = "/aws/ecs/${var.service}/${var.name}" log_configuration = merge( { for k, v in { logDriver = "awslogs", options = { awslogs-region = data.aws_region.current.name, awslogs-group = try(aws_cloudwatch_log_group.this[0].name, ""), awslogs-stream-prefix = "ecs" }, } : k => v if var.enable_cloudwatch_logging }, var.log_configuration ) linux_parameters = var.enable_execute_command ? merge({ "initProcessEnabled" : true }, var.linux_parameters) : merge({ "initProcessEnabled" : false }, var.linux_parameters) definition = { command = length(var.command) > 0 ? var.command : null cpu = var.cpu depends_on = length(var.dependencies) > 0 ? var.dependencies : null # depends_on is a reserved word disable_networking = local.is_not_windows ? var.disable_networking : null dns_search_domains = local.is_not_windows && length(var.dns_search_domains) > 0 ? var.dns_search_domains : null dns_servers = local.is_not_windows && length(var.dns_servers) > 0 ? var.dns_servers : null docker_labels = length(var.docker_labels) > 0 ? var.docker_labels : null docker_security_options = length(var.docker_security_options) > 0 ? var.docker_security_options : null entrypoint = length(var.entrypoint) > 0 ? var.entrypoint : null environment = var.environment environment_diles = length(var.environment_files) > 0 ? var.environment_files : null essential = var.essential extra_hosts = local.is_not_windows && length(var.extra_hosts) > 0 ? var.extra_hosts : null firelens_configuration = length(var.firelens_configuration) > 0 ? var.firelens_configuration : null health_check = length(var.health_check) > 0 ? var.health_check : null hostname = var.hostname image = var.image interactive = var.interactive links = local.is_not_windows && length(var.links) > 0 ? var.links : null linux_parameters = local.is_not_windows && length(local.linux_parameters) > 0 ? local.linux_parameters : null log_configuration = length(local.log_configuration) > 0 ? local.log_configuration : null memory = var.memory memory_reservation = var.memory_reservation mount_points = var.mount_points name = var.name port_mappings = var.port_mappings privileged = local.is_not_windows ? var.privileged : null pseudo_terminal = var.pseudo_terminal readonly_root_filesystem = local.is_not_windows ? var.readonly_root_filesystem : null repository_credentials = length(var.repository_credentials) > 0 ? var.repository_credentials : null resource_requirements = length(var.resource_requirements) > 0 ? var.resource_requirements : null secrets = length(var.secrets) > 0 ? var.secrets : null start_timeout = var.start_timeout stop_timeout = var.stop_timeout system_controls = length(var.system_controls) > 0 ? var.system_controls : null ulimits = local.is_not_windows && length(var.ulimits) > 0 ? var.ulimits : null user = local.is_not_windows ? var.user : null volumes_from = var.volumes_from working_directory = var.working_directory } # Strip out all null values, ECS API will provide defaults in place of null/empty values container_definition = { for k, v in local.definition : k => v if v != null } }

Versions

  • Module version [Required]: 5.7.3
  • Terraform version: v1.6.5
  • Provider version(s): provider registry.terraform.io/hashicorp/aws v5.30.0

Reproduction Code [Required]

Provided above.

Steps to reproduce the behavior:
Just run
terraform init && terraform apply -subnet_ids='[<your own list>]'

Expected behavior

It would be better for the container-definition module to be usable on its own.

Actual behavior

We can only use the service module with all container definition inside it.
No split available.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions