Skip to content

Commit d9e4028

Browse files
authored
MMT-3985: Migrates MMT deployment/runtime infrastructure from Serverless Framework to CDK (#1471)
* MMT-4135: Migrate MMT deployment from Serverless Framework to CDK
1 parent 2134a12 commit d9e4028

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+24255
-24639
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist
22
.eslintrc
3+
cdk.out

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Project build files
22
.dockerignore
33
.DS_Store
4-
.serverless
54
.esbuild
65
coverage
76
dist
@@ -11,11 +10,12 @@ node_modules
1110
secret.config.json
1211
tmp
1312
.env
13+
cdk.out
1414

1515
# VSCode Settings
1616
.vscode
1717

1818
# CI Outputs
1919
junit.xml
2020

21-
cmr
21+
cmr

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20.18.1
1+
lts/jod

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,38 @@ To run GraphDB in docker, run:
135135
npm run start:graphdb
136136
```
137137

138-
#### Running Serverless Offline (API Gateway/Lambdas)
138+
#### Running local API (CDK template)
139139

140-
In order to run serverless-offline, which is used for mimicking API Gateway to call lambda functions, run:
140+
In order to run the API locally from the synthesized CDK template, run:
141141

142142
```bash
143-
EDL_CLIENT_ID=<clientid> EDL_PASSWORD=<password> npm run offline
143+
EDL_CLIENT_ID=<clientid> EDL_PASSWORD=<password> npm run start:api
144144
```
145145

146-
_Note: The EDL_CLIENT_ID and EDL_PASSWORD environment variable is required for group member queries to function._
146+
_Note: The EDL_CLIENT_ID and EDL_PASSWORD environment variables are required for group member queries to function._
147+
148+
_Note: `npm run start:api` runs `prestart:api` automatically, which builds the CDK app (`build:cdk:mmt`) and synthesizes the template (`run-synth`). This is the recommended first-time flow on a fresh checkout/machine._
149+
150+
Typical local flow:
151+
152+
```bash
153+
npm run prestart:api
154+
EDL_CLIENT_ID=<clientid> EDL_PASSWORD=<password> npm run start:api
155+
```
156+
157+
You do not need to run `run-synth` separately in this flow.
158+
159+
If you only need to rebuild CDK TypeScript output (for example after changing files in `cdk/mmt` or if `cdk/mmt/dist` is missing), run:
160+
161+
```bash
162+
npm run build:cdk:mmt
163+
```
164+
165+
If you only need to regenerate the synthesized CloudFormation template (for example after CDK configuration/stack changes and you do not want to run the API yet), run:
166+
167+
```bash
168+
npm run run-synth
169+
```
147170

148171
#### Running MMT
149172

@@ -169,7 +192,7 @@ To run the test suite, run:
169192

170193
### Styling
171194

172-
CSS should follow the following guidelines:
195+
CSS should follow these guidelines:
173196

174197
- Prefer [Bootstrap](https://getbootstrap.com/docs/5.0/) styles when writing custom components
175198
- If the desired look can not be achieved with Bootstrap, additional styling should be accomplished by:

bin/api.mjs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import express from 'express'
2+
import fs from 'fs'
3+
import path from 'path'
4+
5+
import { getApiResources } from './getApiResources.mjs'
6+
7+
/**
8+
* Local API runner that mirrors API Gateway + Lambda wiring from the
9+
* synthesized CDK template.
10+
*/
11+
const templateFilePath = process.argv[2]
12+
if (!templateFilePath) {
13+
console.error('Please provide the path to the CloudFormation template as a command line argument.')
14+
process.exit(1)
15+
}
16+
17+
const stageName = process.env.STAGE_NAME || 'dev'
18+
const port = parseInt(process.env.API_LOCAL_PORT || '4001', 10)
19+
const staticConfigPath = path.resolve(process.cwd(), 'static.config.json')
20+
21+
/**
22+
* Resolves a local `MMT_HOST` value from env or `static.config.json`.
23+
* `MMT_HOST` is used for browser origin (CORS handling)
24+
*
25+
* @returns {string}
26+
*/
27+
const resolveMmtHost = () => {
28+
if (process.env.MMT_HOST) return process.env.MMT_HOST
29+
30+
try {
31+
const file = fs.readFileSync(staticConfigPath, 'utf8')
32+
const staticConfig = JSON.parse(file)
33+
if (staticConfig?.application?.mmtHost) return staticConfig.application.mmtHost
34+
} catch (error) {
35+
console.warn(`Unable to read mmtHost from static.config.json: ${error}`)
36+
}
37+
38+
return 'http://localhost:5173'
39+
}
40+
41+
const localEnvDefaults = {
42+
COOKIE_DOMAIN: '.localhost',
43+
JWT_SECRET: 'local-secret',
44+
JWT_VALID_TIME: '900',
45+
MMT_HOST: resolveMmtHost()
46+
}
47+
48+
// Local stack always runs in offline mode.
49+
process.env.IS_OFFLINE = 'true'
50+
51+
Object.entries(localEnvDefaults).forEach(([key, value]) => {
52+
if (!process.env[key]) {
53+
process.env[key] = value
54+
}
55+
})
56+
57+
const app = express()
58+
59+
/**
60+
* Adds permissive local CORS behavior for API Gateway emulation.
61+
*
62+
* In offline mode we mirror the incoming browser origin so localhost and
63+
* tunneled origins continue to work with credentials/cookies.
64+
*/
65+
app.use((request, response, next) => {
66+
if (process.env.IS_OFFLINE !== 'true') {
67+
next()
68+
69+
return
70+
}
71+
72+
const requestOrigin = request.headers.origin
73+
const allowOrigin = requestOrigin || process.env.MMT_HOST || 'http://localhost:5173'
74+
const requestedHeaders = request.headers['access-control-request-headers']
75+
76+
response.setHeader('Access-Control-Allow-Origin', allowOrigin)
77+
response.setHeader('Vary', 'Origin')
78+
response.setHeader('Access-Control-Allow-Credentials', 'true')
79+
response.setHeader('Access-Control-Allow-Headers', requestedHeaders || 'Content-Type,Authorization,Origin,User-Agent,Accept')
80+
response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS')
81+
82+
if (request.method === 'OPTIONS') {
83+
response.status(204).end()
84+
85+
return
86+
}
87+
88+
next()
89+
})
90+
91+
app.use(express.json({ limit: '10mb' }))
92+
app.use(express.urlencoded({ extended: true }))
93+
94+
/**
95+
* Wraps a parsed API method definition and invokes its Lambda handler using
96+
* API Gateway-like event objects.
97+
*
98+
* @param {{ httpMethod: string, lambdaFunction: { functionName: string, path: string } }} method
99+
* @returns {(request: import('express').Request, response: import('express').Response) => Promise<void>}
100+
*/
101+
const lambdaProxyWrapper = (method) => async (request, response) => {
102+
const { lambdaFunction } = method
103+
const {
104+
path: handlerPath
105+
} = lambdaFunction
106+
107+
const event = {
108+
body: typeof request.body === 'object' ? JSON.stringify(request.body) : request.body,
109+
headers: request.headers,
110+
httpMethod: request.method,
111+
pathParameters: request.params,
112+
queryStringParameters: request.query,
113+
requestContext: {}
114+
}
115+
116+
const { default: handler } = (await import(handlerPath)).default
117+
const lambdaResponse = await handler(event, {})
118+
119+
const {
120+
body,
121+
headers = {},
122+
statusCode = 200
123+
} = lambdaResponse || {}
124+
125+
response.status(statusCode)
126+
Object.entries(headers).forEach(([headerName, headerValue]) => {
127+
response.setHeader(headerName, headerValue)
128+
})
129+
130+
response.send(body)
131+
}
132+
133+
/**
134+
* Registers a route on the Express app for the given HTTP method and path.
135+
*
136+
* @param {string} httpMethod
137+
* @param {string} routePath
138+
* @param {(request: import('express').Request, response: import('express').Response) => Promise<void>} handler
139+
*/
140+
const registerRoute = (httpMethod, routePath, handler) => {
141+
const lowerMethod = httpMethod.toLowerCase()
142+
app[lowerMethod](routePath, handler)
143+
}
144+
145+
/**
146+
* Discovers API routes from the synthesized template and registers
147+
* stage-prefixed local paths (for example `/dev/users`).
148+
*
149+
* @returns {Promise<void>}
150+
*/
151+
const addRoutes = async () => {
152+
const apiResources = getApiResources(templateFilePath)
153+
const keys = Object.keys(apiResources).sort()
154+
155+
keys.forEach((resourcePath) => {
156+
const { fullPath, methods } = apiResources[resourcePath]
157+
158+
methods.forEach((method) => {
159+
const routePath = `/${fullPath.replace(/\/\{(.*?)\}/g, '/:$1')}`
160+
const stagePath = `/${stageName}${routePath}`
161+
162+
console.log(`Adding route: ${method.httpMethod.padEnd(6)} - ${stagePath}`)
163+
registerRoute(method.httpMethod, stagePath, lambdaProxyWrapper(method))
164+
})
165+
})
166+
}
167+
168+
/**
169+
* Boots the local API server after dynamically registering all routes.
170+
*/
171+
addRoutes().then(() => {
172+
app.listen(port, () => {
173+
console.log(`Local API listening on http://localhost:${port}`)
174+
})
175+
})

