Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9bb8bce
KMS-645: Add caching to /concepts endpoubt
Jan 21, 2026
635dba2
KMS-645: Scoping to juse concepts.
Jan 21, 2026
2f7627a
KMS-645: Updated the cache key path
Jan 21, 2026
d09be3c
KMS-645: Send array
Jan 22, 2026
bbde70f
KMS-645: Removed cacheKeyParameters from stage.
Jan 22, 2026
0e43f8a
reduce response.
Jan 22, 2026
494a576
KMS-654: Increased ttl
Jan 22, 2026
975bcc7
KMS-645: Moved caching configuration to stage methodOptions
Jan 22, 2026
763e8c2
KMS-645: Removed integration request parameters, not needed.
Jan 22, 2026
38274f2
KMS-645: Added access logs.
Jan 22, 2026
bdbc45f
KMS-645: Moved to env vars. Now has custom access log.
Jan 22, 2026
6ccc21d
KMS-645: Added execution logs.
Jan 22, 2026
8a58118
KMS-645: Remove execution logs.
Jan 22, 2026
7cbb955
KMS-645: Lint issues
Jan 22, 2026
f5a1fdd
KMS-645: Fixed lint issues
Jan 26, 2026
53dd36a
KMS-645: Fixed lint issues
Jan 26, 2026
68a045f
KMS-645: More lint fixes
Jan 26, 2026
3710b4f
KMS-645: Removed unnecessary eslint override.
Jan 29, 2026
7a02c26
Merge branch 'KMS-645' of https://github.com/nasa/kms into KMS-645
Jan 29, 2026
58c219f
KMS-645: Added caching env variables.
Jan 30, 2026
1ab0b69
KMS-645: Added caching env vars.
Jan 30, 2026
fc57de7
KMS-645: Removed defaults
Jan 30, 2026
4aec377
KMS-647: Fix CDK lambda caching by keying lambdas on handlerPath + fu…
Feb 2, 2026
99de88c
Merge branch 'KMS-647' into KMS-645b
Feb 2, 2026
1389252
KMS-645b: Lets try without fullPath+
Feb 2, 2026
1114648
KMS-645: Temp removed fullPath endpoint
Feb 2, 2026
3d5442f
KMS-645b: Lets deliberately remove {fullPath+}
Feb 2, 2026
d04b918
KMS-645: Lets deliberately remove {fullPath+}
Feb 2, 2026
1e01a75
KMS-645b: Another try to remove {fullPath+}
Feb 2, 2026
bd986ec
KMS-645: Clear out any cdk context info from previous runs.
Feb 2, 2026
0d22f5c
KMS-645: Added cdk context clear
Feb 2, 2026
cf3ef32
KMS-645b: Adding npx cdk context --clear
Feb 2, 2026
2eaead4
KMS-645: Added back in fullPath+
Feb 3, 2026
503d3e2
KMS-645: Updated deploy bamboo
Feb 3, 2026
c0ebea3
KMS-645: Ensure AWS::ApiGateway::Deployment is created only after all…
Feb 3, 2026
5c1c412
KMS-647: Ensure AWS::ApiGateway::Deployment is created only after all…
Feb 3, 2026
80b3f86
Merge branch 'KMS-647' into KMS-645b
Feb 3, 2026
4577239
KMS-645b: Added custom resource for access log group.
Feb 3, 2026
644c727
KMS-647: Use an AwsCustomResource to create the CloudWatch Logs group…
Feb 3, 2026
ed03b67
KMS-647: This should not have access logs, that code belongs in KMS-645
Feb 3, 2026
ef133e6
KMS-647: Audit fix
Feb 3, 2026
821a185
KMS-645: Fix CDK deploy ordering and make access logs idempotent
Feb 3, 2026
5a5b19b
Merge branch 'KMS-647' into KMS-645
Feb 3, 2026
7a1a8f6
Merge branch 'main' into KMS-645
Feb 5, 2026
8030e79
KMS-645: Remove ;
Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"root": true,
// Let eslint know were working as a browser to allow the standard globals (document, window, etc.)
"env": {
"browser": true,
Expand Down Expand Up @@ -84,4 +85,4 @@
}
]
}
}
}
173 changes: 123 additions & 50 deletions cdk/app/lib/KmsStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as logs from 'aws-cdk-lib/aws-logs'
import { Construct } from 'constructs'

