eos-cost-analysis

Use this skill whenever a user asks about End of Standard Support (EOS) or Extended Support (ES) cost, exposure, or inventory for EKS, RDS/Aurora, Lambda, ElastiCache, or OpenSearch — for example "wha

SKILL.md

GitHub

EOS Cost Analysis

Discover AWS resources approaching End of Standard Support, calculate Extended Support charges, and generate an actionable cost-impact report.

When to Use This Skill

  • User asks about End of Support cost exposure, Extended Support charges, or version-deprecation cost
  • User wants to know which resources are incurring or will incur Extended Support charges
  • User wants an EOS cost or inventory report / posture assessment across accounts
  • User wants a cost-impact report for deprecated service versions
  • Proactive scheduled checks for EOS posture across the organization

This skill scopes the EOS cost and inventory picture. It is not an upgrade-execution skill — for EKS pre-upgrade readiness and upgrade planning, use eks-upgrade-readiness; for OpenSearch domain health beyond version lifecycle, use analytics-opensearch-expertise.

Supported Services (Phase 1 — Extended Support Charges)

Service Billing Unit Discovery API
Amazon EKS Per cluster/hour eks:ListClusters, eks:DescribeCluster
Amazon RDS/Aurora Per vCPU/hour per billed instance; per ACU-hour for Aurora Serverless v2 rds:DescribeDBInstances, rds:DescribeDBClusters
AWS Lambda No direct ES charge (security risk only) lambda:ListFunctions
Amazon ElastiCache Per node/hour (Redis OSS v4–v6 only) elasticache:DescribeCacheClusters, elasticache:DescribeReplicationGroups, elasticache:DescribeServerlessCaches
Amazon OpenSearch Per Normalized Instance Hour, or equal to instance cost during an extension period opensearch:ListDomainNames, opensearch:DescribeDomain

Which resources are billed, in what unit and how many units are defined in references/billing-rules.md. Follow it for every classification and cost.

IAM prefix note: the use_aws calls above use opensearch because that is the correct API client name, but the corresponding IAM actions live under the es: prefix (es:ListDomainNames, es:DescribeDomain, es:DescribeDomains). The AIDevOpsAgentAccessPolicy managed policy already grants the discovery actions for all five services.

Extended Support Pricing

Pricing is ALWAYS retrieved live from the AWS Pricing API (pricing:GetProducts) — see Phase 3, Step 3.2. The API is the single source of truth; the rates below are a validation reference only (sanity-check the API result against them; never substitute them for the API value).

Validation reference rates (us-east-1)

Service Year 1-2 rate Year 3 rate Billing unit Tier behavior
EKS $0.50/cluster/hour $0.50/cluster/hour per cluster/hour Flat — no Year 3 escalation. This is the ES premium on top of the $0.10 standard cluster cost.
RDS $0.10/vCPU/hour $0.20/vCPU/hour per vCPU/hour Year 3 = 2× Year 1-2
Aurora Serverless v2 $0.085/ACU-hour for Aurora PostgreSQL 13 — per ACU-hour Read the Yr3 usagetype when it applies
ElastiCache $0.165/node/hour for cache.r6g.large (rate depends on node type, e.g. $0.054 for cache.t3.medium) $0.330/node/hour for cache.r6g.large per node/hour Year 3 = 2× Year 1-2
OpenSearch $0.0065/NIH (m7g.medium.search = 2 NIH per hour); during an extension period, the instance's own hourly price (m7g.medium.search $0.068) — per NIH or per instance-hour No Year 3 escalation
Lambda — — — No Extended Support charge (deprecated runtimes are a security risk only)

After Year 3, AWS may force-upgrade the resource. Cost framing: these rates represent the AVOIDABLE Extended Support premium (what the customer eliminates by upgrading). For EKS, the $0.50/hour is the ES surcharge on top of the $0.10 standard cluster cost the customer pays regardless — report the premium as the cost exposure/savings opportunity, not the standard cost.

Prerequisites

  • Discovery APIs (listed per service above) and the CloudWatch metric reads for Aurora Serverless v2 usage are covered by the AIDevOpsAgentAccessPolicy managed policy
  • The Pricing API actions pricing:GetProducts, pricing:GetAttributeValues and pricing:DescribeServices for live Extended Support pricing, granted by the EnableEosCostAnalysis parameter of the skill-policies CloudFormation template
  • For organization-wide analysis: resources across all associated accounts (via Agent Space cloud source associations)
  • For EOS date validation and upgrade guidance: verify_aws_claim or AWS documentation search

