Skip to content

Instantly share code, notes, and snippets.

@nanoDBA
Last active August 20, 2025 22:30
Show Gist options
  • Save nanoDBA/5d64318ac702fb0faf3390bf7bf5b7d4 to your computer and use it in GitHub Desktop.
Save nanoDBA/5d64318ac702fb0faf3390bf7bf5b7d4 to your computer and use it in GitHub Desktop.
Safely modifies EBS volumes with validation and dry-run support
# ------------------------------------------------------------------------------
# File: Set-EC2VolumeAttribute.ps1
# Description: Bulk EBS volume modification script with enterprise-grade safety features
# Purpose: The Swiss Army knife of EBS volume management! This script lets you
# transform multiple AWS EBS volumes in one go with surgical precision:
# - Convert gp2 to gp3 while preserving performance (because storage
# shouldn't be boring) πŸ“ˆ
# - Scale IOPS and throughput without downtime (your users will thank you) ⚑
# - Bulk resize operations with safety guards (no more "oops" moments) πŸ›‘οΈ
# - Intelligent discovery and filtering (find those volumes hiding in the
# shadows) πŸ”
# Built for DevOps teams who value both speed and safety! 🎯
# Created: 2025-04-10
# Modified: 2025-08-20
# Author: @nanoDBA; collaboratively refactored with assistance from multiple AI models
# ------------------------------------------------------------------------------
# AI-Updated: 2025-08-20 16:30:00 | Marker: AIUPD-20250820163000-5365742d454332566f6c756d654174747269627574652e707331 #|
# Summary:
# πŸ”§ The EBS volume modification script that doesn't make you sweat! Wraps AWS's
# Edit-EC2Volume with enterprise-grade validation, progress tracking, and safety
# features that make bulk storage changes feel like a walk in the park.
#
# Perfect for:
# - 🏒 Production migrations (gp2 to gp3, anyone?)
# - πŸ§ͺ Test environment setup (because devs need fast storage too)
# - πŸ”„ Performance tuning (when your apps start complaining about I/O)
# - πŸ“Š Capacity planning (because storage growth is inevitable)
#
# Features that make DBAs smile:
# - πŸ”’ Idempotent operations (safe to re-run, no duplicate work)
# - 🎯 Smart validation (AWS limits enforced before you hit API errors)
# - ⏳ Flexible waiting (immediate return, quick ack, or full completion)
# - πŸ€– CI/CD friendly (structured output for automation pipelines)
# - πŸ“± Progress bars (because watching progress bars is oddly satisfying)
#
# Prerequisites:
# - AWS PowerShell module and credentials (obviously)
# - PowerShell 5.1+ (because we're not savages)
# - Appropriate IAM permissions (EBS modification + EC2 describe)
#
# Pro Tips:
# - πŸ”§ Test with -PlanOnly first (see what would change without touching anything)
# - ⏰ Run during maintenance windows (your users will appreciate the courtesy)
# - πŸ“Š Use -PassThru for structured output (perfect for monitoring dashboards)
# - πŸš€ Start with -WaitForStart for quick feedback (know it's working)
#
#Requires -Version 5.1
#Requires -Modules AWS.Tools.EC2
#region Constants
# πŸ• TIMING CONSTANTS (because AWS doesn't always play nice with our schedules)
# Polling starts responsive and ramps up via 1.5x backoff to reduce API pressure
# (AWS throttling is like rush hour traffic - you need to pace yourself)
$script:DEFAULT_POLL_SECONDS = 2
# Typical AWS control-plane ack is under 5 minutes (most of the time...)
$script:DEFAULT_WAIT_START_SECONDS = 300
# Many in-place mods finish under ~15 minutes (tune per org policy and volume size)
$script:DEFAULT_WAIT_FULL_SECONDS = 900
# Default when not waiting for start/full (quick feedback for impatient DBAs)
$script:DEFAULT_WAIT_DEFAULT_SECONDS = 60
# Cap the adaptive poll interval (because exponential backoff can get ridiculous)
$script:MAX_POLL_INTERVAL_SECONDS = 10
# Exponential backoff cap when throttled (AWS says "slow down" - we listen)
$script:THROTTLE_BACKOFF_CAP_SECONDS = 10
# Random jitter cap (ms) added to backoff (prevents thundering herd problems)
$script:JITTER_MAX_MS = 1000
# πŸ” VALIDATION CONSTANTS (keeping AWS honest, one volume at a time)
# EBS volume Id basic format check (best effort - AWS format is pretty consistent)
$script:VOLID_REGEX = '^vol-[0-9a-fA-F]{8,}$'
# Common Linux root device names (guard against HDD types - because root on HDD is asking for trouble)
$script:ROOT_DEVICE_NAMES = @('/dev/xvda', '/dev/sda1', '/dev/nvme0n1')
# πŸ“Š PERFORMANCE DEFAULTS (AWS documented values, but we let you override them)
# gp3 AWS documented defaults (overrideable via config/SSM; DryRun preflight validates)
# (These are AWS's "sane defaults" - like having a backup plan)
$script:GP3_DEFAULT_IOPS = 3000
$script:GP3_DEFAULT_THROUGHPUT = 125
# 🚦 OPERATIONAL CONSTANTS (the states we're willing to work with)
# Allowed operational states (available = ready to modify, in-use = attached but modifiable)
$script:SUPPORTED_VOLUME_STATES = @('available','in-use')
# πŸ†˜ RECOVERY MESSAGE STRINGS (because even the best scripts need help sometimes)
# Recovery tip strings (these are like having a DBA mentor in your pocket)
$script:MSG_TIMEOUT_RECOVERY = 'increase -WaitMaxSeconds or use -WaitForStart for quicker acknowledgment (AWS can be slow when busy)'
$script:MSG_INFLIGHT_RECOVERY = 'use -Wait or -ForceReplacePending to proceed despite in-flight modification (AWS is already working on it)'
$script:MSG_SHRINK_RECOVERY = 'create a snapshot, restore to a new smaller volume, then reattach (EBS shrink is like trying to un-bake a cake)'
$script:MSG_IOPS_TYPE_RECOVERY = 'set -TargetEbsType gp3|io1|io2 or omit -TargetIops (not all volume types support IOPS tuning)'
$script:MSG_THROTTLE_RECOVERY = 'reduce batch size, lower call rate, or increase -MaxRetries/-BaseBackoffSeconds (AWS is saying "slow down, cowboy")'
$script:MSG_FILTER_RECOVERY = "pass -Filter @{Name='...';Values=@('...')} with trimmed values or add -StrictDiscovery (filters are picky about formatting)"
$script:MSG_ROOT_HDD_RECOVERY = 'confirm intent and pass -AllowRoot to force HDD on root (because root on HDD is like driving a sports car in reverse)'
# 🚨 ERROR CODES FOR CONSISTENT FORMATTING (because chaos is so 2020)
# Error codes for consistent formatting (these make troubleshooting less painful)
$script:ERR = @{
Timeout = 'E_TIMEOUT' # AWS is taking longer than expected (probably busy)
Inflight = 'E_INFLIGHT' # Volume is already being modified (AWS is working on it)
Shrink = 'E_SHRINK' # Trying to shrink a volume (not supported by AWS)
IopsType = 'E_IOPS_TYPE' # IOPS parameter mismatch with volume type
RootHDD = 'E_ROOT_HDD' # Attempting to set root volume to HDD type (dangerous)
Throttle = 'E_THROTTLE' # AWS rate limiting (slow down, you're hitting it too hard)
Args = 'E_ARGS' # Invalid parameter combination or values
State = 'E_STATE' # Volume in unsupported state (like "deleting" or "error")
}
# βš™οΈ OPTIONAL EXTERNAL CONFIGURATION (because hardcoded values are so last decade)
# Optional external configuration (flexibility without code changes)
$script:CONFIG_PATH_ENV = 'EBS_EDIT_CONFIG' # env var -> JSON path with "limits" overrides
$script:SSM_PARAM_NAME = '/ops/ebs-edit/config' # optional SSM Parameter Store JSON (enterprise-grade config management)
#endregion Constants
#region Custom Exception Classes
# 🚨 Custom exception classes for better error handling and debugging
class VolumeNotFoundException : System.Exception {
[string]$VolumeId
VolumeNotFoundException([string]$volumeId) : base("Volume $volumeId not found") {
$this.VolumeId = $volumeId
}
}
class VolumeModificationException : System.Exception {
[string]$VolumeId
[string]$Operation
VolumeModificationException([string]$volumeId, [string]$operation, [string]$message) : base($message) {
$this.VolumeId = $volumeId
$this.Operation = $operation
}
}
class ValidationException : System.Exception {
[string]$ErrorCode
[string]$Recovery
ValidationException([string]$errorCode, [string]$message, [string]$recovery) : base($message) {
$this.ErrorCode = $errorCode
$this.Recovery = $recovery
}
}
#endregion Custom Exception Classes
#region Circuit Breaker Pattern
# 🚦 Circuit breaker pattern to prevent cascading failures
function script:Invoke-WithCircuitBreaker {
param(
[scriptblock]$ScriptBlock,
[int]$MaxFailures = 5,
[int]$ResetTimeoutSeconds = 300,
[string]$OperationName = "Operation"
)
# πŸ”Œ Circuit breaker implementation to prevent cascading failures
# This is like having a fuse that trips when too many failures occur
$circuitKey = "CircuitBreaker_$OperationName"
# Check circuit state
$circuit = $script:CircuitBreakers[$circuitKey]
if (-not $circuit) {
$script:CircuitBreakers[$circuitKey] = @{
State = 'Closed' # Closed = normal operation
FailureCount = 0 # Consecutive failures
LastFailureTime = $null # When last failure occurred
NextAttemptTime = $null # When to allow next attempt
}
$circuit = $script:CircuitBreakers[$circuitKey]
}
# Check if circuit is open (blocking requests)
if ($circuit.State -eq 'Open') {
if ($circuit.NextAttemptTime -and (Get-Date) -lt $circuit.NextAttemptTime) {
throw [ValidationException]::new(
'E_CIRCUIT_OPEN',
"Circuit breaker is open for $OperationName. Too many consecutive failures.",
"Wait for automatic reset or check underlying issues. Reset in $([Math]::Ceiling(($circuit.NextAttemptTime - (Get-Date)).TotalSeconds)) seconds."
)
} else {
# Time to try again - move to half-open state
$circuit.State = 'HalfOpen'
$circuit.FailureCount = 0
}
}
try {
# Execute the operation
$result = & $ScriptBlock
# Success - reset circuit
if ($circuit.State -eq 'HalfOpen') {
$circuit.State = 'Closed'
}
$circuit.FailureCount = 0
$circuit.LastFailureTime = $null
return $result
}
catch {
# Failure - update circuit state
$circuit.FailureCount++
$circuit.LastFailureTime = Get-Date
if ($circuit.FailureCount -ge $MaxFailures) {
$circuit.State = 'Open'
$circuit.NextAttemptTime = (Get-Date).AddSeconds($ResetTimeoutSeconds)
Write-Warning "Circuit breaker opened for $OperationName after $MaxFailures consecutive failures. Will reset in $ResetTimeoutSeconds seconds."
}
throw
}
}
# Initialize circuit breakers collection
if (-not $script:CircuitBreakers) {
$script:CircuitBreakers = @{}
}
#endregion Circuit Breaker Pattern
#region Initializers & Utilities πŸ› οΈ (The engine room of our EBS modification machine)
function script:Write-Header {
param([switch]$PlainHelp, [switch]$ShowBanner)
if (-not $ShowBanner) { return }
if ($PlainHelp) {
Write-Host @"
# ------------------------------------------------------------------------------
# File: Set-EC2VolumeAttribute.ps1
# Description: Bulk EBS volume modification script with enterprise-grade safety features
# Purpose: The Swiss Army knife of EBS volume management! This script lets you
# transform multiple AWS EBS volumes in one go with surgical precision:
# - Convert gp2 to gp3 while preserving performance
# - Scale IOPS and throughput without downtime
# - Bulk resize operations with safety guards
# - Intelligent discovery and filtering
# Built for DevOps teams who value both speed and safety!
# Created: 2025-04-10
# Modified: 2025-08-20
# Author: @nanoDBA; collaboratively refactored with assistance from multiple AI models
# ------------------------------------------------------------------------------
# AI-Updated: 2025-08-20 16:30:00 | Marker: AIUPD-20250820163000-5365742d454332566f6c756d654174747269627574652e707331 #|
# Summary:
# The EBS volume modification script that doesn't make you sweat! Wraps AWS's
# Edit-EC2Volume with enterprise-grade validation, progress tracking, and safety
# features that make bulk storage changes feel like a walk in the park.
#
# Perfect for:
# - Production migrations (gp2->gp3, anyone?)
# - Test environment setup (because devs need fast storage too)
# - Performance tuning (when your apps start complaining about I/O)
# - Capacity planning (because storage growth is inevitable)
#
# Features that make DBAs smile:
# - Idempotent operations (safe to re-run, no duplicate work)
# - Smart validation (AWS limits enforced before you hit API errors)
# - Flexible waiting (immediate return, quick ack, or full completion)
# - CI/CD friendly (structured output for automation pipelines)
# - Progress bars (because watching progress bars is oddly satisfying)
#
# Prerequisites:
# - AWS PowerShell module and credentials (obviously)
# - PowerShell 5.1+ (because we're not savages)
# - Appropriate IAM permissions (EBS modification + EC2 describe)
#
# Pro Tips:
# - Test with -PlanOnly first (see what would change without touching anything)
# - Run during maintenance windows (your users will appreciate the courtesy)
# - Use -PassThru for structured output (perfect for monitoring dashboards)
# - Start with -WaitForStart for quick feedback (know it's working)
# ------------------------------------------------------------------------------
"@
} else {
Write-Host @"
# ------------------------------------------------------------------------------
# File: Set-EC2VolumeAttribute.ps1
# Description: Bulk EBS volume modification script with enterprise-grade safety features
# Purpose: The Swiss Army knife of EBS volume management! This script lets you
# transform multiple AWS EBS volumes in one go with surgical precision:
# - Convert gp2 to gp3 while preserving performance (because storage
# shouldn't be boring) πŸ“ˆ
# - Scale IOPS and throughput without downtime (your users will thank you) ⚑
# - Bulk resize operations with safety guards (no more "oops" moments) πŸ›‘οΈ
# - Intelligent discovery and filtering (find those volumes hiding in the
# shadows) πŸ”
# Built for DevOps teams who value both speed and safety! 🎯
# Created: 2025-04-10
# Modified: 2025-08-20
# Author: @nanoDBA; collaboratively refactored with assistance from multiple AI models
# ------------------------------------------------------------------------------
# AI-Updated: 2025-08-20 16:30:00 | Marker: AIUPD-20250820163000-5365742d454332566f6c756d654174747269627574652e707331 #|
# Summary:
# πŸ”§ The EBS volume modification script that doesn't make you sweat! Wraps AWS's
# Edit-EC2Volume with enterprise-grade validation, progress tracking, and safety
# features that make bulk storage changes feel like a walk in the park.
#
# Perfect for:
# - 🏒 Production migrations (gp2 to gp3, anyone?)
# - πŸ§ͺ Test environment setup (because devs need fast storage too)
# - πŸ”„ Performance tuning (when your apps start complaining about I/O)
# - πŸ“Š Capacity planning (because storage growth is inevitable)
#
# Features that make DBAs smile:
# - πŸ”’ Idempotent operations (safe to re-run, no duplicate work)
# - 🎯 Smart validation (AWS limits enforced before you hit API errors)
# - ⏳ Flexible waiting (immediate return, quick ack, or full completion)
# - πŸ€– CI/CD friendly (structured output for automation pipelines)
# - πŸ“± Progress bars (because watching progress bars is oddly satisfying)
#
# Prerequisites:
# - AWS PowerShell module and credentials (obviously)
# - PowerShell 5.1+ (because we're not savages)
# - Appropriate IAM permissions (EBS modification + EC2 describe)
#
# Pro Tips:
# - πŸ”§ Test with -PlanOnly first (see what would change without touching anything)
# - ⏰ Run during maintenance windows (your users will appreciate the courtesy)
# - πŸ“Š Use -PassThru for structured output (perfect for monitoring dashboards)
# - πŸš€ Start with -WaitForStart for quick feedback (know it's working)
# ------------------------------------------------------------------------------
"@
}
}
function script:Write-Constants {
param([switch]$PlainHelp, [switch]$ShowBanner)
if (-not $ShowBanner) { return }
if ($PlainHelp) {
Write-Host @"
#region Constants
# TIMING CONSTANTS
# Polling starts responsive and ramps up via 1.5x backoff to reduce API pressure
$script:DEFAULT_POLL_SECONDS = 2
# Typical AWS control-plane ack is under 5 minutes (most of the time...)
$script:DEFAULT_WAIT_START_SECONDS = 300
# Many in-place mods finish under ~15 minutes (tune per org policy and volume size)
$script:DEFAULT_WAIT_FULL_SECONDS = 900
# Default when not waiting for start/full (quick feedback for impatient DBAs)
$script:DEFAULT_WAIT_DEFAULT_SECONDS = 60
# Cap the adaptive poll interval (because exponential backoff can get ridiculous)
$script:MAX_POLL_INTERVAL_SECONDS = 10
# Exponential backoff cap when throttled (AWS says "slow down" - we listen)
$script:THROTTLE_BACKOFF_CAP_SECONDS = 10
# Random jitter cap (ms) added to backoff (prevents thundering herd problems)
$script:JITTER_MAX_MS = 1000
# VALIDATION CONSTANTS
# EBS volume Id basic format check (best effort - AWS format is pretty consistent)
$script:VOLID_REGEX = '^vol-[0-9a-fA-F]{8,}$'
# Common Linux root device names (guard against HDD types - because root on HDD is asking for trouble)
$script:ROOT_DEVICE_NAMES = @('/dev/xvda', '/dev/sda1', '/dev/nvme0n1')
# PERFORMANCE DEFAULTS
# gp3 AWS documented defaults (overrideable via config/SSM; DryRun preflight validates)
$script:GP3_DEFAULT_IOPS = 3000
$script:GP3_DEFAULT_THROUGHPUT = 125
# OPERATIONAL CONSTANTS
# Allowed operational states (available = ready to modify, in-use = attached but modifiable)
$script:SUPPORTED_VOLUME_STATES = @('available','in-use')
# RECOVERY MESSAGE STRINGS
# Recovery tip strings (these are like having a DBA mentor in your pocket)
$script:MSG_TIMEOUT_RECOVERY = 'increase -WaitMaxSeconds or use -WaitForStart for quicker acknowledgment (AWS can be slow when busy)'
$script:MSG_INFLIGHT_RECOVERY = 'use -Wait or -ForceReplacePending to proceed despite in-flight modification (AWS is already working on it)'
$script:MSG_SHRINK_RECOVERY = 'create a snapshot, restore to a new smaller volume, then reattach (EBS shrink is not supported by AWS)'
$script:MSG_IOPS_TYPE_RECOVERY = 'set -TargetEbsType gp3|io1|io2 or omit -TargetIops (not all volume types support IOPS tuning)'
$script:MSG_THROTTLE_RECOVERY = 'reduce batch size, lower call rate, or increase -MaxRetries/-BaseBackoffSeconds (AWS is saying "slow down, cowboy")'
$script:MSG_FILTER_RECOVERY = "pass -Filter @{Name='...';Values=@('...')} with trimmed values or add -StrictDiscovery (filters are picky about formatting)"
$script:MSG_ROOT_HDD_RECOVERY = 'confirm intent and pass -AllowRoot to force HDD on root (because root on HDD is like driving a sports car in reverse)'
# ERROR CODES FOR CONSISTENT FORMATTING
# Error codes for consistent formatting (these make troubleshooting less painful)
$script:ERR = @{
Timeout = 'E_TIMEOUT' # AWS is taking longer than expected (probably busy)
Inflight = 'E_INFLIGHT' # Volume is already being modified (AWS is working on it)
Shrink = 'E_SHRINK' # Trying to shrink a volume (not supported by AWS)
IopsType = 'E_IOPS_TYPE' # IOPS parameter mismatch with volume type
RootHDD = 'E_ROOT_HDD' # Attempting to set root volume to HDD type (dangerous)
Throttle = 'E_THROTTLE' # AWS rate limiting (slow down, you're hitting it too hard)
Args = 'E_ARGS' # Invalid parameter combination or values
State = 'E_STATE' # Volume in unsupported state (like "deleting" or "error")
}
# OPTIONAL EXTERNAL CONFIGURATION
# Optional external configuration (flexibility without code changes)
$script:CONFIG_PATH_ENV = 'EBS_EDIT_CONFIG' # env var -> JSON path with "limits" overrides
$script:SSM_PARAM_NAME = '/ops/ebs-edit/config' # optional SSM Parameter Store JSON (enterprise-grade config management)
#endregion
"@
} else {
Write-Host @"
#region Constants
# πŸ• TIMING CONSTANTS (because AWS doesn't always play nice with our schedules)
# Polling starts responsive and ramps up via 1.5x backoff to reduce API pressure
# (AWS throttling is like rush hour traffic - you need to pace yourself)
$script:DEFAULT_POLL_SECONDS = 2
# Typical AWS control-plane ack is under 5 minutes (most of the time...)
$script:DEFAULT_WAIT_START_SECONDS = 300
# Many in-place mods finish under ~15 minutes (tune per org policy and volume size)
$script:DEFAULT_WAIT_FULL_SECONDS = 900
# Default when not waiting for start/full (quick feedback for impatient DBAs)
$script:DEFAULT_WAIT_DEFAULT_SECONDS = 60
# Cap the adaptive poll interval (because exponential backoff can get ridiculous)
$script:MAX_POLL_INTERVAL_SECONDS = 10
# Exponential backoff cap when throttled (AWS says "slow down" - we listen)
$script:THROTTLE_BACKOFF_CAP_SECONDS = 10
# Random jitter cap (ms) added to backoff (prevents thundering herd problems)
$script:JITTER_MAX_MS = 1000
# πŸ” VALIDATION CONSTANTS (keeping AWS honest, one volume at a time)
# EBS volume Id basic format check (best effort - AWS format is pretty consistent)
$script:VOLID_REGEX = '^vol-[0-9a-fA-F]{8,}$'
# Common Linux root device names (guard against HDD types - because root on HDD is asking for trouble)
$script:ROOT_DEVICE_NAMES = @('/dev/xvda', '/dev/sda1', '/dev/nvme0n1')
# πŸ“Š PERFORMANCE DEFAULTS (AWS documented values, but we let you override them)
# gp3 AWS documented defaults (overrideable via config/SSM; DryRun preflight validates)
# (These are AWS's "sane defaults" - like having a backup plan)
$script:GP3_DEFAULT_IOPS = 3000
$script:GP3_DEFAULT_THROUGHPUT = 125
# 🚦 OPERATIONAL CONSTANTS (the states we're willing to work with)
# Allowed operational states (available = ready to modify, in-use = attached but modifiable)
$script:SUPPORTED_VOLUME_STATES = @('available','in-use')
# πŸ†˜ RECOVERY MESSAGE STRINGS (because even the best scripts need help sometimes)
# Recovery tip strings (these are like having a DBA mentor in your pocket)
$script:MSG_TIMEOUT_RECOVERY = 'increase -WaitMaxSeconds or use -WaitForStart for quicker acknowledgment (AWS can be slow when busy)'
$script:MSG_INFLIGHT_RECOVERY = 'use -Wait or -ForceReplacePending to proceed despite in-flight modification (AWS is already working on it)'
$script:MSG_SHRINK_RECOVERY = 'create a snapshot, restore to a new smaller volume, then reattach (EBS shrink is like trying to un-bake a cake)'
$script:MSG_IOPS_TYPE_RECOVERY = 'set -TargetEbsType gp3|io1|io2 or omit -TargetIops (not all volume types support IOPS tuning)'
$script:MSG_THROTTLE_RECOVERY = 'reduce batch size, lower call rate, or increase -MaxRetries/-BaseBackoffSeconds (AWS is saying "slow down, cowboy")'
$script:MSG_FILTER_RECOVERY = "pass -Filter @{Name='...';Values=@('...')} with trimmed values or add -StrictDiscovery (filters are picky about formatting)"
$script:MSG_ROOT_HDD_RECOVERY = 'confirm intent and pass -AllowRoot to force HDD on root (because root on HDD is like driving a sports car in reverse)'
# 🚨 ERROR CODES FOR CONSISTENT FORMATTING (because chaos is so 2020)
# Error codes for consistent formatting (these make troubleshooting less painful)
$script:ERR = @{
Timeout = 'E_TIMEOUT' # AWS is taking longer than expected (probably busy)
Inflight = 'E_INFLIGHT' # Volume is already being modified (AWS is working on it)
Shrink = 'E_SHRINK' # Trying to shrink a volume (not supported by AWS)
IopsType = 'E_IOPS_TYPE' # IOPS parameter mismatch with volume type
RootHDD = 'E_ROOT_HDD' # Attempting to set root volume to HDD type (dangerous)
Throttle = 'E_THROTTLE' # AWS rate limiting (slow down, you're hitting it too hard)
Args = 'E_ARGS' # Invalid parameter combination or values
State = 'E_STATE' # Volume in unsupported state (like "deleting" or "error")
}
# βš™οΈ OPTIONAL EXTERNAL CONFIGURATION (because hardcoded values are so last decade)
# Optional external configuration (flexibility without code changes)
$script:CONFIG_PATH_ENV = 'EBS_EDIT_CONFIG' # env var -> JSON path with "limits" overrides
$script:SSM_PARAM_NAME = '/ops/ebs-edit/config' # optional SSM Parameter Store JSON (enterprise-grade config management)
#endregion
"@
}
}
function script:Format-Error([string]$Code,[string]$Msg,[string]$Recovery){
# 🚨 Formats error messages with consistent structure and recovery hints
# This makes troubleshooting less painful and gives users a path forward
if ($Recovery) { "[{0}] {1}. Recovery: {2}" -f $Code, $Msg, $Recovery }
else { "[{0}] {1}" -f $Code, $Msg }
}
function script:Initialize-ColorSupport {
param([switch]$Quiet)
# 🎨 Smart color detection for beautiful console output (when possible)
# Detects if we can use colors and gracefully falls back to plain text
# This makes output more readable in interactive sessions while staying pipeline-friendly
$UseColor = $false
try {
$UseColor = (-not $Quiet) -and ($Host.Name -eq 'ConsoleHost') -and ($PSStyle.OutputRendering -ne 'PlainText') -and (-not [Console]::IsOutputRedirected)
if ($env:NO_COLOR) { $UseColor = $false }
} catch { $UseColor = $false }
$fg = @{}
try {
$fg = @{
Red = $PSStyle.Foreground.Red
Green = $PSStyle.Foreground.Green
Yellow = $PSStyle.Foreground.Yellow
Cyan = $PSStyle.Foreground.Cyan
Gray = $PSStyle.Foreground.BrightBlack
}
} catch {}
$Colorize = {
param([string]$Text,[string]$Color)
if ($UseColor -and $fg.ContainsKey($Color)) { "${($fg[$Color])}$Text$($PSStyle.Reset)" } else { $Text }
}
return @{
UseColor = $UseColor
Colorize = $Colorize
}
}
function script:Initialize-ValidationLimits {
# πŸ“Š AWS EBS volume performance limits (the rules AWS enforces)
# These are AWS's hard limits - we validate against them before hitting API errors
# (Because getting rejected by AWS is like being turned away at a restaurant)
@{
gp3 = @{ IopsMin=3000; IopsMax=16000; TpMin=125; TpMax=1000 } # General purpose SSD v3 (the new hotness)
io1 = @{ IopsMin=100; IopsMaxPerGiB=50; IopsCap=64000 } # Provisioned IOPS SSD v1 (legacy performance)
io2 = @{ IopsMin=100; IopsMaxPerGiB=500; IopsCap=256000 } # Provisioned IOPS SSD v2 (performance beast)
}
}
function script:Load-LimitsFromJson {
param([string]$Path)
try {
if (-not $Path -or -not (Test-Path $Path)) { return $null }
return Get-Content -Raw -Path $Path | ConvertFrom-Json -ErrorAction Stop
} catch {
Write-Verbose "Config load failed: $($_.Exception.Message)"
return $null
}
}
function script:Try-LoadLimitsOverrides {
param([hashtable]$Base)
$cfg = $null
# ENV path -> JSON
try {
$envPath = (Get-Item env:$script:CONFIG_PATH_ENV -ErrorAction SilentlyContinue).Value
if ($envPath) { $cfg = script:Load-LimitsFromJson -Path $envPath }
} catch { Write-Verbose "Env read failed: $($_.Exception.Message)" }
# SSM param optional
if (-not $cfg) {
try {
if (Get-Command Get-SSMParameter -ErrorAction SilentlyContinue) {
$p = Get-SSMParameter -Name $script:SSM_PARAM_NAME -WithDecryption:$true -ErrorAction SilentlyContinue -Select Parameter.Value
if ($p) { $cfg = ($p | ConvertFrom-Json) }
} else {
Write-Verbose "AWS.Tools.SimpleSystemsManagement not available - SSM parameter overrides skipped"
}
} catch { Write-Verbose "SSM fetch failed: $($_.Exception.Message)" }
}
if ($cfg -and $cfg.limits) {
foreach ($k in $cfg.limits.PSObject.Properties.Name) {
if ($Base.ContainsKey($k)) {
foreach ($sub in $cfg.limits.$k.PSObject.Properties.Name) {
$Base[$k][$sub] = $cfg.limits.$k.$sub
}
}
}
}
return $Base
}
function script:Initialize-AWSConfig {
param([int]$MaxRetries,[int]$ReadWriteTimeoutSeconds)
try {
$cfg = New-Object Amazon.EC2.AmazonEC2Config
$cfg.MaxErrorRetry = $MaxRetries
$cfg.ReadWriteTimeout = [TimeSpan]::FromSeconds($ReadWriteTimeoutSeconds)
return $cfg
} catch {
Write-Verbose ("[{0}] {1}" -f $MyInvocation.MyCommand.Name, $_.Exception.Message)
return $null
}
}
function script:Test-ParameterCompatibility {
param([string]$TargetEbsType,[switch]$HasTargetIops,[switch]$HasTargetThroughput,[switch]$IsPlan)
# πŸ” Validates parameter combinations before we hit AWS (prevention is better than cure)
# This catches common mistakes like trying to set IOPS on gp2 volumes or throughput on io1
# io1/io2 require IOPS (unless we're just planning - then we're more lenient)
if ($TargetEbsType -in @('io1','io2') -and -not $HasTargetIops -and -not $IsPlan) {
throw (script:Format-Error $script:ERR.IopsType "-TargetIops is required when -TargetEbsType is $TargetEbsType" $script:MSG_IOPS_TYPE_RECOVERY)
}
# Throughput is gp3-only (gp2 doesn't support it, and io1/io2 have their own IOPS-based performance)
if ($HasTargetThroughput -and $TargetEbsType -and $TargetEbsType -ne 'gp3') {
throw (script:Format-Error $script:ERR.IopsType "-TargetThroughput is only valid with -TargetEbsType gp3" "set -TargetEbsType gp3 or remove -TargetThroughput")
}
# IOPS not applicable to gp2/sc1/st1 (these have fixed performance characteristics)
if ($HasTargetIops -and $TargetEbsType -and $TargetEbsType -in @('gp2','sc1','st1')) {
throw (script:Format-Error $script:ERR.IopsType "-TargetIops is not applicable to $TargetEbsType volumes" $script:MSG_IOPS_TYPE_RECOVERY)
}
}
function script:Assert-IopsAllowedOnCurrentType {
param([string]$VolumeType)
# 🚫 Prevents setting IOPS on volume types that don't support it
# This is like trying to put premium gas in a diesel engine - it just won't work
if ($VolumeType -notin @('gp3','io1','io2')) {
throw (script:Format-Error $script:ERR.IopsType "-TargetIops is only valid for gp3/io1/io2. Current type is '$VolumeType'" $script:MSG_IOPS_TYPE_RECOVERY)
}
}
function script:Assert-Params {
param(
[hashtable]$Limits,[string]$TargetType,[int]$TargetSize,
[switch]$HasIops,[int]$Iops,[switch]$HasTp,[int]$TpMiB
)
# πŸ“ Enforces AWS performance limits before we hit the API (because AWS errors are less helpful)
# This validates that your IOPS/throughput values are within AWS's documented ranges
switch ($TargetType) {
'gp3' {
# gp3: 3000-16000 IOPS, 125-1000 MiB/s throughput
if ($HasIops -and ($Iops -lt $Limits.gp3.IopsMin -or $Iops -gt $Limits.gp3.IopsMax)) {
throw (script:Format-Error $script:ERR.Args "gp3 IOPS must be $($Limits.gp3.IopsMin)–$($Limits.gp3.IopsMax)" "choose within the supported range")
}
if ($HasTp -and ($TpMiB -lt $Limits.gp3.TpMin -or $TpMiB -gt $Limits.gp3.TpMax)) {
throw (script:Format-Error $script:ERR.Args "gp3 throughput must be $($Limits.gp3.TpMin)–$($Limits.gp3.TpMax) MiB/s" "choose within the supported range")
}
}
'io1' {
# io1: 100 IOPS minimum, up to 50 IOPS per GiB (capped at 64,000)
$max = [Math]::Min($Limits.io1.IopsCap, $Limits.io1.IopsMaxPerGiB * $TargetSize)
if ($HasIops -and ($Iops -lt $Limits.io1.IopsMin -or $Iops -gt $max)) {
throw (script:Format-Error $script:ERR.Args "io1 IOPS must be $($Limits.io1.IopsMin)–$max for size $TargetSize GiB" "reduce IOPS or increase size")
}
}
'io2' {
# io2: 100 IOPS minimum, up to 500 IOPS per GiB (capped at 256,000)
$max = [Math]::Min($Limits.io2.IopsCap, $Limits.io2.IopsMaxPerGiB * $TargetSize)
if ($HasIops -and ($Iops -lt $Limits.io2.IopsMin -or $Iops -gt $max)) {
throw (script:Format-Error $script:ERR.Args "io2 IOPS must be $($Limits.io2.IopsMin)–$max for size $TargetSize GiB" "reduce IOPS or increase size")
}
}
default {
# Other types (gp2, sc1, st1) don't support IOPS or throughput tuning
if ($HasIops) { throw (script:Format-Error $script:ERR.IopsType "IOPS is not applicable to $TargetType" $script:MSG_IOPS_TYPE_RECOVERY) }
if ($HasTp) { throw (script:Format-Error $script:ERR.Args "Throughput is only valid for gp3" "remove -TargetThroughput or change -TargetEbsType gp3") }
}
}
}
function script:Invoke-Edit {
param(
[hashtable]$ModifySplat,[int]$MaxRetries,[double]$BaseBackoffSeconds,[switch]$DryRun,[ref]$ThrottleCount
)
# πŸ”„ Wraps Edit-EC2Volume with intelligent retry logic and throttling handling
# This is like having a patient friend who knows when to slow down and when to give up
$attempt = 0
while ($true) {
try {
return Edit-EC2Volume @ModifySplat -ErrorAction Stop
} catch {
$msg = $_.Exception.Message
$code = $null; try { $code = $_.Exception.ErrorCode } catch {}
# DryRun operations should succeed with DryRunOperation errors (that's expected)
if ($DryRun -and ($code -eq 'DryRunOperation' -or $msg -match 'DryRunOperation')) { return $null }
$attempt++
if ($msg -notmatch 'Throttl|Rate' -or $attempt -ge $MaxRetries) {
if ($msg -match 'Throttl|Rate') {
$msg = (script:Format-Error $script:ERR.Throttle $msg $script:MSG_THROTTLE_RECOVERY)
}
throw $msg
}
# AWS is saying "slow down" - let's be polite and back off
$ThrottleCount.Value++
$sleepTime = [Math]::Min($script:THROTTLE_BACKOFF_CAP_SECONDS, $BaseBackoffSeconds * [Math]::Pow(2,$attempt)) + (Get-Random -Min 0 -Max $script:JITTER_MAX_MS)/1000.0
Start-Sleep -Seconds $sleepTime
}
}
}
function script:Wait-Modification {
param(
[string]$VolumeId,[string]$Region,[string]$ProfileName,
[int]$MaxSeconds,[int]$PollSeconds,[switch]$ForStart,
[int]$ProgressId = -1,[int]$ParentProgressId = -1,[string]$ProgressActivity = '',[switch]$NoProgress
)
# ⏳ Waits for AWS to complete volume modifications with intelligent polling
# This is like watching paint dry, but with progress bars and AWS status updates
$deadline = (Get-Date).AddSeconds($MaxSeconds)
$poll = $PollSeconds
$start = Get-Date
$completed = $false
try {
do {
# Check current modification status from AWS
$args = @{ VolumeId = $VolumeId; ErrorAction = 'SilentlyContinue' }
if ($Region) { $args.Region = $Region }
if ($ProfileName) { $args.ProfileName = $ProfileName }
$vm = Get-EC2VolumeModification @args | Sort-Object StartTime -Descending | Select-Object -First 1
$state = if ($vm) { $vm.ModificationState } else { $null }
# Update progress bar (because watching progress bars is oddly satisfying)
$elapsed = ((Get-Date) - $start).TotalSeconds
$pct = [int][Math]::Min(99, [Math]::Max(1, ($elapsed / [Math]::Max(1,$MaxSeconds)) * 100))
if ($ProgressId -ge 0 -and -not $NoProgress) {
$status = if ($state) { "state=$state" } else { 'waiting for AWS status' }
Write-Progress -Id $ProgressId -ParentId $ParentProgressId -Activity $ProgressActivity -Status $status -PercentComplete $pct
}
# Check completion conditions
if ($ForStart) {
# Just waiting for AWS to acknowledge the request
if ($state -and $state -ne 'failed') { $completed = $true; return $state }
} else {
# Waiting for full completion (optimizing, completed, or failed)
if ($state -in 'optimizing','completed','failed') { $completed = $true; return $state }
}
# Adaptive polling: start responsive, then slow down to be nice to AWS
Start-Sleep -Seconds $poll
$poll = [Math]::Min($script:MAX_POLL_INTERVAL_SECONDS, [int][Math]::Ceiling($poll * 1.5))
} while ((Get-Date) -lt $deadline)
return $null # Timed out
}
finally {
# Always clean up progress bars
if ($ProgressId -ge 0 -and -not $completed -and -not $NoProgress) { Write-Progress -Id $ProgressId -ParentId $ParentProgressId -Completed -Activity $ProgressActivity }
}
}
function script:Get-Gp2BaselineIops { param([int]$SizeGiB) [Math]::Min(16000, [Math]::Max(3000, 3 * $SizeGiB)) } # πŸš€ Heuristic: Floors at 3k IOPS (gp2 burst ceiling) to avoid perceived performance regressions after gp3 conversion
function script:Get-Gp2BaselineTpMiB { param([int]$SizeGiB) if ($SizeGiB -ge 334) { 250 } else { 128 } }
function script:Diff-Changes {
param($Vol, [hashtable]$Desired)
$actual = @{}
foreach ($k in $Desired.Keys) {
if ($k -eq 'Throughput') {
$effTargetType = if ($Desired.ContainsKey('VolumeType')) { $Desired['VolumeType'] } else { $Vol.VolumeType }
if ($effTargetType -ne 'gp3') { continue } # ignore TP for non-gp3 targets
}
$curr = switch ($k) {
'VolumeType' { $Vol.VolumeType }
'Size' { $Vol.Size }
'Iops' { $Vol.Iops }
'Throughput' { $Vol.Throughput }
}
if ($Desired[$k] -ne $curr) { $actual[$k] = @{ Current=$curr; Target=$Desired[$k] } }
}
$actual
}
function script:New-Result {
param([string]$VolumeId,[string]$Region,[hashtable]$Changes,[string]$Status,[string]$RequestId,[string]$Error,$Vol,[hashtable]$Target,[int]$EstimatedWaitHintSeconds)
[pscustomobject]@{
VolumeId = $VolumeId
Region = $Region
Current = @{ Type=$Vol.VolumeType; SizeGiB=$Vol.Size; Iops=$Vol.Iops; ThroughputMiB=$Vol.Throughput }
Target = @{ Type=$Target.Type; SizeGiB=$Target.Size; Iops=$Target.Iops; ThroughputMiB=$Target.ThroughputMiB }
Changes = $Changes
Status = $Status
RequestId = $RequestId
Error = $Error
EstimatedWaitHintSeconds = $EstimatedWaitHintSeconds
Effective = @{ Iops=$Target.Iops; ThroughputMiB=$Target.ThroughputMiB }
}
}
function script:Get-VolumesByIdChunked {
param([string[]]$Ids,[hashtable]$CommonAwsArgs,[int]$ChunkSize=200)
$all = @()
for ($i=0; $i -lt $Ids.Count; $i+=$ChunkSize) {
$end = [Math]::Min($i+$ChunkSize-1, $Ids.Count-1)
$chunk = $Ids[$i..$end]
$all += Get-EC2Volume -VolumeId $chunk @CommonAwsArgs -ErrorAction Stop
}
return $all
}
function script:Discover-Volumes {
param([string[]]$VolumeIds,[hashtable[]]$Filter,[string]$AZ,[hashtable]$CommonAwsArgs,[switch]$StrictDiscovery)
if ($VolumeIds -and $VolumeIds.Count) {
$bad = $VolumeIds | Where-Object { $_ -notmatch $script:VOLID_REGEX }
if ($bad) { throw (script:Format-Error $script:ERR.Args "Invalid volume ID format: $($bad -join ', ')" "ensure vol-xxxxxxxx or longer format") }
$all = script:Get-VolumesByIdChunked -Ids $VolumeIds -CommonAwsArgs $CommonAwsArgs
$byId = @{}; foreach ($v in $all) { $byId[$v.VolumeId] = $v }
$found = $all.VolumeId
$missing = Compare-Object -ReferenceObject $VolumeIds -DifferenceObject $found -PassThru
if ($missing) {
throw (script:Format-Error $script:ERR.Args ("Volume not found{0}: {1}" -f $(if($CommonAwsArgs.Region){" in $($CommonAwsArgs.Region)"}), ($missing -join ', ')) $script:MSG_FILTER_RECOVERY)
}
return @{ List=$VolumeIds; Map=$byId; UsedDiscovery=$false }
}
$filters = @()
if ($AZ) { $filters += @{ Name='availability-zone'; Values=$AZ } }
if ($Filter) {
foreach ($f in $Filter) {
if (-not ($f.ContainsKey('Name') -and $f.ContainsKey('Values'))) {
throw (script:Format-Error $script:ERR.Args "Invalid -Filter entry. Expected keys: Name, Values" $script:MSG_FILTER_RECOVERY)
}
if ($f.Values -is [string]) { $f.Values = @($f.Values.Trim()) } else { $f.Values = @($f.Values | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ -ne '' }) }
if (-not $f.Values.Count) { continue }
$filters += $f
}
}
if (-not $filters.Count) {
$msg = "No discovery filters specified. $script:MSG_FILTER_RECOVERY"
if ($StrictDiscovery) { throw (script:Format-Error $script:ERR.Args $msg $null) } else { Write-Warning $msg; return @{ List=@(); Map=@{}; UsedDiscovery=$true } }
}
$all = Get-EC2Volume -Filter $filters @CommonAwsArgs -ErrorAction Stop
$ids = ($all | ForEach-Object { $_.VolumeId }) | Sort-Object -Unique
if (-not $ids -or -not $ids.Count) {
$msg = "No volumes found by discovery filters. $script:MSG_FILTER_RECOVERY"
if ($StrictDiscovery) { throw (script:Format-Error $script:ERR.Args $msg $null) } else { Write-Warning $msg; return @{ List=@(); Map=@{}; UsedDiscovery=$true } }
}
$map = @{}; foreach ($v in $all) { $map[$v.VolumeId] = $v }
return @{ List=$ids; Map=$map; UsedDiscovery=$true }
}
function script:Test-IsRootDevice { param($Volume)
$attach = $Volume.Attachments | Select-Object -First 1
if (-not ($attach -and $attach.Device)) { return $false }
return $attach.Device -in $script:ROOT_DEVICE_NAMES
}
function script:Test-IgnoreTag {
param($Volume, [string]$VolId, [switch]$Quiet, [string]$TagIgnoreKey = 'Ignore-EBSAutomation')
# 🏷️ Check if volume should be ignored based on automation tag
try {
$ignoreTag = ($Volume.Tags | Where-Object { $_.Key -eq $TagIgnoreKey -and ($_.Value -match '^(?i:true|yes|1)$') })
if ($ignoreTag) {
if (-not $Quiet) { Write-Verbose ">> $VolId - Ignored by tag $TagIgnoreKey=true" }
return $true
}
return $false
} catch {
Write-Verbose "Tag read failed: $($_.Exception.Message)"
return $false
}
}
function script:Validate-VolumeParameters {
param(
$Vol, [string]$VolId, [hashtable]$Desired, [hashtable]$DesiredBase,
[hashtable]$Limits, [switch]$AllowRoot, [switch]$Quiet, [switch]$Force
)
# πŸ” Comprehensive validation of volume parameters before modification
# Check for volume shrinking (not supported by AWS)
if ($Desired.ContainsKey('Size') -and ($Desired['Size'] -lt $Vol.Size)) {
throw (script:Format-Error $script:ERR.Shrink ("EBS volume shrink not supported: Current={0} GiB, Target={1} GiB" -f $Vol.Size, $Desired['Size']) $script:MSG_SHRINK_RECOVERY)
}
# Warn about gp3 defaults if not specified
if ($Desired.ContainsKey('VolumeType') -and $Desired['VolumeType'] -eq 'gp3' -and -not ($Desired.ContainsKey('Iops') -or $Desired.ContainsKey('Throughput'))) {
# Only warn if current gp2 performance would be perceived as degraded
$currentType = $Vol.VolumeType
$currentSize = $Vol.Size
$shouldWarn = $true
if ($currentType -eq 'gp2') {
$gp2BaselineIops = script:Get-Gp2BaselineIops -SizeGiB $currentSize
$gp2BaselineTp = script:Get-Gp2BaselineTpMiB -SizeGiB $currentSize
# If current gp2 baseline is already at or below gp3 default, no performance degradation
if ($gp2BaselineIops -le $script:GP3_DEFAULT_IOPS -and $gp2BaselineTp -le $script:GP3_DEFAULT_THROUGHPUT) {
$shouldWarn = $false
}
}
# For other current volume types, always warn if no explicit IOPS/TP for gp3
if ($shouldWarn -and -not $Quiet -and -not $Force) { Write-Warning "Changing to gp3 without explicit IOPS/Throughput defaults to $($script:GP3_DEFAULT_IOPS) IOPS / $($script:GP3_DEFAULT_THROUGHPUT) MiB/s, which may reduce performance." }
}
# Root HDD protection
$isRootDevice = script:Test-IsRootDevice -Volume $Vol
if ($isRootDevice -and ($Desired['VolumeType'] -in @('sc1','st1')) -and -not $AllowRoot) {
throw (script:Format-Error $script:ERR.RootHDD "Refusing to set root volume $VolId to HDD type" $script:MSG_ROOT_HDD_RECOVERY)
}
# IOPS validation for current type if not changing type
if ($Desired.ContainsKey('Iops') -and -not $DesiredBase.ContainsKey('VolumeType')) {
script:Assert-IopsAllowedOnCurrentType -VolumeType $Vol.VolumeType
}
# Early throughput validation for non-gp3 volumes
if ($Desired.ContainsKey('Throughput') -and
-not $Desired.ContainsKey('VolumeType') -and
$Vol.VolumeType -ne 'gp3') {
throw (script:Format-Error $script:ERR.Args "TargetThroughput is only valid on gp3 volumes" "set -TargetEbsType gp3 or omit -TargetThroughput")
}
# Performance limits validation
$targetType = if ($Desired.ContainsKey('VolumeType')) { $Desired['VolumeType'] } else { $Vol.VolumeType }
$targetSize = if ($Desired.ContainsKey('Size')) { $Desired['Size'] } else { $Vol.Size }
$hasIops = $Desired.ContainsKey('Iops')
$hasTp = $Desired.ContainsKey('Throughput')
$effIops = if ($hasIops) { [int]$Desired['Iops'] } else { [int]$Vol.Iops }
$effTp = if ($hasTp) { [int]$Desired['Throughput'] } else { [int]$Vol.Throughput }
script:Assert-Params -Limits $Limits -TargetType $targetType -TargetSize $targetSize -HasIops:$hasIops -Iops $effIops -HasTp:$hasTp -TpMiB $effTp
return @{ TargetType = $targetType; TargetSize = $targetSize; EffectiveIops = $effIops; EffectiveThroughput = $effTp }
}
function script:Execute-VolumeModification {
param(
[string]$VolId, [hashtable]$ModifySplat, [hashtable]$CommonAwsArgs,
[switch]$Wait, [switch]$WaitForStart, [switch]$UseDryRun,
[int]$PollSeconds, [int]$WaitMaxSeconds, [int]$ChildId, [int]$ParentProgressId,
[string]$Activity, [switch]$Quiet, [ref]$ThrottleCount, [int]$MaxRetries, [double]$BaseBackoffSeconds,
[hashtable]$Desired, $Vol, [switch]$NoProgress
)
# πŸš€ Execute the actual volume modification with optional waiting
# Submit the modification request
$resp = script:Invoke-Edit -ModifySplat $ModifySplat -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds -ThrottleCount $ThrottleCount
# Handle waiting if requested
$status = $null
if (($Wait -or $WaitForStart) -and -not $UseDryRun) {
if (-not $Quiet -and -not $NoProgress) { Write-Progress -Id $ChildId -ParentId $ParentProgressId -Activity $Activity -Status 'waiting for AWS' -PercentComplete 40 }
$status = script:Wait-Modification -VolumeId $VolId -Region $CommonAwsArgs.Region -ProfileName $CommonAwsArgs.ProfileName `
-MaxSeconds $WaitMaxSeconds -PollSeconds $PollSeconds -ForStart:$WaitForStart `
-ProgressId ($ChildId + 500) -ParentProgressId $ChildId -ProgressActivity "Polling $VolId" -NoProgress:$NoProgress
if ($Wait -and -not $status) {
throw (script:Format-Error $script:ERR.Timeout "Timed out waiting for modification status for $VolId" $script:MSG_TIMEOUT_RECOVERY)
}
if ($Wait -and $status -eq 'failed') {
throw (script:Format-Error $script:ERR.Args "Modification failed for $VolId" "verify parameters and IAM permissions, then retry")
}
}
# Set default status for non-waiting operations
if (-not $Wait -and -not $WaitForStart) { $status = 'submitted' }
# Calculate estimated wait hint for non-waiting operations
$estimatedWait = $null
if (-not $Wait -and -not $WaitForStart) {
# Use default wait times based on operation type
if ($Desired.ContainsKey('VolumeType') -and $Desired['VolumeType'] -ne $Vol.VolumeType) {
# Type change - use full wait default
$estimatedWait = $script:DEFAULT_WAIT_FULL_SECONDS
} elseif ($Desired.ContainsKey('Size') -and $Desired['Size'] -gt $Vol.Size) {
# Size increase - use full wait default
$estimatedWait = $script:DEFAULT_WAIT_FULL_SECONDS
} else {
# Performance tuning only - use start wait default
$estimatedWait = $script:DEFAULT_WAIT_START_SECONDS
}
}
return @{ Status = $status; Response = $resp; EstimatedWaitHintSeconds = $estimatedWait }
}
function script:Cleanup-ProgressBar {
param([int]$ProgressId, [int]$ParentProgressId, [string]$Activity, [switch]$Quiet, [switch]$NoProgress)
if (-not $Quiet -and -not $NoProgress -and $ProgressId -ge 0) {
Write-Progress -Id $ProgressId -ParentId $ParentProgressId -Completed -Activity $Activity
}
}
function script:Build-DesiredForVolume { param($Vol,[hashtable]$Base,[switch]$PreservePerf)
$desired = @{}; foreach ($k in $Base.Keys) { $desired[$k] = $Base[$k] }
$targetType = if ($desired.ContainsKey('VolumeType')) { $desired['VolumeType'] } else { $Vol.VolumeType }
$targetSize = if ($desired.ContainsKey('Size')) { $desired['Size'] } else { $Vol.Size }
if ($PreservePerf -and $Vol.VolumeType -eq 'gp2' -and $targetType -eq 'gp3') {
if (-not $desired.ContainsKey('Iops')) { $desired['Iops'] = script:Get-Gp2BaselineIops -SizeGiB $targetSize }
if (-not $desired.ContainsKey('Throughput')) { $desired['Throughput'] = script:Get-Gp2BaselineTpMiB -SizeGiB $targetSize }
}
return $desired
}
function script:Validate-Preconditions {
param($Vol,[switch]$Wait,[switch]$WaitForStart,[switch]$ForceReplacePending,[hashtable]$CommonAwsArgs,[switch]$Quiet)
if ($Vol.State -notin $script:SUPPORTED_VOLUME_STATES) {
throw (script:Format-Error $script:ERR.State "Volume $($Vol.VolumeId) is in unsupported state '$($Vol.State)'" ("operate only on volumes in " + ($script:SUPPORTED_VOLUME_STATES -join ', ')))
}
$vmCur = try { Get-EC2VolumeModification -VolumeId $Vol.VolumeId @CommonAwsArgs -ErrorAction Stop | Sort-Object StartTime -Descending | Select-Object -First 1 } catch { $null }
if ($vmCur -and $vmCur.ModificationState -in @('modifying','optimizing') -and -not $Wait -and -not $WaitForStart) {
if (-not $ForceReplacePending) {
if (-not $Quiet) { Write-Warning "$($Vol.VolumeId) has in-flight modification ($($vmCur.ModificationState)); skipping. $script:MSG_INFLIGHT_RECOVERY" }
return @{ Skip = $true; Reason = 'inflight' }
}
}
return @{ Skip = $false }
}
function script:Build-ModifySplat { param($VolId,[hashtable]$Actual,[hashtable]$CommonAwsArgs,$Ec2Cfg,[switch]$UseDryRun)
$mod = @{ VolumeId = $VolId }
foreach ($k in $Actual.Keys) {
switch ($k) {
'VolumeType' { $mod.VolumeType = $Actual[$k].Target }
'Size' { $mod.Size = $Actual[$k].Target }
'Iops' { $mod.Iops = $Actual[$k].Target }
'Throughput' { $mod.Throughput = $Actual[$k].Target }
}
}
if ($CommonAwsArgs.Region) { $mod.Region = $CommonAwsArgs.Region }
if ($CommonAwsArgs.ProfileName) { $mod.ProfileName = $CommonAwsArgs.ProfileName }
if ($Ec2Cfg) { $mod.ClientConfig = $Ec2Cfg }
if ($UseDryRun) { $mod.DryRun = $true }
return $mod
}
function script:Process-SingleVolume {
param(
[string]$VolId,$Vol,[hashtable]$CommonAwsArgs,[hashtable]$DesiredBase,
[switch]$PreservePerf,[switch]$AllowRoot,[switch]$ForceReplacePending,
[switch]$Quiet,[switch]$Force,[switch]$PlanOnly,[switch]$Wait,[switch]$WaitForStart,
[switch]$UseDryRun,[int]$PollSeconds,[int]$WaitMaxSeconds,[string]$Region,[string]$ProfileName,
[hashtable]$Limits,[int]$MaxRetries,[double]$BaseBackoffSeconds,$Ec2Cfg,[ref]$ThrottleCount,
[switch]$PassThru,[hashtable]$OutAccumulators,[scriptblock]$ShouldProcessDelegate,
[int]$ParentProgressId = -1,[int]$OverallIndex = 0,[int]$OverallCount = 0,
[switch]$PreflightDryRun,[string]$TagIgnoreKey = 'Ignore-EBSAutomation',[switch]$NoProgress
)
# per-volume progress
$childId = Get-Random -Minimum 1001 -Maximum 2000
$activity = "Volume $VolId ($($OverallIndex+1)/$OverallCount)"
if (-not $Quiet -and -not $NoProgress) { Write-Progress -Id $childId -ParentId $ParentProgressId -Activity $activity -Status 'planning' -PercentComplete 1 }
try {
# πŸ” Validate and prepare volume for modification
$validation = script:Validate-VolumeForModification -Vol $Vol -VolId $VolId -CommonAwsArgs $CommonAwsArgs `
-DesiredBase $DesiredBase -PreservePerf:$PreservePerf -AllowRoot:$AllowRoot -ForceReplacePending:$ForceReplacePending `
-Quiet:$Quiet -Force:$Force -PlanOnly:$PlanOnly -Wait:$Wait -WaitForStart:$WaitForStart `
-Limits $Limits -TagIgnoreKey $TagIgnoreKey
if ($validation.Skip) {
$OutAccumulators.Skipped += $VolId
if ($PassThru) {
$t = @{ Type=$Vol.VolumeType; Size=$Vol.Size; Iops=$Vol.Iops; ThroughputMiB=$Vol.Throughput }
$OutAccumulators.Results += (script:New-Result $VolId $CommonAwsArgs.Region @{} 'skipped' $(if($PlanOnly){'DryRun'}else{$null}) $null $Vol $t $null)
}
script:Cleanup-ProgressBar -ProgressId $childId -ParentId $ParentProgressId -Activity $activity -Quiet:$Quiet -NoProgress:$NoProgress
return
}
# πŸ› οΈ Build modification request
$request = script:Build-ModificationRequest -VolId $VolId -Actual $validation.Actual -CommonAwsArgs $CommonAwsArgs -Ec2Cfg $Ec2Cfg -UseDryRun:$UseDryRun
# πŸ“ Enhance plan text for gp3 defaults when user sets only one performance parameter
$changesText = $request.ChangesText
if ($PlanOnly -and $validation.Actual.ContainsKey('VolumeType') -and $validation.Actual['VolumeType'].Target -eq 'gp3') {
$hasExplicitIops = $validation.Actual.ContainsKey('Iops')
$hasExplicitTp = $validation.Actual.ContainsKey('Throughput')
if (($hasExplicitIops -and -not $hasExplicitTp) -or (-not $hasExplicitIops -and $hasExplicitTp)) {
$effIopsDisplay = if ($hasExplicitIops) { $validation.Validation.EffectiveIops } else { $script:GP3_DEFAULT_IOPS }
$effTpDisplay = if ($hasExplicitTp) { $validation.Validation.EffectiveThroughput } else { $script:GP3_DEFAULT_THROUGHPUT }
$changesText += " (effective: ${effIopsDisplay} IOPS, ${effTpDisplay} MiB/s)"
}
}
if (-not $Quiet) {
Write-Verbose ("** {0} - {1}" -f $VolId, $changesText)
if (-not $NoProgress) { Write-Progress -Id $childId -ParentId $ParentProgressId -Activity $activity -Status 'submitting' -PercentComplete 25 }
}
# πŸš€ Execute modification with progress tracking
$result = script:Execute-ModificationWithProgress -VolId $VolId -ModifySplat $request.ModifySplat -CommonAwsArgs $CommonAwsArgs `
-Wait:$Wait -WaitForStart:$WaitForStart -UseDryRun:$UseDryRun `
-PollSeconds $PollSeconds -WaitMaxSeconds $WaitMaxSeconds `
-ChildId $childId -ParentProgressId $ParentProgressId -Activity $activity `
-Quiet:$Quiet -ThrottleCount $ThrottleCount -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds `
-Desired $validation.Desired -Vol $Vol -NoProgress:$NoProgress -PlanOnly:$PlanOnly -PreflightDryRun:$PreflightDryRun
$status = $result.Status
$resp = $result.Response
if ($PlanOnly) {
$OutAccumulators.Planned += $VolId
if ($PassThru) {
$t = @{ Type=$validation.Validation.TargetType; Size=$validation.Validation.TargetSize; Iops=$validation.Validation.EffectiveIops; ThroughputMiB=$validation.Validation.EffectiveThroughput }
$OutAccumulators.Results += (script:New-Result $VolId $CommonAwsArgs.Region $validation.Actual 'planned' 'DryRun' $null $Vol $t $null)
}
script:Cleanup-ProgressBar -ProgressId $childId -ParentId $ParentProgressId -Activity $activity -Quiet:$Quiet -NoProgress:$NoProgress
return
}
# βœ… Submit with confirmation
$targetDesc = "$VolId" + $(if ($CommonAwsArgs.Region) { " in $($CommonAwsArgs.Region)" } else { '' })
$actionDesc = "Modify: $changesText"
$proceed = $Force -or (& $ShouldProcessDelegate $targetDesc $actionDesc)
if ($proceed) {
if (-not $Quiet) {
$label = if ($null -ne $status) { $status } else { 'submitted' }
if ($WaitForStart -and -not $UseDryRun) { $label = 'started' }
Write-Verbose (" ++ {0} - {1}" -f $VolId, $label)
}
$OutAccumulators.Modified += $VolId
if ($PassThru) {
$t = @{ Type=$validation.Validation.TargetType; Size=$validation.Validation.TargetSize; Iops=$validation.Validation.EffectiveIops; ThroughputMiB=$validation.Validation.EffectiveThroughput }
$reqId = try { $resp.ResponseMetadata.RequestId } catch { $null }
$estimatedWait = $result.EstimatedWaitHintSeconds
$OutAccumulators.Results += (script:New-Result $VolId $CommonAwsArgs.Region $validation.Actual $status $reqId $null $Vol $t $estimatedWait)
}
}
}
catch {
throw
}
finally {
script:Cleanup-ProgressBar -ProgressId $childId -ParentId $ParentProgressId -Activity $activity -Quiet:$Quiet -NoProgress:$NoProgress
}
}
#endregion Initializers & Utilities
function Set-EC2VolumeAttribute {
<#
.SYNOPSIS
πŸš€ Bulk EBS volume modification with enterprise-grade safety features
Use -PlainHelp for enterprise-friendly output (no emojis or playful language)
.DESCRIPTION
The Swiss Army knife of EBS volume management! This function lets you transform
multiple AWS EBS volumes in one go with surgical precision and safety guards.
## What Makes This Special:
- πŸ” **Smart Discovery**: Find volumes by ID, tags, or filters (no more manual hunting)
- 🧭 **Validation First**: AWS limits enforced before hitting API errors (prevention > cure)
- ⏳ **Flexible Waiting**: Immediate return, quick ack, or full completion (your choice)
- πŸ€– **CI/CD Ready**: Structured output and DryRun support for automation pipelines
- πŸ“± **Progress Tracking**: Beautiful progress bars (because watching progress is oddly satisfying)
- πŸ” **Resilient**: Intelligent retry logic with exponential backoff for throttling
## Volume Modification Lifecycle:
AWS EBS modifications follow this predictable pattern:
- πŸ› οΈ `modifying`: AWS is processing your request (like a chef preparing your order)
- πŸš€ `optimizing`: Volume is being tuned (performance may dip temporarily)
- βœ… `completed`: Success! Your volume is ready for action
- ❌ `failed`: Something went wrong (rare, usually parameter validation issues)
## Real-World Use Cases:
- 🏒 **Production Migrations**: Bulk gp2 to gp3 conversions with performance preservation
- πŸ§ͺ **Test Environment Setup**: Fast storage for dev teams (because devs need speed too)
- πŸ”„ **Performance Tuning**: Scale IOPS/throughput when apps start complaining
- πŸ“Š **Capacity Planning**: Proactive storage scaling before you hit limits
## Output Format (when -PassThru):
Structured result objects perfect for monitoring dashboards and automation:
- `Current`: What the volume looks like now
- `Target`: What we're trying to achieve
- `Changes`: What actually got modified
- `Status`: Operation result (skipped, planned, submitted, started, optimizing, completed, failed)
- `RequestId` and `Error`: For troubleshooting and audit trails
- `EstimatedWaitHintSeconds`: Estimated completion time for non-waiting operations (null for skipped/planned/failed)
**Note:** `ThroughputMiB` in outputs corresponds to `TargetThroughput` input parameter.
## Safety Features:
- πŸ”’ **Idempotent**: Safe to re-run, no duplicate work
- 🚫 **No Shrinking**: EBS doesn't support volume shrinking (like trying to un-bake a cake)
- πŸ›‘οΈ **Root Protection**: Prevents dangerous HDD types on root volumes
- ⚠️ **Confirmation**: Asks before making changes (unless -Force is used)
## Display Options:
- 🎨 **Default**: Fun, engaging output with emojis and playful language
- 🏒 **Enterprise**: Use `-PlainHelp` for professional, emoji-free output (affects header and constants display)
.PARAMETER VolumeIds
One or more EBS Volume IDs. Optional if using -Filter/-AZ for discovery
.PARAMETER TargetSize
New size in GiB (growth only - EBS shrink is not supported)
.PARAMETER TargetEbsType
New volume type: gp2, gp3, io1, io2, sc1, st1
.PARAMETER TargetIops
New IOPS (required for io1/io2, optional for gp3)
.PARAMETER TargetThroughput
New throughput in MiB/s (gp3 only - other types don't support it)
.PARAMETER Region
AWS region to operate in (defaults to your configured region)
.PARAMETER ProfileName
AWS profile to use (for multi-account setups)
.PARAMETER AZ
Availability zone filter for discovery (useful for regional operations)
.PARAMETER Filter
One or more EC2 filters in hashtable form. Values are automatically trimmed
.PARAMETER Force
Bypass confirmation prompts (use with caution in production)
.PARAMETER Quiet
Reduce console output (perfect for automation and CI/CD)
.PARAMETER PassThru
Emit structured result objects for pipeline processing
.PARAMETER PlanOnly
Show what would change using AWS DryRun (perfect for testing)
.PARAMETER Wait
Wait for full completion (modifying β†’ optimizing β†’ completed)
.PARAMETER WaitForStart
Wait only for AWS acknowledgment (faster feedback)
.PARAMETER PollSeconds
Initial polling interval for waits (adaptive backoff up to 10 seconds)
.PARAMETER WaitMaxSeconds
Maximum time to wait. Smart defaults: 300s for start, 900s for completion, 60s otherwise
.PARAMETER AllowRoot
Allow HDD types on root volumes (dangerous - use with extreme caution)
.PARAMETER PreservePerf
Preserve gp2 baseline IOPS/throughput when converting to gp3 (maintains performance)
.PARAMETER StrictDiscovery
Treat discovery misses as errors (fails fast vs. continues with warnings)
.PARAMETER ForceReplacePending
Proceed even if an in-flight modification exists (useful for recovery scenarios)
.PARAMETER PlainHelp
Use enterprise-friendly help tone (no emojis, colors, or playful language in header and constants display)
.PARAMETER ShowBanner
Show detailed header and constants display (use with -Verbose for full display)
.PARAMETER NoProgress
Silence progress bars even when -Quiet isn't set
Useful in CI/CD environments where -Quiet might hide other important information
.PARAMETER TagIgnoreKey
Custom tag key for ignoring volumes (default: 'Ignore-EBSAutomation')
Organizations can use their own tag conventions for automation exclusions
.EXAMPLE
# Convert all gp2 volumes to gp3 preserving performance (plan only - see what would change)
Get-EC2Volume -Filter @{Name='volume-type';Values='gp2'} |
Set-EC2VolumeAttribute -TargetEbsType gp3 -PreservePerf -PlanOnly
.EXAMPLE
# Resize and tune gp3, waiting for acknowledgment (fast feedback)
Set-EC2VolumeAttribute -VolumeIds vol-123... -TargetEbsType gp3 -TargetSize 200 -TargetIops 6000 -TargetThroughput 250 -WaitForStart -Force
.EXAMPLE
# Bulk performance tuning for production workloads (full completion wait)
Set-EC2VolumeAttribute -Filter @{Name='volume-type';Values='io1'} -TargetIops 8000 -Wait -PassThru
.EXAMPLE
# Enterprise-friendly output (no emojis or playful language in header and constants)
Set-EC2VolumeAttribute -VolumeIds vol-123... -TargetEbsType gp3 -PlainHelp
.EXAMPLE
# Show detailed banner and constants (for interactive sessions)
Set-EC2VolumeAttribute -VolumeIds vol-123... -TargetEbsType gp3 -ShowBanner
.EXAMPLE
# Use custom tag for ignoring volumes (organizational convention)
Set-EC2VolumeAttribute -Filter @{Name='volume-type';Values='gp2'} -TargetEbsType gp3 -TagIgnoreKey 'Skip-Automation'
.EXAMPLE
# Test changes without making them (PowerShell native -WhatIf support)
Set-EC2VolumeAttribute -VolumeIds vol-123... -TargetEbsType gp3 -WhatIf
.OUTPUTS
When -PassThru is used, returns structured objects perfect for monitoring dashboards,
CI/CD pipelines, and automation workflows. Each object contains a structured
status snapshot for this invocation and current status.
**Note:** `ThroughputMiB` in outputs corresponds to `TargetThroughput` input parameter.
**Note:** Use `-PlainHelp` to control header and constants display tone (enterprise-friendly vs. playful).
**Note:** Use `-ShowBanner` or `-Verbose` to display detailed header and constants information.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName='Submit')]
[OutputType([pscustomobject])]
param(
# Core modification parameters
[Parameter(ParameterSetName='Submit', Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName='WaitFull', Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName='WaitStart', Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName='Plan', Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('VolumeId', 'Id')]
[string[]] $VolumeIds,
[Alias('Size')]
[ValidateRange(1,65536)]
[int] $TargetSize,
[Alias('Type')]
[ValidateSet('gp2','gp3','io1','io2','sc1','st1')]
[string] $TargetEbsType,
[Alias('Iops')]
[int] $TargetIops,
[Alias('Throughput')]
[ValidateRange(125,1000)]
[int] $TargetThroughput,
# AWS configuration
[string] $Region,
[string] $ProfileName,
# Volume discovery
[string] $AZ,
[hashtable[]] $Filter,
# Behavior control
[switch] $Force,
[switch] $Quiet,
[switch] $PassThru,
[switch] $PlanOnly,
[Parameter(ParameterSetName='WaitFull', Mandatory)]
[switch] $Wait,
[Parameter(ParameterSetName='WaitStart', Mandatory)]
[switch] $WaitForStart,
[switch] $AllowRoot,
[switch] $PreservePerf,
[switch] $StrictDiscovery,
[switch] $ForceReplacePending,
[switch] $PreflightDryRun, # NEW: ask AWS to validate (DryRun) even when not PlanOnly
[switch] $PlainHelp, # Use enterprise-friendly help tone (no emojis, colors, or playful language)
[switch] $ShowBanner, # Show detailed header and constants (use with -Verbose for full display)
[switch] $NoProgress, # Silence progress bars (useful in CI where -Quiet might hide other info)
# Tag-based filtering
[string] $TagIgnoreKey = 'Ignore-EBSAutomation', # Tag key to ignore volumes (orgs can customize)
# Performance tuning
[int] $MaxRetries = 3,
[double] $BaseBackoffSeconds = 1.0,
[int] $ReadWriteTimeoutSeconds = 60,
[int] $PollSeconds = $script:DEFAULT_POLL_SECONDS,
[int] $WaitMaxSeconds
)
#region Setup & Validation πŸš€ (Getting ready to rock some EBS volumes)
begin {
if (-not (Get-Command Edit-EC2Volume -ErrorAction SilentlyContinue)) {
throw "AWS.Tools.EC2 is required. Install-Module AWS.Tools.EC2"
}
$color = script:Initialize-ColorSupport -Quiet:$Quiet
$Colorize = $color.Colorize
# Show header/constants only when explicitly requested or this invocation is verbose
if (-not $Quiet -and ($ShowBanner -or $PSBoundParameters.ContainsKey('Verbose'))) {
script:Write-Header -PlainHelp:$PlainHelp -ShowBanner:$true
script:Write-Constants -PlainHelp:$PlainHelp -ShowBanner:$true
}
# Keep this as verbose so it respects -Verbose
$msg = "EBS Volume Modification Utility" + $(if ($Region) { " - Region: $Region" } else { '' })
if ($PlainHelp) { Write-Verbose $msg } else { Write-Verbose (& $Colorize $msg 'Cyan') }
if ($PlanOnly) { Write-Verbose "DRY RUN MODE - Using AWS DryRun for validation" }
# Limits + optional overrides from env/SSM
$Limits = script:Initialize-ValidationLimits
$Limits = script:Try-LoadLimitsOverrides -Base $Limits
# Early validation for throughput on non-gp3 volumes
if ($PSBoundParameters.ContainsKey('TargetThroughput') -and
-not $PSBoundParameters.ContainsKey('TargetEbsType') -and
-not $PSBoundParameters.ContainsKey('Filter') -and
-not $PSBoundParameters.ContainsKey('AZ')) {
# We can't determine current volume types without discovery, so defer validation
Write-Verbose "TargetThroughput specified - will validate per-volume during processing"
}
# Top-level param compatibility
script:Test-ParameterCompatibility -TargetEbsType $TargetEbsType `
-HasTargetIops:($PSBoundParameters.ContainsKey('TargetIops')) `
-HasTargetThroughput:($PSBoundParameters.ContainsKey('TargetThroughput')) `
-IsPlan:($PlanOnly -or $PSBoundParameters.ContainsKey('WhatIf'))
# Smart default waits
if (-not $PSBoundParameters.ContainsKey('WaitMaxSeconds')) {
switch ($PSCmdlet.ParameterSetName) {
'WaitStart' { $WaitMaxSeconds = $script:DEFAULT_WAIT_START_SECONDS }
'WaitFull' { $WaitMaxSeconds = $script:DEFAULT_WAIT_FULL_SECONDS }
default { $WaitMaxSeconds = $script:DEFAULT_WAIT_DEFAULT_SECONDS }
}
}
# AWS client config
$ec2Cfg = script:Initialize-AWSConfig -MaxRetries $MaxRetries -ReadWriteTimeoutSeconds $ReadWriteTimeoutSeconds
# Collections
$collectedIds = @()
$script:ThrottleCount = 0
# Common args
$commonAwsArgs = @{}
if ($Region) { $commonAwsArgs.Region = $Region }
if ($ProfileName) { $commonAwsArgs.ProfileName = $ProfileName }
# Trackers
$Outs = @{
Modified = @()
Skipped = @()
Planned = @()
Failed = @()
Results = @()
}
# ShouldProcess delegate for helpers
$ShouldProc = { param($target,$action) $PSCmdlet.ShouldProcess($target,$action) }
# Set InformationPreference to Continue when -Verbose is used for better visibility
if ($PSBoundParameters.ContainsKey('Verbose')) {
$InformationPreference = 'Continue'
}
}
#endregion
#region Pipeline Collection πŸ“₯ (Gathering volume IDs from the pipeline)
process {
if ($VolumeIds) {
# sanitize & validate IDs early
$raw = $VolumeIds | Where-Object { $_ } | ForEach-Object { $_.ToString().Trim() }
$clean = $raw | Where-Object { $_ -match $script:VOLID_REGEX }
$bad = Compare-Object -ReferenceObject $raw -DifferenceObject $clean -PassThru
if ($bad) { Write-Warning ("Ignoring invalid VolumeId(s): {0}" -f ($bad -join ', ')) }
if ($clean.Count) { $collectedIds += $clean }
}
}
#endregion
#region Execution 🎯 (The main event - modifying those volumes!)
end {
# πŸ” DISCOVERY: Find the volumes we need to work with
$volIdsAll = ($collectedIds | Where-Object { $_ } | Sort-Object -Unique)
$disc = script:Discover-Volumes -VolumeIds $volIdsAll -Filter $Filter -AZ $AZ -CommonAwsArgs $commonAwsArgs -StrictDiscovery:$StrictDiscovery
if (-not $disc.List -or -not $disc.List.Count) { return }
# 🎯 DESIRED STATE: Build the target configuration from parameters
$desiredBase = @{}
if ($TargetEbsType) { $desiredBase['VolumeType'] = $TargetEbsType }
if ($PSBoundParameters.ContainsKey('TargetSize')) { $desiredBase['Size'] = $TargetSize }
if ($PSBoundParameters.ContainsKey('TargetIops')) { $desiredBase['Iops'] = $TargetIops }
if ($PSBoundParameters.ContainsKey('TargetThroughput')) { $desiredBase['Throughput'] = $TargetThroughput }
# πŸ§ͺ DRY RUN: Use AWS DryRun for planning and WhatIf scenarios
$isDryRun = $PlanOnly -or $PSBoundParameters.ContainsKey('WhatIf')
# πŸ“Š PROGRESS TRACKING: Set up the main progress bar for the overall operation
$overallId = Get-Random -Minimum 1 -Maximum 1000
$total = $disc.List.Count
$index = 0
try {
# πŸ”„ MAIN PROCESSING LOOP: Process each volume with progress tracking
foreach ($volId in $disc.List) {
$pctOverall = if ($total -gt 0) { [int](($index / $total) * 100) } else { 0 }
if (-not $Quiet -and -not $NoProgress) { Write-Progress -Id $overallId -Activity "Processing $total volume(s)" -Status "$($index+1)/$total" -PercentComplete $pctOverall }
$vol = $disc.Map[$volId]
try {
# 🎯 Process this individual volume (the heavy lifting happens here)
script:Process-SingleVolume -VolId $volId -Vol $vol -CommonAwsArgs $commonAwsArgs `
-DesiredBase $desiredBase -PreservePerf:$PreservePerf -AllowRoot:$AllowRoot -ForceReplacePending:$ForceReplacePending `
-Quiet:$Quiet -Force:$Force -PlanOnly:$PlanOnly -Wait:$Wait -WaitForStart:$WaitForStart `
-UseDryRun:$isDryRun -PollSeconds $PollSeconds -WaitMaxSeconds $WaitMaxSeconds `
-Region $Region -ProfileName $ProfileName -Limits $Limits -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds -Ec2Cfg $ec2Cfg `
-ThrottleCount ([ref]$script:ThrottleCount) -PassThru:$PassThru -OutAccumulators $Outs -ShouldProcessDelegate $ShouldProc `
-ParentProgressId $overallId -OverallIndex $index -OverallCount $total -PreflightDryRun:$PreflightDryRun -TagIgnoreKey $TagIgnoreKey -NoProgress:$NoProgress
}
catch {
# 🚨 ERROR HANDLING: Capture and format errors for user-friendly reporting
$ae = $_.Exception
$msg = $ae.Message
if ($msg -match 'Throttl|Rate') { $msg = (script:Format-Error $script:ERR.Throttle $msg $script:MSG_THROTTLE_RECOVERY) }
if (-not $Quiet) { Write-Warning ("!! {0} - {1}" -f $volId, $msg) }
Write-Error ("Error processing {0}: {1}" -f $volId, $msg)
$Outs.Failed += $volId
if ($PassThru) {
$t = @{ Type=$vol.VolumeType; Size=$vol.Size; Iops=$vol.Iops; ThroughputMiB=$vol.Throughput }
$Outs.Results += (script:New-Result $volId $commonAwsArgs.Region $null 'failed' $null $msg $vol $t $null)
}
}
finally {
$index++ # Always increment the counter, even on errors
}
}
}
finally {
# 🧹 CLEANUP: Always complete the main progress bar
if (-not $Quiet -and -not $NoProgress) { Write-Progress -Id $overallId -Completed -Activity "Completed processing $total volume(s)" }
}
if (-not $Quiet) {
# πŸ“Š SUMMARY STATISTICS: Compile the results for user review
$summary = @{
Modified = @($Outs.Modified).Count
Skipped = @($Outs.Skipped).Count
Planned = @($Outs.Planned).Count
Errors = @($Outs.Failed).Count
Throttles = $script:ThrottleCount
MaxRetries = $MaxRetries
BaseBackoffSeconds = $BaseBackoffSeconds
}
Write-Information ("SUMMARY: " + ($summary | ConvertTo-Json -Compress))
# CONSOLE SUMMARY: Pretty output for interactive sessions (ASCII-safe)
if ($Host.Name -eq 'ConsoleHost' -and -not [Console]::IsOutputRedirected) {
Write-Host "`n======== SUMMARY ========" -ForegroundColor Yellow
Write-Host ("++ Modified: {0}" -f $summary.Modified) -ForegroundColor Green
Write-Host (">> Skipped: {0}" -f $summary.Skipped) -ForegroundColor Cyan
if ($summary.Planned -gt 0) { Write-Host ("Planned: {0}" -f $summary.Planned) -ForegroundColor Blue }
Write-Host ("!! Errors: {0}" -f $summary.Errors) -ForegroundColor Red
if ($PSVersionTable.PSVersion.Major -ge 7 -and $summary.Throttles) {
Write-Host ("AWS throttled us {0} time(s)" -f $summary.Throttles) -ForegroundColor Magenta
}
}
}
# 🎁 RETURN RESULTS: Structured output for automation and monitoring
if ($PassThru) { return $Outs.Results }
}
#endregion
}
function script:Validate-VolumeForModification {
param(
$Vol, [string]$VolId, [hashtable]$CommonAwsArgs, [hashtable]$DesiredBase,
[switch]$PreservePerf, [switch]$AllowRoot, [switch]$ForceReplacePending,
[switch]$Quiet, [switch]$Force, [switch]$PlanOnly, [switch]$Wait, [switch]$WaitForStart,
[hashtable]$Limits, [string]$TagIgnoreKey
)
# πŸ” Comprehensive validation and preparation for volume modification
# Check if volume should be ignored
if (script:Test-IgnoreTag -Volume $Vol -VolId $VolId -Quiet:$Quiet -TagIgnoreKey $TagIgnoreKey) {
return @{ Skip = $true; Reason = 'ignored'; Volume = $Vol }
}
# Preconditions (state + in-flight)
$pre = script:Validate-Preconditions -Vol $Vol -Wait:$Wait -WaitForStart:$WaitForStart -ForceReplacePending:$ForceReplacePending -CommonAwsArgs $CommonAwsArgs -Quiet:$Quiet
if ($pre.Skip) {
return @{ Skip = $true; Reason = $pre.Reason; Volume = $Vol }
}
# Build desired for this volume
$desired = script:Build-DesiredForVolume -Vol $Vol -Base $DesiredBase -PreservePerf:$PreservePerf
# Diff and basic guardrails
$actual = script:Diff-Changes -Vol $Vol -Desired $desired
if (-not $actual.Count) {
return @{ Skip = $true; Reason = 'no-changes'; Volume = $Vol; Desired = $desired }
}
# Validate & compute effective values
$val = script:Validate-VolumeParameters -Vol $Vol -VolId $VolId -Desired $desired -DesiredBase $DesiredBase `
-Limits $Limits -AllowRoot:$AllowRoot -Quiet:$Quiet -Force:$Force
return @{
Skip = $false;
Volume = $Vol;
Desired = $desired;
Actual = $actual;
Validation = $val
}
}
function script:Build-ModificationRequest {
param(
[string]$VolId, [hashtable]$Actual, [hashtable]$CommonAwsArgs, $Ec2Cfg, [switch]$UseDryRun
)
# πŸ› οΈ Build the modification request and change text
# Build modify request
$mod = script:Build-ModifySplat -VolId $VolId -Actual $Actual -CommonAwsArgs $CommonAwsArgs -Ec2Cfg $Ec2Cfg -UseDryRun:$UseDryRun
# Build change text via StringBuilder
$sb = New-Object System.Text.StringBuilder
$keys = $Actual.Keys | Sort-Object
for ($i=0; $i -lt $keys.Count; $i++) {
$k = $keys[$i]
[void]$sb.AppendFormat("{0}={1}->{2}", $k, $Actual[$k].Current, $Actual[$k].Target)
if ($i -lt $keys.Count-1) { [void]$sb.Append(", ") }
}
return @{
ModifySplat = $mod
ChangesText = $sb.ToString()
}
}
function script:Execute-ModificationWithProgress {
param(
[string]$VolId, [hashtable]$ModifySplat, [hashtable]$CommonAwsArgs,
[switch]$Wait, [switch]$WaitForStart, [switch]$UseDryRun,
[int]$PollSeconds, [int]$WaitMaxSeconds, [int]$ChildId, [int]$ParentProgressId,
[string]$Activity, [switch]$Quiet, [ref]$ThrottleCount, [int]$MaxRetries, [double]$BaseBackoffSeconds,
[hashtable]$Desired, $Vol, [switch]$NoProgress, [switch]$PlanOnly, [switch]$PreflightDryRun
)
# πŸš€ Execute the modification with progress tracking and optional waiting
# Optional DryRun preflight to let AWS validate params even if not PlanOnly
if ($PreflightDryRun -and -not $PlanOnly) {
$null = script:Invoke-Edit -ModifySplat $ModifySplat -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds -DryRun -ThrottleCount $ThrottleCount
}
# Plan path (DryRun only)
if ($PlanOnly) {
$null = script:Invoke-Edit -ModifySplat $ModifySplat -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds -DryRun -ThrottleCount $ThrottleCount
return @{ Status = 'planned'; Response = $null; EstimatedWaitHintSeconds = $null }
}
# Execute the modification using our helper function
$result = script:Execute-VolumeModification -VolId $VolId -ModifySplat $ModifySplat -CommonAwsArgs $CommonAwsArgs `
-Wait:$Wait -WaitForStart:$WaitForStart -UseDryRun:$UseDryRun `
-PollSeconds $PollSeconds -WaitMaxSeconds $WaitMaxSeconds `
-ChildId $ChildId -ParentProgressId $ParentProgressId -Activity $Activity `
-Quiet:$Quiet -ThrottleCount $ThrottleCount -MaxRetries $MaxRetries -BaseBackoffSeconds $BaseBackoffSeconds `
-Desired $Desired -Vol $Vol -NoProgress:$NoProgress
return $result
}
@nanoDBA
Copy link
Author

nanoDBA commented Aug 8, 2025

Now the warning will only appear when you're actually changing the volume type to gp3, not when the volume is already gp3 and you're just modifying other attributes like size

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment