UNPKG

n8n-nodes-aws-finops

Version:

n8n node for AWS Financial Operations and Cost Analysis

564 lines (563 loc) 22.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AwsFinOps = void 0; // nodes/AwsFinOps/AwsFinOps.node.ts const n8n_workflow_1 = require("n8n-workflow"); // Import AWS SDK or the specific libraries used by the MCP server const client_cost_explorer_1 = require("@aws-sdk/client-cost-explorer"); const client_ec2_1 = require("@aws-sdk/client-ec2"); const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2"); // import { ELBv2Client, DescribeLoadBalancersCommand } from '@aws-sdk/client-elastic-load-balancing-v2'; class AwsFinOps { constructor() { this.description = { displayName: 'AWS FinOps', name: 'awsFinOps', icon: 'file:awsfinops.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'AWS Financial Operations and Cost Analysis', defaults: { name: 'AWS FinOps', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'aws', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Get Cost Data', value: 'getCostData', description: 'Retrieve AWS cost and usage data', action: 'Get cost data', }, { name: 'Run FinOps Audit', value: 'runFinOpsAudit', description: 'Run comprehensive FinOps audit', action: 'Run FinOps audit', }, { name: 'Get Unused Resources', value: 'getUnusedResources', description: 'Find unused/underutilized resources', action: 'Get unused resources', }, ], default: 'getCostData', }, { displayName: 'Time Range', name: 'timeRange', type: 'options', displayOptions: { show: { operation: ['getCostData'], }, }, options: [ { name: 'Last 7 Days', value: '7', }, { name: 'Last 30 Days', value: '30', }, { name: 'Last 90 Days', value: '90', }, { name: 'Custom Range', value: 'custom', }, ], default: '30', }, { displayName: 'Start Date', name: 'startDate', type: 'dateTime', displayOptions: { show: { operation: ['getCostData'], timeRange: ['custom'], }, }, default: '', description: 'Start date for cost analysis', }, { displayName: 'End Date', name: 'endDate', type: 'dateTime', displayOptions: { show: { operation: ['getCostData'], timeRange: ['custom'], }, }, default: '', description: 'End date for cost analysis', }, { displayName: 'Group By', name: 'groupBy', type: 'options', displayOptions: { show: { operation: ['getCostData'], }, }, options: [ { name: 'Service', value: 'SERVICE', }, { name: 'Region', value: 'REGION', }, { name: 'Instance Type', value: 'INSTANCE_TYPE', }, { name: 'Usage Type', value: 'USAGE_TYPE', }, ], default: 'SERVICE', }, { displayName: 'Regions', name: 'regions', type: 'multiOptions', displayOptions: { show: { operation: ['runFinOpsAudit', 'getUnusedResources'], }, }, options: [ { name: 'US East (N. Virginia)', value: 'us-east-1', }, { name: 'US West (Oregon)', value: 'us-west-2', }, { name: 'EU (Ireland)', value: 'eu-west-1', }, { name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1', }, // Add more regions as needed ], default: ['us-east-1'], }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; // Get AWS credentials const credentials = await this.getCredentials('aws'); const awsConfig = { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, region: credentials.region || 'us-east-1', }; for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); let responseData = {}; switch (operation) { case 'getCostData': responseData = await getCostData(this, awsConfig, i); break; case 'runFinOpsAudit': responseData = await runFinOpsAudit(this, awsConfig, i); break; case 'getUnusedResources': responseData = await getUnusedResources(this, awsConfig, i); break; default: throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex: i, }); } returnData.push({ json: responseData, pairedItem: { item: i }, }); } catch (error) { if (this.continueOnFail() && error instanceof Error) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, }); continue; } throw error; } } return [returnData]; } } exports.AwsFinOps = AwsFinOps; async function getCostData(context, awsConfig, itemIndex) { const timeRange = context.getNodeParameter('timeRange', itemIndex); const groupBy = context.getNodeParameter('groupBy', itemIndex); // Calculate date range let startDate, endDate; const today = new Date(); if (timeRange === 'custom') { startDate = context.getNodeParameter('startDate', itemIndex).split('T')[0]; endDate = context.getNodeParameter('endDate', itemIndex).split('T')[0]; } else { const daysBack = parseInt(timeRange); const start = new Date(today); start.setDate(today.getDate() - daysBack); startDate = start.toISOString().split('T')[0]; endDate = today.toISOString().split('T')[0]; } try { // Create Cost Explorer client with retry logic const costClient = new client_cost_explorer_1.CostExplorerClient({ ...awsConfig, maxAttempts: 3, }); // Build the command with proper error handling const commandParams = { TimePeriod: { Start: startDate, End: endDate, }, Granularity: 'DAILY', Metrics: ['BlendedCost', 'UnblendedCost'], }; // Add GroupBy only if specified if (groupBy && groupBy !== 'NONE') { commandParams.GroupBy = [{ Type: 'DIMENSION', Key: groupBy, }]; } const command = new client_cost_explorer_1.GetCostAndUsageCommand(commandParams); const response = await costClient.send(command); // Process and format the response const processedResults = processCostResults(response.ResultsByTime || []); return { operation: 'getCostData', timeRange: { startDate, endDate }, groupBy, rawResults: response.ResultsByTime, processedResults, totalCost: calculateTotalCost(response.ResultsByTime || []), currency: response.ResultsByTime?.[0]?.Total?.BlendedCost?.Unit || 'USD', }; } catch (error) { if (error instanceof Error) { throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Failed to fetch cost data: ${error.message}`, { itemIndex }); } else { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Unknown error occurred'); } } } async function runFinOpsAudit(context, awsConfig, itemIndex) { const regions = context.getNodeParameter('regions', itemIndex); const auditResults = { operation: 'runFinOpsAudit', regions, findings: {}, }; // Run audit for each region for (const region of regions) { const regionConfig = { ...awsConfig, region }; auditResults.findings[region] = await auditRegion(context, regionConfig); } return auditResults; } async function getUnusedResources(context, awsConfig, itemIndex) { const regions = context.getNodeParameter('regions', itemIndex); const unusedResources = { operation: 'getUnusedResources', regions, resources: {}, }; for (const region of regions) { const regionConfig = { ...awsConfig, region }; unusedResources.resources[region] = await findUnusedResources(regionConfig); } return unusedResources; } async function auditRegion(context, awsConfig) { const auditResults = { stoppedInstances: [], unattachedVolumes: [], unassociatedEIPs: [], underutilizedInstances: [], errors: [], }; try { const ec2Client = new client_ec2_1.EC2Client(awsConfig); // Find stopped EC2 instances try { const stoppedInstancesCommand = new client_ec2_1.DescribeInstancesCommand({ Filters: [ { Name: 'instance-state-name', Values: ['stopped'], }, ], }); const stoppedInstances = await ec2Client.send(stoppedInstancesCommand); auditResults.stoppedInstances = stoppedInstances.Reservations?.flatMap(r => r.Instances?.map(i => ({ instanceId: i.InstanceId, instanceType: i.InstanceType, launchTime: i.LaunchTime, platform: i.Platform || 'Linux', state: i.State?.Name, })) || []) || []; } catch (error) { if (error instanceof Error) { auditResults.errors.push(`Failed to fetch stopped instances: ${error.message}`); } else { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Unknown error occurred'); } } // Find unattached EBS volumes try { const { DescribeVolumesCommand } = await Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ec2'))); const volumesCommand = new DescribeVolumesCommand({ Filters: [ { Name: 'status', Values: ['available'], }, ], }); const volumes = await ec2Client.send(volumesCommand); auditResults.unattachedVolumes = volumes.Volumes?.map(v => ({ volumeId: v.VolumeId, size: v.Size, volumeType: v.VolumeType, createTime: v.CreateTime, encrypted: v.Encrypted, })) || []; } catch (error) { if (error instanceof Error) { auditResults.errors.push(`Failed to fetch unattached volumes: ${error.message}`); } else { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Unknown error occurred'); } } // Find unassociated Elastic IPs try { const { DescribeAddressesCommand } = await Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ec2'))); const addressesCommand = new DescribeAddressesCommand({ Filters: [ { Name: 'domain', Values: ['vpc'], }, ], }); const addresses = await ec2Client.send(addressesCommand); auditResults.unassociatedEIPs = addresses.Addresses?.filter(a => !a.AssociationId).map(a => ({ allocationId: a.AllocationId, publicIp: a.PublicIp, domain: a.Domain, })) || []; } catch (error) { if (error instanceof Error) { auditResults.errors.push(`Failed to fetch unassociated EIPs: ${error.message}`); } else { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Unknown error occurred'); } } } catch (error) { if (error instanceof Error) { auditResults.errors.push(`General audit error: ${error.message}`); } else { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Unknown error occurred'); } } return auditResults; } async function findUnusedResources(awsConfig) { const ec2Client = new client_ec2_1.EC2Client(awsConfig); const result = { unusedEBSVolumes: [], unattachedEIPs: [], idleLoadBalancers: [], errors: [], }; // Unused Volumes (status = available) try { const volumesCommand = new client_ec2_1.DescribeVolumesCommand({ Filters: [ { Name: 'status', Values: ['available'], }, ], }); const volumes = await ec2Client.send(volumesCommand); result.unusedEBSVolumes = volumes.Volumes?.map(v => ({ volumeId: v.VolumeId, size: v.Size, availabilityZone: v.AvailabilityZone, createTime: v.CreateTime, })) || []; } catch (error) { if (error instanceof Error) { result.errors.push(`Failed to fetch unused EBS volumes: ${error.message}`); } else { result.errors.push('Unknown error fetching unused EBS volumes.'); } } // Unattached EIPs try { const addressesCommand = new client_ec2_1.DescribeAddressesCommand({}); const addresses = await ec2Client.send(addressesCommand); result.unattachedEIPs = addresses.Addresses?.filter(a => !a.AssociationId).map(a => ({ publicIp: a.PublicIp, allocationId: a.AllocationId, })) || []; } catch (error) { if (error instanceof Error) { result.errors.push(`Failed to fetch unattached EIPs: ${error.message}`); } else { result.errors.push('Unknown error fetching unattached EIPs.'); } } // Idle Load Balancers (ALBs/NLBs without any target groups or targets) try { const elbv2Client = new client_elastic_load_balancing_v2_1.ElasticLoadBalancingV2Client(awsConfig); const lbCommand = new client_elastic_load_balancing_v2_1.DescribeLoadBalancersCommand({}); const lbsResponse = await elbv2Client.send(lbCommand); const loadBalancers = lbsResponse.LoadBalancers ?? []; const idleLoadBalancers = []; for (const lb of loadBalancers) { const targetGroupCommand = new client_elastic_load_balancing_v2_1.DescribeTargetGroupsCommand({ LoadBalancerArn: lb.LoadBalancerArn, }); const targetGroupsResponse = await elbv2Client.send(targetGroupCommand); const targetGroups = targetGroupsResponse.TargetGroups ?? []; let allTargetsEmpty = true; for (const tg of targetGroups) { const targetHealthCommand = new client_elastic_load_balancing_v2_1.DescribeTargetHealthCommand({ TargetGroupArn: tg.TargetGroupArn, }); const targetHealthResponse = await elbv2Client.send(targetHealthCommand); const targetDescriptions = targetHealthResponse.TargetHealthDescriptions ?? []; if (targetDescriptions.length > 0) { allTargetsEmpty = false; break; } } if (allTargetsEmpty) { idleLoadBalancers.push({ loadBalancerName: lb.LoadBalancerName, dnsName: lb.DNSName, createdTime: lb.CreatedTime, state: lb.State?.Code, type: lb.Type, }); } } result.idleLoadBalancers = idleLoadBalancers; } catch (error) { if (error instanceof Error) { result.errors.push(`Failed to fetch idle Load Balancers: ${error.message}`); } else { result.errors.push('Unknown error fetching idle Load Balancers.'); } } return result; } function processCostResults(results) { return results.map(result => ({ date: result.TimePeriod?.Start, total: parseFloat(result.Total?.BlendedCost?.Amount || '0'), groups: result.Groups?.map((group) => ({ key: group.Keys?.[0] || 'Unknown', amount: parseFloat(group.Metrics?.BlendedCost?.Amount || '0'), unit: group.Metrics?.BlendedCost?.Unit || 'USD', })) || [], })); } function calculateTotalCost(results) { let total = 0; for (const result of results) { if (result.Groups && result.Groups.length > 0) { for (const group of result.Groups) { const amount = parseFloat(group.Metrics?.BlendedCost?.Amount || '0'); total += amount; } } else if (result.Total?.BlendedCost?.Amount) { total += parseFloat(result.Total.BlendedCost.Amount); } } return Math.round(total * 100) / 100; }