Constraints

  • NEVER guess or estimate EOS dates — always verify from AWS documentation, per exact engine + version
  • NEVER reuse one version's EOS date for another version, even within the same service — verify each version independently
  • NEVER fabricate pricing rates — retrieve live rates from the AWS Pricing API (Step 3.2)
  • When calling the Pricing API, ALWAYS extract only pricePerUnit — never dump the full PriceList (responses are very large)
  • If a version's EOS date is not found, mark it UNVERIFIED and set the dependent fields as in references/billing-rules.md, section 6
  • All output is AI-generated and must be independently verified before taking action
  • Read-only operations only — do not modify any resources

Phase 1: Scope Definition

  1. Identify target service(s) from the user request:

    • If user specifies a service (e.g., "EKS EOS") → analyze that service only
    • If user says "all services" or "full EOS analysis" → analyze all 5 supported services
    • If unclear, ask: "Which service would you like me to analyze? (EKS, RDS, Lambda, ElastiCache, OpenSearch, or all)"
  2. Determine account scope:

    • Default: ALL associated accounts — Scan resources in ALL AWS accounts connected to this Agent Space (primary + all secondary cloud sources)
    • The agent has access to multiple accounts through its cloud source associations — discover resources in EACH account, not just the primary
    • To identify available accounts: attempt resource discovery in all accounts the agent has access to. The Agent Space configuration determines which accounts are available.
    • If user says "only this account" or specifies an account ID → limit to that account
  3. Determine region scope:

    • If user specifies regions → use those
    • Otherwise → scan all commercially available regions
    • Optimization: first check which regions have resources for the target service using a quick List call in each region
  4. State scope and estimate scan size, then proceed (do NOT block on a prompt):

    • Announce: "Analyzing [service(s)] across [N accounts] in [regions]."
    • Estimate scan size before starting: roughly services × accounts × regions List calls (plus per-resource Describe calls). Report the rough magnitude so the user knows what to expect.
    • Large-scope warning: if the scope is large (e.g., >10 accounts, or all services × all regions × many accounts), add a heads-up that this may take a while and involve hundreds of API calls — but still proceed automatically.
    • Default behavior: all services, all associated accounts, all regions — proceed WITHOUT asking, unless the user narrows it. Only the scope announcement and size estimate are required; no confirmation gate.

Phase 2: Resource Discovery

For each account and region in scope, call the appropriate service APIs.

PAGINATION (MANDATORY): All discovery APIs paginate. You MUST follow pagination tokens to retrieve ALL resources — never rely on the first page. Continue calling with the NextToken / Marker / nextToken until no token is returned:

  • lambda:ListFunctions → NextMarker (default page size 50)
  • rds:DescribeDBInstances / rds:DescribeDBClusters → Marker
  • eks:ListClusters → nextToken
  • elasticache:DescribeCacheClusters → Marker
  • elasticache:DescribeReplicationGroups → Marker
  • elasticache:DescribeServerlessCaches → NextToken
  • opensearch:ListDomainNames → returns all in one call (no pagination)

Under-counting due to skipped pages is a correctness failure. Always exhaust pagination.

EKS Discovery

use_aws: eks:ListClusters (region: <region>)
→ For each cluster name:
  use_aws: eks:DescribeCluster (name: <cluster_name>, region: <region>)
  → Extract: clusterName, version, arn, createdAt

Record: account_id, region, cluster_name, cluster_arn, kubernetes_version

RDS Discovery

RDS Extended Support applies to both standalone RDS instances AND Aurora clusters. Query BOTH APIs — Aurora version/engine data comes from the cluster API, not the instance API.

use_aws: rds:DescribeDBInstances (region: <region>)   # standalone RDS + Aurora member instances
→ For each instance:
  → Extract: DBInstanceIdentifier, Engine, EngineVersion, DBInstanceClass,
             MultiAZ, DBInstanceArn, DBClusterIdentifier,
             ReadReplicaSourceDBInstanceIdentifier

use_aws: rds:DescribeDBClusters (region: <region>)    # Aurora clusters (authoritative engine version)
→ For each cluster:
  → Extract: DBClusterIdentifier, Engine, EngineVersion, DBClusterArn,
             DBClusterMembers (instance identifiers)

