diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 69eecd92..26ae5fba 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -50,6 +50,7 @@ updates: - "infrastructure/modules/ecr" - "infrastructure/modules/ecs-cluster" - "infrastructure/modules/ecs-service" + - "infrastructure/modules/efs" - "infrastructure/modules/elasticache" - "infrastructure/modules/github-config" - "infrastructure/modules/guardduty" diff --git a/README.md b/README.md index 1f4d50e6..29670309 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ Rules: | `ecr` | — | ECR repository with security controls | | `ecs-cluster` | terraform-aws-modules/ecs/aws//modules/cluster | ECS Fargate cluster | | `ecs-service` | terraform-aws-modules/ecs/aws//modules/service | ECS service and task definition | +| `efs` | terraform-aws-modules/efs/aws | EFS file system with access points and mount targets | | `elasticache` | — | ElastiCache cluster (Redis/Memcached) | | `github-config` | — | GitHub OIDC provider and runner configuration | | `guardduty` | — | GuardDuty threat detection | diff --git a/infrastructure/modules/cloudwatch-logs/.terraform.lock.hcl b/infrastructure/modules/cloudwatch-logs/.terraform.lock.hcl index 82c67711..3211986b 100644 --- a/infrastructure/modules/cloudwatch-logs/.terraform.lock.hcl +++ b/infrastructure/modules/cloudwatch-logs/.terraform.lock.hcl @@ -8,6 +8,7 @@ provider "registry.terraform.io/hashicorp/aws" { "h1:8m5zm0JaUac+YikXZoZDTV7R0iYL+q3IvTL4Ac8GfDA=", "h1:Dg9X3Gde96OCT3TovLKAKumnR64VqIIQ37m/9NRJ00Y=", "h1:F1r7mTJ289EBvU7Z7XAvWMHUt3VJ3zSFH8SUjLPFiqM=", + "h1:LmIMOhIeFTfSiUDY4gthZTikT5CqEIGMhgQbv/mdurE=", "h1:iXIFHX6zIvG0hoEi1fImeuHG9V4JOop6O6L6f+MOL2c=", "h1:lpXqosKH8yAahK3SA1P5Pdy1ziXJcY+blUidY0q9yGk=", "zh:1ab1d78f2336fed42b4e13fa0077a0be9d86a7899897cda5b9f1a60051ec2e93", diff --git a/infrastructure/modules/efs/.terraform.lock.hcl b/infrastructure/modules/efs/.terraform.lock.hcl new file mode 100644 index 00000000..19b85462 --- /dev/null +++ b/infrastructure/modules/efs/.terraform.lock.hcl @@ -0,0 +1,30 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.55.0" + constraints = ">= 6.14.0, >= 6.28.0" + hashes = [ + "h1:3OSTaIx7d6T+N6gwNtw0gR+IwExbHqb0y4v529Vey/4=", + "h1:99+MYIg/y3gmsZkhAcffwOpMat+liRJ8b+eyCIax6hk=", + "h1:QIQoZv637BE40vg7/0gf/5gQbY/ZTngSuJWDl7fJr2s=", + "h1:tBAaeIprhWQmlsq/CoHLXWOLZZYRdCVA1wbNv2co55g=", + "h1:yM6149NHHl8NWU/OCIV2/x6tLrNgDTRKgR8cVrbk9BU=", + "zh:1161fb2d032ad982587b2662a5229e5d06598c5b7fc5c86b2ad64d49225047cd", + "zh:1f412b09bbece216da0ba08106f3bbb42d8c8971c02d032ab518629915086966", + "zh:2c8b789450bb67181b5f0546714bf6336ba21183c307e001fe848c22dac1f8a6", + "zh:31eec91f896743bab641c06930fe0c277143f17dd25b2510991c08e013c8da67", + "zh:4419d3e906f1ca9c99703b2c4c5082f58aaeb8b8b82e2657a187a6bdf42d8881", + "zh:58e9a7e0581e8cd5f35eb2ce308b2d572073c112facdd0a60aee032146b146b5", + "zh:72fdb02a0cb6351626df460c047d1471f26dad781160cc95abd84f8849daf950", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:aa527913348c33969d80527d424917876657741493f294e160f1191dbc7c1d45", + "zh:b66e5abd756064f55ff06e1ef83eb8d0b7ea6d96625cfcd24408df5472d5f899", + "zh:ba9ab4ba151ac69a854407e27d3a12a2eb260fc4205bbb5c25618e6c8ce69568", + "zh:e1bf63e8a6b9790836f74848b458cdf679a7841e66a25b4b9f6034efc9310b34", + "zh:ec4adcec426f7181faa5de976cedbd35e15d7d1ac2914bbd9cab9f4f8b018b7f", + "zh:f17b485ae74bb4272d2bd680e7d47b0cfd073d192a4f10c8bdfd9d9f25c990a1", + "zh:f6650c2d0d3e614c3ccd623bb027a52bce1f5285c4f1824a26f265eb7529a45a", + "zh:f7383732f8704099db2166a3516e0a803bf46f42c30cd20a0471b55658ae6e51", + ] +} diff --git a/infrastructure/modules/efs/README.md b/infrastructure/modules/efs/README.md new file mode 100644 index 00000000..f0eaa86e --- /dev/null +++ b/infrastructure/modules/efs/README.md @@ -0,0 +1,596 @@ +# EFS + +NHS Screening wrapper around the community +[`terraform-aws-modules/efs/aws`](https://registry.terraform.io/modules/terraform-aws-modules/efs/aws/latest) +module that enforces the platform's baseline controls and consumes +the shared `context.tf` for naming and tagging. + +## What this module enforces + +|Control|How it is enforced| +|---|---| +|Encryption at rest|KMS encryption is mandatory; `var.kms_key_arn` is required| +|Encryption in transit|TLS is always required; non-secure transport is denied by default| +|Security groups|Mount targets require explicit security groups from the caller; no defaults| +|Destructive operations|Delete operations are denied by default; must be explicitly allowed via policy| +|Tagging|All EFS resources tagged via `module.this.tags`| +|Creation gate|Resource creation gated by `module.this.enabled`| + +## Usage + +### Minimal EFS with KMS encryption + +Simplest setup: multi-AZ, general-purpose file system with default security policies. + +```hcl +module "application_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "application" + environment = "production" + name = "app-data" + + kms_key_arn = module.efs_kms.key_arn + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +### Single-AZ EFS for cost savings + +Use `availability_zone_name` for cheaper single-AZ deployments (no high availability). + +```hcl +module "dev_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "development" + environment = "dev" + name = "dev-storage" + + kms_key_arn = module.efs_kms.key_arn + availability_zone_name = "eu-west-2a" # Single AZ deployment + + mount_targets = { + "primary" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs_dev.id] + } + } +} +``` + +### High-performance EFS with provisioned throughput + +For workloads requiring consistent throughput (databases, analytics). + +```hcl +module "database_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "database" + environment = "production" + name = "db-storage" + + kms_key_arn = module.efs_kms.key_arn + performance_mode = "maxIO" + throughput_mode = "provisioned" + provisioned_throughput_in_mibps = 100 + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs_db.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs_db.id] + } + } +} +``` + +### EFS with lifecycle policies and automated backup + +Transition older data to infrequent access storage class automatically. + +```hcl +module "archive_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "archive" + environment = "production" + name = "archive-storage" + + kms_key_arn = module.efs_kms.key_arn + + # Transition data to infrequent access after 30 days of inactivity + lifecycle_policy = { + transition_to_ia = "AFTER_30_DAYS" + transition_to_primary_storage_class = "AFTER_1_DAY" # Restore to standard if accessed + } + + protection = { + enable_backup = true # Enable automated backup via AWS Backup + } + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +### EFS with Access Points for application isolation + +Use Access Points to enforce POSIX user identity and root directory isolation for multi-tenant workloads. + +```hcl +module "multitenant_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "multitenant" + environment = "production" + name = "shared-storage" + + kms_key_arn = module.efs_kms.key_arn + + # Each application gets its own access point with enforced user identity + access_points = { + "web-app" = { + enforced_user_id = "1001" + root_directory_path = "/web-app" + permissions_mode = "700" + } + "api-service" = { + enforced_user_id = "1002" + root_directory_path = "/api-service" + permissions_mode = "700" + } + "batch-job" = { + enforced_user_id = "1003" + root_directory_path = "/batch" + permissions_mode = "755" + } + } + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +### EFS with cross-region replication for disaster recovery + +Replicate to secondary region for business continuity. + +```hcl +module "replicated_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "critical-data" + environment = "production" + name = "replicated-storage" + + kms_key_arn = module.efs_kms.key_arn + + # Enable replication to eu-west-1 + replication_configuration = { + destination = "eu-west-1" + } + + protection = { + replication_overwrite = "DISABLED" # Prevent accidental overwrites + } + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +### Secure EFS with TLS 1.2 enforcement and IP restrictions + +Enforce strong TLS version and restrict access to specific network ranges. + +```hcl +module "secure_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "secure-data" + environment = "production" + name = "secure-storage" + + kms_key_arn = module.efs_kms.key_arn + + # Enforce TLS 1.2 minimum and restrict to VPC CIDR + require_tls_version = "1.2" + allowed_source_ips = ["10.0.0.0/8"] # Your VPC CIDR + + # Prevent accidental deletion (must explicitly allow in custom policy) + deny_destructive_operations = true + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs_secure.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs_secure.id] + } + } +} +``` + +### EFS with custom IAM policy for admin-only deletions + +Demonstrate how to allow destructive operations only for specific IAM roles. + +```hcl +module "efs_with_admin_policy" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "production-data" + environment = "production" + name = "protected-storage" + + kms_key_arn = module.efs_kms.key_arn + + # Enable all default security policies + deny_unsecure_transport = true + require_tls_version = "1.2" + deny_destructive_operations = true + + # Custom policy: allow deletion only by infrastructure-admin role + file_system_policy = jsonencode({ + Statement = [ + { + Sid = "AllowInfraAdminDelete" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/infrastructure-admin" + } + Action = [ + "elasticfilesystem:DeleteFileSystem", + "elasticfilesystem:DeleteAccessPoint" + ] + Resource = "*" + } + ] + }) + + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +### Comprehensive production setup with all features + +Multi-AZ, high-performance, encrypted, replicated, with access points and security policies. + +```hcl +module "production_efs" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/efs?ref=v1.0.0" + + service = "bcss" + project = "platform" + environment = "production" + name = "platform-data" + + kms_key_arn = module.efs_kms.key_arn + + # Performance: provisioned throughput for predictable performance + performance_mode = "generalPurpose" + throughput_mode = "provisioned" + provisioned_throughput_in_mibps = 50 + + # Lifecycle: optimize storage costs over time + lifecycle_policy = { + transition_to_ia = "AFTER_60_DAYS" + } + + # Protection: backup and prevent accidental overwrites + protection = { + enable_backup = true + replication_overwrite = "DISABLED" + } + + # Replication: disaster recovery to secondary region + replication_configuration = { + destination = "eu-west-1" + } + + # Security: enforce TLS 1.2 and restrict to VPC + require_tls_version = "1.2" + allowed_source_ips = ["10.0.0.0/8"] + + # Access control: application isolation via access points + access_points = { + "api" = { + enforced_user_id = "1000" + root_directory_path = "/api" + permissions_mode = "750" + } + "database" = { + enforced_user_id = "1001" + root_directory_path = "/db" + permissions_mode = "700" + } + } + + # Multi-AZ mount targets for high availability + mount_targets = { + "az1" = { + subnet_id = aws_subnet.private[0].id + security_groups = [aws_security_group.efs.id] + } + "az2" = { + subnet_id = aws_subnet.private[1].id + security_groups = [aws_security_group.efs.id] + } + "az3" = { + subnet_id = aws_subnet.private[2].id + security_groups = [aws_security_group.efs.id] + } + } +} +``` + +## Security Group Requirements + +EFS communication uses NFS (NFSv4.1), not SSH or HTTP. Your security groups must allow: + +```hcl +# Allow NFS protocol +resource "aws_security_group_rule" "efs_nfs" { + type = "ingress" + from_port = 2049 + to_port = 2049 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/8"] # Your application's CIDR + security_group_id = aws_security_group.efs.id +} + +# Allow NFS port mapper (optional, only if needed) +resource "aws_security_group_rule" "efs_portmapper" { + type = "ingress" + from_port = 111 + to_port = 111 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/8"] + security_group_id = aws_security_group.efs.id +} +``` + +## Conventions + +- **Encryption:** KMS encryption is mandatory. Callers must provide a KMS key ARN via `var.kms_key_arn`. AWS managed keys are not used. +- **Security groups:** Callers must provide security groups for each mount target. See [Security Group Requirements](#security-group-requirements) above. +- **Mount targets:** Each mount target requires a subnet and security groups. For high availability, provision one mount target per AZ. +- **Custom naming:** By default, the EFS name is derived from `module.this.id`. Override with `var.custom_name` if needed. +- **Access Points:** When using access points, applications must use the access point ARN (not the file system ARN) in their NFS mount. + +## File System Policy (Resource-Based Access Control) + +This module automatically adds security-focused policy statements to the EFS file system policy. These are enabled by default and can be controlled or disabled as needed: + +### Default Security Statements + +| Statement | Default | Purpose | +| --- | --- | --- | +| `DenyUnsecureTransport` | Enabled | Denies all EFS operations over non-TLS connections (`aws:SecureTransport = false`) | +| `DenyOldTLSVersion` | Disabled | Denies operations using TLS versions older than specified via `var.require_tls_version` | +| `DenyUnauthorizedSourceIPs` | Disabled | Restricts EFS access to specific CIDR blocks via `var.allowed_source_ips` | +| `DenyDestructiveOperations` | Enabled | Denies `DeleteFileSystem`, `DeleteAccessPoint`, etc. by default (callers must explicitly allow via custom policy) | + +### Controlling Policy Statements + +```hcl +# Require TLS 1.2 or higher +require_tls_version = "1.2" + +# Restrict to specific VPC CIDR blocks +allowed_source_ips = ["10.0.0.0/8", "172.16.0.0/12"] + +# Disable automatic deny of destructive operations (not recommended) +deny_destructive_operations = false + +# Disable all automatic policy statements +deny_unsecure_transport = false +require_tls_version = null +allowed_source_ips = [] +``` + +### Custom Policy Statements + +Caller-provided policy statements (via `var.file_system_policy`) are merged with the default security statements. To allow destructive operations: + +```hcl +file_system_policy = jsonencode({ + Statement = [ + { + Sid = "AllowDeleteForAdmins" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::ACCOUNT:role/AdminRole" + } + Action = ["elasticfilesystem:DeleteFileSystem"] + Resource = "*" + } + ] +}) +``` + +## Validation + +The following cross-variable constraints are enforced in `validations.tf`: + +- **Provisioned throughput mode requires throughput setting:** If `throughput_mode = "provisioned"`, `provisioned_throughput_in_mibps` must be set. +- **Replication requires mount targets:** If `replication_configuration` is set, at least one mount target must be configured. +- **Access points require mount targets:** If `access_points` is set, at least one mount target must be configured. + +## Outputs + +- `efs_id` — The EFS file system ID (e.g., `fs-12345678`) +- `efs_arn` — The ARN of the file system +- `efs_dns_name` — The DNS name for NFS mounting (e.g., `fs-12345678.efs.eu-west-2.amazonaws.com`) +- `efs_size_in_bytes` — Latest metered size of the file system +- `kms_key_arn` — The KMS key ARN used for encryption +- `mount_targets` — Map of mount target IDs and subnet associations +- `access_points` — Map of access point IDs and ARNs (if configured) +- `replication_configuration` — The destination file system ID (if replication enabled) +- `file_system_policy_id` — The policy ID (if custom policy attached) + +## What this module does NOT do + +- Create KMS keys. Use the `kms` module and pass the key ARN via `var.kms_key_arn`. +- Create security groups. Callers must provide and manage security groups for mount targets. +- Create subnets. Callers must provide target subnets for mount targets. +- Create DNS records. Use Route 53 or your DNS provider to create records pointing to `efs_dns_name` if needed. +- Enable data sync or backup scheduling. Use AWS DataSync or AWS Backup to configure automated workflows. +- Configure VPC endpoints for EFS. The module uses standard EFS DNS; VPC endpoints are optional and managed separately. + + + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.13 | +| [aws](#requirement\_aws) | >= 6.28 | + +## Providers + +| Name | Version | +| ---- | ------- | +| [aws](#provider\_aws) | 6.55.0 | +| [terraform](#provider\_terraform) | n/a | + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [efs](#module\_efs) | terraform-aws-modules/efs/aws | 2.2.0 | +| [this](#module\_this) | ../tags | n/a | + +## Resources + +| Name | Type | +| ---- | ---- | +| [aws_efs_access_point.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_access_point) | resource | +| [aws_efs_file_system_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_file_system_policy) | resource | +| [terraform_data.validations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [access\_points](#input\_access\_points) | Map of EFS Access Point configurations for application-level mount points.
Access Points enforce POSIX user identities and enforce a file system root.
Leave as {} to create no access points.

