Skip to content

Commit

Permalink
Merge pull request #533 from guardian/aa/event-forwarder
Browse files Browse the repository at this point in the history
feat(event-forwarder): Forward CFN events and ASG activity to Central ELK
  • Loading branch information
akash1810 authored Sep 24, 2024
2 parents 3e88f02 + 4ff5df4 commit 74eab60
Show file tree
Hide file tree
Showing 14 changed files with 2,643 additions and 184 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ jobs:
- dist/cdk-playground
cdk-playground-lambda:
- lambda/cdk-playground-lambda.zip
event-forwarder:
- cdk/dist/event-forwarder.zip
9 changes: 8 additions & 1 deletion cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import 'source-map-support/register';
import { GuRoot } from '@guardian/cdk/lib/constructs/root';
import { CdkPlayground } from '../lib/cdk-playground';
import { EventForwarder } from '../lib/event-forwarder';

const app = new GuRoot();
new CdkPlayground(app, 'CdkPlayground', {

const eventForwarder = new EventForwarder(app);

const applicationStack = new CdkPlayground(app, 'CdkPlayground', {
cloudFormationStackName: 'playground-PROD-cdk-playground',
buildIdentifier: process.env.GITHUB_RUN_NUMBER ?? 'DEV',
});

// Configure Riff-Raff to deploy the application stack after the EventForwarder stack has finished.
applicationStack.addDependency(eventForwarder);
3 changes: 3 additions & 0 deletions cdk/event-forwarder/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
STACK=playground
STAGE=DEV
APP=event-forwarder
23 changes: 23 additions & 0 deletions cdk/event-forwarder/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { env } from 'process';

export interface Config {
stack: string;
stage: string;
app: string;
}

function getEnvOrThrow(key: string): string {
const value = env[key];
if (value === undefined) {
throw new Error(`Environment variable ${key} is not set`);
}
return value;
}

export function getConfig(): Config {
return {
stack: getEnvOrThrow('STACK'),
stage: getEnvOrThrow('STAGE'),
app: getEnvOrThrow('APP'),
};
}
103 changes: 103 additions & 0 deletions cdk/event-forwarder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
AutoScalingClient,
DescribeAutoScalingGroupsCommand,
} from '@aws-sdk/client-auto-scaling';
import { fromIni } from '@aws-sdk/credential-providers';
import type { AwsCredentialIdentityProvider } from '@smithy/types';
import { getConfig } from './config';
import type {
AutoscalingEvent,
CloudformationEvent,
HydratedEvent,
} from './types';

interface AwsClientConfig {
region: string;
credentials?: AwsCredentialIdentityProvider;
}

export async function main(event: CloudformationEvent | AutoscalingEvent) {
const { stage } = getConfig();

try {
const hydrated: HydratedEvent = {
...event,
cloudformationStackName: await getCloudformationStackName(event, stage),
};
console.log(JSON.stringify(hydrated));
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
console.log(JSON.stringify(event));
}
}

async function getCloudformationStackName(
event: CloudformationEvent | AutoscalingEvent,
stage: string,
) {
switch (event.source) {
case 'aws.autoscaling': {
return await getCloudformationStackNameForAsg(
event as AutoscalingEvent,
stage,
);
}
case 'aws.cloudformation': {
return getCloudformationStackNameForStack(event as CloudformationEvent);
}
default: {
throw new Error('Unrecognised event source');
}
}
}

async function getCloudformationStackNameForAsg(
event: AutoscalingEvent,
stage: string,
): Promise<string> {
const awsConfig: AwsClientConfig = {
region: 'eu-west-1',
...(stage === 'DEV' && {
credentials: fromIni({ profile: 'developerPlayground' }),
}),
};

const { AutoScalingGroupName } = event.detail;

const client = new AutoScalingClient(awsConfig);
const command = new DescribeAutoScalingGroupsCommand({
AutoScalingGroupNames: [AutoScalingGroupName],
});
const { AutoScalingGroups = [] } = await client.send(command);

if (AutoScalingGroups.length !== 1) {
throw new Error('Unable to locate unique autoscaling group');
}

const [cfnStackName] = AutoScalingGroups.flatMap((_) => _.Tags ?? [])
.filter((_) => _.Key === 'aws:cloudformation:stack-name')
.map((_) => _.Value);

if (!cfnStackName) {
throw new Error(
'Unable to locate unique tag: aws:cloudformation:stack-name',
);
}

return cfnStackName;
}

async function getCloudformationStackNameForStack(
event: CloudformationEvent,
): Promise<string> {
const stackId = event.detail['stack-id'];
const [, stackName] = stackId.split('/');

if (!stackName) {
throw new Error(`Unable to parse stack-id: ${stackId}`);
}

return Promise.resolve(stackName);
}
37 changes: 37 additions & 0 deletions cdk/event-forwarder/run-locally.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
This is a little helper to run a lambda locally.
It purposefully does only one thing to keep it simple.
*/

import type { CloudformationEvent } from './types';
import { main } from './index';

export const sampleCloudformationEvent: CloudformationEvent = {
version: '0',
id: 'unknown',
'detail-type': 'CloudFormation Resource Status Change',
source: 'aws.cloudformation',
account: '000000000000',
time: '2024-08-24T11:38:34Z',
region: 'eu-west-1',
resources: [
'arn:aws:cloudformation:eu-west-1:000000000000:stack/playground-CODE-basic-asg-rolling-update/000000-000000',
],
detail: {
'stack-id':
'arn:aws:cloudformation:eu-west-1:000000000000:stack/playground-CODE-basic-asg-rolling-update/000000-000000',
'logical-resource-id': 'AutoScalingGroupBasicASG123',
'physical-resource-id':
'playground-CODE-basic-asg-rolling-update-AutoScalingGroupBasicASG123-ABC',
'resource-type': 'AWS::AutoScaling::AutoScalingGroup',
'status-details': {
status: 'UPDATE_IN_PROGRESS',
'detailed-status': '',
'status-reason':
'New instance(s) added to autoscaling group - Waiting on 5 resource signal(s) with a timeout of PT5M.',
},
},
};

void main(sampleCloudformationEvent);
36 changes: 36 additions & 0 deletions cdk/event-forwarder/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { EventBridgeEvent } from 'aws-lambda';

export type CloudformationEvent = EventBridgeEvent<
string,
{
'stack-id': string;
'logical-resource-id': string;
'physical-resource-id': string;
'resource-type': string;
'status-details': {
status: string;
'detailed-status': string;
'status-reason': string;
[additional: string]: string;
};
}
>;

export type AutoscalingEvent = EventBridgeEvent<
string,
{
StatusCode: string;
AutoScalingGroupName: string;
ActivityId: string;
Details: Record<string, unknown>;
RequestId: string;
EndTime: string;
EC2InstanceId: string;
StartTime: string;
Cause: string;
}
>;

export type HydratedEvent = (CloudformationEvent | AutoscalingEvent) & {
cloudformationStackName: string;
};
Loading

0 comments on commit 74eab60

Please sign in to comment.