Filter: In scope are the engines and major versions on the RDS and Aurora Extended Support pages (references/billing-rules.md, section 2): today mysql, postgres, aurora-mysql and aurora-postgresql. Other engines (mariadb, oracle-*, sqlserver-*) have no Extended Support charge or Pricing API usagetype, so list them in the discovery summary as out of scope and do not price them. Billed instances: Count every instance on an affected version (primary, standbys, readable standbys, read replicas) by deployment shape, per references/billing-rules.md, section 3. A Multi-AZ DB instance adds one unlisted standby; cluster members, Aurora included, are listed individually and are never doubled. For Aurora, use the cluster's EngineVersion as the authoritative version (instance-level version may lag). A db.serverless instance is priced in ACU-hours (section 5), not vCPUs. Record: account_id, region, resource_id, engine, engine_version, instance_class, multi_az, cluster_id, read_replica_source, billed_instances, arn

Instance class to vCPU mapping (required for cost calculation):

  • Look up the vCPU count for each instance class using documentation search (verify_aws_claim) or the AWS Pricing API product attributes
  • Common mappings: db.t3.micro=2, db.t3.small=2, db.t3.medium=2, db.r5.large=2, db.r5.xlarge=4, db.r5.2xlarge=8, db.r5.4xlarge=16, db.r6g.large=2, db.r6g.xlarge=4, db.r6g.2xlarge=8
  • If an instance class is not in this list, verify its vCPU count rather than guessing

Lambda Discovery

use_aws: lambda:ListFunctions (region: <region>)
→ For each function:
  → Extract: FunctionName, Runtime, FunctionArn, LastModified

Filter: Include functions whose runtime is deprecated or approaching deprecation. Do NOT pattern-match against a hardcoded runtime list — deprecated runtimes change frequently. Instead, collect the DISTINCT set of runtimes in use, then verify each one's deprecation status at runtime via documentation search (verify_aws_claim: "AWS Lambda [runtime] deprecation date" / "AWS Lambda runtime deprecation schedule"). Flag any runtime that is past its deprecation date or has a deprecation date within 6 months. Record: account_id, region, function_name, runtime, arn, last_modified

ElastiCache Discovery

use_aws: elasticache:DescribeCacheClusters (region: <region>, ShowCacheNodeInfo: true)
→ For each cluster:
  → Extract: CacheClusterId, Engine, EngineVersion, CacheNodeType, NumCacheNodes, ARN

use_aws: elasticache:DescribeReplicationGroups (region: <region>)   # for clustered/replicated deployments
→ Extract group-level engine version where applicable

use_aws: elasticache:DescribeServerlessCaches (region: <region>)    # ElastiCache Serverless — SEPARATE resource type
→ For each serverless cache:
  → Extract: ServerlessCacheName, Engine, MajorEngineVersion, FullEngineVersion, ARN

Serverless caches are a distinct resource type — they are NOT returned by DescribeCacheClusters. You MUST call DescribeServerlessCaches separately so every cache is accounted for. Serverless uses MajorEngineVersion / FullEngineVersion (there is no node type / node count), and the Pricing API has no Extended Support usagetype for serverless caches, so never price one as nodes (references/billing-rules.md, section 2). Filter: Discover all engines, but only Redis OSS v4, v5 and v6 have Extended Support (references/billing-rules.md, section 2). Valkey and Memcached are out of scope: list them in the discovery summary as supported, with no charge. Record (node-based): account_id, region, cluster_id, engine, engine_version, node_type, num_nodes, arn Record (serverless): account_id, region, serverless_cache_name, engine, engine_version, is_serverless=true, arn

OpenSearch Discovery

use_aws: opensearch:ListDomainNames (region: <region>)
→ For each domain name:
  use_aws: opensearch:DescribeDomain (DomainName: <name>, region: <region>)
  → Extract: DomainName, EngineVersion, ARN, and every node group in ClusterConfig:
             InstanceType/InstanceCount, DedicatedMasterType/Count, WarmType/Count

Engine family split (date-bleed risk): EngineVersion comes back prefixed as either OpenSearch_2.x or Elasticsearch_7.x. These are TWO different engine families with COMPLETELY different EOS calendars — an OpenSearch version's EOS date must never be applied to an Elasticsearch version, or vice versa. Parse the prefix, treat Elasticsearch_* and OpenSearch_* as separate engines during EOS validation (Phase 3), and verify each independently. The Pricing API usagetype may also differ between the two families — resolve it per family, don't assume one covers both. Record: account_id, region, domain_name, engine_family (OpenSearch|Elasticsearch), engine_version, node groups (type and count each), arn

