From d33dbd0fbb784b6398e24b50854a9e441202cd72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 14:53:04 +0000 Subject: [PATCH] feat(validation): add numeric range guards to all three AWS tier modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terraform silently accepts out-of-range values (e.g. db_allocated_storage_gb = 5, redis_snapshot_retention_days = 40) until the AWS API rejects them at apply time. Adding validation blocks shifts that failure left to terraform validate / plan. Constraints applied: - EBS volumes: 1–16384 GB (gp3 physical limit) - RDS Postgres storage: 20–65536 GB (engine minimum / RDS maximum) - RDS backup retention: 0–35 days (RDS hard limit) - ElastiCache snapshot retention: 0–35 days (ElastiCache hard limit) - RDS Performance Insights retention: 7 or 731 (only two valid values) - ALB idle timeout: 1–4000 s (ALB documented range) - Rollback 5xx threshold: 0–100 % - ASG min/max/desired: ≥ 1 - ASG scaling targets: within documented AWS ranges - S3 Object Lock / lifecycle retention: ≥ 1 day Azure tier modules (ha-hot-hot/azure, unlimited-scale/azure, single-vm/azure) have equivalent gaps and should be addressed in a follow-up. https://claude.ai/code/session_01RNxQACZc1QFiPu1zWDreGe --- modules/ha-hot-hot/aws/variables.tf | 52 +++++++++++++++++ modules/single-vm/aws/variables.tf | 16 ++++++ modules/unlimited-scale/aws/variables.tf | 72 ++++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/modules/ha-hot-hot/aws/variables.tf b/modules/ha-hot-hot/aws/variables.tf index 3ebdeda..9f6bc7e 100644 --- a/modules/ha-hot-hot/aws/variables.tf +++ b/modules/ha-hot-hot/aws/variables.tf @@ -62,6 +62,10 @@ variable "instance_type" { variable "data_volume_size_gb" { type = number default = 200 + validation { + condition = var.data_volume_size_gb >= 1 && var.data_volume_size_gb <= 16384 + error_message = "data_volume_size_gb must be between 1 and 16384 GB." + } } variable "key_name" { @@ -98,6 +102,10 @@ variable "redis_snapshot_retention_days" { description = "Days ElastiCache retains daily snapshots. Sessions are recoverable from Postgres re-login, so this defaults to 0; raise if you want a Redis PITR window." type = number default = 0 + validation { + condition = var.redis_snapshot_retention_days >= 0 && var.redis_snapshot_retention_days <= 35 + error_message = "redis_snapshot_retention_days must be between 0 and 35 (ElastiCache limit)." + } } variable "redis_endpoint_override" { @@ -110,6 +118,10 @@ variable "redis_endpoint_override_port" { description = "Port on the customer-managed Redis endpoint. Ignored unless redis_endpoint_override is set." type = number default = 6379 + validation { + condition = var.redis_endpoint_override_port >= 1 && var.redis_endpoint_override_port <= 65535 + error_message = "redis_endpoint_override_port must be a valid TCP port (1–65535)." + } } variable "redis_endpoint_override_tls" { @@ -121,12 +133,20 @@ variable "redis_endpoint_override_tls" { variable "db_allocated_storage_gb" { type = number default = 100 + validation { + condition = var.db_allocated_storage_gb >= 20 && var.db_allocated_storage_gb <= 65536 + error_message = "db_allocated_storage_gb must be between 20 and 65536 GB (RDS Postgres limits)." + } } variable "db_max_allocated_storage_gb" { description = "Storage autoscaling cap." type = number default = 500 + validation { + condition = var.db_max_allocated_storage_gb >= 21 && var.db_max_allocated_storage_gb <= 65536 + error_message = "db_max_allocated_storage_gb must be between 21 and 65536 GB." + } } variable "db_engine_version" { @@ -154,6 +174,10 @@ variable "enable_customer_managed_key" { variable "alb_idle_timeout_seconds" { type = number default = 120 + validation { + condition = var.alb_idle_timeout_seconds >= 1 && var.alb_idle_timeout_seconds <= 4000 + error_message = "alb_idle_timeout_seconds must be between 1 and 4000 (ALB limit)." + } } variable "enable_alb_deletion_protection" { @@ -172,6 +196,10 @@ variable "alb_access_log_retention_days" { description = "Days to retain ALB access log objects before lifecycle expiration. Default 365 (one calendar year) — long enough for most compliance lookback windows." type = number default = 365 + validation { + condition = var.alb_access_log_retention_days >= 1 + error_message = "alb_access_log_retention_days must be at least 1." + } } variable "alb_min_tls_version" { @@ -220,12 +248,20 @@ variable "db_ec2_data_volume_size_gb" { description = "Size of the encrypted gp3 volume backing /var/lib/postgresql on the self-managed Postgres VM." type = number default = 200 + validation { + condition = var.db_ec2_data_volume_size_gb >= 1 && var.db_ec2_data_volume_size_gb <= 16384 + error_message = "db_ec2_data_volume_size_gb must be between 1 and 16384 GB." + } } variable "rds_backup_retention_period" { description = "Days RDS retains automated daily backups. 7 satisfies the procurement-grade baseline; raise for longer point-in-time-restore windows." type = number default = 7 + validation { + condition = var.rds_backup_retention_period >= 0 && var.rds_backup_retention_period <= 35 + error_message = "rds_backup_retention_period must be between 0 and 35 days (RDS limit)." + } } variable "rds_copy_tags_to_snapshot" { @@ -250,18 +286,30 @@ variable "backup_object_lock_retention_days" { description = "Object Lock (governance mode) retention period for backup objects." type = number default = 30 + validation { + condition = var.backup_object_lock_retention_days >= 1 + error_message = "backup_object_lock_retention_days must be at least 1." + } } variable "backup_noncurrent_version_expiration_days" { description = "Expire noncurrent versions of backup objects after this many days." type = number default = 365 + validation { + condition = var.backup_noncurrent_version_expiration_days >= 1 + error_message = "backup_noncurrent_version_expiration_days must be at least 1." + } } variable "refresh_rollback_5xx_threshold_pct" { description = "Target-group 5xx rate (percent) that trips the patching alarm. Default 1% over 2 evaluation periods of 1 minute." type = number default = 1 + validation { + condition = var.refresh_rollback_5xx_threshold_pct >= 0 && var.refresh_rollback_5xx_threshold_pct <= 100 + error_message = "refresh_rollback_5xx_threshold_pct must be between 0 and 100." + } } variable "waf_web_acl_arn" { @@ -317,6 +365,10 @@ variable "rds_performance_insights_retention_days" { description = "Performance Insights data retention. 7 = free tier (default); 731 = long-term retention (paid)." type = number default = 7 + validation { + condition = contains([7, 731], var.rds_performance_insights_retention_days) + error_message = "rds_performance_insights_retention_days must be 7 (free tier) or 731 (long-term paid)." + } } variable "tags" { diff --git a/modules/single-vm/aws/variables.tf b/modules/single-vm/aws/variables.tf index 3360b4d..2f7b42b 100644 --- a/modules/single-vm/aws/variables.tf +++ b/modules/single-vm/aws/variables.tf @@ -54,12 +54,20 @@ variable "root_volume_size_gb" { description = "Root volume size in GB." type = number default = 50 + validation { + condition = var.root_volume_size_gb >= 8 && var.root_volume_size_gb <= 16384 + error_message = "root_volume_size_gb must be between 8 and 16384 GB." + } } variable "data_volume_size_gb" { description = "Data volume size in GB. Attached as /dev/sdh; the marketplace image mounts and formats on first boot." type = number default = 200 + validation { + condition = var.data_volume_size_gb >= 1 && var.data_volume_size_gb <= 16384 + error_message = "data_volume_size_gb must be between 1 and 16384 GB." + } } variable "enable_customer_managed_key" { @@ -116,12 +124,20 @@ variable "backup_object_lock_retention_days" { description = "Object Lock (governance mode) retention period for backup objects." type = number default = 30 + validation { + condition = var.backup_object_lock_retention_days >= 1 + error_message = "backup_object_lock_retention_days must be at least 1." + } } variable "backup_noncurrent_version_expiration_days" { description = "Expire noncurrent versions of backup objects after this many days." type = number default = 365 + validation { + condition = var.backup_noncurrent_version_expiration_days >= 1 + error_message = "backup_noncurrent_version_expiration_days must be at least 1." + } } variable "tags" { diff --git a/modules/unlimited-scale/aws/variables.tf b/modules/unlimited-scale/aws/variables.tf index f31f0a5..1e8459b 100644 --- a/modules/unlimited-scale/aws/variables.tf +++ b/modules/unlimited-scale/aws/variables.tf @@ -64,6 +64,10 @@ variable "redis_engine_version" { variable "redis_snapshot_retention_days" { type = number default = 0 + validation { + condition = var.redis_snapshot_retention_days >= 0 && var.redis_snapshot_retention_days <= 35 + error_message = "redis_snapshot_retention_days must be between 0 and 35 (ElastiCache limit)." + } } variable "redis_endpoint_override" { @@ -75,6 +79,10 @@ variable "redis_endpoint_override" { variable "redis_endpoint_override_port" { type = number default = 6379 + validation { + condition = var.redis_endpoint_override_port >= 1 && var.redis_endpoint_override_port <= 65535 + error_message = "redis_endpoint_override_port must be a valid TCP port (1–65535)." + } } variable "redis_endpoint_override_tls" { @@ -87,16 +95,28 @@ variable "redis_endpoint_override_tls" { variable "asg_min_size" { type = number default = 3 + validation { + condition = var.asg_min_size >= 1 + error_message = "asg_min_size must be at least 1." + } } variable "asg_max_size" { type = number default = 20 + validation { + condition = var.asg_max_size >= 1 + error_message = "asg_max_size must be at least 1." + } } variable "asg_desired_capacity" { type = number default = 3 + validation { + condition = var.asg_desired_capacity >= 1 + error_message = "asg_desired_capacity must be at least 1." + } } variable "instance_type" { @@ -113,11 +133,19 @@ variable "enable_alb_deletion_protection" { variable "target_cpu_utilization" { type = number default = 60 + validation { + condition = var.target_cpu_utilization >= 1 && var.target_cpu_utilization <= 100 + error_message = "target_cpu_utilization must be between 1 and 100." + } } variable "target_request_count_per_target" { type = number default = 500 + validation { + condition = var.target_request_count_per_target >= 1 + error_message = "target_request_count_per_target must be at least 1." + } } # ----- DB sizing ----- @@ -130,11 +158,19 @@ variable "db_instance_class" { variable "db_allocated_storage_gb" { type = number default = 200 + validation { + condition = var.db_allocated_storage_gb >= 20 && var.db_allocated_storage_gb <= 65536 + error_message = "db_allocated_storage_gb must be between 20 and 65536 GB (RDS Postgres limits)." + } } variable "db_max_allocated_storage_gb" { type = number default = 2000 + validation { + condition = var.db_max_allocated_storage_gb >= 21 && var.db_max_allocated_storage_gb <= 65536 + error_message = "db_max_allocated_storage_gb must be between 21 and 65536 GB." + } } variable "db_engine_version" { @@ -146,11 +182,19 @@ variable "db_backup_retention_days" { description = "Days RDS retains automated daily backups. 30 covers a typical monthly review cycle and aligns with the pre-patch on-demand snapshot lifecycle." type = number default = 30 + validation { + condition = var.db_backup_retention_days >= 0 && var.db_backup_retention_days <= 35 + error_message = "db_backup_retention_days must be between 0 and 35 days (RDS limit)." + } } variable "db_read_replica_count" { type = number default = 2 + validation { + condition = var.db_read_replica_count >= 0 && var.db_read_replica_count <= 5 + error_message = "db_read_replica_count must be between 0 and 5." + } } variable "db_deletion_protection" { @@ -188,6 +232,10 @@ variable "enable_flow_logs" { variable "access_log_retention_days" { type = number default = 90 + validation { + condition = var.access_log_retention_days >= 1 + error_message = "access_log_retention_days must be at least 1." + } } variable "marketplace_product_code" { @@ -220,30 +268,50 @@ variable "backup_object_lock_retention_days" { description = "Object Lock (governance mode) retention period for backup objects. 30 days satisfies the procurement-grade safety net while still allowing privileged operator override for compaction." type = number default = 30 + validation { + condition = var.backup_object_lock_retention_days >= 1 + error_message = "backup_object_lock_retention_days must be at least 1." + } } variable "backup_noncurrent_version_expiration_days" { description = "Expire noncurrent versions of backup objects after this many days. 365 retains a year of pre-patch bundles for rollback." type = number default = 365 + validation { + condition = var.backup_noncurrent_version_expiration_days >= 1 + error_message = "backup_noncurrent_version_expiration_days must be at least 1." + } } variable "instance_refresh_min_healthy_percentage" { description = "Minimum percentage of the ASG that must remain healthy during an instance refresh. 50 drains one instance at a time on a 2-instance ASG; tune higher for larger fleets that can tolerate parallel replacement." type = number default = 50 + validation { + condition = var.instance_refresh_min_healthy_percentage >= 0 && var.instance_refresh_min_healthy_percentage <= 100 + error_message = "instance_refresh_min_healthy_percentage must be between 0 and 100." + } } variable "instance_refresh_instance_warmup_seconds" { description = "Seconds the ASG considers a new instance 'warming up' before counting toward healthy_percentage. 120 is enough for the SAT marketplace AMI to pass the ALB /health probe; raise for slower boot images." type = number default = 120 + validation { + condition = var.instance_refresh_instance_warmup_seconds >= 0 + error_message = "instance_refresh_instance_warmup_seconds must be non-negative." + } } variable "refresh_rollback_5xx_threshold_pct" { description = "Target-group 5xx rate (percent) above which the instance refresh auto-rollback alarm fires. Default 1% over 2 evaluation periods of 1 minute." type = number default = 1 + validation { + condition = var.refresh_rollback_5xx_threshold_pct >= 0 && var.refresh_rollback_5xx_threshold_pct <= 100 + error_message = "refresh_rollback_5xx_threshold_pct must be between 0 and 100." + } } variable "waf_web_acl_arn" { @@ -299,6 +367,10 @@ variable "rds_performance_insights_retention_days" { description = "Performance Insights data retention. 7 = free tier (default); 731 = long-term." type = number default = 7 + validation { + condition = contains([7, 731], var.rds_performance_insights_retention_days) + error_message = "rds_performance_insights_retention_days must be 7 (free tier) or 731 (long-term paid)." + } } variable "tags" {