Skip to content

Commit a8b5f6a

Browse files
authored
feat: end-2-end mqttt bridge health check (#25)
* feat: end-2-end mqttt bridge health check * refactor: move health check into its own stack * refactor: hard code amazon root cert into source code * feat: create fake health check device * fix: delete parameter store When using DeleteParametersCommand to delete parameters more than 10, it will error as Member must have length less than or equal to 10 * feat: add create health check device cli
1 parent fc95988 commit a8b5f6a

18 files changed

+843
-110
lines changed

.github/workflows/test-and-release.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
- name: Fake nRF Cloud account device
6666
run: |
6767
./cli.sh fake-nrfcloud-account-device
68+
./cli.sh create-fake-nrfcloud-health-check-device
6869
6970
- name: Deploy test resources stack
7071
run: |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ need to prepare nRF Cloud API key.
3333
```bash
3434
./cli.sh configure thirdParty nrfcloud apiKey <API key>
3535
./cli.sh initialize-nrfcloud-account
36+
./cli.sh create-health-check-device
3637
```
3738

3839
### Deploy

cdk/BackendLambdas.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type BackendLambdas = {
1010
fetchDeviceShadow: PackedLambda
1111
onDeviceMessage: PackedLambda
1212
storeMessagesInTimestream: PackedLambda
13+
healthCheck: PackedLambda
1314
}

cdk/backend.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const packagesInLayer: string[] = [
4141
'lodash-es',
4242
'@middy/core',
4343
]
44+
45+
const healthCheckPackagesInLayer: string[] = ['mqtt', 'ws']
46+
4447
const certsDir = path.join(
4548
process.cwd(),
4649
'certificates',
@@ -84,6 +87,10 @@ new BackendApp({
8487
id: 'baseLayer',
8588
dependencies: packagesInLayer,
8689
}),
90+
healthCheckLayer: await packLayer({
91+
id: 'healthCheckLayer',
92+
dependencies: healthCheckPackagesInLayer,
93+
}),
8794
iotEndpoint: await getIoTEndpoint({ iot })(),
8895
mqttBridgeCertificate,
8996
caCertificate,

cdk/packBackendLambdas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
2929
'storeMessagesInTimestream',
3030
'lambda/storeMessagesInTimestream.ts',
3131
),
32+
healthCheck: await packLambdaFromPath('healthCheck', 'lambda/healthCheck.ts'),
3233
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
Duration,
3+
aws_events_targets as EventTargets,
4+
aws_events as Events,
5+
aws_iam as IAM,
6+
aws_lambda as Lambda,
7+
Stack,
8+
} from 'aws-cdk-lib'
9+
import { Construct } from 'constructs'
10+
import { type Settings as BridgeSettings } from '../../bridge/settings.js'
11+
import type { PackedLambda } from '../helpers/lambdas/packLambda.js'
12+
import type { DeviceStorage } from './DeviceStorage.js'
13+
import { LambdaLogGroup } from './LambdaLogGroup.js'
14+
import type { WebsocketAPI } from './WebsocketAPI.js'
15+
16+
export type BridgeImageSettings = BridgeSettings
17+
18+
export class HealthCheckMqttBridge extends Construct {
19+
public constructor(
20+
parent: Construct,
21+
{
22+
websocketAPI,
23+
deviceStorage,
24+
layers,
25+
lambdaSources,
26+
}: {
27+
websocketAPI: WebsocketAPI
28+
deviceStorage: DeviceStorage
29+
layers: Lambda.ILayerVersion[]
30+
lambdaSources: {
31+
healthCheck: PackedLambda
32+
}
33+
},
34+
) {
35+
super(parent, 'healthCheckMqttBridge')
36+
37+
const scheduler = new Events.Rule(this, 'scheduler', {
38+
description: `Scheduler to health check mqtt bridge`,
39+
schedule: Events.Schedule.rate(Duration.minutes(1)),
40+
})
41+
42+
// Lambda functions
43+
const healthCheck = new Lambda.Function(this, 'healthCheck', {
44+
handler: lambdaSources.healthCheck.handler,
45+
architecture: Lambda.Architecture.ARM_64,
46+
runtime: Lambda.Runtime.NODEJS_18_X,
47+
timeout: Duration.seconds(15),
48+
memorySize: 1792,
49+
code: Lambda.Code.fromAsset(lambdaSources.healthCheck.zipFile),
50+
description: 'End to end test for mqtt bridge',
51+
environment: {
52+
VERSION: this.node.tryGetContext('version'),
53+
LOG_LEVEL: this.node.tryGetContext('logLevel'),
54+
NODE_NO_WARNINGS: '1',
55+
STACK_NAME: Stack.of(this).stackName,
56+
DEVICES_TABLE_NAME: deviceStorage.devicesTable.tableName,
57+
WEBSOCKET_URL: websocketAPI.websocketURI,
58+
},
59+
initialPolicy: [],
60+
layers,
61+
})
62+
const ssmReadPolicy = new IAM.PolicyStatement({
63+
effect: IAM.Effect.ALLOW,
64+
actions: ['ssm:GetParametersByPath'],
65+
resources: [
66+
`arn:aws:ssm:${Stack.of(this).region}:${
67+
Stack.of(this).account
68+
}:parameter/${Stack.of(this).stackName}/thirdParty/nrfcloud`,
69+
],
70+
})
71+
healthCheck.addToRolePolicy(ssmReadPolicy)
72+
scheduler.addTarget(new EventTargets.LambdaFunction(healthCheck))
73+
deviceStorage.devicesTable.grantWriteData(healthCheck)
74+
new LambdaLogGroup(this, 'healthCheckLog', healthCheck)
75+
}
76+
}

