Private/Azure/New-SilkTCOAWSCostArray.ps1

<#
    .SYNOPSIS
    Calculates AWS Resource costs based on AWS Pricing API.
 
    .DESCRIPTION
    This function calculates costs for AWS resources (EC2 instances and EBS volumes)
    by querying the AWS Price List Service API for current list pricing. Costs are
    calculated for the specified number of days (default: 1 day).
 
    .EXAMPLE
    New-SilkTCOAWSCostArray -vmlist $vmlist -region 'us-east-1' -days 7
 
    Calculates costs for all resources in the vmlist over a 7-day period.
#>


function New-SilkTCOAWSCostArray {
    param(
        [Parameter(Mandatory)]
        [array] $vmlist,
        [Parameter()]
        [string] $region,
        [Parameter()]
        [int] $days = 1
    )

    # Infer region from vmlist if not provided
    if (-not $region -and $vmlist.Count -gt 0) {
        $availabilityZone = $vmlist[0].Placement.AvailabilityZone
        $region = $availabilityZone -replace '[a-z]$', ''  # Remove trailing letter (e.g., us-east-1a -> us-east-1)
        Write-Verbose "Auto-detected region from vmlist: $region" -Verbose
    }
    
    if (-not $region) {
        $region = 'us-east-1'
        Write-Warning "No region detected, defaulting to: $region"
    }

    # Ensure Pricing module is loaded
    $requiredModules = @('AWS.Tools.Pricing', 'AWS.Tools.EC2')
    foreach ($module in $requiredModules) {
        if (-not (Get-Module -ListAvailable -Name $module)) {
            throw "Required module '$module' is not installed. Install it with: Install-Module $module"
        }
        if (-not (Get-Module -Name $module)) {
            Import-Module $module -ErrorAction Stop
        }
    }

    Write-Verbose "Querying AWS Pricing API for current rates..." -Verbose

    # Map AWS region codes to pricing API region names
    $regionMapping = @{
        'us-east-1'      = 'US East (N. Virginia)'
        'us-east-2'      = 'US East (Ohio)'
        'us-west-1'      = 'US West (N. California)'
        'us-west-2'      = 'US West (Oregon)'
        'eu-west-1'      = 'EU (Ireland)'
        'eu-central-1'   = 'EU (Frankfurt)'
        'ap-southeast-1' = 'Asia Pacific (Singapore)'
        'ap-southeast-2' = 'Asia Pacific (Sydney)'
        'ap-northeast-1' = 'Asia Pacific (Tokyo)'
    }

    $pricingRegion = if ($regionMapping.ContainsKey($region)) { 
        $regionMapping[$region] 
    } else { 
        'US East (N. Virginia)' 
    }

    # Cache for pricing lookups
    $ebsPricingCache = @{}
    $ec2PricingCache = @{}

    # Helper function to get EBS pricing from API
    function Get-EBSPricing {
        param(
            [string]$VolumeType,
            [string]$Region
        )

        $cacheKey = "$VolumeType-$Region"
        if ($ebsPricingCache.ContainsKey($cacheKey)) {
            return $ebsPricingCache[$cacheKey]
        }

        try {
            # Query AWS Pricing API for EBS
            $filters = @(
                @{Type='TERM_MATCH'; Field='productFamily'; Value='Storage'}
                @{Type='TERM_MATCH'; Field='volumeApiName'; Value=$VolumeType}
                @{Type='TERM_MATCH'; Field='location'; Value=$Region}
            )

            $products = Get-PLSProduct -ServiceCode AmazonEC2 -Filter $filters -MaxResult 1 -Region us-east-1

            if ($products) {
                $priceJson = $products[0] | ConvertFrom-Json
                $terms = $priceJson.terms.OnDemand
                $firstTerm = $terms.PSObject.Properties.Value | Select-Object -First 1
                $priceDimension = $firstTerm.priceDimensions.PSObject.Properties.Value | Select-Object -First 1
                $pricePerUnit = [decimal]$priceDimension.pricePerUnit.USD

                $ebsPricingCache[$cacheKey] = $pricePerUnit
                return $pricePerUnit
            }
        } catch {
            Write-Verbose "Failed to get pricing for $VolumeType from API: $_" -Verbose
        }

        # Fallback to hardcoded pricing if API fails
        $fallbackPricing = @{
            'gp3' = 0.08; 'gp2' = 0.10; 'io1' = 0.125; 'io2' = 0.125
            'st1' = 0.045; 'sc1' = 0.015; 'standard' = 0.05
        }
        return $fallbackPricing[$VolumeType]
    }

    # Helper function to get EC2 pricing from API
    function Get-EC2Pricing {
        param(
            [string]$InstanceType,
            [string]$Region
        )

        $cacheKey = "$InstanceType-$Region"
        if ($ec2PricingCache.ContainsKey($cacheKey)) {
            return $ec2PricingCache[$cacheKey]
        }

        try {
            # Query AWS Pricing API for EC2
            $filters = @(
                @{Type='TERM_MATCH'; Field='instanceType'; Value=$InstanceType}
                @{Type='TERM_MATCH'; Field='location'; Value=$Region}
                @{Type='TERM_MATCH'; Field='tenancy'; Value='Shared'}
                @{Type='TERM_MATCH'; Field='operatingSystem'; Value='Linux'}
                @{Type='TERM_MATCH'; Field='preInstalledSw'; Value='NA'}
                @{Type='TERM_MATCH'; Field='capacitystatus'; Value='Used'}
            )

            $products = Get-PLSProduct -ServiceCode AmazonEC2 -Filter $filters -MaxResult 1 -Region us-east-1

            if ($products) {
                $priceJson = $products[0] | ConvertFrom-Json
                $terms = $priceJson.terms.OnDemand
                $firstTerm = $terms.PSObject.Properties.Value | Select-Object -First 1
                $priceDimension = $firstTerm.priceDimensions.PSObject.Properties.Value | Select-Object -First 1
                $pricePerHour = [decimal]$priceDimension.pricePerUnit.USD

                $ec2PricingCache[$cacheKey] = $pricePerHour
                return $pricePerHour
            }
        } catch {
            Write-Verbose "Failed to get pricing for $InstanceType from API: $_" -Verbose
        }

        # Fallback to hardcoded pricing if API fails
        $fallbackPricing = @{
            't3.small' = 0.0208; 't3.medium' = 0.0416; 't3.large' = 0.0832
            't3.xlarge' = 0.1664; 't3.2xlarge' = 0.3328
            't3a.small' = 0.0188; 't3a.medium' = 0.0376; 't3a.large' = 0.0752
            't3a.xlarge' = 0.1504; 't3a.2xlarge' = 0.3008
            'm5.large' = 0.096; 'm5.xlarge' = 0.192; 'm5.2xlarge' = 0.384
        }
        return $fallbackPricing[$InstanceType]
    }

    $costReport = @()

    foreach ($vm in $vmlist) {
        $instanceId = $vm.InstanceId
        $instanceType = $vm.InstanceType.Value

        # Get EC2 pricing from API
        $hourlyRate = Get-EC2Pricing -InstanceType $instanceType -Region $pricingRegion
        
        if ($hourlyRate) {
            $dailyVMCost = [Math]::Round($hourlyRate * 24, 4)
            $totalVMCost = [Math]::Round($dailyVMCost * $days, 4)

            $costItem = New-Object psobject
            $costItem | Add-Member -MemberType NoteProperty -Name ResourceId -Value $instanceId
            $costItem | Add-Member -MemberType NoteProperty -Name ResourceName -Value $instanceType
            $costItem | Add-Member -MemberType NoteProperty -Name ResourceType -Value 'EC2 Instance'
            $costItem | Add-Member -MemberType NoteProperty -Name MeterCategory -Value 'Compute'
            $costItem | Add-Member -MemberType NoteProperty -Name MeterSubCategory -Value 'EC2-Instances'
            $costItem | Add-Member -MemberType NoteProperty -Name Cost -Value $totalVMCost
            $costItem | Add-Member -MemberType NoteProperty -Name Currency -Value 'USD'
            $costReport += $costItem

            Write-Verbose " Instance: $instanceId ($instanceType) = `$$dailyVMCost/day × $days days = `$$totalVMCost" -Verbose
        } else {
            Write-Warning "No pricing data available for instance type: $instanceType"
        }

        # Get attached EBS volumes and calculate costs
        $volumes = Get-EC2Volume -Filter @{
            Name   = 'attachment.instance-id'
            Values = @($instanceId)
        } -ErrorAction SilentlyContinue

        if ($volumes) {
            foreach ($vol in $volumes) {
                $volumeId = $vol.VolumeId
                $volumeType = $vol.VolumeType.Value
                $volumeSize = $vol.Size
                $volumeIops = $vol.Iops

                # Get EBS storage pricing from API (per GB-month)
                $gbMonthPrice = Get-EBSPricing -VolumeType $volumeType -Region $pricingRegion
                
                $dailyStorageCost = 0
                if ($gbMonthPrice) {
                    $monthlyStorageCost = $gbMonthPrice * $volumeSize
                    $dailyStorageCost = [Math]::Round($monthlyStorageCost / 30, 4)
                }

                # Add IOPS cost for io1/io2 volumes (hardcoded as API query is complex)
                $dailyIopsCost = 0
                if ($volumeType -in @('io1', 'io2') -and $volumeIops) {
                    $iopsMonthlyRate = 0.065  # $0.065 per IOPS-month
                    $monthlyIopsCost = $iopsMonthlyRate * $volumeIops
                    $dailyIopsCost = [Math]::Round($monthlyIopsCost / 30, 4)
                }

                $totalDailyCost = $dailyStorageCost + $dailyIopsCost
                $totalPeriodCost = [Math]::Round($totalDailyCost * $days, 4)

                if ($totalPeriodCost -gt 0) {
                    $costItem = New-Object psobject
                    $costItem | Add-Member -MemberType NoteProperty -Name ResourceId -Value $volumeId
                    $costItem | Add-Member -MemberType NoteProperty -Name ResourceName -Value $volumeType
                    $costItem | Add-Member -MemberType NoteProperty -Name ResourceType -Value 'EBS Volume'
                    $costItem | Add-Member -MemberType NoteProperty -Name MeterCategory -Value 'Storage'
                    $costItem | Add-Member -MemberType NoteProperty -Name MeterSubCategory -Value 'EBS'
                    $costItem | Add-Member -MemberType NoteProperty -Name Cost -Value $totalPeriodCost
                    $costItem | Add-Member -MemberType NoteProperty -Name Currency -Value 'USD'
                    $costReport += $costItem

                    Write-Verbose " Volume: $volumeId ($volumeType ${volumeSize}GB) = `$$totalDailyCost/day × $days days = `$$totalPeriodCost" -Verbose
                }
            }
        }
    }

    if ($costReport.Count -eq 0) {
        Write-Warning "No cost data calculated for the provided resources."
        return
    }

    Write-Verbose "Calculated costs for $($costReport.Count) resources." -Verbose
    return $costReport
}