Discovery Summary

After discovery completes, present a summary that makes COVERAGE provable — a reader must be able to confirm every discovery path actually ran, even when it found nothing chargeable:

  • APIs called per service — explicitly confirm which discovery APIs were invoked, especially the ones that are easy to skip: elasticache:DescribeServerlessCaches, elasticache:DescribeReplicationGroups, rds:DescribeDBClusters. If an API was not called for a region, say so.
  • Total resources found per service (count of ALL discovered, not just affected).
  • Every discovered resource must be accounted for, including SUPPORTED ones. Do NOT silently drop supported resources — list them (at minimum a per-service, per-engine count of SUPPORTED resources) so the reader can tell "scanned and supported" apart from "never scanned". A bare "12 supported" with no breakdown is not acceptable.
  • Unique versions detected (with counts) and their classification.
  • Accounts and regions with resources.

This visibility is what lets a reviewer distinguish a genuine clean result (e.g., serverless caches found but on supported engines) from a coverage gap (serverless never queried). The final report (Phase 5) MUST carry this through: include a "Discovered but SUPPORTED (no charge)" section listing those resources or per-engine counts.


Phase 3: EOS Classification and Pricing Validation

Step 3.1 — Validate EOS Dates

For each unique version discovered, verify its End of Standard Support date independently:

  1. Search AWS documentation for the EXACT engine + version combination discovered (e.g., the specific engine family AND version number as reported by the discovery API — do not generalize).
  2. Search AWS documentation: "[service] version lifecycle" / "[service] supported versions calendar"
  3. Extract: eos_date (end of standard support), es_end_date (end of extended support)

CRITICAL — Per-version verification (avoid date bleed):

  • Verify the EOS date for EACH distinct version SEPARATELY. Never reuse one version's date for another version, even within the same service.
  • Many AWS services have MULTIPLE engine families or generations with completely different lifecycles (for example, a service may have a legacy engine line and a newer engine line that share a console but have separate support calendars). Treat each engine family + version as its own lookup.
  • Match the EOS date to the EXACT version string returned by discovery. If discovery returns an engine name and a version number, both must match the documentation entry you cite.
  • If two versions appear to have the same EOS date, double-check — identical dates across clearly different versions/engines is a red flag that a date was incorrectly copied.

Sanity checks before accepting an EOS date:

  • Does the date make chronological sense given the version's release date? (Older versions should have earlier EOS dates than newer ones of the same engine.)
  • Is a version that is clearly several generations behind the current release being reported as "supported with a future EOS date"? If so, re-verify — it may already be past EOS.

If documentation search returns no result for a specific version:

  • Mark as UNVERIFIED and set the dependent fields per references/billing-rules.md, section 6
  • Note in the report: "EOS date could not be confirmed from AWS documentation — verify manually"
  • Do NOT substitute a similar version's date as a proxy

Step 3.2 — Get Extended Support Pricing (AWS Pricing API)

Use the AWS Pricing API (pricing:GetProducts) for authoritative, real-time Extended Support pricing. Do NOT use documentation search or model knowledge for pricing.

CRITICAL — Response size: The Pricing API returns large JSON. ALWAYS use the exact usagetype patterns below and extract ONLY pricePerUnit. Never dump the full PriceList into context.

Pricing API constraints:

  • Region: call the Pricing API in us-east-1 (its endpoint region)
  • Requires pricing:GetProducts and pricing:GetAttributeValues (plus pricing:DescribeServices for the empty-result fallback below)
  • The regionCode filter targets the region you're pricing for (e.g., us-east-1), separate from the API endpoint region

Per-service pricing lookup:

