data "aws_region" "current" {} data "aws_partition" "current" {} data "aws_caller_identity" "current" {} locals { account_id = data.aws_caller_identity.current.account_id partition = data.aws_partition.current.partition region = data.aws_region.current.name } ################################################################################ # Service ################################################################################ locals { # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html is_external_deployment = try(var.deployment_controller.type, null) == "EXTERNAL" is_daemon = var.scheduling_strategy == "DAEMON" is_fargate = var.launch_type == "FARGATE" # Flattened `network_configuration` network_configuration = { assign_public_ip = var.assign_public_ip security_groups = flatten(concat([try(aws_security_group.this[0].id, [])], var.security_group_ids)) subnets = var.subnet_ids } create_service = var.create && var.create_service } resource "aws_ecs_service" "this" { count = local.create_service && !var.ignore_task_definition_changes ? 1 : 0 dynamic "alarms" { for_each = length(var.alarms) > 0 ? [var.alarms] : [] content { alarm_names = alarms.value.alarm_names enable = try(alarms.value.enable, true) rollback = try(alarms.value.rollback, true) } } dynamic "capacity_provider_strategy" { # Set by task set if deployment controller is external for_each = { for k, v in var.capacity_provider_strategy : k => v if !local.is_external_deployment } content { base = try(capacity_provider_strategy.value.base, null) capacity_provider = capacity_provider_strategy.value.capacity_provider weight = try(capacity_provider_strategy.value.weight, null) } } cluster = var.cluster_arn dynamic "deployment_circuit_breaker" { for_each = length(var.deployment_circuit_breaker) > 0 ? [var.deployment_circuit_breaker] : [] content { enable = deployment_circuit_breaker.value.enable rollback = deployment_circuit_breaker.value.rollback } } dynamic "deployment_controller" { for_each = length(var.deployment_controller) > 0 ? [var.deployment_controller] : [] content { type = try(deployment_controller.value.type, null) } } deployment_maximum_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_maximum_percent deployment_minimum_healthy_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_minimum_healthy_percent desired_count = local.is_daemon || local.is_external_deployment ? null : var.desired_count enable_ecs_managed_tags = var.enable_ecs_managed_tags enable_execute_command = var.enable_execute_command force_new_deployment = local.is_external_deployment ? null : var.force_new_deployment health_check_grace_period_seconds = var.health_check_grace_period_seconds iam_role = local.iam_role_arn launch_type = local.is_external_deployment || length(var.capacity_provider_strategy) > 0 ? null : var.launch_type dynamic "load_balancer" { # Set by task set if deployment controller is external for_each = { for k, v in var.load_balancer : k => v if !local.is_external_deployment } content { container_name = load_balancer.value.container_name container_port = load_balancer.value.container_port elb_name = try(load_balancer.value.elb_name, null) target_group_arn = try(load_balancer.value.target_group_arn, null) } } name = var.name dynamic "network_configuration" { # Set by task set if deployment controller is external for_each = var.network_mode == "awsvpc" && !local.is_external_deployment ? [local.network_configuration] : [] content { assign_public_ip = network_configuration.value.assign_public_ip security_groups = network_configuration.value.security_groups subnets = network_configuration.value.subnets } } dynamic "ordered_placement_strategy" { for_each = var.ordered_placement_strategy content { field = try(ordered_placement_strategy.value.field, null) type = ordered_placement_strategy.value.type } } dynamic "placement_constraints" { for_each = var.placement_constraints content { expression = try(placement_constraints.value.expression, null) type = placement_constraints.value.type } } # Set by task set if deployment controller is external platform_version = local.is_fargate && !local.is_external_deployment ? var.platform_version : null scheduling_strategy = local.is_fargate ? "REPLICA" : var.scheduling_strategy dynamic "service_connect_configuration" { for_each = length(var.service_connect_configuration) > 0 ? [var.service_connect_configuration] : [] content { enabled = try(service_connect_configuration.value.enabled, true) dynamic "log_configuration" { for_each = try([service_connect_configuration.value.log_configuration], []) content { log_driver = try(log_configuration.value.log_driver, null) options = try(log_configuration.value.options, null) dynamic "secret_option" { for_each = try(log_configuration.value.secret_option, []) content { name = secret_option.value.name value_from = secret_option.value.value_from } } } } namespace = lookup(service_connect_configuration.value, "namespace", null) dynamic "service" { for_each = try([service_connect_configuration.value.service], []) content { dynamic "client_alias" { for_each = try([service.value.client_alias], []) content { dns_name = try(client_alias.value.dns_name, null) port = client_alias.value.port } } discovery_name = try(service.value.discovery_name, null) ingress_port_override = try(service.value.ingress_port_override, null) port_name = service.value.port_name } } } } dynamic "service_registries" { # Set by task set if deployment controller is external for_each = length(var.service_registries) > 0 ? [{ for k, v in var.service_registries : k => v if !local.is_external_deployment }] : [] content { container_name = try(service_registries.value.container_name, null) container_port = try(service_registries.value.container_port, null) port = try(service_registries.value.port, null) registry_arn = service_registries.value.registry_arn } } task_definition = local.task_definition triggers = var.triggers wait_for_steady_state = var.wait_for_steady_state propagate_tags = var.propagate_tags tags = merge(var.tags, var.service_tags) timeouts { create = try(var.timeouts.create, null) update = try(var.timeouts.update, null) delete = try(var.timeouts.delete, null) } depends_on = [aws_iam_role_policy_attachment.service] lifecycle { ignore_changes = [ desired_count, # Always ignored ] } } ################################################################################ # Service - Ignore `task_definition` ################################################################################ resource "aws_ecs_service" "ignore_task_definition" { count = local.create_service && var.ignore_task_definition_changes ? 1 : 0 dynamic "alarms" { for_each = length(var.alarms) > 0 ? [var.alarms] : [] content { alarm_names = alarms.value.alarm_names enable = try(alarms.value.enable, true) rollback = try(alarms.value.rollback, true) } } dynamic "capacity_provider_strategy" { # Set by task set if deployment controller is external for_each = { for k, v in var.capacity_provider_strategy : k => v if !local.is_external_deployment } content { base = try(capacity_provider_strategy.value.base, null) capacity_provider = capacity_provider_strategy.value.capacity_provider weight = try(capacity_provider_strategy.value.weight, null) } } cluster = var.cluster_arn dynamic "deployment_circuit_breaker" { for_each = length(var.deployment_circuit_breaker) > 0 ? [var.deployment_circuit_breaker] : [] content { enable = deployment_circuit_breaker.value.enable rollback = deployment_circuit_breaker.value.rollback } } dynamic "deployment_controller" { for_each = length(var.deployment_controller) > 0 ? [var.deployment_controller] : [] content { type = try(deployment_controller.value.type, null) } } deployment_maximum_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_maximum_percent deployment_minimum_healthy_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_minimum_healthy_percent desired_count = local.is_daemon || local.is_external_deployment ? null : var.desired_count enable_ecs_managed_tags = var.enable_ecs_managed_tags enable_execute_command = var.enable_execute_command force_new_deployment = local.is_external_deployment ? null : var.force_new_deployment health_check_grace_period_seconds = var.health_check_grace_period_seconds iam_role = local.iam_role_arn launch_type = local.is_external_deployment || length(var.capacity_provider_strategy) > 0 ? null : var.launch_type dynamic "load_balancer" { # Set by task set if deployment controller is external for_each = { for k, v in var.load_balancer : k => v if !local.is_external_deployment } content { container_name = load_balancer.value.container_name container_port = load_balancer.value.container_port elb_name = try(load_balancer.value.elb_name, null) target_group_arn = try(load_balancer.value.target_group_arn, null) } } name = var.name dynamic "network_configuration" { # Set by task set if deployment controller is external for_each = var.network_mode == "awsvpc" ? [{ for k, v in local.network_configuration : k => v if !local.is_external_deployment }] : [] content { assign_public_ip = network_configuration.value.assign_public_ip security_groups = network_configuration.value.security_groups subnets = network_configuration.value.subnets } } dynamic "ordered_placement_strategy" { for_each = var.ordered_placement_strategy content { field = try(ordered_placement_strategy.value.field, null) type = ordered_placement_strategy.value.type } } dynamic "placement_constraints" { for_each = var.placement_constraints content { expression = try(placement_constraints.value.expression, null) type = placement_constraints.value.type } } # Set by task set if deployment controller is external platform_version = local.is_fargate && !local.is_external_deployment ? var.platform_version : null scheduling_strategy = local.is_fargate ? "REPLICA" : var.scheduling_strategy dynamic "service_connect_configuration" { for_each = length(var.service_connect_configuration) > 0 ? [var.service_connect_configuration] : [] content { enabled = try(service_connect_configuration.value.enabled, true) dynamic "log_configuration" { for_each = try([service_connect_configuration.value.log_configuration], []) content { log_driver = try(log_configuration.value.log_driver, null) options = try(log_configuration.value.options, null) dynamic "secret_option" { for_each = try(log_configuration.value.secret_option, []) content { name = secret_option.value.name value_from = secret_option.value.value_from } } } } namespace = lookup(service_connect_configuration.value, "namespace", null) dynamic "service" { for_each = try([service_connect_configuration.value.service], []) content { dynamic "client_alias" { for_each = try([service.value.client_alias], []) content { dns_name = try(client_alias.value.dns_name, null) port = client_alias.value.port } } discovery_name = try(service.value.discovery_name, null) ingress_port_override = try(service.value.ingress_port_override, null) port_name = service.value.port_name } } } } dynamic "service_registries" { # Set by task set if deployment controller is external for_each = length(var.service_registries) > 0 ? [{ for k, v in var.service_registries : k => v if !local.is_external_deployment }] : [] content { container_name = try(service_registries.value.container_name, null) container_port = try(service_registries.value.container_port, null) port = try(service_registries.value.port, null) registry_arn = service_registries.value.registry_arn } } task_definition = local.task_definition triggers = var.triggers wait_for_steady_state = var.wait_for_steady_state propagate_tags = var.propagate_tags tags = var.tags timeouts { create = try(var.timeouts.create, null) update = try(var.timeouts.update, null) delete = try(var.timeouts.delete, null) } depends_on = [aws_iam_role_policy_attachment.service] lifecycle { ignore_changes = [ desired_count, # Always ignored task_definition, load_balancer, ] } } ################################################################################ # Service - IAM Role ################################################################################ locals { # Role is not required if task definition uses `awsvpc` network mode or if a load balancer is not used needs_iam_role = var.network_mode != "awsvpc" && length(var.load_balancer) > 0 create_iam_role = var.create && var.create_iam_role && local.needs_iam_role iam_role_arn = local.needs_iam_role ? try(aws_iam_role.service[0].arn, var.iam_role_arn) : null iam_role_name = try(coalesce(var.iam_role_name, var.name), "") } data "aws_iam_policy_document" "service_assume" { count = local.create_iam_role ? 1 : 0 statement { sid = "ECSServiceAssumeRole" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs.amazonaws.com"] } } } resource "aws_iam_role" "service" { count = local.create_iam_role ? 1 : 0 name = var.iam_role_use_name_prefix ? null : local.iam_role_name name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null path = var.iam_role_path description = var.iam_role_description assume_role_policy = data.aws_iam_policy_document.service_assume[0].json permissions_boundary = var.iam_role_permissions_boundary force_detach_policies = true tags = merge(var.tags, var.iam_role_tags) } data "aws_iam_policy_document" "service" { count = local.create_iam_role ? 1 : 0 statement { sid = "ECSService" resources = ["*"] actions = [ "ec2:Describe*", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:Describe*", "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "elasticloadbalancing:RegisterTargets" ] } dynamic "statement" { for_each = var.iam_role_statements content { sid = try(statement.value.sid, null) actions = try(statement.value.actions, null) not_actions = try(statement.value.not_actions, null) effect = try(statement.value.effect, null) resources = try(statement.value.resources, null) not_resources = try(statement.value.not_resources, null) dynamic "principals" { for_each = try(statement.value.principals, []) content { type = principals.value.type identifiers = principals.value.identifiers } } dynamic "not_principals" { for_each = try(statement.value.not_principals, []) content { type = not_principals.value.type identifiers = not_principals.value.identifiers } } dynamic "condition" { for_each = try(statement.value.conditions, []) content { test = condition.value.test values = condition.value.values variable = condition.value.variable } } } } } resource "aws_iam_policy" "service" { count = local.create_iam_role ? 1 : 0 name = var.iam_role_use_name_prefix ? null : local.iam_role_name name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null description = coalesce(var.iam_role_description, "ECS service policy that allows Amazon ECS to make calls to your load balancer on your behalf") policy = data.aws_iam_policy_document.service[0].json tags = merge(var.tags, var.iam_role_tags) } resource "aws_iam_role_policy_attachment" "service" { count = local.create_iam_role ? 1 : 0 role = aws_iam_role.service[0].name policy_arn = aws_iam_policy.service[0].arn } ################################################################################ # Container Definition ################################################################################ module "container_definition" { source = "../container-definition" for_each = { for k, v in var.container_definitions : k => v if local.create_task_definition && try(v.create, true) } operating_system_family = try(var.runtime_platform.operating_system_family, "LINUX") # Container Definition command = try(each.value.command, var.container_definition_defaults.command, []) cpu = try(each.value.cpu, var.container_definition_defaults.cpu, null) dependencies = try(each.value.dependencies, var.container_definition_defaults.dependencies, []) # depends_on is a reserved word disable_networking = try(each.value.disable_networking, var.container_definition_defaults.disable_networking, null) dns_search_domains = try(each.value.dns_search_domains, var.container_definition_defaults.dns_search_domains, []) dns_servers = try(each.value.dns_servers, var.container_definition_defaults.dns_servers, []) docker_labels = try(each.value.docker_labels, var.container_definition_defaults.docker_labels, {}) docker_security_options = try(each.value.docker_security_options, var.container_definition_defaults.docker_security_options, []) enable_execute_command = try(each.value.enable_execute_command, var.container_definition_defaults.enable_execute_command, var.enable_execute_command) entrypoint = try(each.value.entrypoint, var.container_definition_defaults.entrypoint, []) environment = try(each.value.environment, var.container_definition_defaults.environment, []) environment_files = try(each.value.environment_files, var.container_definition_defaults.environment_files, []) essential = try(each.value.essential, var.container_definition_defaults.essential, null) extra_hosts = try(each.value.extra_hosts, var.container_definition_defaults.extra_hosts, []) firelens_configuration = try(each.value.firelens_configuration, var.container_definition_defaults.firelens_configuration, {}) health_check = try(each.value.health_check, var.container_definition_defaults.health_check, {}) hostname = try(each.value.hostname, var.container_definition_defaults.hostname, null) image = try(each.value.image, var.container_definition_defaults.image, null) interactive = try(each.value.interactive, var.container_definition_defaults.interactive, false) links = try(each.value.links, var.container_definition_defaults.links, []) linux_parameters = try(each.value.linux_parameters, var.container_definition_defaults.linux_parameters, {}) log_configuration = try(each.value.log_configuration, var.container_definition_defaults.log_configuration, {}) memory = try(each.value.memory, var.container_definition_defaults.memory, null) memory_reservation = try(each.value.memory_reservation, var.container_definition_defaults.memory_reservation, null) mount_points = try(each.value.mount_points, var.container_definition_defaults.mount_points, []) name = try(each.value.name, each.key) port_mappings = try(each.value.port_mappings, var.container_definition_defaults.port_mappings, []) privileged = try(each.value.privileged, var.container_definition_defaults.privileged, false) pseudo_terminal = try(each.value.pseudo_terminal, var.container_definition_defaults.pseudo_terminal, false) readonly_root_filesystem = try(each.value.readonly_root_filesystem, var.container_definition_defaults.readonly_root_filesystem, true) repository_credentials = try(each.value.repository_credentials, var.container_definition_defaults.repository_credentials, {}) resource_requirements = try(each.value.resource_requirements, var.container_definition_defaults.resource_requirements, []) secrets = try(each.value.secrets, var.container_definition_defaults.secrets, []) start_timeout = try(each.value.start_timeout, var.container_definition_defaults.start_timeout, 30) stop_timeout = try(each.value.stop_timeout, var.container_definition_defaults.stop_timeout, 120) system_controls = try(each.value.system_controls, var.container_definition_defaults.system_controls, []) ulimits = try(each.value.ulimits, var.container_definition_defaults.ulimits, []) user = try(each.value.user, var.container_definition_defaults.user, 0) volumes_from = try(each.value.volumes_from, var.container_definition_defaults.volumes_from, []) working_directory = try(each.value.working_directory, var.container_definition_defaults.working_directory, null) # CloudWatch Log Group service = var.name enable_cloudwatch_logging = try(each.value.enable_cloudwatch_logging, var.container_definition_defaults.enable_cloudwatch_logging, true) create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.container_definition_defaults.create_cloudwatch_log_group, true) cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.container_definition_defaults.cloudwatch_log_group_name, null) cloudwatch_log_group_use_name_prefix = try(each.value.cloudwatch_log_group_use_name_prefix, var.container_definition_defaults.cloudwatch_log_group_use_name_prefix, false) cloudwatch_log_group_retention_in_days = try(each.value.cloudwatch_log_group_retention_in_days, var.container_definition_defaults.cloudwatch_log_group_retention_in_days, 14) cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.container_definition_defaults.cloudwatch_log_group_kms_key_id, null) tags = var.tags } ################################################################################ # Task Definition ################################################################################ locals { create_task_definition = var.create && var.create_task_definition # This allows us to query both the existing as well as Terraform's state and get # and get the max version of either source, useful for when external resources # update the container definition max_task_def_revision = local.create_task_definition ? max(aws_ecs_task_definition.this[0].revision, data.aws_ecs_task_definition.this[0].revision) : 0 task_definition = local.create_task_definition ? "${aws_ecs_task_definition.this[0].family}:${local.max_task_def_revision}" : var.task_definition_arn } # This allows us to query both the existing as well as Terraform's state and get # and get the max version of either source, useful for when external resources # update the container definition data "aws_ecs_task_definition" "this" { count = local.create_task_definition ? 1 : 0 task_definition = aws_ecs_task_definition.this[0].family depends_on = [ # Needs to exist first on first deployment aws_ecs_task_definition.this ] } resource "aws_ecs_task_definition" "this" { count = local.create_task_definition ? 1 : 0 # Convert map of maps to array of maps before JSON encoding container_definitions = jsonencode([for k, v in module.container_definition : v.container_definition]) cpu = var.cpu dynamic "ephemeral_storage" { for_each = length(var.ephemeral_storage) > 0 ? [var.ephemeral_storage] : [] content { size_in_gib = ephemeral_storage.value.size_in_gib } } execution_role_arn = try(aws_iam_role.task_exec[0].arn, var.task_exec_iam_role_arn) family = coalesce(var.family, var.name) dynamic "inference_accelerator" { for_each = var.inference_accelerator content { device_name = inference_accelerator.value.device_name device_type = inference_accelerator.value.device_type } } ipc_mode = var.ipc_mode memory = var.memory network_mode = var.network_mode pid_mode = var.pid_mode dynamic "placement_constraints" { for_each = var.task_definition_placement_constraints content { expression = try(placement_constraints.value.expression, null) type = placement_constraints.value.type } } dynamic "proxy_configuration" { for_each = length(var.proxy_configuration) > 0 ? [var.proxy_configuration] : [] content { container_name = proxy_configuration.value.container_name properties = try(proxy_configuration.value.properties, null) type = try(proxy_configuration.value.type, null) } } requires_compatibilities = var.requires_compatibilities dynamic "runtime_platform" { for_each = length(var.runtime_platform) > 0 ? [var.runtime_platform] : [] content { cpu_architecture = try(runtime_platform.value.cpu_architecture, null) operating_system_family = try(runtime_platform.value.operating_system_family, null) } } skip_destroy = var.skip_destroy task_role_arn = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) dynamic "volume" { for_each = var.volume content { dynamic "docker_volume_configuration" { for_each = try([volume.value.docker_volume_configuration], []) content { autoprovision = try(docker_volume_configuration.value.autoprovision, null) driver = try(docker_volume_configuration.value.driver, null) driver_opts = try(docker_volume_configuration.value.driver_opts, null) labels = try(docker_volume_configuration.value.labels, null) scope = try(docker_volume_configuration.value.scope, null) } } dynamic "efs_volume_configuration" { for_each = try([volume.value.efs_volume_configuration], []) content { dynamic "authorization_config" { for_each = try([efs_volume_configuration.value.authorization_config], []) content { access_point_id = try(authorization_config.value.access_point_id, null) iam = try(authorization_config.value.iam, null) } } file_system_id = efs_volume_configuration.value.file_system_id root_directory = try(efs_volume_configuration.value.root_directory, null) transit_encryption = try(efs_volume_configuration.value.transit_encryption, null) transit_encryption_port = try(efs_volume_configuration.value.transit_encryption_port, null) } } dynamic "fsx_windows_file_server_volume_configuration" { for_each = try([volume.value.fsx_windows_file_server_volume_configuration], []) content { dynamic "authorization_config" { for_each = try([fsx_windows_file_server_volume_configuration.value.authorization_config], []) content { credentials_parameter = authorization_config.value.credentials_parameter domain = authorization_config.value.domain } } file_system_id = fsx_windows_file_server_volume_configuration.value.file_system_id root_directory = fsx_windows_file_server_volume_configuration.value.root_directory } } host_path = try(volume.value.host_path, null) name = try(volume.value.name, volume.key) } } tags = merge(var.tags, var.task_tags) lifecycle { create_before_destroy = true } } ################################################################################ # Task Execution - IAM Role # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html ################################################################################ locals { task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, var.name), "") create_task_exec_iam_role = local.create_task_definition && var.create_task_exec_iam_role create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy } data "aws_iam_policy_document" "task_exec_assume" { count = local.create_task_exec_iam_role ? 1 : 0 statement { sid = "ECSTaskExecutionAssumeRole" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } resource "aws_iam_role" "task_exec" { count = local.create_task_exec_iam_role ? 1 : 0 name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null path = var.task_exec_iam_role_path description = coalesce(var.task_exec_iam_role_description, "Task execution role for ${local.task_exec_iam_role_name}") assume_role_policy = data.aws_iam_policy_document.task_exec_assume[0].json max_session_duration = var.task_exec_iam_role_max_session_duration permissions_boundary = var.task_exec_iam_role_permissions_boundary force_detach_policies = true tags = merge(var.tags, var.task_exec_iam_role_tags) } resource "aws_iam_role_policy_attachment" "task_exec_additional" { for_each = { for k, v in var.task_exec_iam_role_policies : k => v if local.create_task_exec_iam_role } role = aws_iam_role.task_exec[0].name policy_arn = each.value } data "aws_iam_policy_document" "task_exec" { count = local.create_task_exec_policy ? 1 : 0 # Pulled from AmazonECSTaskExecutionRolePolicy statement { sid = "Logs" actions = [ "logs:CreateLogStream", "logs:PutLogEvents", ] resources = ["*"] } # Pulled from AmazonECSTaskExecutionRolePolicy statement { sid = "ECR" actions = [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", ] resources = ["*"] } dynamic "statement" { for_each = length(var.task_exec_ssm_param_arns) > 0 ? [1] : [] content { sid = "GetSSMParams" actions = ["ssm:GetParameters"] resources = var.task_exec_ssm_param_arns } } dynamic "statement" { for_each = length(var.task_exec_secret_arns) > 0 ? [1] : [] content { sid = "GetSecrets" actions = ["secretsmanager:GetSecretValue"] resources = var.task_exec_secret_arns } } dynamic "statement" { for_each = var.task_exec_iam_statements content { sid = try(statement.value.sid, null) actions = try(statement.value.actions, null) not_actions = try(statement.value.not_actions, null) effect = try(statement.value.effect, null) resources = try(statement.value.resources, null) not_resources = try(statement.value.not_resources, null) dynamic "principals" { for_each = try(statement.value.principals, []) content { type = principals.value.type identifiers = principals.value.identifiers } } dynamic "not_principals" { for_each = try(statement.value.not_principals, []) content { type = not_principals.value.type identifiers = not_principals.value.identifiers } } dynamic "condition" { for_each = try(statement.value.conditions, []) content { test = condition.value.test values = condition.value.values variable = condition.value.variable } } } } } resource "aws_iam_policy" "task_exec" { count = local.create_task_exec_policy ? 1 : 0 name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null description = coalesce(var.task_exec_iam_role_description, "Task execution role IAM policy") policy = data.aws_iam_policy_document.task_exec[0].json tags = merge(var.tags, var.task_exec_iam_role_tags) } resource "aws_iam_role_policy_attachment" "task_exec" { count = local.create_task_exec_policy ? 1 : 0 role = aws_iam_role.task_exec[0].name policy_arn = aws_iam_policy.task_exec[0].arn } ################################################################################ # Tasks - IAM role # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html ################################################################################ locals { tasks_iam_role_name = try(coalesce(var.tasks_iam_role_name, var.name), "") create_tasks_iam_role = local.create_task_definition && var.create_tasks_iam_role } data "aws_iam_policy_document" "tasks_assume" { count = local.create_tasks_iam_role ? 1 : 0 statement { sid = "ECSTasksAssumeRole" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#create_task_iam_policy_and_role condition { test = "ArnLike" variable = "aws:SourceArn" values = ["arn:${local.partition}:ecs:${local.region}:${local.account_id}:*"] } condition { test = "StringEquals" variable = "aws:SourceAccount" values = [local.account_id] } } } resource "aws_iam_role" "tasks" { count = local.create_tasks_iam_role ? 1 : 0 name = var.tasks_iam_role_use_name_prefix ? null : local.tasks_iam_role_name name_prefix = var.tasks_iam_role_use_name_prefix ? "${local.tasks_iam_role_name}-" : null path = var.tasks_iam_role_path description = var.tasks_iam_role_description assume_role_policy = data.aws_iam_policy_document.tasks_assume[0].json permissions_boundary = var.tasks_iam_role_permissions_boundary force_detach_policies = true tags = merge(var.tags, var.tasks_iam_role_tags) } resource "aws_iam_role_policy_attachment" "tasks" { for_each = { for k, v in var.tasks_iam_role_policies : k => v if local.create_tasks_iam_role } role = aws_iam_role.tasks[0].name policy_arn = each.value } data "aws_iam_policy_document" "tasks" { count = local.create_tasks_iam_role && (length(var.tasks_iam_role_statements) > 0 || var.enable_execute_command) ? 1 : 0 dynamic "statement" { for_each = var.enable_execute_command ? [1] : [] content { sid = "ECSExec" actions = [ "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", ] resources = ["*"] } } dynamic "statement" { for_each = var.tasks_iam_role_statements content { sid = try(statement.value.sid, null) actions = try(statement.value.actions, null) not_actions = try(statement.value.not_actions, null) effect = try(statement.value.effect, null) resources = try(statement.value.resources, null) not_resources = try(statement.value.not_resources, null) dynamic "principals" { for_each = try(statement.value.principals, []) content { type = principals.value.type identifiers = principals.value.identifiers } } dynamic "not_principals" { for_each = try(statement.value.not_principals, []) content { type = not_principals.value.type identifiers = not_principals.value.identifiers } } dynamic "condition" { for_each = try(statement.value.conditions, []) content { test = condition.value.test values = condition.value.values variable = condition.value.variable } } } } } resource "aws_iam_role_policy" "tasks" { count = local.create_tasks_iam_role && (length(var.tasks_iam_role_statements) > 0 || var.enable_execute_command) ? 1 : 0 name = var.tasks_iam_role_use_name_prefix ? null : local.tasks_iam_role_name name_prefix = var.tasks_iam_role_use_name_prefix ? "${local.tasks_iam_role_name}-" : null policy = data.aws_iam_policy_document.tasks[0].json role = aws_iam_role.tasks[0].id } ################################################################################ # Task Set ################################################################################ resource "aws_ecs_task_set" "this" { # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskset.html count = local.create_task_definition && local.is_external_deployment && !var.ignore_task_definition_changes ? 1 : 0 service = try(aws_ecs_service.this[0].id, aws_ecs_service.ignore_task_definition[0].id) cluster = var.cluster_arn external_id = var.external_id task_definition = local.task_definition dynamic "network_configuration" { for_each = var.network_mode == "awsvpc" ? [local.network_configuration] : [] content { assign_public_ip = network_configuration.value.assign_public_ip security_groups = network_configuration.value.security_groups subnets = network_configuration.value.subnets } } dynamic "load_balancer" { for_each = var.load_balancer content { load_balancer_name = try(load_balancer.value.load_balancer_name, null) target_group_arn = try(load_balancer.value.target_group_arn, null) container_name = load_balancer.value.container_name container_port = try(load_balancer.value.container_port, null) } } dynamic "service_registries" { for_each = length(var.service_registries) > 0 ? [var.service_registries] : [] content { container_name = try(service_registries.value.container_name, null) container_port = try(service_registries.value.container_port, null) port = try(service_registries.value.port, null) registry_arn = service_registries.value.registry_arn } } launch_type = length(var.capacity_provider_strategy) > 0 ? null : var.launch_type dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategy content { base = try(capacity_provider_strategy.value.base, null) capacity_provider = capacity_provider_strategy.value.capacity_provider weight = try(capacity_provider_strategy.value.weight, null) } } platform_version = local.is_fargate ? var.platform_version : null dynamic "scale" { for_each = length(var.scale) > 0 ? [var.scale] : [] content { unit = try(scale.value.unit, null) value = try(scale.value.value, null) } } force_delete = var.force_delete wait_until_stable = var.wait_until_stable wait_until_stable_timeout = var.wait_until_stable_timeout tags = merge(var.tags, var.task_tags) lifecycle { ignore_changes = [ scale, # Always ignored ] } } ################################################################################ # Task Set - Ignore `task_definition` ################################################################################ resource "aws_ecs_task_set" "ignore_task_definition" { # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskset.html count = local.create_task_definition && local.is_external_deployment && var.ignore_task_definition_changes ? 1 : 0 service = try(aws_ecs_service.this[0].id, aws_ecs_service.ignore_task_definition[0].id) cluster = var.cluster_arn external_id = var.external_id task_definition = local.task_definition dynamic "network_configuration" { for_each = var.network_mode == "awsvpc" ? [local.network_configuration] : [] content { assign_public_ip = network_configuration.value.assign_public_ip security_groups = network_configuration.value.security_groups subnets = network_configuration.value.subnets } } dynamic "load_balancer" { for_each = var.load_balancer content { load_balancer_name = try(load_balancer.value.load_balancer_name, null) target_group_arn = try(load_balancer.value.target_group_arn, null) container_name = load_balancer.value.container_name container_port = try(load_balancer.value.container_port, null) } } dynamic "service_registries" { for_each = length(var.service_registries) > 0 ? [var.service_registries] : [] content { container_name = try(service_registries.value.container_name, null) container_port = try(service_registries.value.container_port, null) port = try(service_registries.value.port, null) registry_arn = service_registries.value.registry_arn } } launch_type = length(var.capacity_provider_strategy) > 0 ? null : var.launch_type dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategy content { base = try(capacity_provider_strategy.value.base, null) capacity_provider = capacity_provider_strategy.value.capacity_provider weight = try(capacity_provider_strategy.value.weight, null) } } platform_version = local.is_fargate ? var.platform_version : null dynamic "scale" { for_each = length(var.scale) > 0 ? [var.scale] : [] content { unit = try(scale.value.unit, null) value = try(scale.value.value, null) } } force_delete = var.force_delete wait_until_stable = var.wait_until_stable wait_until_stable_timeout = var.wait_until_stable_timeout tags = merge(var.tags, var.task_tags) lifecycle { ignore_changes = [ scale, # Always ignored task_definition, ] } } ################################################################################ # Autoscaling ################################################################################ locals { enable_autoscaling = local.create_service && var.enable_autoscaling && !local.is_daemon cluster_name = element(split("/", var.cluster_arn), 1) } resource "aws_appautoscaling_target" "this" { count = local.enable_autoscaling ? 1 : 0 # Desired needs to be between or equal to min/max min_capacity = min(var.autoscaling_min_capacity, var.desired_count) max_capacity = max(var.autoscaling_max_capacity, var.desired_count) resource_id = "service/${local.cluster_name}/${try(aws_ecs_service.this[0].name, aws_ecs_service.ignore_task_definition[0].name)}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" tags = var.tags } resource "aws_appautoscaling_policy" "this" { for_each = { for k, v in var.autoscaling_policies : k => v if local.enable_autoscaling } name = try(each.value.name, each.key) policy_type = try(each.value.policy_type, "TargetTrackingScaling") resource_id = aws_appautoscaling_target.this[0].resource_id scalable_dimension = aws_appautoscaling_target.this[0].scalable_dimension service_namespace = aws_appautoscaling_target.this[0].service_namespace dynamic "step_scaling_policy_configuration" { for_each = try([each.value.step_scaling_policy_configuration], []) content { adjustment_type = try(step_scaling_policy_configuration.value.adjustment_type, null) cooldown = try(step_scaling_policy_configuration.value.cooldown, null) metric_aggregation_type = try(step_scaling_policy_configuration.value.metric_aggregation_type, null) min_adjustment_magnitude = try(step_scaling_policy_configuration.value.min_adjustment_magnitude, null) dynamic "step_adjustment" { for_each = try(step_scaling_policy_configuration.value.step_adjustment, []) content { metric_interval_lower_bound = try(step_adjustment.value.metric_interval_lower_bound, null) metric_interval_upper_bound = try(step_adjustment.value.metric_interval_upper_bound, null) scaling_adjustment = try(step_adjustment.value.scaling_adjustment, null) } } } } dynamic "target_tracking_scaling_policy_configuration" { for_each = try(each.value.policy_type, null) == "TargetTrackingScaling" ? try([each.value.target_tracking_scaling_policy_configuration], []) : [] content { dynamic "customized_metric_specification" { for_each = try([target_tracking_scaling_policy_configuration.value.customized_metric_specification], []) content { dynamic "dimensions" { for_each = try(customized_metric_specification.value.dimensions, []) content { name = dimensions.value.name value = dimensions.value.value } } metric_name = customized_metric_specification.value.metric_name namespace = customized_metric_specification.value.namespace statistic = customized_metric_specification.value.statistic unit = try(customized_metric_specification.value.unit, null) } } disable_scale_in = try(target_tracking_scaling_policy_configuration.value.disable_scale_in, null) dynamic "predefined_metric_specification" { for_each = try([target_tracking_scaling_policy_configuration.value.predefined_metric_specification], []) content { predefined_metric_type = predefined_metric_specification.value.predefined_metric_type resource_label = try(predefined_metric_specification.value.resource_label, null) } } scale_in_cooldown = try(target_tracking_scaling_policy_configuration.value.scale_in_cooldown, 300) scale_out_cooldown = try(target_tracking_scaling_policy_configuration.value.scale_out_cooldown, 60) target_value = try(target_tracking_scaling_policy_configuration.value.target_value, 75) } } } resource "aws_appautoscaling_scheduled_action" "this" { for_each = { for k, v in var.autoscaling_scheduled_actions : k => v if local.enable_autoscaling } name = try(each.value.name, each.key) service_namespace = aws_appautoscaling_target.this[0].service_namespace resource_id = aws_appautoscaling_target.this[0].resource_id scalable_dimension = aws_appautoscaling_target.this[0].scalable_dimension scalable_target_action { min_capacity = each.value.min_capacity max_capacity = each.value.max_capacity } schedule = each.value.schedule start_time = try(each.value.start_time, null) end_time = try(each.value.end_time, null) timezone = try(each.value.timezone, null) } ################################################################################ # Security Group ################################################################################ locals { create_security_group = var.create && var.create_security_group && var.network_mode == "awsvpc" security_group_name = try(coalesce(var.security_group_name, var.name), "") } data "aws_subnet" "this" { count = local.create_security_group ? 1 : 0 id = element(var.subnet_ids, 0) } resource "aws_security_group" "this" { count = local.create_security_group ? 1 : 0 name = var.security_group_use_name_prefix ? null : local.security_group_name name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null description = var.security_group_description vpc_id = data.aws_subnet.this[0].vpc_id tags = merge( var.tags, { "Name" = local.security_group_name }, var.security_group_tags ) lifecycle { create_before_destroy = true } } resource "aws_security_group_rule" "this" { for_each = { for k, v in var.security_group_rules : k => v if local.create_security_group } # Required security_group_id = aws_security_group.this[0].id protocol = each.value.protocol from_port = each.value.from_port to_port = each.value.to_port type = each.value.type # Optional description = lookup(each.value, "description", null) cidr_blocks = lookup(each.value, "cidr_blocks", null) ipv6_cidr_blocks = lookup(each.value, "ipv6_cidr_blocks", null) prefix_list_ids = lookup(each.value, "prefix_list_ids", null) self = lookup(each.value, "self", null) source_security_group_id = lookup(each.value, "source_security_group_id", null) }