diff --git a/AGENTS.md b/AGENTS.md index f38a358..e47a34e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -269,6 +269,16 @@ Follow [Semantic Versioning](https://semver.org/): | **MINOR** | New features | New module, new optional variable, new output | | **PATCH** | Bug fixes | Documentation fix, validation fix, non-breaking default change | +### Module Definition Release Metadata + +For changes to `*-definition.yml` files, update the top-level `release.version` and `release.description` according to semantic versioning when the branch has not already bumped that definition for the current change set. + +- Before bumping, inspect the current branch diff against its base branch and check whether `release.version` or `release.description` for that same definition has already changed. +- If the branch already contains a release metadata bump for that definition, update the existing `release.description` only when needed to accurately summarize the combined branch changes; do not bump the version again. +- If no bump exists yet on the branch, choose the semver bump from the authored version based on the user-facing impact: major for breaking config or behavior changes, minor for new modules/features/optional inputs/outputs, and patch for fixes or documentation-only corrections. +- Keep `release.description` concise and user-facing. It should summarize the publishable change, not mention local publish attempts or implementation details. +- After making module-definition changes, publish a local development version for testing unless the user explicitly says not to. Use `make publish-local-dev MODULE=` or the equivalent tooling path. + For local development publishes, do **not** bump `release.version` just to publish a new local copy. The local publish tooling automatically appends the next numeric prerelease suffix to the authored version, such as `0.2.1-1`, `0.2.1-2`, and so on. ## Testing Requirements diff --git a/compute/ecs_service/.terraform.lock.hcl b/compute/ecs_service/.terraform.lock.hcl index 28ffb2f..6f311ad 100644 --- a/compute/ecs_service/.terraform.lock.hcl +++ b/compute/ecs_service/.terraform.lock.hcl @@ -2,24 +2,24 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/hashicorp/aws" { - version = "6.39.0" - constraints = ">= 5.0.0" + version = "6.50.0" + constraints = ">= 6.0.0" hashes = [ - "h1:c9SG8ZdYgzqpxORpTqeLFeXW4qQQ8GMGCcUkU+FAfQM=", - "zh:00a6c0d8b5b86833087e367b632e9ab73fb8db9c43569020ebd0489dc2c919ce", - "zh:05f2b56211f4c8a0b66a093d025187cbc7be086dedef62306f5a28290598ebdc", - "zh:24d97a31d5ab814c33ed32a5b7674f1a15544b2367a95bddd00cfdd8d6b82740", - "zh:258194e24ac07ee194d580ca25a25fa7bc48fa40fed4fd58352b0a64da0da4c9", - "zh:315337e5f0ccafeadf490f117151b52c6d66244bf652f4fee975eddda662af3b", - "zh:38573dd56cca8c0ffe33396cf17cc8bd13de1d27d3c4da4177e485d174f1eaf0", - "zh:4baa806c5eb8faae95cea3f1dfafb153b5e3e96c5b30a2102072da4f032d2d9b", - "zh:4f258106baca7e00a6904b2353579d283e4400a75cd0353a25e057921e8a8d96", - "zh:62e5d4628d03883a6c2a6e3c297eb54df9b5935e9e3a655dbb1c6c5ddaf7ea33", - "zh:8af5fae01c1cef65d149fa6fe47e94cf46ffa97d29e8f2dfe41aeae01da590ea", - "zh:a8240b40f7be408ac24897597a85dc4fe56f390224b11ecad2c1327e686fca58", - "zh:c549eee2a0cf0e2c4a676614d990121b685beab0047b1073407ee26247c4be13", - "zh:cfed074ba8948c75445c74c69722cb17c960024b1917b4f26905aa9c9ac4e667", - "zh:d6f4f4fa01e33d0d546705e2776f38d0b4f2847827b3f07ecde87cc02ef3d23e", - "zh:e7239b349c3149e4670750481b687c5c828908fd09f2196d7af1ac1b4d83e80b", + "h1:2WSBGr1BUGgVkvpNlcCEyrHbgkm6n3prLb2Xw751aE8=", + "zh:2065aabc934e93af312f6ae8374cacfee4f66b686a9acc857c334f7df46bdef3", + "zh:4b19f2d29ac99baf7e6ec5dabacf8416652e9b33f94c11973e68073870083673", + "zh:5a80191222224c45eec1bfc1aa37d8149d83c978bee666ac15a4742748cc8216", + "zh:5ed5b511634acc031790801019d7c01c45f9c296ace9995f1317aa162b45ba57", + "zh:6d079dd1db4a182302a17f2e68fa8f8b4cb890aa3f38a88b0371a05e96e1580d", + "zh:73e12273e0c714b4663ec566217c80c9b084aaffa37fe0b0d4f8684b2726868b", + "zh:7d3d11ee54b9b586faf46d6228d35a702735efa66955687fdd48616e31ee467e", + "zh:9971d51e1e1a2ce63a55e317ca6e323cead814fb6aa125fb72d18c2f39faa4ea", + "zh:997314c70d5a05fefc745c1596ba71a242f0979b9ba58c66acc21ddc14fab8cc", + "zh:aadfc526f9d3789ad55e040379ec827f075387435df0250adcbc781bea8b533e", + "zh:b4f927cc68b2b943061b39eb7a2bff4b8f66cefff808ff820345f8841cf28a40", + "zh:d7a9edc0b114208ce4202d6f4e67d00833875bea4080af877bade4568dcd8530", + "zh:ec431648c837add49efb59f9e7284b759bcfcfbd186d0678e86b70ebcfa3aac6", + "zh:f5c0cbddbe385b00a7ec2f56717bf3b1d1dcdfc33055dd64085f955af6f771c3", + "zh:fc0b7a34bc164c687967a282487bcee12a46ec8d713b37dc8f6cfa7a3ff66f2c", ] } diff --git a/compute/ecs_service/README.md b/compute/ecs_service/README.md index 6bb543d..de20cdf 100644 --- a/compute/ecs_service/README.md +++ b/compute/ecs_service/README.md @@ -16,7 +16,7 @@ This module creates an Amazon ECS service with a placeholder task definition, lo - Application Auto Scaling with target tracking and scheduled scaling - AWS Cloud Map service discovery integration - Blue/green deployment infrastructure (managed by an external deployment controller) -- Support for EFS and Docker volume configurations +- Support for EFS, S3 Files, Docker, and EC2 host path volume configurations - Capacity provider strategy support for mixed Fargate/EC2 deployments ## Usage @@ -291,7 +291,7 @@ module "worker_service" { | network_mode | Docker networking mode (awsvpc, bridge, host, none) | `string` | `"awsvpc"` | no | | requires_compatibilities | Launch type compatibility requirements | `list(string)` | `["FARGATE"]` | no | | runtime_platform | Runtime platform configuration (OS family, CPU architecture) | `object` | `{}` | no | -| volumes | List of volume definitions (EFS or Docker) | `list(object)` | `[]` | no | +| volumes | List of volume definitions (EFS, S3 Files, Docker, or EC2 host path) | `list(object)` | `[]` | no | ### CloudWatch Logs @@ -471,7 +471,7 @@ The `service_discovery` object includes: │ │ Task Definition │ │ │ │ • Container definitions (placeholder) • CPU/Memory allocation │ │ │ │ • Execution role • Task role │ │ -│ │ • Network mode (awsvpc) • Volumes (EFS/Docker) │ │ +│ │ • Network mode (awsvpc) • Volumes (EFS/S3/Docker/Host)│ │ │ └────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ @@ -613,7 +613,7 @@ The `service_discovery` object includes: ║ │ aws_ecs_task_definition.this │ ║ ║ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ ║ ║ │ Configures: family, CPU, memory, network mode, container definitions (placeholder), │ ║ -║ │ execution_role_arn, task_role_arn, runtime_platform, volumes (EFS/Docker) │ ║ +║ │ execution_role_arn, task_role_arn, runtime_platform, volumes (EFS/S3/Docker/Host) │ ║ ║ │ Lifecycle: ignore_changes = all (external deployment controller manages updates) │ ║ ║ └──────────────────────────────────────────────────────────────────────┬──────────────────────────────────────┘ ║ ║ │ ║ @@ -926,6 +926,44 @@ volumes = [ Note: The placeholder task definition does not mount volumes. Your application task definition (deployed by the external controller) should include the volume mounts. +### How do I attach S3 Files volumes to my tasks? + +Configure the `volumes` variable with an S3 Files configuration: + +```hcl +volumes = [ + { + name = "my-s3files-volume" + s3files_volume_configuration = { + file_system_arn = "arn:aws:s3files:us-east-1:123456789012:file-system/fs-12345678" + root_directory = "/" + access_point_arn = "arn:aws:s3files:us-east-1:123456789012:file-system/fs-12345678/access-point/fsap-12345678" + transit_encryption_port = 2999 + } + } +] +``` + +The task definition must include a task IAM role with the permissions required to attach the S3 Files file system to ECS tasks. If `task_role_arn` is null, attach the required permissions with `task_role_policies` or `task_role_inline_policies`. + +### How do I attach an EC2 host path to my tasks? + +Configure the `volumes` variable with a `host_path`. Host path volumes are supported only for EC2-compatible ECS tasks. + +```hcl +launch_type = "EC2" +requires_compatibilities = ["EC2"] + +volumes = [ + { + name = "host-data" + host_path = "/var/lib/app-data" + } +] +``` + +Mount the named volume from your deployed application task definition using a container mount point. Data remains on the EC2 container instance path and does not move with the task if ECS places a replacement task on another instance. + ### How do I enable ECS Exec for debugging? Set `execute_command_enabled = true`. This will: diff --git a/compute/ecs_service/rvn-ecs-web-definition.yml b/compute/ecs_service/rvn-ecs-web-definition.yml index 97ff041..68104d8 100644 --- a/compute/ecs_service/rvn-ecs-web-definition.yml +++ b/compute/ecs_service/rvn-ecs-web-definition.yml @@ -3,8 +3,8 @@ definition: name: ECS Web Server description: Web server ECS service for running an HTTP application behind an ECS cluster load balancer. release: - version: 0.4.0 - description: Add pre- and post-deploy commands. + version: 0.5.0 + description: Add S3 file mounts with required access points and EC2 host path task volumes. module: inputs: - id: section_cluster @@ -625,6 +625,35 @@ module: placeholder: |- - name: DATABASE_URL valueFrom: arn:aws:ssm:... + - id: mount_points + label: App mount points + type: object_array + description: Optional task volume mounts for the app container. The source volume must match a task volume name configured under task volumes. + collapsible: true + item_inputs: + - id: source_volume + label: Source volume + type: string + description: Task volume name to mount into the app container. + placeholder: shared-data + required: true + - id: container_path + label: Container path + type: string + description: Path inside the app container where the volume is mounted. + placeholder: /data + required: true + patterns: + - message: Use an absolute container path. + pattern: ^/ + - id: read_only + label: Read only + type: boolean + description: Mount the volume as read-only inside the app container. + default: false + item_label: Mount point + required: false + default: [] - id: section_deploy_commands label: Pre and post deploy type: section @@ -1160,7 +1189,7 @@ module: - id: volumes label: Task volumes type: object_array - description: Optional ECS task volumes. Use EFS for persistent shared storage that can be mounted by Fargate or EC2 Linux tasks, such as user uploads or shared application data. Use Docker volumes only for EC2 launch type tasks when data can live on the container instance and does not need Fargate support. Each volume can use either EFS or Docker, not both. + description: Optional ECS task volumes. Use EFS for persistent shared storage, S3 Files for mounting an existing Amazon S3 Files file system, Docker volumes for EC2-local Docker-managed storage, or Host path for binding an EC2 container instance directory. Each volume can use only one backing type. item_inputs: - id: name label: Volume name @@ -1184,6 +1213,12 @@ module: - description: Local Docker-managed volume for EC2 launch type tasks only. label: Docker value: docker + - description: Bind a directory from the EC2 container instance into the task. EC2 launch type only. + label: Host path + value: host + - description: Amazon S3 Files file system mounted into ECS tasks. + label: S3 Files + value: s3files - id: efs_volume_configuration label: EFS volume configuration type: object @@ -1211,6 +1246,60 @@ module: required: true show_when: volume_type: docker + - id: host_path + label: Host path + type: string + description: Absolute path on the EC2 container instance to bind into the ECS task. ECS creates the directory if it does not exist. Host path volumes are not supported on Fargate. + placeholder: /var/lib/app-data + required: true + show_when: + volume_type: host + patterns: + - message: Use an absolute Linux host path. + pattern: ^/ + - id: s3files_file_system_arn + label: S3 Files file system ARN + type: string + description: Full ARN of the Amazon S3 Files file system to mount. + placeholder: arn:aws:s3files:us-east-1:123456789012:file-system/fs-abc123 + required: true + show_when: + volume_type: s3files + patterns: + - message: Use a valid S3 Files file system ARN. + pattern: ^arn:aws[a-zA-Z-]*:s3files:[a-z0-9-]+:[0-9]{12}:file-system\/.+ + - id: s3files_root_directory + label: S3 Files root directory + type: string + description: Directory within the S3 Files file system to mount as the root directory. Use `/` for the file system root. + placeholder: / + required: false + default: / + show_when: + volume_type: s3files + patterns: + - message: Use an absolute S3 Files path. + pattern: ^/ + - id: s3files_access_point_arn + label: S3 Files access point ARN + type: string + description: Full ARN of the S3 Files access point to use. When set, the root directory must be blank or `/`. + placeholder: arn:aws:s3files:us-east-1:123456789012:file-system/fs-abc123/access-point/fsap-abc123 + required: true + show_when: + volume_type: s3files + patterns: + - message: Use a valid S3 Files access point ARN. + pattern: ^arn:aws[a-zA-Z-]*:s3files:[a-z0-9-]+:[0-9]{12}:file-system\/.+\/access-point\/.+ + - id: s3files_transit_encryption_port + label: S3 Files transit encryption port + type: number + description: Optional port used for encrypted traffic between the ECS host and the S3 Files file system. Leave blank to use the S3 Files mount helper default. + min: 1 + max: 65535 + required: false + show_when: + volume_type: s3files item_label: Volume required: false default: [] @@ -1384,7 +1473,11 @@ module: task_role_policies: << module.input.task_role_policies >> volumes: >- << module.input.volumes != nil ? map(module.input.volumes, #.volume_type == "efs" ? {"name": - #.name, "efs_volume_configuration": #.efs_volume_configuration} : {"name": #.name, + #.name, "efs_volume_configuration": #.efs_volume_configuration} : #.volume_type == "s3files" ? + {"name": #.name, "s3files_volume_configuration": {"file_system_arn": #.s3files_file_system_arn, + "root_directory": #.s3files_root_directory || "/", "access_point_arn": #.s3files_access_point_arn, + "transit_encryption_port": #.s3files_transit_encryption_port || nil}} : #.volume_type == + "host" ? {"name": #.name, "host_path": #.host_path} : {"name": #.name, "docker_volume_configuration": #.docker_volume_configuration}) : [] >> vpc_id: << module.input.vpc_id >> wait_for_steady_state: false @@ -1451,7 +1544,8 @@ module: stack.output.ecr_repository_url + ":" + deploy.input.image_ref)), "linux_parameters": {"init_process_enabled": true}, "log_configuration": {"log_driver": "awsfirelens"}, "memory": (module.input.capacity_provider == "ec2" ? int(float(module.input.task_memory) * 1024) : - int(float(module.input.fargate_size.memory_gb) * 1024)), "name": (stack.output.container_name), + int(float(module.input.fargate_size.memory_gb) * 1024)), "mount_points": + (module.input.mount_points != nil ? module.input.mount_points : []), "name": (stack.output.container_name), "port_mappings": [{"app_protocol": "http", "container_port": (module.input.container_port), "protocol": "tcp"}], "readonly_root_filesystem": false, "repository_credentials": (module.input.image_registry_credentials_secret_arn ? {credentials_parameter: @@ -1486,7 +1580,8 @@ module: stack.output.log_group_name, "awslogs-region": stack.output.region, "awslogs-stream-prefix": stack.output.log_stream_prefix}}, "memory": (module.input.capacity_provider == "ec2" ? int(float(module.input.task_memory) * 1024) : int(float(module.input.fargate_size.memory_gb) * - 1024)), "name": (stack.output.container_name), "port_mappings": [{"app_protocol": "http", + 1024)), "mount_points": (module.input.mount_points != nil ? module.input.mount_points : []), + "name": (stack.output.container_name), "port_mappings": [{"app_protocol": "http", "container_port": (module.input.container_port), "protocol": "tcp"}], "readonly_root_filesystem": false, "repository_credentials": (module.input.image_registry_credentials_secret_arn ? {credentials_parameter: module.input.image_registry_credentials_secret_arn} : nil), "secrets": @@ -1508,8 +1603,12 @@ module: task_role_arn: <> volumes: >- << module.input.volumes != nil ? map(module.input.volumes, #.volume_type == "efs" ? {"name": #.name, - "efs_volume_configuration": #.efs_volume_configuration} : {"name": #.name, - "docker_volume_configuration": #.docker_volume_configuration}) : [] >> + "efs_volume_configuration": #.efs_volume_configuration} : #.volume_type == "s3files" ? {"name": + #.name, "s3files_volume_configuration": {"file_system_arn": #.s3files_file_system_arn, + "root_directory": #.s3files_root_directory || "/", "access_point_arn": #.s3files_access_point_arn, + "transit_encryption_port": #.s3files_transit_encryption_port || nil}} : #.volume_type == + "host" ? {"name": #.name, "host_path": #.host_path} : {"name": + #.name, "docker_volume_configuration": #.docker_volume_configuration}) : [] >> pre_deploy: >- << module.input.pre_deploy_enabled && len(module.input.pre_deploy_command) > 0 ? {"container_overrides": [{"name": stack.output.container_name, "command": module.input.pre_deploy_command, "environment": module.input.pre_deploy_environment_variables, @@ -1796,6 +1895,21 @@ module: Use Build environment variables for values needed during image builds. Values can be plain strings or references loaded from Parameter Store or Secrets Manager. For Dockerfile builds, enable Inject environment variables in Dockerfile to pass those values as Docker build arguments. + ## Storage + + Use Task volumes to define storage available to the ECS task, then use App mount points to mount a named task volume into the app container. Sidecars can also mount the same named volumes with their own mount points. + + | Volume type | When to use it | + | ----------- | -------------- | + | EFS | Persistent shared file storage for Fargate or EC2 Linux tasks | + | S3 Files | Mount an existing Amazon S3 Files file system into the task | + | Docker | Local Docker-managed storage for EC2 launch type tasks only | + | Host path | Bind an EC2 container instance directory into the task | + + S3 Files volumes require the S3 Files file system ARN and access point ARN. Root directory and transit encryption port are optional fields. The task role must include the IAM permissions required to attach the S3 Files file system to ECS tasks. + + Host path volumes require EC2 capacity. The host path is an absolute Linux path on the EC2 container instance. Data remains on that instance and does not follow the task if ECS places a replacement task on a different instance. + ## Deployment Deployment type defaults to Rolling. Blue/green is available when you need separate target groups for CodeDeploy-style traffic shifting. @@ -1852,6 +1966,8 @@ module: | App memory in GB | Yes* | 3.5 | Required for EC2 capacity | | CPU architecture | No | X86_64 | x86_64 compatibility or ARM64 cost optimization | | ECS exec | No | false | Enable ECS Exec for debugging containers | + | App mount points | No | [] | Mount named task volumes into the app container | + | Task volumes | No | [] | Define EFS, S3 Files, Docker, or Host path task volumes | | Run pre-deploy command | No | false | Enable a task before each deployment | | Pre-deploy command arguments | Yes* | [] | Command arguments run before each deployment | | Pre-deploy environment variables| No | [] | Extra environment variables for the pre-deploy task | diff --git a/compute/ecs_service/task_definition.tf b/compute/ecs_service/task_definition.tf index c3ba97a..3bff5f3 100644 --- a/compute/ecs_service/task_definition.tf +++ b/compute/ecs_service/task_definition.tf @@ -212,7 +212,8 @@ resource "aws_ecs_task_definition" "this" { dynamic "volume" { for_each = var.volumes content { - name = volume.value.name + name = volume.value.name + host_path = volume.value.host_path dynamic "efs_volume_configuration" { for_each = volume.value.efs_volume_configuration != null ? [volume.value.efs_volume_configuration] : [] @@ -242,6 +243,16 @@ resource "aws_ecs_task_definition" "this" { labels = docker_volume_configuration.value.labels } } + + dynamic "s3files_volume_configuration" { + for_each = volume.value.s3files_volume_configuration != null ? [volume.value.s3files_volume_configuration] : [] + content { + file_system_arn = s3files_volume_configuration.value.file_system_arn + access_point_arn = s3files_volume_configuration.value.access_point_arn + root_directory = s3files_volume_configuration.value.root_directory + transit_encryption_port = s3files_volume_configuration.value.transit_encryption_port + } + } } } @@ -255,4 +266,3 @@ resource "aws_ecs_task_definition" "this" { ignore_changes = all } } - diff --git a/compute/ecs_service/variables.tf b/compute/ecs_service/variables.tf index 4b4dcec..236a19a 100644 --- a/compute/ecs_service/variables.tf +++ b/compute/ecs_service/variables.tf @@ -210,9 +210,40 @@ variable "volumes" { driver_opts = optional(map(string), null) labels = optional(map(string), null) }), null) + + host_path = optional(string, null) + + s3files_volume_configuration = optional(object({ + file_system_arn = string + access_point_arn = string + root_directory = optional(string, "/") + transit_encryption_port = optional(number, null) + }), null) })) description = "List of volume definitions for the task." default = [] + + validation { + condition = alltrue([ + for volume in var.volumes : length(compact([ + volume.efs_volume_configuration != null ? "efs" : "", + volume.docker_volume_configuration != null ? "docker" : "", + volume.host_path != null ? "host" : "", + volume.s3files_volume_configuration != null ? "s3files" : "" + ])) == 1 + ]) + error_message = "Each volume must specify exactly one of efs_volume_configuration, docker_volume_configuration, host_path, or s3files_volume_configuration." + } + + validation { + condition = alltrue([for volume in var.volumes : volume.host_path == null || startswith(volume.host_path, "/")]) + error_message = "Each host_path volume must use an absolute Linux path." + } + + validation { + condition = alltrue([for volume in var.volumes : volume.host_path == null || var.launch_type == "EC2" || contains(var.requires_compatibilities, "EC2")]) + error_message = "Host path volumes are supported only for EC2-compatible ECS tasks." + } } ################################################################################ diff --git a/compute/ecs_service/versions.tf b/compute/ecs_service/versions.tf index bec739b..3d872e9 100644 --- a/compute/ecs_service/versions.tf +++ b/compute/ecs_service/versions.tf @@ -10,9 +10,8 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.50" } } } -