import { ApiCacheSetup } from './helper/ApiCacheSetup'
import { ApiResources } from './helper/ApiResources'
import { IamSetup } from './helper/IamSetup'
import { LambdaFunctions } from './helper/KmsLambdaFunctions'
Expand All @@ -15,19 +17,22 @@ import { VpcSetup } from './helper/VpcSetup'
* @interface
*/
export interface KmsStackProps extends cdk.StackProps {
existingApiId: string | undefined,
prefix: string
rootResourceId: string | undefined,
stage: string
vpcId: string
existingApiId: string | undefined;
prefix: string;
rootResourceId: string | undefined;
stage: string;
vpcId: string;
environment: {
CMR_BASE_URL: string
EDL_PASSWORD: string
RDF4J_PASSWORD: string
RDF4J_SERVICE_URL: string
RDF4J_USER_NAME: string
RDF_BUCKET_NAME: string
LOG_LEVEL: string
CMR_BASE_URL: string;
EDL_PASSWORD: string;
KMS_CACHE_CLUSTER_ENABLED?: string;
KMS_CACHE_CLUSTER_SIZE_GB?: string;
KMS_CACHE_TTL_SECONDS?: string;
LOG_LEVEL: string;
RDF_BUCKET_NAME: string;
RDF4J_PASSWORD: string;
RDF4J_SERVICE_URL: string;
RDF4J_USER_NAME: string;
}
}

