Skip to content

Commit 76ed137

Browse files
authored
KMS-654: Ensure KMS logs from the lambdas are available in Splunk (#101)
* KMS-654: Initial revision * KMS-654: Setup log group name, log account * KMS-654: Updated to fix deployment issue with account * KMS-654: Add secLog account * KMS-654: Update account setup * KMS-654: Fix lint issues * KMS-654: Update for conddition 'useLocalStack' * KMS-654: Fix linter issue * KMS-654: Use one param for log account * KMS-654: Use dummy value for logDestinationArn for local stack * KMS-654: Refactoring * KMS-654: Use subscriptionFilter * KMS-654: Rename log groups * KMS-654: Update log group * KMS-654: Update log group name * KMS-654: Increase log detention to one month
1 parent df3adc1 commit 76ed137

6 files changed

Lines changed: 181 additions & 4 deletions

File tree

bin/deploy-bamboo.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ dockerRun() {
7373
--env "LOG_LEVEL=$bamboo_LOG_LEVEL" \
7474
--env "KMS_REDIS_ENABLED=$bamboo_KMS_REDIS_ENABLED" \
7575
--env "KMS_REDIS_NODE_TYPE=$bamboo_KMS_REDIS_NODE_TYPE" \
76+
--env "LOG_DESTINATION_ARN=$bamboo_LOG_DESTINATION_ARN" \
7677
$dockerTag "$@"
7778
}
7879

cdk/app/lib/CmrEventProcessingStack.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'
1010
import * as sqs from 'aws-cdk-lib/aws-sqs'
1111
import { Construct } from 'constructs'
1212

13+
import { LogForwardingSetup } from './helper/LogForwardingSetup'
14+
1315
/**
1416
* Properties for the CMR event processing stack.
1517
*/
1618
export interface CmrEventProcessingStackProps extends cdk.StackProps {
1719
prefix: string
1820
stage: string
1921
topicArn: string
22+
logDestinationArn: string
2023
}
2124

2225
/**
@@ -38,6 +41,7 @@ export class CmrEventProcessingStack extends cdk.Stack {
3841
constructor(scope: Construct, id: string, props: CmrEventProcessingStackProps) {
3942
super(scope, id, props)
4043

44+
const useLocalstack = this.node.tryGetContext('useLocalstack') === 'true'
4145
const queueName = `${props.prefix}-${props.stage}-cmr-keyword-events`
4246
const topic = sns.Topic.fromTopicArn(this, 'KeywordEventsTopic', props.topicArn)
4347

@@ -55,7 +59,7 @@ export class CmrEventProcessingStack extends cdk.Stack {
5559
})
5660

5761
const listenerLambda = new NodejsFunction(this, `${props.prefix}-cmr-keyword-events-processor`, {
58-
functionName: `${props.prefix}-cmr-keyword-events-processor`,
62+
functionName: `${props.prefix}-${props.stage}-cmr-keyword-events-processor`,
5963
entry: path.join(__dirname, '../../../serverless/src/cmrKeywordEventsListener/handler.js'),
6064
handler: 'cmrKeywordEventsListener',
6165
runtime: lambda.Runtime.NODEJS_22_X,
@@ -72,6 +76,20 @@ export class CmrEventProcessingStack extends cdk.Stack {
7276

7377
queue.grantConsumeMessages(listenerLambda)
7478

79+
// Set up CloudWatch Logs forwarding to Splunk via NGAP SecLog account
80+
// Skip log forwarding for localstack deployments
81+
if (!useLocalstack) {
82+
// eslint-disable-next-line no-new
83+
new LogForwardingSetup(this, 'LogForwarding', {
84+
prefix: props.prefix,
85+
stage: props.stage,
86+
logDestinationArn: props.logDestinationArn,
87+
lambdas: {
88+
'cmrKeywordEventsListener/handler.js::cmr-keyword-events-processor': listenerLambda
89+
}
90+
})
91+
}
92+
7593
this.keywordEventsQueueUrlOutput = new cdk.CfnOutput(this, 'CmrKeywordEventsQueueUrl', {
7694
description: 'Queue URL for CMR keyword event processing',
7795
exportName: `${props.prefix}-CmrKeywordEventsQueueUrl`,

cdk/app/lib/KmsStack.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Construct } from 'constructs'
99
import { ApiResources } from './helper/ApiResources'
1010
import { IamSetup } from './helper/IamSetup'
1111
import { LambdaFunctions } from './helper/KmsLambdaFunctions'
12+
import { LogForwardingSetup } from './helper/LogForwardingSetup'
1213
import { VpcSetup } from './helper/VpcSetup'
1314

1415
/**
@@ -21,6 +22,7 @@ export interface KmsStackProps extends cdk.StackProps {
2122
rootResourceId: string | undefined
2223
stage: string
2324
vpcId: string
25+
logDestinationArn: string
2426
environment: {
2527
CMR_BASE_URL: string
2628
EDL_PASSWORD: string
@@ -79,7 +81,13 @@ export class KmsStack extends cdk.Stack {
7981
constructor(scope: Construct, id: string, props: KmsStackProps) {
8082
super(scope, id, props)
8183
const {
82-
environment, existingApiId, prefix, rootResourceId, stage, vpcId
84+
environment,
85+
existingApiId,
86+
logDestinationArn,
87+
prefix,
88+
rootResourceId,
89+
stage,
90+
vpcId
8391
} = props
8492
this.stage = stage
8593

@@ -160,6 +168,19 @@ export class KmsStack extends cdk.Stack {
160168
const lambdas = this.lambdaFunctions.getAllLambdas()
161169
const methods = this.lambdaFunctions.getAllMethods()
162170

171+
// Set up CloudWatch Logs forwarding to Splunk via NGAP SecLog account
172+
// Skip log forwarding for localstack deployments
173+
if (!useLocalstack) {
174+
// Create log forwarding setup - no reference needed as it registers itself
175+
// eslint-disable-next-line no-new
176+
new LogForwardingSetup(this, 'LogForwarding', {
177+
prefix,
178+
stage,
179+
logDestinationArn,
180+
lambdas
181+
})
182+
}
183+
163184
// Create a new deployment
164185
const deployment = new apigateway.Deployment(
165186
this,

cdk/app/lib/helper/KmsLambdaFunctions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ export class LambdaFunctions {
617617
let lambdaFunction = this.lambdas[lambdaKey]
618618
if (!lambdaFunction) {
619619
const nodejsFunctionProps: NodejsFunctionProps = {
620-
functionName: `${this.props.prefix}-${functionName}`,
620+
functionName: `${this.props.prefix}-${this.props.stage}-${functionName}`,
621621
entry: path.join(__dirname, '../../../../serverless/src', handlerPath),
622622
handler: handlerName,
623623
runtime: lambda.Runtime.NODEJS_22_X,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import * as cdk from 'aws-cdk-lib'
2+
import * as lambda from 'aws-cdk-lib/aws-lambda'
3+
import * as logs from 'aws-cdk-lib/aws-logs'
4+
import { Construct } from 'constructs'
5+
6+
/**
7+
* Interface for LogForwardingSetup properties
8+
*/
9+
interface LogForwardingSetupProps {
10+
/** Prefix for naming resources */
11+
prefix: string
12+
/** Deployment stage (sit, uat, prod) */
13+
stage: string
14+
/** ARN of the log destination in NGAP SecLog account */
15+
logDestinationArn: string
16+
/** Map of Lambda functions to configure log forwarding for */
17+
lambdas: { [key: string]: lambda.Function }
18+
/** Log retention period in days */
19+
logRetentionDays?: logs.RetentionDays
20+
}
21+
22+
/**
23+
* Sets up CloudWatch Log Groups and Subscription Filters to forward logs to NGAP SecLog account.
24+
*
25+
* This class configures log forwarding to Splunk via NGAP's centralized logging infrastructure.
26+
* Logs are forwarded to a Kinesis stream in the SecLog account which then forwards them to Splunk.
27+
*
28+
* @see https://wiki.earthdata.nasa.gov/pages/viewpage.action?pageId=147423378
29+
*/
30+
export class LogForwardingSetup {
31+
/** The ARN of the log destination for log forwarding */
32+
private readonly logDestinationArn: string
33+
34+
/** Log retention period */
35+
private readonly logRetentionDays: logs.RetentionDays
36+
37+
/** Prefix for naming resources */
38+
private readonly prefix: string
39+
40+
/** Deployment stage */
41+
private readonly stage: string
42+
43+
/** Stack name for resource naming */
44+
private readonly stackName: string
45+
46+
/**
47+
* Constructs a new LogForwardingSetup instance
48+
* @param {Construct} scope - The scope in which to define this construct
49+
* @param {string} id - The construct ID
50+
* @param {LogForwardingSetupProps} props - Configuration properties
51+
*/
52+
constructor(scope: Construct, id: string, props: LogForwardingSetupProps) {
53+
this.logRetentionDays = props.logRetentionDays || logs.RetentionDays.ONE_MONTH
54+
this.prefix = props.prefix
55+
this.stage = props.stage
56+
this.stackName = cdk.Stack.of(scope).stackName
57+
58+
if (!props.logDestinationArn) {
59+
throw new Error(
60+
'logDestinationArn is required for log forwarding. '
61+
+ 'Please set the LOG_DESTINATION_ARN environment variable in your Bamboo deployment.'
62+
)
63+
}
64+
65+
this.logDestinationArn = props.logDestinationArn
66+
67+
// Set up log groups and subscription filters for each Lambda function
68+
this.setupLogForwarding(scope, props)
69+
}
70+
71+
/**
72+
* Sets up log groups and subscription filters for all Lambda functions.
73+
*
74+
* For each Lambda function:
75+
* 1. Creates an explicit CloudWatch Log Group with retention policy
76+
* 2. Creates a Subscription Filter to forward logs to NGAP SecLog destination
77+
*
78+
* Note: We use the pre-configured destination ARN in the SecLog account,
79+
* not a role from our account. The destination already has the necessary
80+
* permissions to receive logs from our account.
81+
*
82+
* @param {Construct} scope - The scope in which to define constructs
83+
* @param {LogForwardingSetupProps} props - Configuration properties
84+
* @private
85+
*/
86+
private setupLogForwarding(scope: Construct, props: LogForwardingSetupProps) {
87+
const { lambdas } = props
88+
89+
// Set up log forwarding for each Lambda function
90+
Object.entries(lambdas).forEach(([key, lambdaFunction]) => {
91+
const sanitizedKey = key.replace(/[^a-zA-Z0-9]/g, '-')
92+
93+
// Create explicit log group with retention
94+
// Log group naming: /aws/lambda/{prefix}-{stage}-{functionName}
95+
// This enables easy Splunk searches: source="/aws/lambda/kms-sit-*"
96+
const logGroup = new logs.LogGroup(scope, `${sanitizedKey}-LogGroup`, {
97+
logGroupName: `/aws/lambda/${lambdaFunction.functionName}`,
98+
retention: this.logRetentionDays,
99+
removalPolicy: cdk.RemovalPolicy.DESTROY
100+
})
101+
102+
// Create subscription filter to forward logs to NGAP SecLog destination
103+
// The destination ARN is pre-configured in the SecLog account and has
104+
// the necessary permissions to accept logs from our account
105+
const subscriptionFilter = new logs.CfnSubscriptionFilter(
106+
scope,
107+
`${sanitizedKey}-subscriptionFilter`,
108+
{
109+
logGroupName: logGroup.logGroupName,
110+
filterPattern: '', // Empty pattern forwards all logs
111+
destinationArn: this.logDestinationArn,
112+
filterName: `${this.stackName}-${this.stage}-${sanitizedKey}-subscriptionFilter`
113+
// No roleArn needed - the destination is in the SecLog account
114+
}
115+
)
116+
117+
// Ensure proper dependency order
118+
subscriptionFilter.node.addDependency(logGroup)
119+
})
120+
}
121+
}

cdk/bin/main.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ async function main() {
4343
const stage = process.env.STAGE_NAME || 'dev'
4444
const existingApiId = process.env.EXISTING_API_ID
4545
const rootResourceId = process.env.ROOT_RESOURCE_ID
46+
let logDestinationArn = process.env.LOG_DESTINATION_ARN
4647

4748
const app = new cdk.App({
4849
context: {
@@ -68,6 +69,19 @@ async function main() {
6869
throw new Error('VPC_ID environment variable is not set')
6970
}
7071

72+
// Validate required logging parameters for non-localstack deployments
73+
if (!useLocalstack && !logDestinationArn) {
74+
throw new Error(
75+
'LOG_DESTINATION_ARN environment variable is required for log forwarding. '
76+
+ 'Please set bamboo_LOG_DESTINATION_ARN in your Bamboo deployment configuration.'
77+
)
78+
}
79+
80+
// Set dummy ARN for localstack since log forwarding is skipped
81+
if (useLocalstack && !logDestinationArn) {
82+
logDestinationArn = 'arn:aws:logs:us-east-1:000000000000:destination:localstack-dummy'
83+
}
84+
7185
let iamStack: IamStack | undefined
7286
let lbStack: LoadBalancerStack | undefined
7387
let ebsStack: EbsStack | undefined
@@ -151,6 +165,7 @@ async function main() {
151165
stage,
152166
existingApiId,
153167
rootResourceId,
168+
logDestinationArn: logDestinationArn!,
154169
environment: {
155170
RDF4J_SERVICE_URL: useLocalstack
156171
? 'http://rdf4j-server:8080'
@@ -187,7 +202,8 @@ async function main() {
187202
prefix,
188203
stage,
189204
stackName: `${prefix}-CmrEventProcessingStack`,
190-
topicArn: kmsStack.keywordEventsTopic.topicArn
205+
topicArn: kmsStack.keywordEventsTopic.topicArn,
206+
logDestinationArn: logDestinationArn!
191207
})
192208

193209
cmrEventProcessingStack.addDependency(kmsStack)

0 commit comments

Comments
 (0)