bin/deploy-bamboo.sh

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@ node_modules
4747
.DS_Store
4848
.git
4949
.github
50-
.serverless
5150
cmr
5251
coverage
5352
dist
5453
node_modules
5554
tmp
55+
cdk.out
5656
EOF
5757
5858
cat <<EOF > Dockerfile
59-
FROM node:20.18.1
59+
FROM node:22
6060
COPY . /build
6161
WORKDIR /build
6262
RUN npm ci --omit=dev && npm run build
@@ -68,38 +68,43 @@ docker build -t $dockerTag .
6868
# Convenience function to invoke `docker run` with appropriate env vars instead of baking them into image
6969
dockerRun() {
7070
docker run \
71+
-e "AWS_ACCOUNT=$bamboo_AWS_ACCOUNT" \
7172
-e "AWS_ACCESS_KEY_ID=$bamboo_AWS_ACCESS_KEY_ID" \
73+
-e "AWS_REGION=${bamboo_AWS_REGION:-us-east-1}" \
7274
-e "AWS_SECRET_ACCESS_KEY=$bamboo_AWS_SECRET_ACCESS_KEY" \
75+
-e "AWS_SESSION_TOKEN=$bamboo_AWS_SESSION_TOKEN" \
76+
-e "COLLECTION_TEMPLATES_BUCKET_NAME=${bamboo_COLLECTION_TEMPLATES_BUCKET_NAME}" \
7377
-e "COOKIE_DOMAIN=$bamboo_COOKIE_DOMAIN" \
7478
-e "DISPLAY_PROD_WARNING=$bamboo_DISPLAY_PROD_WARNING" \
75-
-e "EDL_PASSWORD=$bamboo_EDL_PASSWORD" \
7679
-e "EDL_CLIENT_ID=$bamboo_EDL_CLIENT_ID" \
80+
-e "EDL_PASSWORD=$bamboo_EDL_PASSWORD" \
7781
-e "JWT_SECRET=$bamboo_JWT_SECRET" \
7882
-e "JWT_VALID_TIME=$bamboo_JWT_VALID_TIME" \
7983
-e "LAMBDA_TIMEOUT=$bamboo_LAMBDA_TIMEOUT" \
84+
-e "LOG_DESTINATION_ARN=$bamboo_LOG_DESTINATION_ARN" \
8085
-e "MMT_HOST=$bamboo_MMT_HOST" \
8186
-e "NODE_ENV=production" \
8287
-e "NODE_OPTIONS=--max_old_space_size=4096" \
88+
-e "SITE_BUCKET=${bamboo_SITE_BUCKET}" \
89+
-e "STAGE_NAME=$bamboo_STAGE_NAME" \
8390
-e "SUBNET_ID_A=$bamboo_SUBNET_ID_A" \
8491
-e "SUBNET_ID_B=$bamboo_SUBNET_ID_B" \
8592
-e "SUBNET_ID_C=$bamboo_SUBNET_ID_C" \
8693
-e "VPC_ID=$bamboo_VPC_ID" \
8794
$dockerTag "$@"
8895
}
8996
90-
# Execute serverless commands in Docker
97+
# Execute CDK commands in Docker
9198
#######################################
9299
93-
stageOpts="--stage $bamboo_STAGE_NAME"
94-
95100
# Deploy AWS Infrastructure Resources
96101
echo 'Deploying AWS Infrastructure Resources...'
97-
dockerRun npx serverless deploy $stageOpts --config serverless-infrastructure.yml
102+
dockerRun npm run deploy-infrastructure
98103
99104
# Deploy AWS Application Resources
100105
echo 'Deploying AWS Application Resources...'
101-
dockerRun npx serverless deploy $stageOpts
106+
dockerRun npm run deploy-application
102107
103108
# Deploy static assets
104109
echo 'Deploying static assets to S3...'
105-
dockerRun npx serverless client deploy $stageOpts --no-confirm
110+
dockerRun npm run deploy-static

0 commit comments

Comments
 (0)