cdk/stacks/BackendStack.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ContinuousDeployment } from '../resources/ContinuousDeployment.js'
1313
import { ConvertDeviceMessages } from '../resources/ConvertDeviceMessages.js'
1414
import { DeviceShadow } from '../resources/DeviceShadow.js'
1515
import { DeviceStorage } from '../resources/DeviceStorage.js'
16+
import { HealthCheckMqttBridge } from '../resources/HealthCheckMqttBridge.js'
1617
import { HistoricalData } from '../resources/HistoricalData.js'
1718
import {
1819
Integration,
@@ -28,6 +29,7 @@ export class BackendStack extends Stack {
2829
{
2930
lambdaSources,
3031
layer,
32+
healthCheckLayer,
3133
iotEndpoint,
3234
mqttBridgeCertificate,
3335
caCertificate,
@@ -38,6 +40,7 @@ export class BackendStack extends Stack {
3840
}: {
3941
lambdaSources: BackendLambdas
4042
layer: PackedLayer
43+
healthCheckLayer: PackedLayer
4144
iotEndpoint: string
4245
mqttBridgeCertificate: CertificateFiles
4346
caCertificate: CAFiles
@@ -72,6 +75,15 @@ export class BackendStack extends Stack {
7275
'parameterStoreExtensionLayer',
7376
parameterStoreLayerARN[Stack.of(this).region] as string,
7477
)
78+
const healthCheckLayerVersion = new Lambda.LayerVersion(
79+
this,
80+
'healthCheckLayer',
81+
{
82+
code: Lambda.Code.fromAsset(healthCheckLayer.layerZipFile),
83+
compatibleArchitectures: [Lambda.Architecture.ARM_64],
84+
compatibleRuntimes: [Lambda.Runtime.NODEJS_18_X],
85+
},
86+
)
7587

7688
const lambdaLayers: Lambda.ILayerVersion[] = [
7789
baseLayer,
@@ -100,6 +112,13 @@ export class BackendStack extends Stack {
100112
bridgeImageSettings,
101113
})
102114

115+
new HealthCheckMqttBridge(this, {
116+
websocketAPI,
117+
deviceStorage,
118+
layers: [...lambdaLayers, healthCheckLayerVersion],
119+
lambdaSources,
120+
})
121+
103122
new ConvertDeviceMessages(this, {
104123
deviceStorage,
105124
websocketAPI,

cli/cli.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import psjon from '../package.json'
1414
import type { CommandDefinition } from './commands/CommandDefinition'
1515
import { configureDeviceCommand } from './commands/configure-device.js'
1616
import { configureCommand } from './commands/configure.js'
17-
import { createFakeNrfCloudAccountDeviceCredentials } from './commands/createFakeNrfCloudAccountDeviceCredentials.js'
17+
import { createFakeNrfCloudAccountDeviceCredentials } from './commands/create-fake-nrfcloud-account-device-credentials.js'
18+
import { createFakeNrfCloudHealthCheckDevice } from './commands/create-fake-nrfcloud-health-check-device.js'
19+
import { createHealthCheckDevice } from './commands/create-health-check-device.js'
1820
import { importDevicesCommand } from './commands/import-devices.js'
1921
import { initializeNRFCloudAccountCommand } from './commands/initialize-nrfcloud-account.js'
2022
import { logsCommand } from './commands/logs.js'
@@ -66,6 +68,12 @@ const CLI = async ({ isCI }: { isCI: boolean }) => {
6668
ssm,
6769
}),
6870
)
71+
commands.push(
72+
createFakeNrfCloudHealthCheckDevice({
73+
iot,
74+
ssm,
75+
}),
76+
)
6977
} else {
7078
commands.push(
7179
initializeNRFCloudAccountCommand({
@@ -74,6 +82,13 @@ const CLI = async ({ isCI }: { isCI: boolean }) => {
7482
stackName: STACK_NAME,
7583
}),
7684
)
85+
commands.push(
86+
createHealthCheckDevice({
87+
ssm,
88+
stackName: STACK_NAME,
89+
env: accountEnv,
90+
}),
91+
)
7792
try {
7893
const outputs = await stackOutput(
7994
new CloudFormationClient({}),

cli/commands/createFakeNrfCloudAccountDeviceCredentials.ts renamed to cli/commands/create-fake-nrfcloud-account-device-credentials.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import {
1818
SSMClient,
1919
} from '@aws-sdk/client-ssm'
2020
import chalk from 'chalk'
21+
import { chunk } from 'lodash-es'
2122
import { randomUUID } from 'node:crypto'
2223
import { getIoTEndpoint } from '../../aws/getIoTEndpoint.js'
2324
import { STACK_NAME } from '../../cdk/stacks/stackConfig.js'
2425
import { updateSettings, type Settings } from '../../nrfcloud/settings.js'
2526
import { isString } from '../../util/isString.js'
2627
import { settingsPath } from '../../util/settings.js'
27-
import type { CommandDefinition } from './CommandDefinition'
28+
import type { CommandDefinition } from './CommandDefinition.js'
2829

2930
export const createFakeNrfCloudAccountDeviceCredentials = ({
3031
iot,
@@ -114,11 +115,14 @@ export const createFakeNrfCloudAccountDeviceCredentials = ({
114115
...(parameters.Parameters?.map((p) => p.Name) ?? []),
115116
fakeTenantParameter,
116117
]
117-
await ssm.send(
118-
new DeleteParametersCommand({
119-
Names: names as string[],
120-
}),
121-
)
118+
const namesChunk = chunk(names, 10)
119+
for (const names of namesChunk) {
120+
await ssm.send(
121+
new DeleteParametersCommand({
122+
Names: names as string[],
123+
}),
124+
)
125+
}
122126
return
123127
}
124128
const tenantId = randomUUID()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
AttachPolicyCommand,
3+
CreateKeysAndCertificateCommand,
4+
IoTClient,
5+
} from '@aws-sdk/client-iot'
6+
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'
7+
import chalk from 'chalk'
8+
import { STACK_NAME } from '../../cdk/stacks/stackConfig.js'
9+
import {
10+
updateSettings,
11+
type Settings,
12+
} from '../../nrfcloud/healthCheckSettings.js'
13+
import { isString } from '../../util/isString.js'
14+
import type { CommandDefinition } from './CommandDefinition.js'
15+
16+
export const createFakeNrfCloudHealthCheckDevice = ({
17+
iot,
18+
ssm,
19+
}: {
20+
iot: IoTClient
21+
ssm: SSMClient
22+
}): CommandDefinition => ({
23+
command: 'create-fake-nrfcloud-health-check-device',
24+
action: async () => {
25+
const fakeTenantParameter = `/${STACK_NAME}/fakeTenant`
26+
const tenantId = (
27+
await ssm.send(
28+
new GetParameterCommand({
29+
Name: fakeTenantParameter,
30+
}),
31+
)
32+
).Parameter?.Value
33+
if (tenantId === undefined) {
34+
throw new Error(`${STACK_NAME} has no fake nRF Cloud Account device`)
35+
}
36+
37+
const policyName = `fake-nrfcloud-account-device-policy-${tenantId}`
38+
console.debug(chalk.magenta(`Creating IoT certificate`))
39+
const credentials = await iot.send(
40+
new CreateKeysAndCertificateCommand({
41+
setAsActive: true,
42+
}),
43+
)
44+
45+
console.debug(chalk.magenta(`Attaching policy to IoT certificate`))
46+
await iot.send(
47+
new AttachPolicyCommand({
48+
policyName,
49+
target: credentials.certificateArn,
50+
}),
51+
)
52+
53+
const pk = credentials.keyPair?.PrivateKey
54+
if (
55+
!isString(credentials.certificatePem) ||
56+
!isString(pk) ||
57+
!isString(credentials.certificateArn)
58+
) {
59+
throw new Error(`Failed to create certificate!`)
60+
}
61+
62+
const settings: Settings = {
63+
healthCheckClientCert: credentials.certificatePem,
64+
healthCheckPrivateKey: pk,
65+
healthCheckClientId: 'health-check',
66+
healthCheckModel: 'PCA20035+solar',
67+
healthCheckFingerPrint: '29a.ch3ckr',
68+
}
69+
await updateSettings({ ssm, stackName: STACK_NAME })(settings)
70+
71+
console.debug(chalk.white(`Fake nRF Cloud health check device settings:`))
72+
Object.entries(settings).forEach(([k, v]) => {
73+
console.debug(chalk.yellow(`${k}:`), chalk.blue(v))
74+
})
75+
},
76+
help: 'Creates fake nRF Cloud health check device used by the stack to end-to-end health check',
77+
})

0 commit comments

Comments
 (0)