Service ServiceCode Usage Type Pattern Region Prefix Billing Unit Year 3
EKS AmazonEKS {PREFIX}-AmazonEKS-Hours:extendedSupport Yes, in every region (USE1- in us-east-1) per cluster/hour Flat (no escalation)
RDS AmazonRDS {PREFIX}-ExtendedSupport:Yr1-Yr2:{EngineVersion} / {PREFIX}-ExtendedSupport:Yr3:{EngineVersion}; Aurora Serverless v2 adds ASv2 (references/billing-rules.md, section 5) None in us-east-1, prefixed elsewhere (USW2-, EU-, APS3-...) per vCPU/hour; per ACU-hour for ASv2 2× Yr1-2
ElastiCache AmazonElastiCache {PREFIX}-ExtendedSupportYr1_Yr2-NodeUsage:{nodeType} / {PREFIX}-ExtendedSupportYr3-NodeUsage:{nodeType} Yes, in every region (USE1- in us-east-1) per node/hour 2× Yr1-2
OpenSearch AmazonES {PREFIX}-OpenSearchExtendedSupport (NIH mode); {PREFIX}-ESInstance:{type} (extension-period mode, references/billing-rules.md, section 4) None in us-east-1, prefixed elsewhere (EU-, APS3-...) per NIH/hour or per instance-hour Single tier (no escalation)
Lambda — No Extended Support charge — — —

Whether us-east-1 carries a prefix depends on the service, so never assume. Checked against the live Pricing API:

  • RDS and OpenSearch have NO prefix in us-east-1 (ExtendedSupport:Yr1-Yr2:MySQL8.0, OpenSearchExtendedSupport) and a prefix in every other region (APS3-ExtendedSupport:Yr1-Yr2:MySQL8.0, EU-OpenSearchExtendedSupport). Outside us-east-1 the bare form returns 0 products, which would silently turn the resource into PRICING_UNAVAILABLE or $0.
  • EKS and ElastiCache carry a prefix everywhere, USE1- in us-east-1.
  • RDS {EngineVersion} is the engine plus its major version, not the bare engine: MySQL8.0, MySQL5.7, PostgreSQL13, AuroraMySQL2, AuroraMySQL3, AuroraPostgreSQL13. Build it from the discovered Engine and major EngineVersion, and confirm it with pricing:GetAttributeValues. If no usagetype exists for that engine and version, mark the cost PRICING_UNAVAILABLE instead of guessing.

Region prefix mapping (EKS, RDS, ElastiCache, OpenSearch):

The AUTHORITATIVE way to get the region prefix is the Pricing API — do NOT rely on a hardcoded prefix for any region you are not certain about. For ANY region outside the common set below, call pricing:GetAttributeValues on the usagetype attribute (filtered by ServiceCode) and match the prefix for your target regionCode. This is the DEFAULT method, not a fallback, and it applies to every service.

Common prefixes (safe to use directly; still verify via API if a lookup returns empty). These apply to every service except where the list above says there is no prefix in us-east-1:

  • us-east-1 → USE1 | us-east-2 → USE2 | us-west-1 → USW1 | us-west-2 → USW2
  • eu-west-1 → EU (not EUW1, which returns nothing) | eu-west-2 → EUW2 | eu-central-1 → EUC1
  • ap-northeast-1 → APN1 | ap-southeast-1 → APS1 | ap-southeast-2 → APS2
  • ca-central-1 → CAN1 | sa-east-1 → SAE1

Regions you MUST resolve via the API (prefixes vary / are not reliably guessable — includes ap-south-1, eu-north-1, eu-south-1, me-south-1, af-south-1, ap-east-1, and any region not listed above): call pricing:GetAttributeValues and read the actual usagetype prefix rather than guessing. Never construct a usagetype from an unverified prefix — a wrong prefix silently returns empty and causes a resource to be under-costed as $0.

Lookup procedure per affected resource:

  1. Build the usagetype string using the pattern + region prefix + resource attribute (engine version / node type as applicable). Take the prefix from the rules above: none for RDS and OpenSearch in us-east-1, USE1- for EKS and ElastiCache in us-east-1, and for any other region resolve it via pricing:GetAttributeValues (do not guess). For example ExtendedSupport:Yr1-Yr2:AuroraMySQL2 in us-east-1 but USW2-ExtendedSupport:Yr1-Yr2:AuroraMySQL2 in us-west-2.
  2. Call pricing:GetProducts:
    use_aws: pricing:GetProducts (
      region: "us-east-1",
      ServiceCode: "<ServiceCode>",
      Filters: [
        {Type: "TERM_MATCH", Field: "usagetype", Value: "<constructed usagetype>"},
        {Type: "TERM_MATCH", Field: "regionCode", Value: "<target region>"}
      ]
    )
    
    Apply a jmespath_filter at the API call to return ONLY the price, so the large PriceList JSON never enters context. Pricing API returns each product as a JSON-encoded string in PriceList; filter/parse down to the pricePerUnit.USD value inside the on-demand price dimensions rather than pulling the whole payload. Filtering at the call level is more reliable than asking the model to extract it afterward.
  3. Extract ONLY pricePerUnit.USD and the price dimension's unit (for example vCPU-hour, ACU-hour, Hrs) — ignore everything else. The unit decides what you count in Phase 4 (references/billing-rules.md, section 1)
  4. If the exact usagetype returns empty, call pricing:GetAttributeValues for the usagetype attribute to discover the correct format, then retry. If it is still empty, use pricing:DescribeServices to confirm the ServiceCode and the exact attribute names for that service before constructing another GetAttributeValues/GetProducts call — a wrong ServiceCode or attribute name also returns empty.

