Implement P4 AWS scanner: ECS/Lambda/RDS discovery with tag-based ownership
- ECS: list clusters → list services → describe → extract tags, capture task def + counts - Lambda: paginated list functions → list tags, capture runtime/memory/timeout - RDS: describe instances → list tags, capture engine/class/storage/multi-AZ - Owner resolution from aws tags (owner/team/Owner/Team) - Partial failure handling preserved (per-service try/catch)
This commit is contained in:
@@ -1,15 +1,32 @@
|
||||
import pino from 'pino';
|
||||
import {
|
||||
ECSClient,
|
||||
ListClustersCommand,
|
||||
ListServicesCommand,
|
||||
DescribeServicesCommand,
|
||||
ListTagsForResourceCommand,
|
||||
} from '@aws-sdk/client-ecs';
|
||||
import {
|
||||
LambdaClient,
|
||||
ListFunctionsCommand,
|
||||
ListTagsCommand,
|
||||
} from '@aws-sdk/client-lambda';
|
||||
import {
|
||||
RDSClient,
|
||||
DescribeDBInstancesCommand,
|
||||
ListTagsForResourceCommand as RDSListTagsCommand,
|
||||
} from '@aws-sdk/client-rds';
|
||||
|
||||
const logger = pino({ name: 'discovery-aws' });
|
||||
|
||||
export interface DiscoveredService {
|
||||
name: string;
|
||||
type: string; // 'ecs-service', 'lambda', 'rds', 'ec2', etc.
|
||||
type: string;
|
||||
arn: string;
|
||||
region: string;
|
||||
account: string;
|
||||
tags: Record<string, string>;
|
||||
owner?: string; // From tags or CODEOWNERS
|
||||
owner?: string;
|
||||
ownerSource?: 'aws-tag' | 'codeowners' | 'config' | 'heuristic';
|
||||
metadata: Record<string, any>;
|
||||
discoveredAt: Date;
|
||||
@@ -22,51 +39,46 @@ export interface ScanResult {
|
||||
services: DiscoveredService[];
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS Discovery Scanner.
|
||||
* Scans ECS services, Lambda functions, RDS instances.
|
||||
* Partial failures preserve existing catalog (BMad must-have).
|
||||
*/
|
||||
export class AwsDiscoveryScanner {
|
||||
private ecsClient: any;
|
||||
private lambdaClient: any;
|
||||
private rdsClient: any;
|
||||
private ecsClient: ECSClient;
|
||||
private lambdaClient: LambdaClient;
|
||||
private rdsClient: RDSClient;
|
||||
private region: string;
|
||||
|
||||
constructor(clients: { ecs: any; lambda: any; rds: any }) {
|
||||
this.ecsClient = clients.ecs;
|
||||
this.lambdaClient = clients.lambda;
|
||||
this.rdsClient = clients.rds;
|
||||
constructor(region: string, credentials?: any) {
|
||||
const config = { region, ...(credentials ? { credentials } : {}) };
|
||||
this.ecsClient = new ECSClient(config);
|
||||
this.lambdaClient = new LambdaClient(config);
|
||||
this.rdsClient = new RDSClient(config);
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
async scan(region: string, account: string): Promise<ScanResult> {
|
||||
async scan(account: string): Promise<ScanResult> {
|
||||
const services: DiscoveredService[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
// Scan ECS
|
||||
try {
|
||||
const ecsServices = await this.scanEcs(region, account);
|
||||
const ecsServices = await this.scanEcs(account);
|
||||
services.push(...ecsServices);
|
||||
} catch (err) {
|
||||
errors.push(`ECS scan failed: ${(err as Error).message}`);
|
||||
logger.warn({ region, error: (err as Error).message }, 'ECS scan failed');
|
||||
logger.warn({ region: this.region, error: (err as Error).message }, 'ECS scan failed');
|
||||
}
|
||||
|
||||
// Scan Lambda
|
||||
try {
|
||||
const lambdaFns = await this.scanLambda(region, account);
|
||||
const lambdaFns = await this.scanLambda(account);
|
||||
services.push(...lambdaFns);
|
||||
} catch (err) {
|
||||
errors.push(`Lambda scan failed: ${(err as Error).message}`);
|
||||
logger.warn({ region, error: (err as Error).message }, 'Lambda scan failed');
|
||||
logger.warn({ region: this.region, error: (err as Error).message }, 'Lambda scan failed');
|
||||
}
|
||||
|
||||
// Scan RDS
|
||||
try {
|
||||
const rdsInstances = await this.scanRds(region, account);
|
||||
const rdsInstances = await this.scanRds(account);
|
||||
services.push(...rdsInstances);
|
||||
} catch (err) {
|
||||
errors.push(`RDS scan failed: ${(err as Error).message}`);
|
||||
logger.warn({ region, error: (err as Error).message }, 'RDS scan failed');
|
||||
logger.warn({ region: this.region, error: (err as Error).message }, 'RDS scan failed');
|
||||
}
|
||||
|
||||
const status = errors.length === 0
|
||||
@@ -78,18 +90,138 @@ export class AwsDiscoveryScanner {
|
||||
return { status, discovered: services.length, errors, services };
|
||||
}
|
||||
|
||||
private async scanEcs(region: string, account: string): Promise<DiscoveredService[]> {
|
||||
// TODO: List clusters → list services → describe services → extract tags
|
||||
return [];
|
||||
private async scanEcs(account: string): Promise<DiscoveredService[]> {
|
||||
const results: DiscoveredService[] = [];
|
||||
|
||||
const clusters = await this.ecsClient.send(new ListClustersCommand({}));
|
||||
for (const clusterArn of clusters.clusterArns ?? []) {
|
||||
const svcList = await this.ecsClient.send(new ListServicesCommand({ cluster: clusterArn }));
|
||||
if (!svcList.serviceArns?.length) continue;
|
||||
|
||||
const described = await this.ecsClient.send(new DescribeServicesCommand({
|
||||
cluster: clusterArn,
|
||||
services: svcList.serviceArns,
|
||||
}));
|
||||
|
||||
for (const svc of described.services ?? []) {
|
||||
const tags = await this.getEcsTags(svc.serviceArn!);
|
||||
const owner = tags['owner'] ?? tags['team'] ?? tags['Owner'] ?? tags['Team'];
|
||||
|
||||
results.push({
|
||||
name: svc.serviceName!,
|
||||
type: 'ecs-service',
|
||||
arn: svc.serviceArn!,
|
||||
region: this.region,
|
||||
account,
|
||||
tags,
|
||||
owner,
|
||||
ownerSource: owner ? 'aws-tag' : undefined,
|
||||
metadata: {
|
||||
cluster: clusterArn.split('/').pop(),
|
||||
desiredCount: svc.desiredCount,
|
||||
runningCount: svc.runningCount,
|
||||
launchType: svc.launchType,
|
||||
taskDefinition: svc.taskDefinition,
|
||||
},
|
||||
discoveredAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async scanLambda(region: string, account: string): Promise<DiscoveredService[]> {
|
||||
// TODO: List functions → get tags → map to DiscoveredService
|
||||
return [];
|
||||
private async scanLambda(account: string): Promise<DiscoveredService[]> {
|
||||
const results: DiscoveredService[] = [];
|
||||
let marker: string | undefined;
|
||||
|
||||
do {
|
||||
const response = await this.lambdaClient.send(new ListFunctionsCommand({ Marker: marker }));
|
||||
|
||||
for (const fn of response.Functions ?? []) {
|
||||
let tags: Record<string, string> = {};
|
||||
try {
|
||||
const tagResponse = await this.lambdaClient.send(new ListTagsCommand({ Resource: fn.FunctionArn }));
|
||||
tags = (tagResponse.Tags ?? {}) as Record<string, string>;
|
||||
} catch {
|
||||
// Some functions may not allow tag listing
|
||||
}
|
||||
|
||||
const owner = tags['owner'] ?? tags['team'] ?? tags['Owner'] ?? tags['Team'];
|
||||
|
||||
results.push({
|
||||
name: fn.FunctionName!,
|
||||
type: 'lambda',
|
||||
arn: fn.FunctionArn!,
|
||||
region: this.region,
|
||||
account,
|
||||
tags,
|
||||
owner,
|
||||
ownerSource: owner ? 'aws-tag' : undefined,
|
||||
metadata: {
|
||||
runtime: fn.Runtime,
|
||||
memorySize: fn.MemorySize,
|
||||
timeout: fn.Timeout,
|
||||
lastModified: fn.LastModified,
|
||||
handler: fn.Handler,
|
||||
},
|
||||
discoveredAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
marker = response.NextMarker;
|
||||
} while (marker);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async scanRds(region: string, account: string): Promise<DiscoveredService[]> {
|
||||
// TODO: Describe DB instances → extract tags
|
||||
return [];
|
||||
private async scanRds(account: string): Promise<DiscoveredService[]> {
|
||||
const results: DiscoveredService[] = [];
|
||||
|
||||
const response = await this.rdsClient.send(new DescribeDBInstancesCommand({}));
|
||||
|
||||
for (const db of response.DBInstances ?? []) {
|
||||
let tags: Record<string, string> = {};
|
||||
try {
|
||||
const tagResponse = await this.rdsClient.send(new RDSListTagsCommand({ ResourceName: db.DBInstanceArn }));
|
||||
tags = Object.fromEntries((tagResponse.TagList ?? []).map(t => [t.Key!, t.Value!]));
|
||||
} catch {
|
||||
// Tag listing may fail
|
||||
}
|
||||
|
||||
const owner = tags['owner'] ?? tags['team'] ?? tags['Owner'] ?? tags['Team'];
|
||||
|
||||
results.push({
|
||||
name: db.DBInstanceIdentifier!,
|
||||
type: `rds/${db.DBInstanceClass}`,
|
||||
arn: db.DBInstanceArn!,
|
||||
region: this.region,
|
||||
account,
|
||||
tags,
|
||||
owner,
|
||||
ownerSource: owner ? 'aws-tag' : undefined,
|
||||
metadata: {
|
||||
engine: db.Engine,
|
||||
engineVersion: db.EngineVersion,
|
||||
instanceClass: db.DBInstanceClass,
|
||||
storageType: db.StorageType,
|
||||
allocatedStorage: db.AllocatedStorage,
|
||||
multiAZ: db.MultiAZ,
|
||||
status: db.DBInstanceStatus,
|
||||
},
|
||||
discoveredAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async getEcsTags(arn: string): Promise<Record<string, string>> {
|
||||
try {
|
||||
const response = await this.ecsClient.send(new ListTagsForResourceCommand({ resourceArn: arn }));
|
||||
return Object.fromEntries((response.tags ?? []).map(t => [t.key!, t.value!]));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user