Expand All @@ -49,34 +54,29 @@ export class KmsStack extends cdk.Stack {
private readonly lambdaFunctions: LambdaFunctions

/**
* Represents a CDK stack for KMS (Keyword Management System), API Gateway, and Lambda resources.
* This stack sets up the infrastructure for a serverless keyword management system, including:
*
* 1. VPC and Security Group configuration for Lambda functions
* 2. IAM roles and policies for secure access
* 3. Integration with an existing API Gateway
* 4. Creation and configuration of multiple Lambda functions for various operations:
* - Read operations (e.g., get concepts, get schemes)
* - Tree operations (e.g., get keyword tree)
* - CRUD operations (e.g., create, update, delete concepts)
* - Scheduled operations (e.g., export RDF to S3)
* 5. API Gateway deployment
* 6. CloudFormation outputs for important resources
*
* The stack is designed to work with an existing VPC and API Gateway, extending their
* functionality to support a comprehensive keyword management system.
*
* @extends cdk.Stack
*/
* Represents a CDK stack for KMS (Keyword Management System), API Gateway, and Lambda resources.
* This stack sets up the infrastructure for a serverless keyword management system, including:
*
* 1. VPC and Security Group configuration for Lambda functions
* 2. IAM roles and policies for secure access
* 3. Integration with an existing API Gateway
* 4. Creation and configuration of multiple Lambda functions for various operations:
* - Read operations (e.g., get concepts, get schemes)
* - Tree operations (e.g., get keyword tree)
* - CRUD operations (e.g., create, update, delete concepts)
* - Scheduled operations (e.g., export RDF to S3)
* 5. API Gateway deployment
* 6. CloudFormation outputs for important resources
*
* The stack is designed to work with an existing VPC and API Gateway, extending their
* functionality to support a comprehensive keyword management system.
*
* @extends cdk.Stack
*/
constructor(scope: Construct, id: string, props: KmsStackProps) {
super(scope, id, props)
const {
environment,
existingApiId,
prefix,
rootResourceId,
stage,
vpcId
environment, existingApiId, prefix, rootResourceId, stage, vpcId
} = props
this.stage = stage

Expand All @@ -88,15 +88,62 @@ export class KmsStack extends cdk.Stack {
this.securityGroup = vpcSetup.securityGroup

// Set up IAM roles
const iamSetup = new IamSetup(this, 'IamSetup', this.stage, this.account, this.region, this.stackName)
const iamSetup = new IamSetup(
this,
'IamSetup',
this.stage,
this.account,
this.region,
this.stackName
)
this.lambdaRole = iamSetup.lambdaRole

const cacheTtlSeconds = Number(props.environment.KMS_CACHE_TTL_SECONDS)
const cacheTtl = Number.isFinite(cacheTtlSeconds) && cacheTtlSeconds > 0
Comment thread
eudoroolivares2016 marked this conversation as resolved.
? cdk.Duration.seconds(cacheTtlSeconds)
: cdk.Duration.hours(1)

const cacheClusterSize = props.environment.KMS_CACHE_CLUSTER_SIZE_GB

const cacheMethodOptions = ApiCacheSetup.cacheMethodOptions(cacheTtl)

const cacheClusterEnabled = props.environment.KMS_CACHE_CLUSTER_ENABLED !== 'false'
Comment thread
cgokey marked this conversation as resolved.

const accessLogGroup = useLocalstack
? undefined
: new logs.LogGroup(this, 'ApiAccessLogs', {
logGroupName: `/aws/apigateway/${prefix}-${stage}-access`
})

const accessLogOptions = useLocalstack
? {}
: {
accessLogDestination: new apigateway.LogGroupLogDestination(
accessLogGroup as logs.LogGroup
),
accessLogFormat: apigateway.AccessLogFormat.custom(
JSON.stringify({
requestId: '$context.requestId',
method: '$context.httpMethod',
path: '$context.path',
queryString: '$context.queryString',
status: '$context.status',
responseLatency: '$context.responseLatency',
integrationLatency: '$context.integrationLatency'
})
)
}

if (existingApiId && rootResourceId) {
// Import existing API Gateway
this.api = apigateway.RestApi.fromRestApiAttributes(this, 'ApiGatewayRestApi', {
restApiId: existingApiId,
rootResourceId
})
this.api = apigateway.RestApi.fromRestApiAttributes(
this,
'ApiGatewayRestApi',
{
restApiId: existingApiId,
rootResourceId
}
)
} else {
// Create a new API Gateway
this.api = new apigateway.RestApi(this, 'ApiGatewayRestApi', {
Expand All @@ -105,7 +152,15 @@ export class KmsStack extends cdk.Stack {
endpointTypes: [apigateway.EndpointType.PRIVATE],
deploy: true,
deployOptions: {
stageName: stage
stageName: stage,
...(useLocalstack
? {}
: {
cacheClusterEnabled,
cacheClusterSize,
methodOptions: cacheMethodOptions,
...accessLogOptions
})
},
policy: iamSetup.createApiGatewayPolicy()
})
Expand Down Expand Up @@ -134,11 +189,15 @@ export class KmsStack extends cdk.Stack {
const lambdas = this.lambdaFunctions.getAllLambdas()

// Create a new deployment
const deployment = new apigateway.Deployment(this, `ApiDeployment-${Date.now().toString()}`, {
api: this.api,
retainDeployments: false,
description: `Deployment for ${stage} at ${new Date().toISOString()}`
})
const deployment = new apigateway.Deployment(
this,
`ApiDeployment-${Date.now().toString()}`,
{
api: this.api,
retainDeployments: false,
description: `Deployment for ${stage} at ${new Date().toISOString()}`
}
)

// Ensure deployment happens after all routes/methods/integrations exist
if (lambdas) {
Expand All @@ -153,7 +212,15 @@ export class KmsStack extends cdk.Stack {
new apigateway.Stage(this, 'ApiStage', {
deployment,
stageName: stage,
description: `${stage} stage name for KMS API`
description: `${stage} stage name for KMS API`,
...(useLocalstack
? {}
: {
cacheClusterEnabled,
cacheClusterSize,
methodOptions: cacheMethodOptions,
...accessLogOptions
})
})
} else {
// For new API, stage is auto-created
Expand All @@ -167,6 +234,11 @@ export class KmsStack extends cdk.Stack {
exportName: `${prefix}-NewApiDeploymentId`
})

// Configure API Gateway caching
if (existingApiId && !useLocalstack) {
ApiCacheSetup.configure(this, this.api)
}

this.addOutputs(prefix)
}

Expand Down Expand Up @@ -210,7 +282,8 @@ export class KmsStack extends cdk.Stack {
})

new cdk.CfnOutput(this, 'KMSServerlessAppRoleArn', {
description: 'Role used to execute commands across the serverless application',
description:
'Role used to execute commands across the serverless application',
exportName: `${this.stage}-KMSServerlessCdkAppRole`,
value: this.lambdaRole.roleArn
})
Expand Down
133 changes: 133 additions & 0 deletions cdk/app/lib/helper/ApiCacheSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import { Construct } from 'constructs'

/**
* Helper class to configure API Gateway caching for multiple endpoints
*/
export class ApiCacheSetup {
public static cacheMethodOptions(
cacheTtl: cdk.Duration
): { [path: string]: apigateway.MethodDeploymentOptions } {
const cachePaths = [
'/concepts/GET',
'/concepts/concept_scheme/{conceptScheme}/GET',
'/concepts/concept_scheme/{conceptScheme}/pattern/{pattern}/GET',
'/concepts/pattern/{pattern}/GET'
]

return Object.fromEntries(
cachePaths.map((path) => [
path,
{
cachingEnabled: true,
cacheTtl
}
])
) as { [path: string]: apigateway.MethodDeploymentOptions }
}

/**
* Configures method request, integration request, and caching for specified endpoints
* @param scope - The construct scope
* @param api - The API Gateway REST API
* @param deployment - The API Gateway deployment
* @param stageName - The stage name
*/
public static configure(
_scope: Construct,
api: apigateway.IRestApi
): void {
// Define query parameters for caching
const queryParams = {
Comment thread
cgokey marked this conversation as resolved.
'method.request.querystring.page_num': false,
'method.request.querystring.page_size': false,
'method.request.querystring.format': false,
'method.request.querystring.version': false
}

const cacheKeyParameters = [
'method.request.querystring.page_num',
'method.request.querystring.page_size',
'method.request.querystring.format',
'method.request.querystring.version'
]

// Helper to configure a single resource
const configureResource = (
resourcePath: string[],
cacheNamespace: string
) => {
const resource = resourcePath.reduce((acc, part) => {
const child = acc.getResource(part)

return child || acc
}, api.root)

if (!resourcePath.every((part) => resource.path.includes(part))) {
return
}

const getMethod = resource.node.findChild('GET') as apigateway.Method
if (getMethod) {
const cfnMethod = getMethod.node.defaultChild as apigateway.CfnMethod
cfnMethod.requestParameters = queryParams

const existingIntegration = cfnMethod.integration as
apigateway.CfnMethod.IntegrationProperty
if (existingIntegration) {
/* eslint-disable max-len */
cfnMethod.integration = {
type: existingIntegration.type,
uri: existingIntegration.uri,
integrationHttpMethod: existingIntegration.integrationHttpMethod,
cacheNamespace,
cacheKeyParameters,
...(existingIntegration.credentials && {
credentials: existingIntegration.credentials
}),
...(existingIntegration.requestTemplates && {
requestTemplates: existingIntegration.requestTemplates
}),
...(existingIntegration.integrationResponses && {
integrationResponses: existingIntegration.integrationResponses
}),
...(existingIntegration.passthroughBehavior && {
passthroughBehavior: existingIntegration.passthroughBehavior
}),
...(existingIntegration.connectionId && {
connectionId: existingIntegration.connectionId
}),
...(existingIntegration.connectionType && {
connectionType: existingIntegration.connectionType
}),
...(existingIntegration.contentHandling && {
contentHandling: existingIntegration.contentHandling
}),
...(existingIntegration.timeoutInMillis && {
timeoutInMillis: existingIntegration.timeoutInMillis
})
}
/* eslint-enable max-len */
}
}
}

// Configure method and integration request for all endpoints
configureResource(['concepts'], 'concepts')
configureResource(
['concepts', 'concept_scheme', '{conceptScheme}'],
'concepts-scheme'
)

configureResource([
'concepts',
'concept_scheme',
'{conceptScheme}',
'pattern',
'{pattern}'
], 'concepts-scheme-pattern')

configureResource(['concepts', 'pattern', '{pattern}'], 'concepts-pattern')
}
}
Loading