Example:
access\_points = {
"app-root" = {
enforced\_user\_id = "1000"
root\_directory\_path = "/app"
permissions\_mode = "755"
}
"db-root" = {
enforced\_user\_id = "1001"
root\_directory\_path = "/data"
permissions\_mode = "700"
}
} | `any` | `{}` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allowed\_source\_ips](#input\_allowed\_source\_ips) | List of CIDR blocks allowed to access the EFS. When set, a Deny statement restricts access to these IPs. Leave as [] to skip IP-based restrictions. | `list(string)` | `[]` | no | +| [application\_role](#input\_application\_role) | The role the application is performing | `string` | `"General"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [availability\_zone\_name](#input\_availability\_zone\_name) | AWS Availability Zone for One Zone storage class. When set, the file system uses single-AZ storage for lower cost. Leave null for multi-AZ. | `string` | `null` | no | +| [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | +| [creation\_token](#input\_creation\_token) | A unique name (max 64 chars) used as reference when creating the file system. Enables idempotent creation. When null, Terraform generates a token. | `string` | `null` | no | +| [custom\_name](#input\_custom\_name) | Optional explicit EFS name. When null, the name is derived from module.this.id. | `string` | `null` | no | +| [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | +| [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deny\_destructive\_operations](#input\_deny\_destructive\_operations) | Whether to add a Deny statement for destructive operations (DeleteFileSystem, DeleteAccessPoint) by default. Callers must explicitly allow these via var.file\_system\_policy. Recommended: true. | `bool` | `true` | no | +| [deny\_unsecure\_transport](#input\_deny\_unsecure\_transport) | Whether to automatically add a Deny statement for non-TLS (unsecure) transport to the file system policy. Recommended: true. | `bool` | `true` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | +| [file\_system\_policy](#input\_file\_system\_policy) | Optional IAM policy document (as JSON string) to attach to the file system.
Controls who can perform what actions on the EFS resource.
When null, no resource-based policy is attached.

Security baseline: Callers should consider denying nonsecure (non-TLS) transport:
- aws:SecureTransport condition set to false | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | ARN of the KMS key used for encryption at rest. Encryption is mandatory; this variable is required. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lifecycle\_policy](#input\_lifecycle\_policy) | Optional lifecycle policy for automatic transition to other storage classes.
Leave as {} to disable. Example:
lifecycle\_policy = {
transition\_to\_ia = "AFTER\_30\_DAYS"
transition\_to\_primary\_storage\_class = "AFTER\_1\_DAY"
} | `any` | `{}` | no | +| [mount\_targets](#input\_mount\_targets) | Map of mount target configurations. Each mount target must specify:
- subnet\_id: Subnet where the mount target resides (required)
- security\_groups: List of security group IDs (required; caller must enforce ingress rules for NFSv4.1)
- ip\_address: (optional) Static IP address for the mount target
- ip\_address\_type: (optional) IP address type (ipv4 or ipv6)
- ipv6\_address: (optional) IPv6 address for the mount target

Example:
mount\_targets = {
"az1" = {
subnet\_id = "subnet-12345"
security\_groups = ["sg-12345"]
}
"az2" = {
subnet\_id = "subnet-67890"
security\_groups = ["sg-67890"]
ip\_address = "10.0.1.100"
}
} | `any` | `{}` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [on\_off\_pattern](#input\_on\_off\_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | +| [owner](#input\_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | +| [performance\_mode](#input\_performance\_mode) | The file system performance mode. Either 'generalPurpose' or 'maxIO'. | `string` | `"generalPurpose"` | no | +| [project](#input\_project) | ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api` | `string` | `null` | no | +| [protection](#input\_protection) | Configuration for replication protection and backup policy.

Defaults:
- enable\_backup: true (enables automated backups via AWS Backup)
- replication\_overwrite: DISABLED (prevents accidental overwrites of replicas during replication) |
object({
enable_backup = optional(bool, true)
replication_overwrite = optional(string, "DISABLED")
})
| `{}` | no | +| [provisioned\_throughput\_in\_mibps](#input\_provisioned\_throughput\_in\_mibps) | The throughput to provision for the file system in MiB/s. Required when throughput\_mode is 'provisioned'. Range: 1–1024. | `number` | `null` | no | +| [public\_facing](#input\_public\_facing) | Whether this resource is public facing | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | ID element \_(Rarely used, not included by default)\_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region | `string` | `null` | no | +| [replication\_configuration](#input\_replication\_configuration) | Replication configuration for cross-region disaster recovery.
Leave as {} to disable replication.

Example:
replication\_configuration = {
destination = "eu-west-1"
} | `any` | `{}` | no | +| [require\_tls\_version](#input\_require\_tls\_version) | Minimum TLS version to enforce via file system policy (e.g., '1.2', '1.3'). When set, a Deny statement is added for lower versions. Leave null to skip TLS version enforcement. | `string` | `null` | no | +| [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | no | +| [stack](#input\_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks` | `string` | `null` | no | +| [tag\_version](#input\_tag\_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to the caller module path when not set. | `string` | `null` | no | +| [throughput\_mode](#input\_throughput\_mode) | Throughput mode for the file system. Either 'bursting' (default) or 'provisioned'. | `string` | `"bursting"` | no | +| [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | +| [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [access\_points](#output\_access\_points) | Map of EFS Access Point IDs and their ARNs, keyed by the input map keys. | +| [efs\_arn](#output\_efs\_arn) | The Amazon Resource Name (ARN) of the EFS file system. | +| [efs\_dns\_name](#output\_efs\_dns\_name) | The DNS name of the EFS file system. | +| [efs\_encrypted](#output\_efs\_encrypted) | Always true; encryption is enforced by this module. | +| [efs\_id](#output\_efs\_id) | The ID of the EFS file system. | +| [efs\_size\_in\_bytes](#output\_efs\_size\_in\_bytes) | The latest metered size of the EFS in bytes. | +| [file\_system\_policy\_id](#output\_file\_system\_policy\_id) | The file system policy ID (if policy was attached). | +| [kms\_key\_arn](#output\_kms\_key\_arn) | The ARN of the KMS key used for encryption. | +| [mount\_targets](#output\_mount\_targets) | Map of mount target IDs and their associated subnet IDs. | +| [replication\_configuration](#output\_replication\_configuration) | The replication configuration of the EFS file system, if enabled. | + + + diff --git a/infrastructure/modules/efs/context.tf b/infrastructure/modules/efs/context.tf new file mode 100644 index 00000000..9a4652b0 --- /dev/null +++ b/infrastructure/modules/efs/context.tf @@ -0,0 +1,377 @@ +# tflint-ignore-file: terraform_standard_module_structure, terraform_unused_declarations +# +# ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/NHSDigital/screening-terraform-modules-aws/blob/master/infrastructure/modules/tags/exports/context.tf +# and then place it in your Terraform module to automatically get +# tag module standard configuration inputs suitable for passing +# to other modules. +# +# curl -sL https://raw.githubusercontent.com/NHSDigital/screening-terraform-modules-aws/master/infrastructure/modules/tags/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + # tflint-ignore: terraform_module_pinned_source + source = "../tags" + + enabled = var.enabled + service = var.service + project = var.project + region = var.region + environment = var.environment + stack = var.stack + workspace = var.workspace + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + terraform_source = coalesce(var.terraform_source, path.module) + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of screening-terraform-modules-aws/tags/variables.tf here +# tflint-ignore: terraform_unused_declarations +variable "aws_region" { + type = string + description = "The AWS region" + default = "eu-west-2" + validation { + condition = contains(["eu-west-1", "eu-west-2", "us-east-1"], var.aws_region) + error_message = "AWS Region must be one of eu-west-1, eu-west-2, us-east-1" + } +} + +variable "context" { + type = any + default = { + enabled = true + service = null + project = null + region = null + environment = null + stack = null + workspace = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + terraform_source = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "terraform_source" { + type = string + default = null + description = "Source location to record in the Terraform_source tag. Defaults to the caller module path when not set." +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "service" { + type = string + default = null + description = "ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique" +} + +variable "region" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region" +} + +variable "project" { + type = string + default = null + description = "ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api`" +} +variable "stack" { + type = string + default = null + description = "ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`" +} +variable "workspace" { + type = string + default = null + description = "ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces" +} +variable "environment" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +variable "owner" { + type = string + description = "The name and or NHS.net email address of the service owner" + default = "None" +} + +variable "tag_version" { + type = string + description = "Used to identify the tagging version in use" + default = "1.0" +} + +variable "data_classification" { + type = string + description = "Used to identify the data classification of the resource, e.g 1-5" + default = "n/a" + validation { + condition = contains(["n/a", "1", "2", "3", "4", "5"], var.data_classification) + error_message = "Data Classification must be \"n/a\" or between 1-5" + } +} + +variable "data_type" { + type = string + description = "The tag data_type" + default = "None" + validation { + condition = contains(["None", "PCD", "PID", "Anonymised", "UserAccount", "Audit"], var.data_type) + error_message = "Data Type must be one of None, PCD, PID, Anonymised, UserAccount, Audit" + } +} + + +variable "public_facing" { + type = bool + description = "Whether this resource is public facing" + default = false +} + +variable "service_category" { + type = string + description = "The tag service_category" + default = "n/a" + validation { + condition = contains(["n/a", "Bronze", "Silver", "Gold", "Platinum"], var.service_category) + error_message = "The Service Category must be one of n/a, Bronze, Silver, Gold, Platinum" + } +} +variable "on_off_pattern" { + type = string + description = "Used to turn resources on and off based on a time pattern" + default = "n/a" +} + +variable "application_role" { + type = string + description = "The role the application is performing" + default = "General" +} + +variable "tool" { + type = string + description = "The tool used to deploy the resource" + default = "Terraform" +} + +#### End of copy of screening-terraform-modules-aws/tags/variables.tf diff --git a/infrastructure/modules/efs/locals.tf b/infrastructure/modules/efs/locals.tf new file mode 100644 index 00000000..45d35821 --- /dev/null +++ b/infrastructure/modules/efs/locals.tf @@ -0,0 +1,87 @@ +################################################################ +# Local values +# +# EFS name is derived from the context, with optional override. +# File system policy is built with optional security statements. +################################################################ + +locals { + # Naming logic — derive from context, allow caller override + efs_name = var.custom_name != null ? var.custom_name : module.this.id + + # Build list of default security statements to add to the policy + default_policy_statements = concat( + # Deny unsecure (non-TLS) transport + var.deny_unsecure_transport ? [ + { + Sid = "DenyUnsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "elasticfilesystem:*" + Resource = "*" + Condition = { + Bool = { + "aws:SecureTransport" = "false" + } + } + } + ] : [], + + # Deny old TLS versions (require TLS 1.2+) + var.require_tls_version != null ? [ + { + Sid = "DenyOldTLSVersion" + Effect = "Deny" + Principal = "*" + Action = "elasticfilesystem:*" + Resource = "*" + Condition = { + StringLessThan = { + "aws:TlsVersion" = var.require_tls_version + } + } + } + ] : [], + + # Deny access from IPs outside allowed list + length(var.allowed_source_ips) > 0 ? [ + { + Sid = "DenyUnauthorizedSourceIPs" + Effect = "Deny" + Principal = "*" + Action = "elasticfilesystem:*" + Resource = "*" + Condition = { + NotIpAddress = { + "aws:SourceIp" = var.allowed_source_ips + } + } + } + ] : [], + + # Deny destructive operations by default + var.deny_destructive_operations ? [ + { + Sid = "DenyDestructiveOperations" + Effect = "Deny" + Principal = "*" + Action = [ + "elasticfilesystem:DeleteFileSystem", + "elasticfilesystem:DeleteAccessPoint", + "elasticfilesystem:DeleteMountTarget", + "elasticfilesystem:DeleteReplicationConfiguration" + ] + Resource = "*" + } + ] : [] + ) + + # File system policy: merge caller policy with default security statements + file_system_policy_doc = length(local.default_policy_statements) > 0 || var.file_system_policy != null ? jsonencode({ + Version = "2012-10-17" + Statement = concat( + var.file_system_policy != null ? jsondecode(var.file_system_policy).Statement : [], + local.default_policy_statements + ) + }) : null +} diff --git a/infrastructure/modules/efs/main.tf b/infrastructure/modules/efs/main.tf new file mode 100644 index 00000000..42282774 --- /dev/null +++ b/infrastructure/modules/efs/main.tf @@ -0,0 +1,117 @@ +################################################################ +# EFS (Elastic File System) +# +# Thin NHS wrapper around the community terraform-aws-modules/efs/aws +# module that enforces the screening platform's baseline controls: +# +# * Encryption: KMS encryption at rest is mandatory +# * Transport: NFSv4.1 security groups are caller-supplied (required) +# * Throughput: caller controls performance mode and throughput mode +# * Access: optional backup/transition lifecycle policies +# * Policy: auto-generated security statements for transport, TLS, IPs, and destructive ops +# +# Naming and tagging are derived from context.tf via module.this. +# Cross-variable input constraints are enforced in validations.tf. +################################################################ + +module "efs" { + source = "terraform-aws-modules/efs/aws" + version = "2.2.0" + + create = module.this.enabled + + # ---------------------------------------------------------------- + # Naming and metadata. + # ---------------------------------------------------------------- + name = local.efs_name + creation_token = var.creation_token + + # ---------------------------------------------------------------- + # Encryption at rest: mandatory KMS encryption. + # ---------------------------------------------------------------- + encrypted = true + kms_key_arn = var.kms_key_arn + + # ---------------------------------------------------------------- + # Performance and throughput modes. Defaults are suitable for + # general purpose; caller can override for higher-performance needs. + # ---------------------------------------------------------------- + performance_mode = var.performance_mode + throughput_mode = var.throughput_mode + provisioned_throughput_in_mibps = var.throughput_mode == "provisioned" ? var.provisioned_throughput_in_mibps : null + + # ---------------------------------------------------------------- + # Transition and lifecycle policies (optional). + # ---------------------------------------------------------------- + lifecycle_policy = var.lifecycle_policy + + # ---------------------------------------------------------------- + # Mount targets: caller must provide security group(s) and subnets. + # ---------------------------------------------------------------- + mount_targets = var.mount_targets + + # ---------------------------------------------------------------- + # Backup policy (optional). + # ---------------------------------------------------------------- + create_backup_policy = var.protection.enable_backup + enable_backup_policy = var.protection.enable_backup + + # ---------------------------------------------------------------- + # Replication configuration (optional). + # ---------------------------------------------------------------- + create_replication_configuration = length(var.replication_configuration) > 0 + replication_configuration_destination = var.replication_configuration + + # ---------------------------------------------------------------- + # Tagging: all resources tagged via module.this. + # ---------------------------------------------------------------- + tags = module.this.tags +} + +################################################################ +# File System Policy (optional, resource-based access control) +################################################################ + +resource "aws_efs_file_system_policy" "this" { + count = module.this.enabled && local.file_system_policy_doc != null ? 1 : 0 + + file_system_id = module.efs.id + policy = local.file_system_policy_doc + + depends_on = [module.efs] +} + +################################################################ +# Access Points (optional, application-level isolation) +################################################################ +resource "aws_efs_access_point" "this" { + for_each = module.this.enabled ? var.access_points : {} + + file_system_id = module.efs.id + + # Root directory enforces a chroot jail for the access point + root_directory { + path = each.value.root_directory_path + + creation_info { + owner_gid = each.value.enforced_user_id + owner_uid = each.value.enforced_user_id + permissions = each.value.permissions_mode + } + } + + # Enforce a POSIX user identity for all requests through this access point + posix_user { + gid = each.value.enforced_user_id + uid = each.value.enforced_user_id + } + + tags = merge( + module.this.tags, + { + Name = "${module.this.id}-${each.key}" + } + ) + + depends_on = [module.efs] +} diff --git a/infrastructure/modules/efs/outputs.tf b/infrastructure/modules/efs/outputs.tf new file mode 100644 index 00000000..42c7582b --- /dev/null +++ b/infrastructure/modules/efs/outputs.tf @@ -0,0 +1,49 @@ +output "efs_id" { + description = "The ID of the EFS file system." + value = module.efs.id +} + +output "efs_arn" { + description = "The Amazon Resource Name (ARN) of the EFS file system." + value = module.efs.arn +} + +output "efs_dns_name" { + description = "The DNS name of the EFS file system." + value = module.efs.dns_name +} + +output "efs_size_in_bytes" { + description = "The latest metered size of the EFS in bytes." + value = module.efs.size_in_bytes +} + +output "efs_encrypted" { + description = "Always true; encryption is enforced by this module." + value = true +} + +output "kms_key_arn" { + description = "The ARN of the KMS key used for encryption." + value = var.kms_key_arn +} + +output "mount_targets" { + description = "Map of mount target IDs and their associated subnet IDs." + value = module.efs.mount_targets +} + +output "replication_configuration" { + description = "The replication configuration of the EFS file system, if enabled." + value = try(module.efs.replication_configuration_destination_file_system_id, null) +} + +output "file_system_policy_id" { + description = "The file system policy ID (if policy was attached)." + value = try(aws_efs_file_system_policy.this[0].id, null) +} + +output "access_points" { + description = "Map of EFS Access Point IDs and their ARNs, keyed by the input map keys." + value = { for k, ap in aws_efs_access_point.this : k => { id = ap.id, arn = ap.arn } } +} diff --git a/infrastructure/modules/efs/validations.tf b/infrastructure/modules/efs/validations.tf new file mode 100644 index 00000000..a1ec4adc --- /dev/null +++ b/infrastructure/modules/efs/validations.tf @@ -0,0 +1,58 @@ +################################################################ +# Input validation +# +# Validates cross-variable constraints that cannot be expressed +# through individual variable validation blocks: +# +# * throughput_mode = "provisioned" requires provisioned_throughput_in_mibps +# * replication_configuration must have mount_targets defined +# * access_points must have mount_targets defined +# * lifecycle_policy keys must be valid EFS policy keys +# * replication_overwrite must be "DISABLED" or "ENABLED" +# * access_point permissions_mode must be valid octal (3-4 digits) +# * availability_zone_name (one-zone) requires exactly one mount target +################################################################ + +resource "terraform_data" "validations" { + count = module.this.enabled ? 1 : 0 + + lifecycle { + precondition { + condition = !(var.throughput_mode == "provisioned" && var.provisioned_throughput_in_mibps == null) + error_message = "throughput_mode = 'provisioned' requires provisioned_throughput_in_mibps to be set." + } + precondition { + condition = !(length(var.replication_configuration) > 0 && length(var.mount_targets) == 0) + error_message = "replication_configuration requires at least one mount target to be configured." + } + precondition { + condition = !(length(var.access_points) > 0 && length(var.mount_targets) == 0) + error_message = "access_points requires at least one mount target to be configured." + } + precondition { + condition = var.lifecycle_policy == null ? true : ( + alltrue([ + for key in keys(var.lifecycle_policy) : contains([ + "transition_to_ia", + "transition_to_primary_storage_class" + ], key) + ]) + ) + error_message = "lifecycle_policy keys must be one of: transition_to_ia, transition_to_primary_storage_class." + } + precondition { + condition = var.protection.replication_overwrite == null ? true : contains(["DISABLED", "ENABLED"], var.protection.replication_overwrite) + error_message = "protection.replication_overwrite must be 'DISABLED' or 'ENABLED'." + } + precondition { + condition = alltrue([ + for ap_key, ap in var.access_points : can(regex("^[0-7]{3,4}$", ap.permissions_mode)) + ]) + error_message = "access_point permissions_mode must be valid octal notation (e.g., '755', '700')." + } + precondition { + condition = !(var.availability_zone_name != null && length(var.mount_targets) != 1) + error_message = "availability_zone_name (one-zone storage) requires exactly one mount target; found ${length(var.mount_targets)}." + } + } +} diff --git a/infrastructure/modules/efs/variables.tf b/infrastructure/modules/efs/variables.tf new file mode 100644 index 00000000..d7c07fc9 --- /dev/null +++ b/infrastructure/modules/efs/variables.tf @@ -0,0 +1,237 @@ +################################################################ +# EFS-specific inputs. +################################################################ + +variable "availability_zone_name" { + description = "AWS Availability Zone for One Zone storage class. When set, the file system uses single-AZ storage for lower cost. Leave null for multi-AZ." + type = string + default = null +} + +variable "creation_token" { + description = "A unique name (max 64 chars) used as reference when creating the file system. Enables idempotent creation. When null, Terraform generates a token." + type = string + default = null +} + +variable "custom_name" { + description = "Optional explicit EFS name. When null, the name is derived from module.this.id." + type = string + default = null +} + +variable "kms_key_arn" { + description = "ARN of the KMS key used for encryption at rest. Encryption is mandatory; this variable is required." + type = string + + validation { + condition = can(regex("^arn:aws:kms:", var.kms_key_arn)) + error_message = "kms_key_arn must be a valid KMS ARN (arn:aws:kms:...)." + } +} + +variable "performance_mode" { + description = "The file system performance mode. Either 'generalPurpose' or 'maxIO'." + type = string + default = "generalPurpose" + + validation { + condition = contains(["generalPurpose", "maxIO"], var.performance_mode) + error_message = "performance_mode must be either 'generalPurpose' or 'maxIO'." + } +} + +variable "throughput_mode" { + description = "Throughput mode for the file system. Either 'bursting' (default) or 'provisioned'." + type = string + default = "bursting" + + validation { + condition = contains(["bursting", "provisioned"], var.throughput_mode) + error_message = "throughput_mode must be either 'bursting' or 'provisioned'." + } +} + +variable "provisioned_throughput_in_mibps" { + description = "The throughput to provision for the file system in MiB/s. Required when throughput_mode is 'provisioned'. Range: 1–1024." + type = number + default = null + + validation { + condition = var.provisioned_throughput_in_mibps == null || ( + var.provisioned_throughput_in_mibps >= 1 && var.provisioned_throughput_in_mibps <= 1024 + ) + error_message = "provisioned_throughput_in_mibps must be between 1 and 1024 MiB/s when set." + } +} + +variable "mount_targets" { + description = <<-EOT + Map of mount target configurations. Each mount target must specify: + - subnet_id: Subnet where the mount target resides (required) + - security_groups: List of security group IDs (required; caller must enforce ingress rules for NFSv4.1) + - ip_address: (optional) Static IP address for the mount target + - ip_address_type: (optional) IP address type (ipv4 or ipv6) + - ipv6_address: (optional) IPv6 address for the mount target + + Example: + mount_targets = { + "az1" = { + subnet_id = "subnet-12345" + security_groups = ["sg-12345"] + } + "az2" = { + subnet_id = "subnet-67890" + security_groups = ["sg-67890"] + ip_address = "10.0.1.100" + } + } + EOT + type = any + default = {} + + validation { + condition = alltrue([ + for mt_key, mt in var.mount_targets : ( + can(mt.subnet_id) && + can(mt.security_groups) && + length(mt.security_groups) > 0 + ) + ]) + error_message = "Each mount target must have subnet_id and non-empty security_groups list." + } +} + +variable "lifecycle_policy" { + description = <<-EOT + Optional lifecycle policy for automatic transition to other storage classes. + Leave as {} to disable. Example: + lifecycle_policy = { + transition_to_ia = "AFTER_30_DAYS" + transition_to_primary_storage_class = "AFTER_1_DAY" + } + EOT + type = any + default = {} +} + +variable "protection" { + description = <<-EOT + Configuration for replication protection and backup policy. + + Defaults: + - enable_backup: true (enables automated backups via AWS Backup) + - replication_overwrite: DISABLED (prevents accidental overwrites of replicas during replication) + EOT + type = object({ + enable_backup = optional(bool, true) + replication_overwrite = optional(string, "DISABLED") + }) + default = {} +} + +variable "file_system_policy" { + description = <<-EOT + Optional IAM policy document (as JSON string) to attach to the file system. + Controls who can perform what actions on the EFS resource. + When null, no resource-based policy is attached. + + Security baseline: Callers should consider denying nonsecure (non-TLS) transport: + - aws:SecureTransport condition set to false + EOT + type = string + default = null + + validation { + condition = var.file_system_policy == null || ( + can(jsondecode(var.file_system_policy)) && + can(jsondecode(var.file_system_policy).Statement) + ) + error_message = "file_system_policy must be a valid JSON IAM policy document." + } +} + +variable "deny_unsecure_transport" { + description = "Whether to automatically add a Deny statement for non-TLS (unsecure) transport to the file system policy. Recommended: true." + type = bool + default = true +} + +variable "access_points" { + description = <<-EOT + Map of EFS Access Point configurations for application-level mount points. + Access Points enforce POSIX user identities and enforce a file system root. + Leave as {} to create no access points. + + Example: + access_points = { + "app-root" = { + enforced_user_id = "1000" + root_directory_path = "/app" + permissions_mode = "755" + } + "db-root" = { + enforced_user_id = "1001" + root_directory_path = "/data" + permissions_mode = "700" + } + } + EOT + type = any + default = {} + + validation { + condition = alltrue([ + for ap_key, ap in var.access_points : ( + can(ap.enforced_user_id) && + can(ap.root_directory_path) && + can(ap.permissions_mode) + ) + ]) + error_message = "Each access point must have enforced_user_id, root_directory_path, and permissions_mode." + } +} + +variable "replication_configuration" { + description = <<-EOT + Replication configuration for cross-region disaster recovery. + Leave as {} to disable replication. + + Example: + replication_configuration = { + destination = "eu-west-1" + } + EOT + type = any + default = {} +} + +variable "require_tls_version" { + description = "Minimum TLS version to enforce via file system policy (e.g., '1.2', '1.3'). When set, a Deny statement is added for lower versions. Leave null to skip TLS version enforcement." + type = string + default = null + + validation { + condition = var.require_tls_version == null || can(regex("^1\\.[2-3]$", var.require_tls_version)) + error_message = "require_tls_version must be null, '1.2', or '1.3'." + } +} + +variable "allowed_source_ips" { + description = "List of CIDR blocks allowed to access the EFS. When set, a Deny statement restricts access to these IPs. Leave as [] to skip IP-based restrictions." + type = list(string) + default = [] + + validation { + condition = alltrue([ + for cidr in var.allowed_source_ips : can(regex("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}(/[0-9]{1,2})?$", cidr)) + ]) + error_message = "allowed_source_ips must contain valid CIDR blocks (e.g., '10.0.0.0/8')." + } +} + +variable "deny_destructive_operations" { + description = "Whether to add a Deny statement for destructive operations (DeleteFileSystem, DeleteAccessPoint) by default. Callers must explicitly allow these via var.file_system_policy. Recommended: true." + type = bool + default = true +} diff --git a/infrastructure/modules/efs/versions.tf b/infrastructure/modules/efs/versions.tf new file mode 100644 index 00000000..bdaae8ef --- /dev/null +++ b/infrastructure/modules/efs/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.28" + } + } +} diff --git a/mise.lock b/mise.lock index 0a809bcc..1a0c0f04 100644 --- a/mise.lock +++ b/mise.lock @@ -24,30 +24,6 @@ checksum = "sha256:4592b096e553f67799ae70d4b6167eeda3ec74587d68c7aecbf4e7b1df136 url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda" checksum = "sha256:2370ef0ffcbae5bede3c4bf136add4abc257245eb91f724c99bb4a43116c5a83" -[conda-packages.linux-x64."_openmp_mutex-4.5-20_gnu"] -url = "https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda" -checksum = "sha256:1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9" - -[conda-packages.linux-x64."libgcc-15.2.0-he0feb66_19"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda" -checksum = "sha256:8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46" - -[conda-packages.linux-x64."libgomp-15.2.0-he0feb66_19"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda" -checksum = "sha256:5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b" - -[conda-packages.linux-x64-musl."_openmp_mutex-4.5-20_gnu"] -url = "https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda" -checksum = "sha256:1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9" - -[conda-packages.linux-x64-musl."libgcc-15.2.0-he0feb66_19"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda" -checksum = "sha256:8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46" - -[conda-packages.linux-x64-musl."libgomp-15.2.0-he0feb66_19"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda" -checksum = "sha256:5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b" - [conda-packages.windows-x64."_openmp_mutex-4.5-20_gnu"] url = "https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda" checksum = "sha256:8a1cee28bd0ee7451ada1cd50b64720e57e17ff994fc62dd8329bef570d382e4" @@ -75,36 +51,43 @@ backend = "aqua:rhysd/actionlint" [tools.actionlint."platforms.linux-arm64"] checksum = "sha256:325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924897" provenance = "github-attestations" [tools.actionlint."platforms.linux-arm64-musl"] checksum = "sha256:325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924897" provenance = "github-attestations" [tools.actionlint."platforms.linux-x64"] checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896" provenance = "github-attestations" [tools.actionlint."platforms.linux-x64-musl"] checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896" provenance = "github-attestations" [tools.actionlint."platforms.macos-arm64"] checksum = "sha256:aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924893" provenance = "github-attestations" [tools.actionlint."platforms.macos-x64"] checksum = "sha256:5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924880" provenance = "github-attestations" [tools.actionlint."platforms.windows-x64"] checksum = "sha256:6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9" url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_windows_amd64.zip" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924919" provenance = "github-attestations" [[tools.gitleaks]] @@ -114,30 +97,37 @@ backend = "aqua:gitleaks/gitleaks" [tools.gitleaks."platforms.linux-arm64"] checksum = "sha256:e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332057" [tools.gitleaks."platforms.linux-arm64-musl"] checksum = "sha256:e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332057" [tools.gitleaks."platforms.linux-x64"] checksum = "sha256:551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332058" [tools.gitleaks."platforms.linux-x64-musl"] checksum = "sha256:551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332058" [tools.gitleaks."platforms.macos-arm64"] checksum = "sha256:b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332059" [tools.gitleaks."platforms.macos-x64"] checksum = "sha256:dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_darwin_x64.tar.gz" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378332053" [tools.gitleaks."platforms.windows-x64"] checksum = "sha256:d29144deff3a68aa93ced33dddf84b7fdc26070add4aa0f4513094c8332afc4e" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_windows_x64.zip" +url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378333178" [[tools.go]] version = "1.26.5" @@ -182,36 +172,43 @@ backend = "aqua:jqlang/jq" [tools.jq."platforms.linux-arm64"] checksum = "sha256:8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012756" provenance = "github-attestations" [tools.jq."platforms.linux-arm64-musl"] checksum = "sha256:8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012756" provenance = "github-attestations" [tools.jq."platforms.linux-x64"] checksum = "sha256:b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012752" provenance = "github-attestations" [tools.jq."platforms.linux-x64-musl"] checksum = "sha256:b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012752" provenance = "github-attestations" [tools.jq."platforms.macos-arm64"] checksum = "sha256:2d75340ba57a4b4b4c8708a21c2dc8e958a48aaa8bba13b27f77f6e4c0eca07e" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-macos-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012783" provenance = "github-attestations" [tools.jq."platforms.macos-x64"] checksum = "sha256:e94b266e3c26690550006abe63152b782280f4e14374accdf04cbde844f00bc0" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-macos-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012782" provenance = "github-attestations" [tools.jq."platforms.windows-x64"] checksum = "sha256:a6fc67fedaf9128a3309a1e2ebb8b986aeccf70122ee46d2cb4849e423f0c627" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-windows-amd64.exe" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012788" provenance = "github-attestations" [[tools.make]] @@ -242,28 +239,20 @@ conda_deps = [ [tools.make."platforms.linux-x64"] checksum = "sha256:d652c7bd4d3b6f82b0f6d063b0d8df6f54cc47531092d7ff008e780f3261bdda" url = "https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda" -conda_deps = [ - "libgcc-15.2.0-he0feb66_19", - "_openmp_mutex-4.5-20_gnu", - "libgomp-15.2.0-he0feb66_19", -] [tools.make."platforms.linux-x64-musl"] checksum = "sha256:d652c7bd4d3b6f82b0f6d063b0d8df6f54cc47531092d7ff008e780f3261bdda" url = "https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_2.conda" -conda_deps = [ - "libgcc-15.2.0-he0feb66_19", - "_openmp_mutex-4.5-20_gnu", - "libgomp-15.2.0-he0feb66_19", -] [tools.make."platforms.macos-arm64"] checksum = "sha256:90ca65e788406d9029ae23ad4bd944a8b5353ad5f59bd6ce326f980cde46f37e" url = "https://conda.anaconda.org/conda-forge/osx-arm64/make-4.4.1-hc9fafa5_2.conda" +conda_deps = [] [tools.make."platforms.macos-x64"] checksum = "sha256:5a5ab3ee828309185e0a76ca80f5da85f31d8480d923abb508ca00fe194d1b5a" url = "https://conda.anaconda.org/conda-forge/osx-64/make-4.4.1-h00291cd_2.conda" +conda_deps = [] [tools.make."platforms.windows-x64"] checksum = "sha256:a810cdca3d5fa50d562cda23c0c1195b45ff5f9b0c41e0d4c8c2dd3c043ff4f2" @@ -315,26 +304,32 @@ backend = "aqua:pre-commit/pre-commit" [tools.pre-commit."platforms.linux-arm64"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [tools.pre-commit."platforms.linux-arm64-musl"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [tools.pre-commit."platforms.linux-x64"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [tools.pre-commit."platforms.linux-x64-musl"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [tools.pre-commit."platforms.macos-arm64"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [tools.pre-commit."platforms.macos-x64"] checksum = "sha256:ea8a0c84902e48c1875558f2f362ed8476773aa5fc8c16c5d8f2acc2a2830a65" url = "https://github.com/pre-commit/pre-commit/releases/download/v4.6.0/pre-commit-4.6.0.pyz" +url_api = "https://api.github.com/repos/pre-commit/pre-commit/releases/assets/401888770" [[tools.python]] version = "3.12.13" @@ -382,30 +377,37 @@ backend = "aqua:koalaman/shellcheck" [tools.shellcheck."platforms.linux-arm64"] checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934" [tools.shellcheck."platforms.linux-arm64-musl"] checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934" [tools.shellcheck."platforms.linux-x64"] checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" [tools.shellcheck."platforms.linux-x64-musl"] checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" [tools.shellcheck."platforms.macos-arm64"] checksum = "sha256:56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056932" [tools.shellcheck."platforms.macos-x64"] checksum = "sha256:3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056930" [tools.shellcheck."platforms.windows-x64"] checksum = "sha256:8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e" url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056944" [[tools.terraform]] version = "1.13.5" @@ -446,30 +448,37 @@ backend = "aqua:terraform-docs/terraform-docs" [tools.terraform-docs."platforms.linux-arm64"] checksum = "sha256:d12bd7b73c1fc9c64efc79f8157dd713dabd559f1ecf3cfc0f42e32279a155fd" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956212" [tools.terraform-docs."platforms.linux-arm64-musl"] checksum = "sha256:d12bd7b73c1fc9c64efc79f8157dd713dabd559f1ecf3cfc0f42e32279a155fd" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956212" [tools.terraform-docs."platforms.linux-x64"] checksum = "sha256:9005daf969de0b50134493a2c00078b49f5f5b39d021cda7c89bf4d4f3d776d3" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-linux-amd64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956208" [tools.terraform-docs."platforms.linux-x64-musl"] checksum = "sha256:9005daf969de0b50134493a2c00078b49f5f5b39d021cda7c89bf4d4f3d776d3" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-linux-amd64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956208" [tools.terraform-docs."platforms.macos-arm64"] checksum = "sha256:f6b114f4b032f3f9202ab6c23bfd28c3c8e68aeeb8a8f12fc118bf2073081d71" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956237" [tools.terraform-docs."platforms.macos-x64"] checksum = "sha256:3c3f7f18f908457fd1209cbe341418f7f6bae78c08126cfbe8de0d1b06aa8781" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-darwin-amd64.tar.gz" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956256" [tools.terraform-docs."platforms.windows-x64"] checksum = "sha256:afc02cbdf63726d3e5fb26a077ca1f24acd4820d7e44c21b03a3e030f9266490" url = "https://github.com/terraform-docs/terraform-docs/releases/download/v0.24.0/terraform-docs-v0.24.0-windows-amd64.zip" +url_api = "https://api.github.com/repos/terraform-docs/terraform-docs/releases/assets/416956245" [[tools.tflint]] version = "0.62.1" @@ -478,36 +487,43 @@ backend = "aqua:terraform-linters/tflint" [tools.tflint."platforms.linux-arm64"] checksum = "sha256:9f3bce43f7f58f05ddcf193f0bf1f7e7a9c7a79d7f46f72dd38e97f96fcaf14c" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_linux_arm64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448758" provenance = "github-attestations" [tools.tflint."platforms.linux-arm64-musl"] checksum = "sha256:9f3bce43f7f58f05ddcf193f0bf1f7e7a9c7a79d7f46f72dd38e97f96fcaf14c" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_linux_arm64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448758" provenance = "github-attestations" [tools.tflint."platforms.linux-x64"] checksum = "sha256:c004ec45ade3caf87cd4089feb1d2af9f7df57b13140a36df8a63c0a8cc69f14" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_linux_amd64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448794" provenance = "github-attestations" [tools.tflint."platforms.linux-x64-musl"] checksum = "sha256:c004ec45ade3caf87cd4089feb1d2af9f7df57b13140a36df8a63c0a8cc69f14" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_linux_amd64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448794" provenance = "github-attestations" [tools.tflint."platforms.macos-arm64"] checksum = "sha256:927866fef68382138b8fed038721a03c0928ce9486e1616b18bd3dd11e7cdacb" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_darwin_arm64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448760" provenance = "github-attestations" [tools.tflint."platforms.macos-x64"] checksum = "sha256:7f55df3a25deb610c267f600eb4247657e3ff776d0322916ceecd7b58142a73a" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_darwin_amd64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448796" provenance = "github-attestations" [tools.tflint."platforms.windows-x64"] checksum = "sha256:bef472fd62dcbdb616bbdf67800f1bcf8db64598529c09610154686555180edd" url = "https://github.com/terraform-linters/tflint/releases/download/v0.62.1/tflint_windows_amd64.zip" +url_api = "https://api.github.com/repos/terraform-linters/tflint/releases/assets/419448795" provenance = "github-attestations" [[tools.vale]] @@ -517,30 +533,37 @@ backend = "aqua:vale-cli/vale" [tools.vale."platforms.linux-arm64"] checksum = "sha256:45295ab07fd99c87a9cac20dce5d230d7b63ffec1394b38c8d5d4496347a069b" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588938" [tools.vale."platforms.linux-arm64-musl"] checksum = "sha256:45295ab07fd99c87a9cac20dce5d230d7b63ffec1394b38c8d5d4496347a069b" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588938" [tools.vale."platforms.linux-x64"] checksum = "sha256:d27cd03f920202a7be5bba3fd9e784a22fe8b33f838911853588ce3ce594a25d" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_Linux_64-bit.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588941" [tools.vale."platforms.linux-x64-musl"] checksum = "sha256:d27cd03f920202a7be5bba3fd9e784a22fe8b33f838911853588ce3ce594a25d" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_Linux_64-bit.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588941" [tools.vale."platforms.macos-arm64"] checksum = "sha256:df71e5fcbefca9413479737fa946e105249ec73e2a328ef1784e5558d92414b0" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_macOS_arm64.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588939" [tools.vale."platforms.macos-x64"] checksum = "sha256:4d1df61fc3b1e4109bd965b478dad6cb8998e2ca7836100b936c16c331c8c9f2" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_macOS_64-bit.tar.gz" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588940" [tools.vale."platforms.windows-x64"] checksum = "sha256:6b205cd9ee4df016d42287cd0b2b8f234acb45900fc371cda97a5c49bc69b350" url = "https://github.com/vale-cli/vale/releases/download/v3.6.0/vale_3.6.0_Windows_64-bit.zip" +url_api = "https://api.github.com/repos/vale-cli/vale/releases/assets/174588942" [[tools.yq]] version = "4.53.3" @@ -549,27 +572,41 @@ backend = "aqua:mikefarah/yq" [tools.yq."platforms.linux-arm64"] checksum = "sha256:578648e463a11c1b6db6010cbf41eafed6bee79466fcffa1bb446672cf7945ea" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146727" +provenance = "cosign" [tools.yq."platforms.linux-arm64-musl"] checksum = "sha256:578648e463a11c1b6db6010cbf41eafed6bee79466fcffa1bb446672cf7945ea" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146727" +provenance = "cosign" [tools.yq."platforms.linux-x64"] checksum = "sha256:fa52a4e758c63d38299163fbdd1edfb4c4963247918bf9c1c5d31d84789eded4" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146735" +provenance = "cosign" [tools.yq."platforms.linux-x64-musl"] checksum = "sha256:fa52a4e758c63d38299163fbdd1edfb4c4963247918bf9c1c5d31d84789eded4" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146735" +provenance = "cosign" [tools.yq."platforms.macos-arm64"] -checksum = "sha256:877de31753a4dd2401aa048937aa9a7fc4d5f6ce858cf31508c5802954297213" -url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_darwin_arm64" +checksum = "sha256:b4ba1ecce3c47f00803f4f964de38394326c7a32eb6540616e04fb2935a0f08d" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_darwin_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146744" +provenance = "cosign" [tools.yq."platforms.macos-x64"] checksum = "sha256:b4ba1ecce3c47f00803f4f964de38394326c7a32eb6540616e04fb2935a0f08d" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_darwin_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146744" +provenance = "cosign" [tools.yq."platforms.windows-x64"] checksum = "sha256:e279bc506a452eeafcdf364f91a025455e402a8001169083caf01f4b64a544e2" url = "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_windows_amd64.exe" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/440146721" +provenance = "cosign" diff --git a/scripts/config/generate-available-modules.yaml b/scripts/config/generate-available-modules.yaml index 05111041..2913a4f1 100644 --- a/scripts/config/generate-available-modules.yaml +++ b/scripts/config/generate-available-modules.yaml @@ -69,6 +69,10 @@ ecs-service: description: "ECS service and task definition" wraps: "terraform-aws-modules/ecs/aws//modules/service" +efs: + description: "EFS file system with access points and mount targets" + wraps: "terraform-aws-modules/efs/aws" + elasticache: description: "ElastiCache cluster (Redis/Memcached)" wraps: "—"