Validate the returned rate against the reference table in the Extended Support Pricing section (us-east-1 validation reference). The API result is authoritative — the table is only a sanity check. If the API rate is wildly different from the reference (orders of magnitude), re-check the usagetype before trusting it; never replace the API result with the reference number.

Step 3.3 — Classify Each Resource

Compare each resource's version against the validated EOS dates:

Status Condition Include in Report?
IN_EXTENDED_SUPPORT EOS date is past, ES end date is future YES — currently incurring charges
APPROACHING_EOS EOS date is within 6 months from today YES — will soon incur charges
PAST_EXTENDED_SUPPORT Both EOS and ES end dates are past YES — critical, may be force-upgraded
SUPPORTED EOS date is more than 6 months away, or the engine is out of scope NO — exclude from cost report (list in the discovery summary)
UNVERIFIED No EOS date confirmed for the exact engine and version YES — listed separately, kept out of the totals

Step 3.4 — Search Upgrade Recommendations

For each affected version:

  1. Use verify_aws_claim (or AWS documentation search): "[service] upgrade from [version] to latest"
  2. Extract: recommended target version, key breaking changes, migration guide URL

Phase 4: Cost Calculation

Step 4.1 — Determine pricing tier

Extended Support pricing tiers are based on how long a resource has been past End of Standard Support. The tier structure differs by service:

  • Calculate months since EOS: months_since_eos = (today - eos_date).months
  • Year 1-2 — months_since_eos < 24 (months 0–23 past EOS): base rate
  • Year 3 — months_since_eos >= 24 (month 24 onward past EOS): elevated rate

Boundary is exclusive at 24: month 23 is still Year 1-2, month 24 is the first Year 3 month. No month falls in both tiers.

Tier behavior by service (rates themselves come from the Pricing API; see the validation reference table):

  • EKS: flat — no Year 3 escalation
  • RDS: Year 3 = 2× Year 1-2
  • ElastiCache: Year 3 = 2× Year 1-2 (per node, varies by node type)
  • OpenSearch: single tier — no escalation

Use the exact rate returned by the Pricing API for the resource's tier (Step 3.2). Query the Yr3 usagetype pattern for resources 24+ months past EOS.

When the Year 3 usagetype is not yet published: for a resource whose Year 3 window starts in the future (e.g. it is currently in Year 1-2 but will cross 24 months later), the Yr3 usagetype may not exist in the Pricing API yet and will return empty. This is expected, not a lookup failure — do NOT mark it PRICING_UNAVAILABLE. Use the current Year 1-2 rate for the current cost, and for the Year 3 projection derive it from the service's documented escalation (RDS and ElastiCache: 2× the Year 1-2 rate; EKS and OpenSearch: no escalation, so Year 3 = Year 1-2). Clearly label such a projected Year 3 figure as derived (not API-returned) and note the date the Year 3 rate begins.

Step 4.2 — Calculate per-resource monthly cost

Every cost uses one formula (references/billing-rules.md, section 1): monthly_cost = billed_units_per_hour × es_rate × 730. es_rate is the per-tier rate the Pricing API returned for the resource (Step 3.2), never a hardcoded value, and billed_units_per_hour is counted in that rate's unit:

Service billed_units_per_hour
EKS 1 per cluster
RDS / Aurora provisioned vCPUs of the instance class × billed instances (section 3): one standby added for a Multi-AZ DB instance, read replicas counted, cluster members (Aurora included) each counted once and never doubled
Aurora Serverless v2 Average ACUs consumed (CloudWatch ServerlessDatabaseCapacity, section 5), else USAGE_DEPENDENT
ElastiCache Nodes on Redis OSS v4–v6
OpenSearch NIH mode: Σ node groups of instance_count × normalization factor from the section 4 table (medium = 2, large = 4, xlarge = 8, …). Extension-period mode: instances, at the instance type's own hourly rate
Lambda 0 (flag as security risk only)

