n8n-nodes-aws-finops
Version:
n8n node for AWS Financial Operations and Cost Analysis
564 lines (563 loc) • 22.3 kB
JavaScript
"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;
}