Skip to content
Merged
58 changes: 58 additions & 0 deletions examples/windows-gmsa-service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This example demonstrates how to configure a Windows task to use gMSA

# 1. Dummy Secrets Manager Secret (The credential spec ARN is stored here)
resource "aws_secretsmanager_secret" "gmsa_secret" {
name = "gmsa-credential-spec-example"
}

# 2. Container Definition using the updated module
module "app_container" {
source = "../../modules/container-definition" # Relative path to the module you are modifying

name = "windows-app"
image = "mcr.microsoft.com/windows/servercore:ltsc2022"
cpu = 512
memory = 1024
essential = true

# IMPORTANT: The OS must be Windows for credentialSpecs to be valid
# Note: The 'operating_system_family' variable must also be present in the module's HCL
operating_system_family = "WINDOWS_SERVER_2022_CORE"

# The new feature being demonstrated, using a known placeholder ARN
credentialSpecs = [
aws_secretsmanager_secret.gmsa_secret.arn
]
}

# 3. Dummy Task Execution Role (Required for a runnable Task Definition)
resource "aws_iam_role" "ecs_task_execution_role" {
name = "ecs-gMSA-example-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
}

# 4. Task Definition that uses the container
resource "aws_ecs_task_definition" "windows_task" {
family = "windows-gmsa-task"
requires_compatibilities = ["FARGATE"]
cpu = 1024
memory = 2048
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn

# The container definition must be JSON encoded
container_definitions = jsonencode([module.app_container.container_definition])

runtime_platform {
operating_system_family = "WINDOWS_SERVER_2022_CORE"
}
}
4 changes: 4 additions & 0 deletions examples/windows-gmsa-service/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "task_definition_arn" {
description = "The ARN of the ECS Task Definition created with gMSA credentials."
value = aws_ecs_task_definition.windows_task.arn
}
9 changes: 9 additions & 0 deletions examples/windows-gmsa-service/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0"
}
}
}
1 change: 1 addition & 0 deletions modules/container-definition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ No modules.
| <a name="input_cloudwatch_log_group_retention_in_days"></a> [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Number of days to retain log events. Set to `0` to keep logs indefinitely | `number` | `14` | no |
| <a name="input_cloudwatch_log_group_use_name_prefix"></a> [cloudwatch\_log\_group\_use\_name\_prefix](#input\_cloudwatch\_log\_group\_use\_name\_prefix) | Determines whether the log group name should be used as a prefix | `bool` | `false` | no |
| <a name="input_command"></a> [command](#input\_command) | The command that's passed to the container | `list(string)` | `null` | no |
| <a name="input_credentialSpecs"></a> [credentialSpecs](#input\_credentialSpecs) | Specs for Credentials for gMSA (Windows containers) | `list(string)` | `null` | no |
| <a name="input_cpu"></a> [cpu](#input\_cpu) | The number of cpu units to reserve for the container. This is optional for tasks using Fargate launch type and the total amount of `cpu` of all containers in a task will need to be lower than the task-level cpu value | `number` | `null` | no |
| <a name="input_create_cloudwatch_log_group"></a> [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no |
| <a name="input_dependsOn"></a> [dependsOn](#input\_dependsOn) | The dependencies defined for container startup and shutdown. A container can contain multiple dependencies. When a dependency is defined for container startup, for container shutdown it is reversed. The condition can be one of START, COMPLETE, SUCCESS or HEALTHY | <pre>list(object({<br/> condition = string<br/> containerName = string<br/> }))</pre> | `null` | no |
Expand Down
1 change: 1 addition & 0 deletions modules/container-definition/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ locals {
definition = {
command = var.command
cpu = var.cpu
credentialSpecs = var.credentialSpecs
dependsOn = var.dependsOn
disableNetworking = local.is_not_windows ? var.disableNetworking : null
dnsSearchDomains = local.is_not_windows ? var.dnsSearchDomains : null
Expand Down
6 changes: 6 additions & 0 deletions modules/container-definition/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ variable "cpu" {
default = null
}

variable "credentialSpecs" {
description = "Specs for Credentials for gMSA (Windows containers)."
type = list(string)
default = []
}

# tflint-ignore: terraform_naming_convention
variable "dependsOn" {
description = "The dependencies defined for container startup and shutdown. A container can contain multiple dependencies. When a dependency is defined for container startup, for container shutdown it is reversed. The condition can be one of START, COMPLETE, SUCCESS or HEALTHY"
Expand Down
2 changes: 1 addition & 1 deletion modules/service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ module "ecs_service" {
| <a name="input_availability_zone_rebalancing"></a> [availability\_zone\_rebalancing](#input\_availability\_zone\_rebalancing) | ECS automatically redistributes tasks within a service across Availability Zones (AZs) to mitigate the risk of impaired application availability due to underlying infrastructure failures and task lifecycle activities. The valid values are `ENABLED` and `DISABLED`. Defaults to `DISABLED` | `string` | `null` | no |
| <a name="input_capacity_provider_strategy"></a> [capacity\_provider\_strategy](#input\_capacity\_provider\_strategy) | Capacity provider strategies to use for the service. Can be one or more | <pre>map(object({<br/> base = optional(number)<br/> capacity_provider = string<br/> weight = optional(number)<br/> }))</pre> | `null` | no |
| <a name="input_cluster_arn"></a> [cluster\_arn](#input\_cluster\_arn) | ARN of the ECS cluster where the resources will be provisioned | `string` | `""` | no |
| <a name="input_container_definitions"></a> [container\_definitions](#input\_container\_definitions) | 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 | <pre>map(object({<br/> create = optional(bool, true)<br/> operating_system_family = optional(string)<br/> tags = optional(map(string))<br/><br/> # Container definition<br/> command = optional(list(string))<br/> cpu = optional(number)<br/> dependsOn = optional(list(object({<br/> condition = string<br/> containerName = string<br/> })))<br/> disableNetworking = optional(bool)<br/> dnsSearchDomains = optional(list(string))<br/> dnsServers = optional(list(string))<br/> dockerLabels = optional(map(string))<br/> dockerSecurityOptions = optional(list(string))<br/> # enable_execute_command = optional(bool, false) Set in standalone variable<br/> entrypoint = optional(list(string))<br/> environment = optional(list(object({<br/> name = string<br/> value = string<br/> })))<br/> environmentFiles = optional(list(object({<br/> type = string<br/> value = string<br/> })))<br/> essential = optional(bool)<br/> extraHosts = optional(list(object({<br/> hostname = string<br/> ipAddress = string<br/> })))<br/> firelensConfiguration = optional(object({<br/> options = optional(map(string))<br/> type = optional(string)<br/> }))<br/> healthCheck = optional(object({<br/> command = optional(list(string), [])<br/> interval = optional(number, 30)<br/> retries = optional(number, 3)<br/> startPeriod = optional(number)<br/> timeout = optional(number, 5)<br/> }))<br/> hostname = optional(string)<br/> image = optional(string)<br/> interactive = optional(bool)<br/> links = optional(list(string))<br/> linuxParameters = optional(object({<br/> capabilities = optional(object({<br/> add = optional(list(string))<br/> drop = optional(list(string))<br/> }))<br/> devices = optional(list(object({<br/> containerPath = optional(string)<br/> hostPath = optional(string)<br/> permissions = optional(list(string))<br/> })))<br/> initProcessEnabled = optional(bool)<br/> maxSwap = optional(number)<br/> sharedMemorySize = optional(number)<br/> swappiness = optional(number)<br/> tmpfs = optional(list(object({<br/> containerPath = string<br/> mountOptions = optional(list(string))<br/> size = number<br/> })))<br/> }))<br/> logConfiguration = optional(object({<br/> logDriver = optional(string)<br/> options = optional(map(string))<br/> secretOptions = optional(list(object({<br/> name = string<br/> valueFrom = string<br/> })))<br/> }))<br/> memory = optional(number)<br/> memoryReservation = optional(number)<br/> mountPoints = optional(list(object({<br/> containerPath = optional(string)<br/> readOnly = optional(bool)<br/> sourceVolume = optional(string)<br/> })))<br/> name = optional(string)<br/> portMappings = optional(list(object({<br/> appProtocol = optional(string)<br/> containerPort = optional(number)<br/> containerPortRange = optional(string)<br/> hostPort = optional(number)<br/> name = optional(string)<br/> protocol = optional(string)<br/> })))<br/> privileged = optional(bool)<br/> pseudoTerminal = optional(bool)<br/> readonlyRootFilesystem = optional(bool)<br/> repositoryCredentials = optional(object({<br/> credentialsParameter = optional(string)<br/> }))<br/> resourceRequirements = optional(list(object({<br/> type = string<br/> value = string<br/> })))<br/> restartPolicy = optional(object({<br/> enabled = optional(bool)<br/> ignoredExitCodes = optional(list(number))<br/> restartAttemptPeriod = optional(number)<br/> })<br/> )<br/> secrets = optional(list(object({<br/> name = string<br/> valueFrom = string<br/> })))<br/> startTimeout = optional(number, 30)<br/> stopTimeout = optional(number, 120)<br/> systemControls = optional(list(object({<br/> namespace = optional(string)<br/> value = optional(string)<br/> })))<br/> ulimits = optional(list(object({<br/> hardLimit = number<br/> name = string<br/> softLimit = number<br/> })))<br/> user = optional(string)<br/> versionConsistency = optional(string)<br/> volumesFrom = optional(list(object({<br/> readOnly = optional(bool)<br/> sourceContainer = optional(string)<br/> })))<br/> workingDirectory = optional(string)<br/><br/> # Cloudwatch Log Group<br/> service = optional(string)<br/> enable_cloudwatch_logging = optional(bool)<br/> create_cloudwatch_log_group = optional(bool)<br/> cloudwatch_log_group_name = optional(string)<br/> cloudwatch_log_group_use_name_prefix = optional(bool)<br/> cloudwatch_log_group_class = optional(string)<br/> cloudwatch_log_group_retention_in_days = optional(number)<br/> cloudwatch_log_group_kms_key_id = optional(string)<br/> }))</pre> | `{}` | no |
| <a name="input_container_definitions"></a> [container\_definitions](#input\_container\_definitions) | 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 | <pre>map(object({<br/> create = optional(bool, true)<br/> operating_system_family = optional(string)<br/> tags = optional(map(string))<br/><br/> # Container definition<br/> command = optional(list(string))<br/> cpu = optional(number)<br/> <br/> credentialSpecs = optional(list(string))<br/> dependsOn = optional(list(object({<br/> condition = string<br/> containerName = string<br/> })))<br/> disableNetworking = optional(bool)<br/> dnsSearchDomains = optional(list(string))<br/> dnsServers = optional(list(string))<br/> dockerLabels = optional(map(string))<br/> dockerSecurityOptions = optional(list(string))<br/> # enable_execute_command = optional(bool, false) Set in standalone variable<br/> entrypoint = optional(list(string))<br/> environment = optional(list(object({<br/> name = string<br/> value = string<br/> })))<br/> environmentFiles = optional(list(object({<br/> type = string<br/> value = string<br/> })))<br/> essential = optional(bool)<br/> extraHosts = optional(list(object({<br/> hostname = string<br/> ipAddress = string<br/> })))<br/> firelensConfiguration = optional(object({<br/> options = optional(map(string))<br/> type = optional(string)<br/> }))<br/> healthCheck = optional(object({<br/> command = optional(list(string), [])<br/> interval = optional(number, 30)<br/> retries = optional(number, 3)<br/> startPeriod = optional(number)<br/> timeout = optional(number, 5)<br/> }))<br/> hostname = optional(string)<br/> image = optional(string)<br/> interactive = optional(bool)<br/> links = optional(list(string))<br/> linuxParameters = optional(object({<br/> capabilities = optional(object({<br/> add = optional(list(string))<br/> drop = optional(list(string))<br/> }))<br/> devices = optional(list(object({<br/> containerPath = optional(string)<br/> hostPath = optional(string)<br/> permissions = optional(list(string))<br/> })))<br/> initProcessEnabled = optional(bool)<br/> maxSwap = optional(number)<br/> sharedMemorySize = optional(number)<br/> swappiness = optional(number)<br/> tmpfs = optional(list(object({<br/> containerPath = string<br/> mountOptions = optional(list(string))<br/> size = number<br/> })))<br/> }))<br/> logConfiguration = optional(object({<br/> logDriver = optional(string)<br/> options = optional(map(string))<br/> secretOptions = optional(list(object({<br/> name = string<br/> valueFrom = string<br/> })))<br/> }))<br/> memory = optional(number)<br/> memoryReservation = optional(number)<br/> mountPoints = optional(list(object({<br/> containerPath = optional(string)<br/> readOnly = optional(bool)<br/> sourceVolume = optional(string)<br/> })))<br/> name = optional(string)<br/> portMappings = optional(list(object({<br/> appProtocol = optional(string)<br/> containerPort = optional(number)<br/> containerPortRange = optional(string)<br/> hostPort = optional(number)<br/> name = optional(string)<br/> protocol = optional(string)<br/> })))<br/> privileged = optional(bool)<br/> pseudoTerminal = optional(bool)<br/> readonlyRootFilesystem = optional(bool)<br/> repositoryCredentials = optional(object({<br/> credentialsParameter = optional(string)<br/> }))<br/> resourceRequirements = optional(list(object({<br/> type = string<br/> value = string<br/> })))<br/> restartPolicy = optional(object({<br/> enabled = optional(bool)<br/> ignoredExitCodes = optional(list(number))<br/> restartAttemptPeriod = optional(number)<br/> })<br/> )<br/> secrets = optional(list(object({<br/> name = string<br/> valueFrom = string<br/> })))<br/> startTimeout = optional(number, 30)<br/> stopTimeout = optional(number, 120)<br/> systemControls = optional(list(object({<br/> namespace = optional(string)<br/> value = optional(string)<br/> })))<br/> ulimits = optional(list(object({<br/> hardLimit = number<br/> name = string<br/> softLimit = number<br/> })))<br/> user = optional(string)<br/> versionConsistency = optional(string)<br/> volumesFrom = optional(list(object({<br/> readOnly = optional(bool)<br/> sourceContainer = optional(string)<br/> })))<br/> workingDirectory = optional(string)<br/><br/> # Cloudwatch Log Group<br/> service = optional(string)<br/> enable_cloudwatch_logging = optional(bool)<br/> create_cloudwatch_log_group = optional(bool)<br/> cloudwatch_log_group_name = optional(string)<br/> cloudwatch_log_group_use_name_prefix = optional(bool)<br/> cloudwatch_log_group_class = optional(string)<br/> cloudwatch_log_group_retention_in_days = optional(number)<br/> cloudwatch_log_group_kms_key_id = optional(string)<br/> }))</pre> | `{}` | no |
| <a name="input_cpu"></a> [cpu](#input\_cpu) | Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `1024` | no |
| <a name="input_create"></a> [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no |
| <a name="input_create_iam_role"></a> [create\_iam\_role](#input\_create\_iam\_role) | Determines whether the ECS service IAM role should be created | `bool` | `true` | no |
Expand Down
1 change: 1 addition & 0 deletions modules/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ module "container_definition" {
# Container Definition
command = each.value.command
cpu = each.value.cpu
credentialSpecs = each.value.credentialSpecs
dependsOn = each.value.dependsOn
disableNetworking = each.value.disableNetworking
dnsSearchDomains = each.value.dnsSearchDomains
Expand Down
5 changes: 3 additions & 2 deletions modules/service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,9 @@ variable "container_definitions" {
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
Expand Down