Notes:

  • The EKS ES rate is the Extended Support premium (the avoidable cost). The standard cluster cost applies regardless and is NOT part of the exposure.
  • For RDS, map instance class to vCPU count (e.g., db.r6g.xlarge = 4 vCPUs).
  • For OpenSearch, take the charge mode for the version and date from section 4, and never derive a normalization factor from instance count or by extrapolation.

Step 4.2b — Reactive (current) vs Proactive (projected) cost

Every affected resource has TWO cost figures. Compute both so the report shows what the customer pays now AND what they will pay if they do nothing:

  • Current monthly ES cost (reactive) — what the resource is incurring RIGHT NOW.

    • IN_EXTENDED_SUPPORT / PAST_EXTENDED_SUPPORT: the Year 1-2 or Year 3 rate from Step 4.2. This is real spend today.
    • APPROACHING_EOS: $0 — not yet in Extended Support, incurring nothing today.
  • Projected monthly ES cost (proactive) — what the resource WILL cost once it enters Extended Support, if not upgraded first.

    • APPROACHING_EOS: compute the full Year 1-2 ES cost using the SAME per-resource formula (Step 4.2), as if it were already in ES. This is the cost that starts on its EOS date (within the next 6 months).
    • IN_EXTENDED_SUPPORT / PAST_EXTENDED_SUPPORT: projected = current (already incurring). If the resource will cross into Year 3 (>= 24 months) within the projection window, project the Year 3 (elevated) rate and note the escalation.
    • UNVERIFIED, PRICING_UNAVAILABLE and USAGE_DEPENDENT resources: set the fields per references/billing-rules.md, section 6; keep them out of the totals and count them separately.

This split is the whole point of the skill: reactive = stop the bleeding on resources already in ES; proactive = show the cost cliff approaching in the next 6 months so the customer can upgrade before it hits. Report both, never collapse them into one number.

Step 4.3 — Assign urgency

Urgency Condition
CRITICAL IN_EXTENDED_SUPPORT or PAST_EXTENDED_SUPPORT (already incurring charges)
HIGH APPROACHING_EOS within 3 months
MEDIUM APPROACHING_EOS within 3–6 months
UNVERIFIED UNVERIFIED status (no confirmed EOS date): verify manually

Step 4.4 — Aggregate totals

Calculate:

  • Total current monthly ES cost (reactive) — sum of current cost across resources already in/past ES (excludes APPROACHING_EOS, which is $0 now)
  • Total current annual ES cost (current monthly × 12)
  • Total projected monthly ES cost (proactive) — sum of projected cost across ALL affected resources (includes APPROACHING_EOS at their future ES rate). This is the monthly run-rate if nothing is upgraded in the next 6 months.
  • Incremental cost cliff — projected monthly − current monthly = the additional spend that switches on as APPROACHING_EOS resources cross their EOS dates
  • Top 10 most impacted accounts (by projected monthly cost)
  • Cost by service breakdown (current and projected)
  • Cost by urgency breakdown
  • Not in totals: counts of UNVERIFIED, PRICING_UNAVAILABLE and USAGE_DEPENDENT resources, each listed

Phase 5: Report Generation

Step 5.1 — Present Summary

## EOS Cost Impact Summary

| Metric | Value |
|--------|-------|
| Current Monthly ES Cost (incurring now) | $X,XXX |
| Current Annual ES Cost | $XX,XXX |
| Projected Monthly ES Cost (if no upgrades in next 6 mo) | $X,XXX |
| Incremental Cost Cliff (projected − current) | $X,XXX/month |
| Affected Resources | N |
| Affected Accounts | N |
| Services Analyzed | [list] |
| Regions Scanned | N |

### By Urgency
- CRITICAL: N resources — $X,XXX/month (currently incurring charges)
- HIGH: N resources — $X,XXX/month (entering ES within 3 months)
- MEDIUM: N resources — $X,XXX/month (entering ES within 3-6 months)

### By Service
- EKS: N clusters — $X,XXX/month
- RDS: N instances — $X,XXX/month
- ElastiCache: N nodes — $X,XXX/month
- OpenSearch: N instances — $X,XXX/month
- Lambda: N functions — $0 (security risk, no ES charge)

