From 6777f4353e539a6e57cdc2c590d5dcb446b9eb03 Mon Sep 17 00:00:00 2001 From: Christian Budde Christensen Date: Fri, 22 Aug 2025 16:38:20 +0100 Subject: [PATCH 1/2] feat: extract task definition --- examples/task/README.md | 62 +++++ examples/task/main.tf | 111 ++++++++ examples/task/outputs.tf | 47 ++++ examples/task/versions.tf | 10 + main.tf | 67 +++++ modules/service/main.tf | 544 ++++-------------------------------- modules/service/outputs.tf | 34 +-- modules/task/README.md | 102 +++++++ modules/task/main.tf | 523 +++++++++++++++++++++++++++++++++++ modules/task/outputs.tf | 67 +++++ modules/task/variables.tf | 550 +++++++++++++++++++++++++++++++++++++ modules/task/versions.tf | 10 + outputs.tf | 5 + variables.tf | 283 +++++++++++++++++++ wrappers/task/README.md | 79 ++++++ wrappers/task/main.tf | 59 ++++ wrappers/task/outputs.tf | 4 + wrappers/task/variables.tf | 11 + wrappers/task/versions.tf | 10 + 19 files changed, 2072 insertions(+), 506 deletions(-) create mode 100644 examples/task/README.md create mode 100644 examples/task/main.tf create mode 100644 examples/task/outputs.tf create mode 100644 examples/task/versions.tf create mode 100644 modules/task/README.md create mode 100644 modules/task/main.tf create mode 100644 modules/task/outputs.tf create mode 100644 modules/task/variables.tf create mode 100644 modules/task/versions.tf create mode 100644 wrappers/task/README.md create mode 100644 wrappers/task/main.tf create mode 100644 wrappers/task/outputs.tf create mode 100644 wrappers/task/variables.tf create mode 100644 wrappers/task/versions.tf diff --git a/examples/task/README.md b/examples/task/README.md new file mode 100644 index 00000000..e5cacb10 --- /dev/null +++ b/examples/task/README.md @@ -0,0 +1,62 @@ +# ECS Task Definition Example + +Configuration in this directory creates: + +- ECS Task Definition using the standalone task module +- ECS Cluster with a task definition using the complete module +- Associated IAM roles for task execution and tasks + +## Usage + +To run this example you need to execute: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` + +Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. + + + +## Requirements + +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.5.7 | +| [aws](#requirement_aws) | >= 6.4 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +| ----------------------------------------------------------------------- | ------------------ | ------- | +| [ecs_complete](#module_ecs_complete) | ../../ | n/a | +| [ecs_task](#module_ecs_task) | ../../modules/task | n/a | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| [cluster_arn](#output_cluster_arn) | ARN that identifies the cluster | +| [cluster_id](#output_cluster_id) | ID that identifies the cluster | +| [cluster_name](#output_cluster_name) | Name that identifies the cluster | +| [task_definition_arn](#output_task_definition_arn) | Full ARN of the task definition | +| [task_definition_family](#output_task_definition_family) | The unique name of the task definition | +| [task_exec_iam_role_arn](#output_task_exec_iam_role_arn) | Task execution IAM role ARN | +| [tasks](#output_tasks) | Map of tasks created and their attributes | +| [tasks_iam_role_arn](#output_tasks_iam_role_arn) | Tasks IAM role ARN | + + diff --git a/examples/task/main.tf b/examples/task/main.tf new file mode 100644 index 00000000..eefdc3ba --- /dev/null +++ b/examples/task/main.tf @@ -0,0 +1,111 @@ +provider "aws" { + region = local.region +} + +locals { + region = "us-east-1" + name = "ex-${basename(path.cwd)}" + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs" + } +} + +################################################################################ +# ECS Module - Task Only +################################################################################ + +module "ecs_task" { + source = "../../modules/task" + + name = "${local.name}-task" + + # Container definitions + container_definitions = { + nginx = { + cpu = 256 + memory = 512 + essential = true + image = "public.ecr.aws/nginx/nginx:latest" + portMappings = [ + { + name = "nginx" + containerPort = 80 + protocol = "tcp" + } + ] + + # Enable logging + enable_cloudwatch_logging = true + create_cloudwatch_log_group = true + cloudwatch_log_group_retention_in_days = 1 + } + } + + cpu = 512 + memory = 1024 + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + + runtime_platform = { + operating_system_family = "LINUX" + cpu_architecture = "X86_64" + } + + # Task execution role + create_task_exec_iam_role = true + + # Task role + create_tasks_iam_role = true + + tags = local.tags +} + +################################################################################ +# ECS Module - Complete (Cluster + Task) +################################################################################ + +module "ecs_complete" { + source = "../../" + + cluster_name = local.name + + # Task definitions + tasks = { + standalone-task = { + name = "${local.name}-standalone" + + container_definitions = { + httpd = { + cpu = 256 + memory = 512 + essential = true + image = "public.ecr.aws/docker/library/httpd:latest" + portMappings = [ + { + name = "httpd" + containerPort = 80 + protocol = "tcp" + } + ] + + enable_cloudwatch_logging = true + create_cloudwatch_log_group = true + cloudwatch_log_group_retention_in_days = 1 + } + } + + cpu = 512 + memory = 1024 + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + + create_task_exec_iam_role = true + create_tasks_iam_role = true + } + } + + tags = local.tags +} diff --git a/examples/task/outputs.tf b/examples/task/outputs.tf new file mode 100644 index 00000000..954a5b4f --- /dev/null +++ b/examples/task/outputs.tf @@ -0,0 +1,47 @@ +################################################################################ +# Task Module +################################################################################ + +output "task_definition_arn" { + description = "Full ARN of the task definition" + value = module.ecs_task.task_definition_arn +} + +output "task_definition_family" { + description = "The unique name of the task definition" + value = module.ecs_task.task_definition_family +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_task.task_exec_iam_role_arn +} + +output "tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = module.ecs_task.tasks_iam_role_arn +} + +################################################################################ +# Complete Module +################################################################################ + +output "cluster_arn" { + description = "ARN that identifies the cluster" + value = module.ecs_complete.cluster_arn +} + +output "cluster_id" { + description = "ID that identifies the cluster" + value = module.ecs_complete.cluster_id +} + +output "cluster_name" { + description = "Name that identifies the cluster" + value = module.ecs_complete.cluster_name +} + +output "tasks" { + description = "Map of tasks created and their attributes" + value = module.ecs_complete.tasks +} diff --git a/examples/task/versions.tf b/examples/task/versions.tf new file mode 100644 index 00000000..497e3e61 --- /dev/null +++ b/examples/task/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.4" + } + } +} diff --git a/main.tf b/main.tf index 5e1d1fea..e8013c96 100644 --- a/main.tf +++ b/main.tf @@ -192,3 +192,70 @@ module "service" { tags = merge(var.tags, each.value.tags) } + +################################################################################ +# Task(s) +################################################################################ + +module "task" { + source = "./modules/task" + + for_each = var.create && var.tasks != null ? var.tasks : {} + + create = each.value.create + region = var.region + + # Task definition + name = coalesce(each.value.name, each.key) + enable_execute_command = each.value.enable_execute_command + create_task_definition = each.value.create_task_definition + task_definition_arn = each.value.task_definition_arn + container_definitions = each.value.container_definitions + cpu = each.value.cpu + enable_fault_injection = each.value.enable_fault_injection + ephemeral_storage = each.value.ephemeral_storage + family = each.value.family + ipc_mode = each.value.ipc_mode + memory = each.value.memory + network_mode = each.value.network_mode + pid_mode = each.value.pid_mode + proxy_configuration = each.value.proxy_configuration + requires_compatibilities = each.value.requires_compatibilities + runtime_platform = each.value.runtime_platform + skip_destroy = each.value.skip_destroy + task_definition_placement_constraints = each.value.task_definition_placement_constraints + track_latest = each.value.track_latest + volume = each.value.volume + task_tags = each.value.task_tags + + # Task Execution IAM role + create_task_exec_iam_role = each.value.create_task_exec_iam_role + task_exec_iam_role_arn = each.value.task_exec_iam_role_arn + task_exec_iam_role_name = each.value.task_exec_iam_role_name + task_exec_iam_role_use_name_prefix = each.value.task_exec_iam_role_use_name_prefix + task_exec_iam_role_path = each.value.task_exec_iam_role_path + task_exec_iam_role_description = each.value.task_exec_iam_role_description + task_exec_iam_role_permissions_boundary = each.value.task_exec_iam_role_permissions_boundary + task_exec_iam_role_tags = each.value.task_exec_iam_role_tags + task_exec_iam_role_policies = each.value.task_exec_iam_role_policies + task_exec_iam_role_max_session_duration = each.value.task_exec_iam_role_max_session_duration + create_task_exec_policy = each.value.create_task_exec_policy + task_exec_ssm_param_arns = each.value.task_exec_ssm_param_arns + task_exec_secret_arns = each.value.task_exec_secret_arns + task_exec_iam_statements = each.value.task_exec_iam_statements + task_exec_iam_policy_path = each.value.task_exec_iam_policy_path + + # Tasks IAM role + create_tasks_iam_role = each.value.create_tasks_iam_role + tasks_iam_role_arn = each.value.tasks_iam_role_arn + tasks_iam_role_name = each.value.tasks_iam_role_name + tasks_iam_role_use_name_prefix = each.value.tasks_iam_role_use_name_prefix + tasks_iam_role_path = each.value.tasks_iam_role_path + tasks_iam_role_description = each.value.tasks_iam_role_description + tasks_iam_role_permissions_boundary = each.value.tasks_iam_role_permissions_boundary + tasks_iam_role_tags = each.value.tasks_iam_role_tags + tasks_iam_role_policies = each.value.tasks_iam_role_policies + tasks_iam_role_statements = each.value.tasks_iam_role_statements + + tags = merge(var.tags, each.value.tags) +} diff --git a/modules/service/main.tf b/modules/service/main.tf index d463378d..2f829489 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -793,509 +793,79 @@ resource "aws_iam_role_policy_attachment" "service" { policy_arn = aws_iam_policy.service[0].arn } -################################################################################ -# Container Definition -################################################################################ - -module "container_definition" { - source = "../container-definition" - - region = var.region - - for_each = { for k, v in var.container_definitions : k => v if local.create_task_definition && v.create } - - enable_execute_command = var.enable_execute_command - operating_system_family = var.runtime_platform.operating_system_family - - # Container Definition - command = each.value.command - cpu = each.value.cpu - dependsOn = each.value.dependsOn - disableNetworking = each.value.disableNetworking - dnsSearchDomains = each.value.dnsSearchDomains - dnsServers = each.value.dnsServers - dockerLabels = each.value.dockerLabels - dockerSecurityOptions = each.value.dockerSecurityOptions - entrypoint = each.value.entrypoint - environment = each.value.environment - environmentFiles = each.value.environmentFiles - essential = each.value.essential - extraHosts = each.value.extraHosts - firelensConfiguration = each.value.firelensConfiguration - healthCheck = each.value.healthCheck - hostname = each.value.hostname - image = each.value.image - interactive = each.value.interactive - links = each.value.links - linuxParameters = each.value.linuxParameters - logConfiguration = each.value.logConfiguration - memory = each.value.memory - memoryReservation = each.value.memoryReservation - mountPoints = each.value.mountPoints - name = coalesce(each.value.name, each.key) - portMappings = each.value.portMappings - privileged = each.value.privileged - pseudoTerminal = each.value.pseudoTerminal - readonlyRootFilesystem = each.value.readonlyRootFilesystem - repositoryCredentials = each.value.repositoryCredentials - resourceRequirements = each.value.resourceRequirements - restartPolicy = each.value.restartPolicy - secrets = each.value.secrets - startTimeout = each.value.startTimeout - stopTimeout = each.value.stopTimeout - systemControls = each.value.systemControls - ulimits = each.value.ulimits - user = each.value.user - versionConsistency = each.value.versionConsistency - volumesFrom = each.value.volumesFrom - workingDirectory = each.value.workingDirectory - - # CloudWatch Log Group - service = var.name - enable_cloudwatch_logging = each.value.enable_cloudwatch_logging - create_cloudwatch_log_group = each.value.create_cloudwatch_log_group - cloudwatch_log_group_name = each.value.cloudwatch_log_group_name - cloudwatch_log_group_use_name_prefix = each.value.cloudwatch_log_group_use_name_prefix - cloudwatch_log_group_class = each.value.cloudwatch_log_group_class - cloudwatch_log_group_retention_in_days = each.value.cloudwatch_log_group_retention_in_days - cloudwatch_log_group_kms_key_id = each.value.cloudwatch_log_group_kms_key_id - - tags = var.tags -} - ################################################################################ # Task Definition ################################################################################ -locals { - create_task_definition = var.create && var.create_task_definition - task_definition = local.create_task_definition ? aws_ecs_task_definition.this[0].arn : var.task_definition_arn -} +module "task" { + source = "../task" -resource "aws_ecs_task_definition" "this" { count = local.create_task_definition ? 1 : 0 + create = var.create region = var.region - # 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 - enable_fault_injection = var.enable_fault_injection - - dynamic "ephemeral_storage" { - for_each = var.ephemeral_storage != null ? [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) - - 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 != null ? var.task_definition_placement_constraints : {} - - content { - expression = placement_constraints.value.expression - type = placement_constraints.value.type - } - } - - dynamic "proxy_configuration" { - for_each = var.proxy_configuration != null ? [var.proxy_configuration] : [] - - content { - container_name = proxy_configuration.value.container_name - properties = proxy_configuration.value.properties - type = proxy_configuration.value.type - } - } - - requires_compatibilities = var.requires_compatibilities - - dynamic "runtime_platform" { - for_each = var.runtime_platform != null ? [var.runtime_platform] : [] - - content { - cpu_architecture = runtime_platform.value.cpu_architecture - operating_system_family = runtime_platform.value.operating_system_family - } - } - - skip_destroy = var.skip_destroy - task_role_arn = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) - track_latest = var.track_latest - - dynamic "volume" { - for_each = var.volume != null ? var.volume : {} - - content { - configure_at_launch = volume.value.configure_at_launch - - dynamic "docker_volume_configuration" { - for_each = volume.value.docker_volume_configuration != null ? [volume.value.docker_volume_configuration] : [] - - content { - autoprovision = docker_volume_configuration.value.autoprovision - driver = docker_volume_configuration.value.driver - driver_opts = docker_volume_configuration.value.driver_opts - labels = docker_volume_configuration.value.labels - scope = docker_volume_configuration.value.scope - } - } - - dynamic "efs_volume_configuration" { - for_each = volume.value.efs_volume_configuration != null ? [volume.value.efs_volume_configuration] : [] - - content { - dynamic "authorization_config" { - for_each = efs_volume_configuration.value.authorization_config != null ? [efs_volume_configuration.value.authorization_config] : [] - - content { - access_point_id = authorization_config.value.access_point_id - iam = authorization_config.value.iam - } - } - - file_system_id = efs_volume_configuration.value.file_system_id - root_directory = efs_volume_configuration.value.root_directory - transit_encryption = efs_volume_configuration.value.transit_encryption - transit_encryption_port = efs_volume_configuration.value.transit_encryption_port - } - } - - dynamic "fsx_windows_file_server_volume_configuration" { - for_each = volume.value.fsx_windows_file_server_volume_configuration != null ? [volume.value.fsx_windows_file_server_volume_configuration] : [] - - content { - dynamic "authorization_config" { - for_each = fsx_windows_file_server_volume_configuration.value.authorization_config != null ? [fsx_windows_file_server_volume_configuration.value.authorization_config] : [] - - content { - credentials_parameter = authorization_config.value.credentials_parameter - domain = authorization_config.value.domain - } - } + # Task definition + name = var.name + enable_execute_command = var.enable_execute_command + create_task_definition = var.create_task_definition + task_definition_arn = var.task_definition_arn + container_definitions = var.container_definitions + cpu = var.cpu + enable_fault_injection = var.enable_fault_injection + ephemeral_storage = var.ephemeral_storage + family = var.family + ipc_mode = var.ipc_mode + memory = var.memory + network_mode = var.network_mode + pid_mode = var.pid_mode + proxy_configuration = var.proxy_configuration + requires_compatibilities = var.requires_compatibilities + runtime_platform = var.runtime_platform + skip_destroy = var.skip_destroy + task_definition_placement_constraints = var.task_definition_placement_constraints + track_latest = var.track_latest + volume = var.volume + task_tags = var.task_tags + + # Task Execution IAM role + create_task_exec_iam_role = var.create_task_exec_iam_role + task_exec_iam_role_arn = var.task_exec_iam_role_arn + task_exec_iam_role_name = var.task_exec_iam_role_name + task_exec_iam_role_use_name_prefix = var.task_exec_iam_role_use_name_prefix + task_exec_iam_role_path = var.task_exec_iam_role_path + task_exec_iam_role_description = var.task_exec_iam_role_description + task_exec_iam_role_permissions_boundary = var.task_exec_iam_role_permissions_boundary + task_exec_iam_role_tags = var.task_exec_iam_role_tags + task_exec_iam_role_policies = var.task_exec_iam_role_policies + task_exec_iam_role_max_session_duration = var.task_exec_iam_role_max_session_duration + create_task_exec_policy = var.create_task_exec_policy + task_exec_ssm_param_arns = var.task_exec_ssm_param_arns + task_exec_secret_arns = var.task_exec_secret_arns + task_exec_iam_statements = var.task_exec_iam_statements + task_exec_iam_policy_path = var.task_exec_iam_policy_path + + # Tasks IAM role + create_tasks_iam_role = var.create_tasks_iam_role + tasks_iam_role_arn = var.tasks_iam_role_arn + tasks_iam_role_name = var.tasks_iam_role_name + tasks_iam_role_use_name_prefix = var.tasks_iam_role_use_name_prefix + tasks_iam_role_path = var.tasks_iam_role_path + tasks_iam_role_description = var.tasks_iam_role_description + tasks_iam_role_permissions_boundary = var.tasks_iam_role_permissions_boundary + tasks_iam_role_tags = var.tasks_iam_role_tags + tasks_iam_role_policies = var.tasks_iam_role_policies + tasks_iam_role_statements = var.tasks_iam_role_statements - 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 = volume.value.host_path - name = coalesce(volume.value.name, volume.key) - } - } - - tags = merge(var.tags, var.task_tags) - - depends_on = [ - aws_iam_role_policy_attachment.tasks, - aws_iam_role_policy_attachment.task_exec, - aws_iam_role_policy_attachment.task_exec_additional, - ] - - 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 = coalesce(var.task_exec_iam_role_name, var.name, "NotProvided") - - 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 != null ? var.task_exec_iam_statements : [] - - content { - sid = statement.value.sid - actions = statement.value.actions - not_actions = statement.value.not_actions - effect = statement.value.effect - resources = statement.value.resources - not_resources = statement.value.not_resources - - dynamic "principals" { - for_each = statement.value.principals != null ? statement.value.principals : [] - - content { - type = principals.value.type - identifiers = principals.value.identifiers - } - } - - dynamic "not_principals" { - for_each = statement.value.not_principals != null ? statement.value.not_principals : [] - - content { - type = not_principals.value.type - identifiers = not_principals.value.identifiers - } - } - - dynamic "condition" { - for_each = statement.value.condition != null ? statement.value.condition : [] - - 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 - path = var.task_exec_iam_policy_path - 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 + tags = var.tags } -################################################################################ -# Tasks - IAM role -# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html -################################################################################ - locals { - tasks_iam_role_name = coalesce(var.tasks_iam_role_name, var.name, "NotProvided") - 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) -} - -data "aws_iam_policy_document" "tasks" { - count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || 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 != null ? var.tasks_iam_role_statements : [] - - content { - sid = statement.value.sid - actions = statement.value.actions - not_actions = statement.value.not_actions - effect = statement.value.effect - resources = statement.value.resources - not_resources = statement.value.not_resources - - dynamic "principals" { - for_each = statement.value.principals != null ? statement.value.principals : [] - - content { - type = principals.value.type - identifiers = principals.value.identifiers - } - } - - dynamic "not_principals" { - for_each = statement.value.not_principals != null ? statement.value.not_principals : [] - - content { - type = not_principals.value.type - identifiers = not_principals.value.identifiers - } - } - - dynamic "condition" { - for_each = statement.value.condition != null ? statement.value.condition : [] - - content { - test = condition.value.test - values = condition.value.values - variable = condition.value.variable - } - } - } - } -} - -resource "aws_iam_policy" "tasks" { - count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || 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 - description = coalesce(var.tasks_iam_role_description, "Task role IAM policy") - policy = data.aws_iam_policy_document.tasks[0].json - path = var.tasks_iam_role_path - tags = merge(var.tags, var.tasks_iam_role_tags) -} - -resource "aws_iam_role_policy_attachment" "tasks_internal" { - count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || var.enable_execute_command) ? 1 : 0 - - role = aws_iam_role.tasks[0].name - policy_arn = aws_iam_policy.tasks[0].arn + create_task_definition = var.create && var.create_task_definition + task_definition = local.create_task_definition ? module.task[0].task_definition_arn : var.task_definition_arn } -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 -} ################################################################################ # Task Set diff --git a/modules/service/outputs.tf b/modules/service/outputs.tf index 0d81c8f9..883d1d57 100644 --- a/modules/service/outputs.tf +++ b/modules/service/outputs.tf @@ -32,31 +32,27 @@ output "iam_role_unique_id" { } ################################################################################ -# Container Definition -################################################################################ - -output "container_definitions" { - description = "Container definitions" - value = module.container_definition -} - -################################################################################ -# Task Definition +# Task Module ################################################################################ output "task_definition_arn" { description = "Full ARN of the Task Definition (including both `family` and `revision`)" - value = try(aws_ecs_task_definition.this[0].arn, var.task_definition_arn) + value = local.create_task_definition ? module.task[0].task_definition_arn : var.task_definition_arn } output "task_definition_revision" { description = "Revision of the task in a particular family" - value = try(aws_ecs_task_definition.this[0].revision, null) + value = local.create_task_definition ? module.task[0].task_definition_revision : null } output "task_definition_family" { description = "The unique name of the task definition" - value = try(aws_ecs_task_definition.this[0].family, null) + value = local.create_task_definition ? module.task[0].task_definition_family : null +} + +output "container_definitions" { + description = "Container definitions" + value = local.create_task_definition ? module.task[0].container_definitions : {} } ################################################################################ @@ -66,17 +62,17 @@ output "task_definition_family" { output "task_exec_iam_role_name" { description = "Task execution IAM role name" - value = try(aws_iam_role.task_exec[0].name, null) + value = local.create_task_definition ? module.task[0].task_exec_iam_role_name : null } output "task_exec_iam_role_arn" { description = "Task execution IAM role ARN" - value = try(aws_iam_role.task_exec[0].arn, var.task_exec_iam_role_arn) + value = local.create_task_definition ? module.task[0].task_exec_iam_role_arn : var.task_exec_iam_role_arn } output "task_exec_iam_role_unique_id" { description = "Stable and unique string identifying the task execution IAM role" - value = try(aws_iam_role.task_exec[0].unique_id, null) + value = local.create_task_definition ? module.task[0].task_exec_iam_role_unique_id : null } ################################################################################ @@ -86,17 +82,17 @@ output "task_exec_iam_role_unique_id" { output "tasks_iam_role_name" { description = "Tasks IAM role name" - value = try(aws_iam_role.tasks[0].name, null) + value = local.create_task_definition ? module.task[0].tasks_iam_role_name : null } output "tasks_iam_role_arn" { description = "Tasks IAM role ARN" - value = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) + value = local.create_task_definition ? module.task[0].tasks_iam_role_arn : var.tasks_iam_role_arn } output "tasks_iam_role_unique_id" { description = "Stable and unique string identifying the tasks IAM role" - value = try(aws_iam_role.tasks[0].unique_id, null) + value = local.create_task_definition ? module.task[0].tasks_iam_role_unique_id : null } ################################################################################ diff --git a/modules/task/README.md b/modules/task/README.md new file mode 100644 index 00000000..55bb548d --- /dev/null +++ b/modules/task/README.md @@ -0,0 +1,102 @@ +# AWS ECS Task Terraform sub-module + +Terraform sub-module which creates ECS (Elastic Container Service) task definition and related IAM resources on AWS. + +## Usage + +```hcl +module "ecs_task" { + source = "terraform-aws-modules/ecs/aws//modules/task" + + name = "my-task" + + container_definitions = { + app = { + cpu = 512 + memory = 1024 + essential = true + image = "nginx:latest" + portMappings = [ + { + name = "app" + containerPort = 80 + protocol = "tcp" + } + ] + } + } + + # Task execution IAM role + create_task_exec_iam_role = true + task_exec_iam_role_name = "my-task-exec-role" + + # Tasks IAM role + create_tasks_iam_role = true + tasks_iam_role_name = "my-task-role" + + tags = { + Environment = "dev" + Project = "example" + } +} +``` + +## Examples + +- [Complete ECS Task](../../examples/) + + + +## Requirements + +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.5.7 | +| [aws](#requirement_aws) | >= 6.4 | + +## Providers + +| Name | Version | +| ------------------------------------------------ | ------- | +| [aws](#provider_aws) | >= 6.4 | + +## Modules + +| Name | Source | Version | +| ----------------------------------------------------------------------------------------------- | ----------------------- | ------- | +| [container_definition](#module_container_definition) | ../container-definition | n/a | + +## Resources + +| Name | Type | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_policy.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_exec_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.tasks_internal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_exec_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.tasks_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +See [variables.tf](./variables.tf) for a complete list and description of all configurable inputs. + +## Outputs + +See [outputs.tf](./outputs.tf) for a complete list and description of all outputs. + + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/modules/task/main.tf b/modules/task/main.tf new file mode 100644 index 00000000..f0ba9056 --- /dev/null +++ b/modules/task/main.tf @@ -0,0 +1,523 @@ +data "aws_region" "current" { + region = var.region + + count = var.create ? 1 : 0 +} +data "aws_partition" "current" { + count = var.create ? 1 : 0 +} +data "aws_caller_identity" "current" { + count = var.create ? 1 : 0 +} + +locals { + account_id = try(data.aws_caller_identity.current[0].account_id, "") + partition = try(data.aws_partition.current[0].partition, "") + region = try(data.aws_region.current[0].region, "") +} + +################################################################################ +# Container Definition +################################################################################ + +module "container_definition" { + source = "../container-definition" + + region = var.region + + for_each = { for k, v in var.container_definitions : k => v if local.create_task_definition && v.create } + + enable_execute_command = var.enable_execute_command + operating_system_family = var.runtime_platform.operating_system_family + + # Container Definition + command = each.value.command + cpu = each.value.cpu + dependsOn = each.value.dependsOn + disableNetworking = each.value.disableNetworking + dnsSearchDomains = each.value.dnsSearchDomains + dnsServers = each.value.dnsServers + dockerLabels = each.value.dockerLabels + dockerSecurityOptions = each.value.dockerSecurityOptions + entrypoint = each.value.entrypoint + environment = each.value.environment + environmentFiles = each.value.environmentFiles + essential = each.value.essential + extraHosts = each.value.extraHosts + firelensConfiguration = each.value.firelensConfiguration + healthCheck = each.value.healthCheck + hostname = each.value.hostname + image = each.value.image + interactive = each.value.interactive + links = each.value.links + linuxParameters = each.value.linuxParameters + logConfiguration = each.value.logConfiguration + memory = each.value.memory + memoryReservation = each.value.memoryReservation + mountPoints = each.value.mountPoints + name = coalesce(each.value.name, each.key) + portMappings = each.value.portMappings + privileged = each.value.privileged + pseudoTerminal = each.value.pseudoTerminal + readonlyRootFilesystem = each.value.readonlyRootFilesystem + repositoryCredentials = each.value.repositoryCredentials + resourceRequirements = each.value.resourceRequirements + restartPolicy = each.value.restartPolicy + secrets = each.value.secrets + startTimeout = each.value.startTimeout + stopTimeout = each.value.stopTimeout + systemControls = each.value.systemControls + ulimits = each.value.ulimits + user = each.value.user + versionConsistency = each.value.versionConsistency + volumesFrom = each.value.volumesFrom + workingDirectory = each.value.workingDirectory + + # CloudWatch Log Group + service = var.name + enable_cloudwatch_logging = each.value.enable_cloudwatch_logging + create_cloudwatch_log_group = each.value.create_cloudwatch_log_group + cloudwatch_log_group_name = each.value.cloudwatch_log_group_name + cloudwatch_log_group_use_name_prefix = each.value.cloudwatch_log_group_use_name_prefix + cloudwatch_log_group_class = each.value.cloudwatch_log_group_class + cloudwatch_log_group_retention_in_days = each.value.cloudwatch_log_group_retention_in_days + cloudwatch_log_group_kms_key_id = each.value.cloudwatch_log_group_kms_key_id + + tags = var.tags +} + +################################################################################ +# Task Definition +################################################################################ + +locals { + create_task_definition = var.create && var.create_task_definition + task_definition = local.create_task_definition ? aws_ecs_task_definition.this[0].arn : var.task_definition_arn +} + +resource "aws_ecs_task_definition" "this" { + count = local.create_task_definition ? 1 : 0 + + region = var.region + + # 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 + enable_fault_injection = var.enable_fault_injection + + dynamic "ephemeral_storage" { + for_each = var.ephemeral_storage != null ? [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) + + 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 != null ? var.task_definition_placement_constraints : {} + + content { + expression = placement_constraints.value.expression + type = placement_constraints.value.type + } + } + + dynamic "proxy_configuration" { + for_each = var.proxy_configuration != null ? [var.proxy_configuration] : [] + + content { + container_name = proxy_configuration.value.container_name + properties = proxy_configuration.value.properties + type = proxy_configuration.value.type + } + } + + requires_compatibilities = var.requires_compatibilities + + dynamic "runtime_platform" { + for_each = var.runtime_platform != null ? [var.runtime_platform] : [] + + content { + cpu_architecture = runtime_platform.value.cpu_architecture + operating_system_family = runtime_platform.value.operating_system_family + } + } + + skip_destroy = var.skip_destroy + task_role_arn = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) + track_latest = var.track_latest + + dynamic "volume" { + for_each = var.volume != null ? var.volume : {} + + content { + configure_at_launch = volume.value.configure_at_launch + + dynamic "docker_volume_configuration" { + for_each = volume.value.docker_volume_configuration != null ? [volume.value.docker_volume_configuration] : [] + + content { + autoprovision = docker_volume_configuration.value.autoprovision + driver = docker_volume_configuration.value.driver + driver_opts = docker_volume_configuration.value.driver_opts + labels = docker_volume_configuration.value.labels + scope = docker_volume_configuration.value.scope + } + } + + dynamic "efs_volume_configuration" { + for_each = volume.value.efs_volume_configuration != null ? [volume.value.efs_volume_configuration] : [] + + content { + dynamic "authorization_config" { + for_each = efs_volume_configuration.value.authorization_config != null ? [efs_volume_configuration.value.authorization_config] : [] + + content { + access_point_id = authorization_config.value.access_point_id + iam = authorization_config.value.iam + } + } + + file_system_id = efs_volume_configuration.value.file_system_id + root_directory = efs_volume_configuration.value.root_directory + transit_encryption = efs_volume_configuration.value.transit_encryption + transit_encryption_port = efs_volume_configuration.value.transit_encryption_port + } + } + + dynamic "fsx_windows_file_server_volume_configuration" { + for_each = volume.value.fsx_windows_file_server_volume_configuration != null ? [volume.value.fsx_windows_file_server_volume_configuration] : [] + + content { + dynamic "authorization_config" { + for_each = fsx_windows_file_server_volume_configuration.value.authorization_config != null ? [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 = volume.value.host_path + name = coalesce(volume.value.name, volume.key) + } + } + + tags = merge(var.tags, var.task_tags) + + depends_on = [ + aws_iam_role_policy_attachment.tasks, + aws_iam_role_policy_attachment.task_exec, + aws_iam_role_policy_attachment.task_exec_additional, + ] + + 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 = coalesce(var.task_exec_iam_role_name, var.name, "NotProvided") + + 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 != null ? var.task_exec_iam_statements : [] + + content { + sid = statement.value.sid + actions = statement.value.actions + not_actions = statement.value.not_actions + effect = statement.value.effect + resources = statement.value.resources + not_resources = statement.value.not_resources + + dynamic "principals" { + for_each = statement.value.principals != null ? statement.value.principals : [] + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = statement.value.not_principals != null ? statement.value.not_principals : [] + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = statement.value.condition != null ? statement.value.condition : [] + + 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 + path = var.task_exec_iam_policy_path + 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 = coalesce(var.tasks_iam_role_name, var.name, "NotProvided") + 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) +} + +data "aws_iam_policy_document" "tasks" { + count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || 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 != null ? var.tasks_iam_role_statements : [] + + content { + sid = statement.value.sid + actions = statement.value.actions + not_actions = statement.value.not_actions + effect = statement.value.effect + resources = statement.value.resources + not_resources = statement.value.not_resources + + dynamic "principals" { + for_each = statement.value.principals != null ? statement.value.principals : [] + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = statement.value.not_principals != null ? statement.value.not_principals : [] + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = statement.value.condition != null ? statement.value.condition : [] + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "tasks" { + count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || 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 + description = coalesce(var.tasks_iam_role_description, "Task role IAM policy") + policy = data.aws_iam_policy_document.tasks[0].json + path = var.tasks_iam_role_path + tags = merge(var.tags, var.tasks_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "tasks_internal" { + count = local.create_tasks_iam_role && (var.tasks_iam_role_statements != null || var.enable_execute_command) ? 1 : 0 + + role = aws_iam_role.tasks[0].name + policy_arn = aws_iam_policy.tasks[0].arn +} + +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 +} + + diff --git a/modules/task/outputs.tf b/modules/task/outputs.tf new file mode 100644 index 00000000..806f548a --- /dev/null +++ b/modules/task/outputs.tf @@ -0,0 +1,67 @@ +################################################################################ +# Container Definition +################################################################################ + +output "container_definitions" { + description = "Container definitions" + value = module.container_definition +} + +################################################################################ +# Task Definition +################################################################################ + +output "task_definition_arn" { + description = "Full ARN of the Task Definition (including both `family` and `revision`)" + value = try(aws_ecs_task_definition.this[0].arn, var.task_definition_arn) +} + +output "task_definition_revision" { + description = "Revision of the task in a particular family" + value = try(aws_ecs_task_definition.this[0].revision, null) +} + +output "task_definition_family" { + description = "The unique name of the task definition" + value = try(aws_ecs_task_definition.this[0].family, null) +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = try(aws_iam_role.task_exec[0].name, null) +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = try(aws_iam_role.task_exec[0].arn, var.task_exec_iam_role_arn) +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = try(aws_iam_role.task_exec[0].unique_id, null) +} + +################################################################################ +# Tasks - IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +################################################################################ + +output "tasks_iam_role_name" { + description = "Tasks IAM role name" + value = try(aws_iam_role.tasks[0].name, null) +} + +output "tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) +} + +output "tasks_iam_role_unique_id" { + description = "Stable and unique string identifying the tasks IAM role" + value = try(aws_iam_role.tasks[0].unique_id, null) +} diff --git a/modules/task/variables.tf b/modules/task/variables.tf new file mode 100644 index 00000000..34681f54 --- /dev/null +++ b/modules/task/variables.tf @@ -0,0 +1,550 @@ +variable "create" { + description = "Determines whether resources will be created (affects all resources)" + type = bool + default = true + nullable = false +} + +variable "region" { + description = "Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration" + type = string + default = null +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} + nullable = false +} + +variable "name" { + description = "Name of the task (up to 255 letters, numbers, hyphens, and underscores)" + type = string + default = null +} + +variable "enable_execute_command" { + description = "Specifies whether to enable Amazon ECS Exec for the tasks" + type = bool + default = false + nullable = false +} + +################################################################################ +# Task Definition +################################################################################ + +variable "create_task_definition" { + description = "Determines whether to create a task definition or use existing/provided" + type = bool + default = true + nullable = false +} + +variable "task_definition_arn" { + description = "Existing task definition ARN. Required when `create_task_definition` is `false`" + type = string + default = null +} + +variable "container_definitions" { + description = "A map of valid [container definitions](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html). Please note that you should only provide values that are part of the container definition document" + type = map(object({ + create = optional(bool, true) + operating_system_family = optional(string) + tags = optional(map(string)) + + # Container definition + command = optional(list(string)) + cpu = optional(number) + dependsOn = optional(list(object({ + condition = string + containerName = string + }))) + disableNetworking = optional(bool) + dnsSearchDomains = optional(list(string)) + dnsServers = optional(list(string)) + dockerLabels = optional(map(string)) + dockerSecurityOptions = optional(list(string)) + entrypoint = optional(list(string)) + environment = optional(list(object({ + name = string + value = string + }))) + environmentFiles = optional(list(object({ + type = string + value = string + }))) + essential = optional(bool) + extraHosts = optional(list(object({ + hostname = string + ipAddress = string + }))) + firelensConfiguration = optional(object({ + options = optional(map(string)) + type = optional(string) + })) + healthCheck = optional(object({ + command = optional(list(string), []) + interval = optional(number, 30) + retries = optional(number, 3) + startPeriod = optional(number) + timeout = optional(number, 5) + })) + hostname = optional(string) + image = optional(string) + interactive = optional(bool) + links = optional(list(string)) + linuxParameters = optional(object({ + capabilities = optional(object({ + add = optional(list(string)) + drop = optional(list(string)) + })) + devices = optional(list(object({ + containerPath = optional(string) + hostPath = optional(string) + permissions = optional(list(string)) + }))) + initProcessEnabled = optional(bool) + maxSwap = optional(number) + sharedMemorySize = optional(number) + swappiness = optional(number) + tmpfs = optional(list(object({ + containerPath = string + mountOptions = optional(list(string)) + size = number + }))) + })) + logConfiguration = optional(object({ + logDriver = optional(string) + options = optional(map(string)) + secretOptions = optional(list(object({ + name = string + valueFrom = string + }))) + })) + memory = optional(number) + memoryReservation = optional(number) + mountPoints = optional(list(object({ + containerPath = optional(string) + readOnly = optional(bool) + sourceVolume = optional(string) + }))) + name = optional(string) + portMappings = optional(list(object({ + appProtocol = optional(string) + containerPort = optional(number) + containerPortRange = optional(string) + hostPort = optional(number) + name = optional(string) + protocol = optional(string) + }))) + privileged = optional(bool) + pseudoTerminal = optional(bool) + readonlyRootFilesystem = optional(bool) + repositoryCredentials = optional(object({ + credentialsParameter = optional(string) + })) + resourceRequirements = optional(list(object({ + type = string + value = string + }))) + restartPolicy = optional(object({ + enabled = optional(bool) + ignoredExitCodes = optional(list(number)) + restartAttemptPeriod = optional(number) + })) + secrets = optional(list(object({ + name = string + valueFrom = string + }))) + startTimeout = optional(number, 30) + stopTimeout = optional(number, 120) + systemControls = optional(list(object({ + namespace = optional(string) + value = optional(string) + }))) + ulimits = optional(list(object({ + hardLimit = number + name = string + softLimit = number + }))) + user = optional(string) + versionConsistency = optional(string) + volumesFrom = optional(list(object({ + readOnly = optional(bool) + sourceContainer = optional(string) + }))) + workingDirectory = optional(string) + + # Cloudwatch Log Group + service = optional(string) + enable_cloudwatch_logging = optional(bool) + create_cloudwatch_log_group = optional(bool) + cloudwatch_log_group_name = optional(string) + cloudwatch_log_group_use_name_prefix = optional(bool) + cloudwatch_log_group_class = optional(string) + cloudwatch_log_group_retention_in_days = optional(number) + cloudwatch_log_group_kms_key_id = optional(string) + })) + default = {} +} + +variable "cpu" { + description = "Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 1024 +} + +variable "enable_fault_injection" { + description = "Enables fault injection and allows for fault injection requests to be accepted from the task's containers. Default is `false`" + type = bool + default = null +} + +variable "ephemeral_storage" { + description = "The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate" + type = object({ + size_in_gib = number + }) + default = null +} + +variable "family" { + description = "A unique name for your task definition" + type = string + default = null +} + +variable "ipc_mode" { + description = "IPC resource namespace to be used for the containers in the task The valid values are `host`, `task`, and `none`" + type = string + default = null +} + +variable "memory" { + description = "Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 2048 +} + +variable "network_mode" { + description = "Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host`" + type = string + default = "awsvpc" + nullable = false +} + +variable "pid_mode" { + description = "Process namespace to use for the containers in the task. The valid values are `host` and `task`" + type = string + default = null +} + +variable "proxy_configuration" { + description = "Configuration block for the App Mesh proxy" + type = object({ + container_name = string + properties = optional(map(string)) + type = optional(string) + }) + default = null +} + +variable "requires_compatibilities" { + description = "Set of launch types required by the task. The valid values are `EC2` and `FARGATE`" + type = list(string) + default = ["FARGATE"] + nullable = false +} + +variable "runtime_platform" { + description = "Configuration block for `runtime_platform` that containers in your task may use" + type = object({ + cpu_architecture = optional(string, "X86_64") + operating_system_family = optional(string, "LINUX") + }) + default = { + operating_system_family = "LINUX" + cpu_architecture = "X86_64" + } + nullable = false +} + +variable "skip_destroy" { + description = "If true, the task is not deleted when the service is deleted" + type = bool + default = null +} + +variable "task_definition_placement_constraints" { + description = "Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the task definition, see `placement_constraints` for setting at the service" + type = map(object({ + expression = optional(string) + type = string + })) + default = null +} + +variable "track_latest" { + description = "Whether should track latest `ACTIVE` task definition on AWS or the one created with the resource stored in state. Useful in the event the task definition is modified outside of this resource" + type = bool + default = true + nullable = false +} + +variable "volume" { + description = "Configuration block for volumes that containers in your task may use" + type = map(object({ + configure_at_launch = optional(bool) + docker_volume_configuration = optional(object({ + autoprovision = optional(bool) + driver = optional(string) + driver_opts = optional(map(string)) + labels = optional(map(string)) + scope = optional(string) + })) + efs_volume_configuration = optional(object({ + authorization_config = optional(object({ + access_point_id = optional(string) + iam = optional(string) + })) + file_system_id = string + root_directory = optional(string) + transit_encryption = optional(string) + transit_encryption_port = optional(number) + })) + fsx_windows_file_server_volume_configuration = optional(object({ + authorization_config = optional(object({ + credentials_parameter = string + domain = string + })) + file_system_id = string + root_directory = string + })) + host_path = optional(string) + name = optional(string) + })) + default = null +} + +variable "task_tags" { + description = "A map of additional tags to add to the task definition/set created" + type = map(string) + default = {} + nullable = false +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +variable "create_task_exec_iam_role" { + description = "Determines whether the ECS task definition IAM role should be created" + type = bool + default = true + nullable = false +} + +variable "task_exec_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "task_exec_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "task_exec_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "task_exec_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "task_exec_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "task_exec_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "task_exec_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} + nullable = false +} + +variable "task_exec_iam_role_policies" { + description = "Map of IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = {} + nullable = false +} + +variable "task_exec_iam_role_max_session_duration" { + description = "Maximum session duration (in seconds) for ECS task execution role. Default is 3600." + type = number + default = null +} + +variable "create_task_exec_policy" { + description = "Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters" + type = bool + default = true + nullable = false +} + +variable "task_exec_ssm_param_arns" { + description = "List of SSM parameter ARNs the task execution role will be permitted to get/read" + type = list(string) + default = [] + nullable = false +} + +variable "task_exec_secret_arns" { + description = "List of SecretsManager secret ARNs the task execution role will be permitted to get/read" + type = list(string) + default = [] + nullable = false +} + +variable "task_exec_iam_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = list(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string) + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + values = list(string) + variable = string + }))) + })) + default = null +} + +variable "task_exec_iam_policy_path" { + description = "Path for the iam role" + type = string + default = null +} + +################################################################################ +# Tasks - IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +################################################################################ + +variable "create_tasks_iam_role" { + description = "Determines whether the ECS tasks IAM role should be created" + type = bool + default = true + nullable = false +} + +variable "tasks_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "tasks_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "tasks_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "tasks_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "tasks_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "tasks_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "tasks_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} + nullable = false +} + +variable "tasks_iam_role_policies" { + description = "Map of additioanl IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = {} + nullable = false +} + +variable "tasks_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = list(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string) + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + values = list(string) + variable = string + }))) + })) + default = null +} + + diff --git a/modules/task/versions.tf b/modules/task/versions.tf new file mode 100644 index 00000000..497e3e61 --- /dev/null +++ b/modules/task/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.4" + } + } +} diff --git a/outputs.tf b/outputs.tf index 6e462c6e..e7fc3dae 100644 --- a/outputs.tf +++ b/outputs.tf @@ -60,3 +60,8 @@ output "services" { description = "Map of services created and their attributes" value = module.service } + +output "tasks" { + description = "Map of tasks created and their attributes" + value = module.task +} diff --git a/variables.tf b/variables.tf index 9aba2f23..7f6d633f 100644 --- a/variables.tf +++ b/variables.tf @@ -815,3 +815,286 @@ variable "services" { })) default = null } + +################################################################################ +# Task(s) +################################################################################ + +variable "tasks" { + description = "Map of task definitions to create" + type = map(object({ + create = optional(bool) + tags = optional(map(string)) + + # Task definition + name = optional(string) # Will fall back to use map key if not set + enable_execute_command = optional(bool) + create_task_definition = optional(bool) + task_definition_arn = optional(string) + container_definitions = optional(map(object({ + create = optional(bool, true) + operating_system_family = optional(string) + tags = optional(map(string)) + + # Container definition + command = optional(list(string)) + cpu = optional(number) + dependsOn = optional(list(object({ + condition = string + containerName = string + }))) + disableNetworking = optional(bool) + dnsSearchDomains = optional(list(string)) + dnsServers = optional(list(string)) + dockerLabels = optional(map(string)) + dockerSecurityOptions = optional(list(string)) + entrypoint = optional(list(string)) + environment = optional(list(object({ + name = string + value = string + }))) + environmentFiles = optional(list(object({ + type = string + value = string + }))) + essential = optional(bool) + extraHosts = optional(list(object({ + hostname = string + ipAddress = string + }))) + firelensConfiguration = optional(object({ + options = optional(map(string)) + type = optional(string) + })) + healthCheck = optional(object({ + command = optional(list(string)) + interval = optional(number) + retries = optional(number) + startPeriod = optional(number) + timeout = optional(number) + })) + hostname = optional(string) + image = optional(string) + interactive = optional(bool) + links = optional(list(string)) + linuxParameters = optional(object({ + capabilities = optional(object({ + add = optional(list(string)) + drop = optional(list(string)) + })) + devices = optional(list(object({ + containerPath = optional(string) + hostPath = optional(string) + permissions = optional(list(string)) + }))) + initProcessEnabled = optional(bool) + maxSwap = optional(number) + sharedMemorySize = optional(number) + swappiness = optional(number) + tmpfs = optional(list(object({ + containerPath = string + mountOptions = optional(list(string)) + size = number + }))) + })) + logConfiguration = optional(object({ + logDriver = optional(string) + options = optional(map(string)) + secretOptions = optional(list(object({ + name = string + valueFrom = string + }))) + })) + memory = optional(number) + memoryReservation = optional(number) + mountPoints = optional(list(object({ + containerPath = optional(string) + readOnly = optional(bool) + sourceVolume = optional(string) + }))) + name = optional(string) + portMappings = optional(list(object({ + appProtocol = optional(string) + containerPort = optional(number) + containerPortRange = optional(string) + hostPort = optional(number) + name = optional(string) + protocol = optional(string) + }))) + privileged = optional(bool) + pseudoTerminal = optional(bool) + readonlyRootFilesystem = optional(bool) + repositoryCredentials = optional(object({ + credentialsParameter = optional(string) + })) + resourceRequirements = optional(list(object({ + type = string + value = string + }))) + restartPolicy = optional(object({ + enabled = optional(bool) + ignoredExitCodes = optional(list(number)) + restartAttemptPeriod = optional(number) + })) + secrets = optional(list(object({ + name = string + valueFrom = string + }))) + startTimeout = optional(number) + stopTimeout = optional(number) + systemControls = optional(list(object({ + namespace = optional(string) + value = optional(string) + }))) + ulimits = optional(list(object({ + hardLimit = number + name = string + softLimit = number + }))) + user = optional(string) + versionConsistency = optional(string) + volumesFrom = optional(list(object({ + readOnly = optional(bool) + sourceContainer = optional(string) + }))) + workingDirectory = optional(string) + + # Cloudwatch Log Group + service = optional(string) + enable_cloudwatch_logging = optional(bool) + create_cloudwatch_log_group = optional(bool) + cloudwatch_log_group_name = optional(string) + cloudwatch_log_group_use_name_prefix = optional(bool) + cloudwatch_log_group_class = optional(string) + cloudwatch_log_group_retention_in_days = optional(number) + cloudwatch_log_group_kms_key_id = optional(string) + }))) + cpu = optional(number, 1024) + enable_fault_injection = optional(bool) + ephemeral_storage = optional(object({ + size_in_gib = number + })) + family = optional(string) + ipc_mode = optional(string) + memory = optional(number, 2048) + network_mode = optional(string) + pid_mode = optional(string) + proxy_configuration = optional(object({ + container_name = string + properties = optional(map(string)) + type = optional(string) + })) + requires_compatibilities = optional(list(string)) + runtime_platform = optional(object({ + cpu_architecture = optional(string) + operating_system_family = optional(string) + })) + skip_destroy = optional(bool) + task_definition_placement_constraints = optional(map(object({ + expression = optional(string) + type = string + }))) + track_latest = optional(bool) + volume = optional(map(object({ + configure_at_launch = optional(bool) + docker_volume_configuration = optional(object({ + autoprovision = optional(bool) + driver = optional(string) + driver_opts = optional(map(string)) + labels = optional(map(string)) + scope = optional(string) + })) + efs_volume_configuration = optional(object({ + authorization_config = optional(object({ + access_point_id = optional(string) + iam = optional(string) + })) + file_system_id = string + root_directory = optional(string) + transit_encryption = optional(string) + transit_encryption_port = optional(number) + })) + fsx_windows_file_server_volume_configuration = optional(object({ + authorization_config = optional(object({ + credentials_parameter = string + domain = string + })) + file_system_id = string + root_directory = string + })) + host_path = optional(string) + name = optional(string) + }))) + task_tags = optional(map(string)) + + # Task Execution IAM role + create_task_exec_iam_role = optional(bool) + task_exec_iam_role_arn = optional(string) + task_exec_iam_role_name = optional(string) + task_exec_iam_role_use_name_prefix = optional(bool) + task_exec_iam_role_path = optional(string) + task_exec_iam_role_description = optional(string) + task_exec_iam_role_permissions_boundary = optional(string) + task_exec_iam_role_tags = optional(map(string)) + task_exec_iam_role_policies = optional(map(string)) + task_exec_iam_role_max_session_duration = optional(number) + create_task_exec_policy = optional(bool) + task_exec_ssm_param_arns = optional(list(string)) + task_exec_secret_arns = optional(list(string)) + task_exec_iam_statements = optional(list(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string) + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + values = list(string) + variable = string + }))) + }))) + task_exec_iam_policy_path = optional(string) + + # Tasks IAM role + create_tasks_iam_role = optional(bool) + tasks_iam_role_arn = optional(string) + tasks_iam_role_name = optional(string) + tasks_iam_role_use_name_prefix = optional(bool) + tasks_iam_role_path = optional(string) + tasks_iam_role_description = optional(string) + tasks_iam_role_permissions_boundary = optional(string) + tasks_iam_role_tags = optional(map(string)) + tasks_iam_role_policies = optional(map(string)) + tasks_iam_role_statements = optional(list(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string) + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + values = list(string) + variable = string + }))) + }))) + })) + default = null +} diff --git a/wrappers/task/README.md b/wrappers/task/README.md new file mode 100644 index 00000000..37aba126 --- /dev/null +++ b/wrappers/task/README.md @@ -0,0 +1,79 @@ +# Terraform AWS ECS Task Module Wrapper + +Configuration in this directory creates ECS task definition resources in various combinations. + +This module is a wrapper over the [task](../../modules/task/) module, which allows managing several task resources in one place. + +## Usage + +```hcl +module "ecs_task_wrapper" { + source = "terraform-aws-modules/ecs/aws//wrappers/task" + + defaults = { + create_task_exec_iam_role = true + create_tasks_iam_role = true + } + + items = { + task1 = { + name = "my-task-1" + container_definitions = { + app = { + image = "nginx:latest" + } + } + } + task2 = { + name = "my-task-2" + container_definitions = { + app = { + image = "httpd:latest" + } + } + } + } +} +``` + +## Examples + +See the [examples](../../examples/) directory for a complete example. + + + +## Requirements + +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.5.7 | +| [aws](#requirement_aws) | >= 6.4 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +| -------------------------------------------------------- | ------------------ | ------- | +| [wrapper](#module_wrapper) | ../../modules/task | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +| --------------------------------------------------------- | ------------------------------------------------------- | ----- | ------- | :------: | +| [defaults](#input_defaults) | Map of default values which will be used for each item. | `any` | `{}` | no | +| [items](#input_items) | Map of objects. Each object represents one item. | `any` | `{}` | no | + +## Outputs + +| Name | Description | +| -------------------------------------------------------- | ---------------------------- | +| [wrapper](#output_wrapper) | Map of outputs of a wrapper. | + + diff --git a/wrappers/task/main.tf b/wrappers/task/main.tf new file mode 100644 index 00000000..1da9a74b --- /dev/null +++ b/wrappers/task/main.tf @@ -0,0 +1,59 @@ +module "wrapper" { + source = "../../modules/task" + + for_each = var.items + + create = try(each.value.create, var.defaults.create, true) + region = try(each.value.region, var.defaults.region, null) + tags = try(each.value.tags, var.defaults.tags, {}) + name = try(each.value.name, var.defaults.name, null) + enable_execute_command = try(each.value.enable_execute_command, var.defaults.enable_execute_command, false) + create_task_definition = try(each.value.create_task_definition, var.defaults.create_task_definition, true) + task_definition_arn = try(each.value.task_definition_arn, var.defaults.task_definition_arn, null) + container_definitions = try(each.value.container_definitions, var.defaults.container_definitions, {}) + cpu = try(each.value.cpu, var.defaults.cpu, 1024) + enable_fault_injection = try(each.value.enable_fault_injection, var.defaults.enable_fault_injection, null) + ephemeral_storage = try(each.value.ephemeral_storage, var.defaults.ephemeral_storage, null) + family = try(each.value.family, var.defaults.family, null) + ipc_mode = try(each.value.ipc_mode, var.defaults.ipc_mode, null) + memory = try(each.value.memory, var.defaults.memory, 2048) + network_mode = try(each.value.network_mode, var.defaults.network_mode, "awsvpc") + pid_mode = try(each.value.pid_mode, var.defaults.pid_mode, null) + proxy_configuration = try(each.value.proxy_configuration, var.defaults.proxy_configuration, null) + requires_compatibilities = try(each.value.requires_compatibilities, var.defaults.requires_compatibilities, ["FARGATE"]) + runtime_platform = try(each.value.runtime_platform, var.defaults.runtime_platform, { operating_system_family = "LINUX", cpu_architecture = "X86_64" }) + skip_destroy = try(each.value.skip_destroy, var.defaults.skip_destroy, null) + task_definition_placement_constraints = try(each.value.task_definition_placement_constraints, var.defaults.task_definition_placement_constraints, null) + track_latest = try(each.value.track_latest, var.defaults.track_latest, true) + volume = try(each.value.volume, var.defaults.volume, null) + task_tags = try(each.value.task_tags, var.defaults.task_tags, {}) + + # Task Execution IAM role + create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, true) + task_exec_iam_role_arn = try(each.value.task_exec_iam_role_arn, var.defaults.task_exec_iam_role_arn, null) + task_exec_iam_role_name = try(each.value.task_exec_iam_role_name, var.defaults.task_exec_iam_role_name, null) + task_exec_iam_role_use_name_prefix = try(each.value.task_exec_iam_role_use_name_prefix, var.defaults.task_exec_iam_role_use_name_prefix, true) + task_exec_iam_role_path = try(each.value.task_exec_iam_role_path, var.defaults.task_exec_iam_role_path, null) + task_exec_iam_role_description = try(each.value.task_exec_iam_role_description, var.defaults.task_exec_iam_role_description, null) + task_exec_iam_role_permissions_boundary = try(each.value.task_exec_iam_role_permissions_boundary, var.defaults.task_exec_iam_role_permissions_boundary, null) + task_exec_iam_role_tags = try(each.value.task_exec_iam_role_tags, var.defaults.task_exec_iam_role_tags, {}) + task_exec_iam_role_policies = try(each.value.task_exec_iam_role_policies, var.defaults.task_exec_iam_role_policies, {}) + task_exec_iam_role_max_session_duration = try(each.value.task_exec_iam_role_max_session_duration, var.defaults.task_exec_iam_role_max_session_duration, null) + create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) + task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) + task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) + task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) + task_exec_iam_policy_path = try(each.value.task_exec_iam_policy_path, var.defaults.task_exec_iam_policy_path, null) + + # Tasks IAM role + create_tasks_iam_role = try(each.value.create_tasks_iam_role, var.defaults.create_tasks_iam_role, true) + tasks_iam_role_arn = try(each.value.tasks_iam_role_arn, var.defaults.tasks_iam_role_arn, null) + tasks_iam_role_name = try(each.value.tasks_iam_role_name, var.defaults.tasks_iam_role_name, null) + tasks_iam_role_use_name_prefix = try(each.value.tasks_iam_role_use_name_prefix, var.defaults.tasks_iam_role_use_name_prefix, true) + tasks_iam_role_path = try(each.value.tasks_iam_role_path, var.defaults.tasks_iam_role_path, null) + tasks_iam_role_description = try(each.value.tasks_iam_role_description, var.defaults.tasks_iam_role_description, null) + tasks_iam_role_permissions_boundary = try(each.value.tasks_iam_role_permissions_boundary, var.defaults.tasks_iam_role_permissions_boundary, null) + tasks_iam_role_tags = try(each.value.tasks_iam_role_tags, var.defaults.tasks_iam_role_tags, {}) + tasks_iam_role_policies = try(each.value.tasks_iam_role_policies, var.defaults.tasks_iam_role_policies, {}) + tasks_iam_role_statements = try(each.value.tasks_iam_role_statements, var.defaults.tasks_iam_role_statements, null) +} diff --git a/wrappers/task/outputs.tf b/wrappers/task/outputs.tf new file mode 100644 index 00000000..d4602543 --- /dev/null +++ b/wrappers/task/outputs.tf @@ -0,0 +1,4 @@ +output "wrapper" { + description = "Map of outputs of a wrapper." + value = module.wrapper +} diff --git a/wrappers/task/variables.tf b/wrappers/task/variables.tf new file mode 100644 index 00000000..26153c17 --- /dev/null +++ b/wrappers/task/variables.tf @@ -0,0 +1,11 @@ +variable "defaults" { + description = "Map of default values which will be used for each item." + type = any + default = {} +} + +variable "items" { + description = "Map of objects. Each object represents one item." + type = any + default = {} +} diff --git a/wrappers/task/versions.tf b/wrappers/task/versions.tf new file mode 100644 index 00000000..497e3e61 --- /dev/null +++ b/wrappers/task/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.4" + } + } +} From 81c74b1182675ffec67c1a1ff5d8557236cd0b8f Mon Sep 17 00:00:00 2001 From: Christian Budde Christensen Date: Fri, 22 Aug 2025 16:44:36 +0100 Subject: [PATCH 2/2] fix: update output variable method --- modules/service/outputs.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/service/outputs.tf b/modules/service/outputs.tf index 883d1d57..ce800c07 100644 --- a/modules/service/outputs.tf +++ b/modules/service/outputs.tf @@ -37,22 +37,22 @@ output "iam_role_unique_id" { output "task_definition_arn" { description = "Full ARN of the Task Definition (including both `family` and `revision`)" - value = local.create_task_definition ? module.task[0].task_definition_arn : var.task_definition_arn + value = local.create_task_definition ? try(module.task[0].task_definition_arn, null) : var.task_definition_arn } output "task_definition_revision" { description = "Revision of the task in a particular family" - value = local.create_task_definition ? module.task[0].task_definition_revision : null + value = local.create_task_definition ? try(module.task[0].task_definition_revision, null) : null } output "task_definition_family" { description = "The unique name of the task definition" - value = local.create_task_definition ? module.task[0].task_definition_family : null + value = local.create_task_definition ? try(module.task[0].task_definition_family, null) : null } output "container_definitions" { description = "Container definitions" - value = local.create_task_definition ? module.task[0].container_definitions : {} + value = local.create_task_definition ? try(module.task[0].container_definitions, {}) : {} } ################################################################################ @@ -62,7 +62,7 @@ output "container_definitions" { output "task_exec_iam_role_name" { description = "Task execution IAM role name" - value = local.create_task_definition ? module.task[0].task_exec_iam_role_name : null + value = local.create_task_definition ? try(module.task[0].task_exec_iam_role_name, null) : null } output "task_exec_iam_role_arn" { @@ -72,7 +72,7 @@ output "task_exec_iam_role_arn" { output "task_exec_iam_role_unique_id" { description = "Stable and unique string identifying the task execution IAM role" - value = local.create_task_definition ? module.task[0].task_exec_iam_role_unique_id : null + value = local.create_task_definition ? try(module.task[0].task_exec_iam_role_unique_id, null) : null } ################################################################################ @@ -82,7 +82,7 @@ output "task_exec_iam_role_unique_id" { output "tasks_iam_role_name" { description = "Tasks IAM role name" - value = local.create_task_definition ? module.task[0].tasks_iam_role_name : null + value = local.create_task_definition ? try(module.task[0].tasks_iam_role_name, null) : null } output "tasks_iam_role_arn" { @@ -92,7 +92,7 @@ output "tasks_iam_role_arn" { output "tasks_iam_role_unique_id" { description = "Stable and unique string identifying the tasks IAM role" - value = local.create_task_definition ? module.task[0].tasks_iam_role_unique_id : null + value = local.create_task_definition ? try(module.task[0].tasks_iam_role_unique_id, null) : null } ################################################################################