### Top Impacted Accounts
1. [account_name] ([account_id]) — $X,XXX/month
2. ...

Step 5.2 — Generate CSV Artifact

Produce a CSV artifact with these columns:

  • account_id — AWS account ID
  • account_name — Account name (if available from Organizations)
  • resource_arn — Full ARN of the resource
  • resource_name — Human-readable identifier (cluster name, instance ID, function name)
  • region — AWS region
  • service — AWS service (EKS, RDS, Lambda, ElastiCache, OpenSearch)
  • version — Current version/engine version/runtime
  • instance_class — Instance type (for RDS, ElastiCache, OpenSearch) or N/A
  • billed_units — Billed units per hour and their unit (e.g. 12 vCPU-hour for 6 billed instances × 2 vCPUs, 8 NIH, 4 ACU-hour)
  • eos_date — End of Standard Support date, or UNVERIFIED
  • eos_status — IN_EXTENDED_SUPPORT / APPROACHING_EOS / PAST_EXTENDED_SUPPORT / UNVERIFIED
  • es_year — Which pricing year the resource is in: 1-2 or 3. For resources not yet in Extended Support (APPROACHING_EOS), use the sentinel N/A (they are not incurring ES charges yet, so no pricing year applies).
  • current_monthly_es_cost — Monthly ES cost being incurred RIGHT NOW ($0 for APPROACHING_EOS; the Year 1-2/Year 3 rate for resources already in/past ES)
  • projected_monthly_es_cost — Monthly ES cost once the resource enters Extended Support (full ES rate for APPROACHING_EOS; equals current for resources already in ES, or the Year 3 rate if it escalates within the window)
  • es_start_date — Date the projected cost begins (the EOS date for APPROACHING_EOS; blank/N/A if already incurring)
  • annual_es_cost — Annual ES cost based on projected_monthly_es_cost × 12 (the forward-looking run-rate)
  • urgency — CRITICAL / HIGH / MEDIUM / UNVERIFIED
  • recommended_target — Recommended upgrade version
  • upgrade_guide_url — Link to AWS migration documentation

Sort by projected_monthly_es_cost descending (surfaces the biggest future exposure first).

Title the artifact: "EOS Cost Impact Report — [date]"

Step 5.3 — Provide Upgrade Recommendations

For each affected version, include:

  • Current version → Recommended target version
  • Key considerations or breaking changes
  • AWS documentation link for the upgrade path
  • Estimated effort: mention sequential upgrade requirements if applicable

Step 5.4 — Lambda Security Risk Section (if applicable)

If deprecated Lambda runtimes were found:

## Security Risk: Deprecated Lambda Runtimes

These functions are running on deprecated runtimes that no longer receive
security patches. While no Extended Support charge applies, they represent
a security and compliance risk.

| Function | Runtime | Last Modified | Account |
|----------|---------|---------------|---------|
| ... | python3.8 | 2023-... | ... |

Error Handling

Error Behavior
Role assumption fails for a linked account Skip that account, note in report: "Account [ID] skipped — role assumption failed"
Service API returns AccessDenied Skip that service/region, note: "Insufficient permissions for [service] in [region]"
Documentation search returns no EOS date Mark version as UNVERIFIED (status, urgency and cost fields per references/billing-rules.md, section 6), include in report with note
Pricing API returns no product after the GetAttributeValues usagetype retry Set current_monthly_es_cost and projected_monthly_es_cost to "PRICING_UNAVAILABLE", do not estimate
No usage data for a usage-priced resource (Aurora Serverless v2) Set the cost fields to USAGE_DEPENDENT with the rate and capacity range
No resources found for a service Report: "No [service] resources found in scope"
Rate limiting (throttling) Retry with exponential backoff (1s, 2s, 4s), max 3 retries
Timeout on large organization (1000+ accounts) Report partial results, note which accounts were completed

Tips

  • Start with a single service for faster results — EKS is the simplest (flat per-cluster rate)
  • RDS has the most complex pricing due to vCPU mapping and counting every billed instance (standbys and read replicas)
  • Lambda has no cost impact but deprecated runtimes are a security risk worth flagging
  • Year 3 doubles the rate for RDS and ElastiCache (EKS and OpenSearch stay flat), so a resource 24+ months past EOS costs twice what it did in Year 1-2
  • Verify EOS dates from AWS documentation at runtime — deprecation schedules shift, so confirm each engine + version rather than trusting cached knowledge