Skip to content

Commit d133692

Browse files
committed
feat: add custom domain and API health check resource
1 parent 3412bec commit d133692

29 files changed

+651
-61
lines changed

.github/workflows/deploy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ env:
2020
FORCE_COLOR: 3
2121
JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: 1
2222
REGISTRY: ghcr.io
23+
API_DOMAIN_NAME: api.nordicsemi.world
2324

2425
jobs:
2526
print-inputs:

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ npx cdk bootstrap # if this is the first time you use CDK in this account
5757
npx cdk deploy
5858
```
5959

60+
## Custom domain name
61+
62+
You can specify a custom domain name for the deployed API using the environment
63+
variable `API_DOMAIN_NAME`.
64+
65+
If you do so, make sure to create a certificate in the region for this domain
66+
name.
67+
68+
After deploying the stack, make sure to set up a CNAME record for this domain
69+
that points to the hostname of the deployed API (available in the stack output
70+
`gatewayDomainName`).
71+
6072
## Continuous Deployment using GitHub Actions
6173

6274
After deploying the stack manually once,

aws/acm.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
ACMClient,
3+
CertificateStatus,
4+
ListCertificatesCommand,
5+
} from '@aws-sdk/client-acm'
6+
import chalk from 'chalk'
7+
8+
export type DomainCert = {
9+
domain: string
10+
certificateArn: string
11+
}
12+
13+
const getDomainCertificate =
14+
(acm: ACMClient) =>
15+
async (domain: string): Promise<DomainCert> => {
16+
const { CertificateSummaryList } = await acm.send(
17+
new ListCertificatesCommand({
18+
CertificateStatuses: [CertificateStatus.ISSUED],
19+
}),
20+
)
21+
const cert = (CertificateSummaryList ?? []).find(
22+
({ DomainName }) => DomainName === domain,
23+
)
24+
if (cert === undefined)
25+
throw new Error(`Failed to find certificate for ${domain}!`)
26+
return {
27+
domain,
28+
certificateArn: cert.CertificateArn as string,
29+
}
30+
}
31+
32+
export const getCertificateForDomain =
33+
(acm: ACMClient) =>
34+
async (domainName: string): Promise<DomainCert> => {
35+
try {
36+
return await getDomainCertificate(acm)(domainName)
37+
} catch (err) {
38+
console.error(
39+
chalk.red(
40+
`Failed to determine certificate for API domain ${domainName}!`,
41+
),
42+
)
43+
console.error(err)
44+
process.exit(1)
45+
}
46+
}

cdk/BackendApp.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { App } from 'aws-cdk-lib'
2-
import { BackendStack } from './stacks/BackendStack.js'
2+
import { BackendStack } from './BackendStack.js'
33

44
export class BackendApp extends App {
55
public constructor({
66
isTest,
77
version,
8-
domain,
98
...backendArgs
109
}: ConstructorParameters<typeof BackendStack>[1] & {
1110
isTest: boolean
@@ -16,7 +15,6 @@ export class BackendApp extends App {
1615
context: {
1716
isTest,
1817
version,
19-
domain,
2018
},
2119
})
2220

cdk/BackendLambdas.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ type BackendLambdas = {
1313
openSSL: PackedLambda
1414
senMLToLwM2M: PackedLambda
1515
senMLImportLogs: PackedLambda
16+
apiHealthCheck: PackedLambda
1617
}

cdk/stacks/BackendStack.ts renamed to cdk/BackendStack.ts

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ import {
55
Stack,
66
aws_ecr as ECR,
77
} from 'aws-cdk-lib'
8-
import type { BackendLambdas } from '../BackendLambdas.js'
8+
import type { BackendLambdas } from './BackendLambdas.js'
99
import type { PackedLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
1010
import { LambdaSource } from '@bifravst/aws-cdk-lambda-helpers/cdk'
11-
import { ConnectionInformationGeoLocation } from '../resources/ConnectionInformationGeoLocation.js'
12-
import { LwM2MShadow } from '../resources/LwM2MShadow.js'
13-
import { PublicDevices } from '../resources/PublicDevices.js'
14-
import { ShareAPI } from '../resources/ShareAPI.js'
11+
import { ConnectionInformationGeoLocation } from './resources/ConnectionInformationGeoLocation.js'
12+
import { LwM2MShadow } from './resources/LwM2MShadow.js'
13+
import { PublicDevices } from './resources/PublicDevices.js'
14+
import { ShareAPI } from './resources/ShareAPI.js'
1515
import { STACK_NAME } from './stackConfig.js'
16-
import { DevicesAPI } from '../resources/DevicesAPI.js'
17-
import { LwM2MObjectsHistory } from '../resources/LwM2MObjectsHistory.js'
18-
import { CustomDevicesAPI } from '../resources/CustomDevicesAPI.js'
19-
import { SenMLMessages } from '../resources/SenMLMessage.js'
20-
import { ContainerRepositoryId } from '../../aws/ecr.js'
16+
import { DevicesAPI } from './resources/DevicesAPI.js'
17+
import { LwM2MObjectsHistory } from './resources/LwM2MObjectsHistory.js'
18+
import { CustomDevicesAPI } from './resources/CustomDevicesAPI.js'
19+
import { SenMLMessages } from './resources/SenMLMessage.js'
20+
import { ContainerRepositoryId } from '../aws/ecr.js'
2121
import { repositoryName } from '@bifravst/aws-cdk-ecr-helpers/repository'
2222
import { ContinuousDeployment } from '@bifravst/ci'
23-
import { API } from '../resources/api/API.js'
23+
import { API } from './resources/api/API.js'
24+
import type { DomainCert } from '../aws/acm.js'
25+
import { ApiHealthCheck } from './resources/api/HealthCheck.js'
26+
import { CustomDomain } from './resources/api/CustomDomain.js'
2427

2528
/**
2629
* Provides resources for the backend serving data to hello.nrfcloud.com/map
@@ -29,12 +32,16 @@ export class BackendStack extends Stack {
2932
constructor(
3033
parent: App,
3134
{
35+
domain,
36+
apiDomain,
3237
layer,
3338
lambdaSources,
3439
openSSLLambdaContainerTag,
3540
repository,
3641
gitHubOICDProviderArn,
3742
}: {
43+
domain: string
44+
apiDomain?: DomainCert
3845
layer: PackedLayer
3946
lambdaSources: BackendLambdas
4047
openSSLLambdaContainerTag: string
@@ -59,8 +66,41 @@ export class BackendStack extends Stack {
5966
})
6067

6168
const publicDevices = new PublicDevices(this)
69+
new CfnOutput(this, 'publicDevicesTableName', {
70+
exportName: `${this.stackName}:publicDevicesTableName`,
71+
description: 'name of the public devices table',
72+
value: publicDevices.publicDevicesTable.tableName,
73+
})
6274

6375
const api = new API(this)
76+
api.addRoute(
77+
'GET /health',
78+
new ApiHealthCheck(this, { baseLayer, lambdaSources }).fn,
79+
)
80+
81+
if (apiDomain === undefined) {
82+
new CfnOutput(this, 'APIURL', {
83+
exportName: `${this.stackName}:APIURL`,
84+
description: 'API endpoint',
85+
value: api.URL,
86+
})
87+
} else {
88+
const domain = new CustomDomain(this, {
89+
api,
90+
apiDomain,
91+
})
92+
new CfnOutput(this, 'gatewayDomainName', {
93+
exportName: `${this.stackName}:gatewayDomainName`,
94+
description:
95+
'The domain name associated with the regional endpoint for the custom domain name. Use this as the target for the CNAME record for your custom domain name.',
96+
value: domain.gatewayDomainName.toString(),
97+
})
98+
new CfnOutput(this, 'APIURL', {
99+
exportName: `${this.stackName}:APIURL`,
100+
description: 'API endpoint',
101+
value: domain.URL,
102+
})
103+
}
64104

65105
new LwM2MShadow(this, {
66106
baseLayer,
@@ -84,6 +124,7 @@ export class BackendStack extends Stack {
84124
})
85125

86126
const shareAPI = new ShareAPI(this, {
127+
domain,
87128
baseLayer,
88129
lambdaSources,
89130
publicDevices,
@@ -124,22 +165,12 @@ export class BackendStack extends Stack {
124165

125166
api.addRoute('POST /credentials', customDevicesAPI.createCredentials)
126167

168+
// CD
169+
127170
const cd = new ContinuousDeployment(this, {
128171
repository,
129172
gitHubOICDProviderArn,
130173
})
131-
132-
// Outputs
133-
new CfnOutput(this, 'APIURL', {
134-
exportName: `${this.stackName}:APIURL`,
135-
description: 'API endpoint',
136-
value: api.URL,
137-
})
138-
new CfnOutput(this, 'publicDevicesTableName', {
139-
exportName: `${this.stackName}:publicDevicesTableName`,
140-
description: 'name of the public devices table',
141-
value: publicDevices.publicDevicesTable.tableName,
142-
})
143174
new CfnOutput(this, 'cdRoleArn', {
144175
exportName: `${this.stackName}:cdRoleArn`,
145176
description: 'Role ARN to use in the deploy GitHub Actions Workflow',
@@ -149,7 +180,20 @@ export class BackendStack extends Stack {
149180
}
150181

151182
export type StackOutputs = {
152-
APIURL: string // e.g. 'https://iiet67bnlmbtuhiblik4wcy4ni0oujot.execute-api.eu-west-1.amazonaws.com/2024-04-12/'
183+
/**
184+
* The URL of the deployed API
185+
* @example https://api.nordicsemi.world/2024-04-12/
186+
* @example https://9gsm5gind2.execute-api.eu-west-1.amazonaws.com/2024-04-12
187+
*/
188+
APIURL: string
189+
/**
190+
* The domain name associated with the regional endpoint for the custom domain name. Use this as the target for the CNAME record for your custom domain name.
191+
*
192+
* Only present if custom domain is used
193+
*
194+
* @example d-nygno3o155.execute-api.eu-west-1.amazonaws.com
195+
*/
196+
gatewayDomainName?: string
153197
publicDevicesTableName: string
154198
/**
155199
* Role ARN to use in the deploy GitHub Actions Workflow

cdk/backend.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { BackendApp } from './BackendApp.js'
55
import { pack as packBaseLayer } from './baseLayer.js'
66
import { ensureGitHubOIDCProvider } from '@bifravst/ci'
77
import { packBackendLambdas } from './packBackendLambdas.js'
8+
import { ACMClient } from '@aws-sdk/client-acm'
9+
import { getCertificateForDomain } from '../aws/acm.js'
810

911
const repoUrl = new URL(pJSON.repository.url)
1012
const repository = {
@@ -13,22 +15,30 @@ const repository = {
1315
}
1416

1517
const iam = new IAMClient({})
18+
const acm = new ACMClient({})
1619

1720
// Ensure needed container images exist
1821
const { openSSLLambdaContainerTag } = fromEnv({
1922
openSSLLambdaContainerTag: 'OPENSSL_LAMBDA_CONTAINER_TAG',
2023
})(process.env)
2124

25+
const isTest = process.env.IS_TEST === '1'
26+
const apiDomainName = process.env.API_DOMAIN_NAME
27+
2228
new BackendApp({
2329
lambdaSources: await packBackendLambdas(),
2430
layer: await packBaseLayer(),
2531
repository,
2632
gitHubOICDProviderArn: await ensureGitHubOIDCProvider({
2733
iam,
2834
}),
29-
isTest: process.env.IS_TEST === '1',
35+
isTest,
3036
openSSLLambdaContainerTag,
3137
domain: 'hello.nrfcloud.com',
38+
apiDomain:
39+
apiDomainName !== undefined
40+
? await getCertificateForDomain(acm)(apiDomainName)
41+
: undefined,
3242
version: (() => {
3343
const v = process.env.VERSION
3444
const defaultVersion = '0.0.0-development'

cdk/packBackendLambdas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
1818
openSSL: await pack('openSSL'),
1919
senMLToLwM2M: await pack('senMLToLwM2M'),
2020
senMLImportLogs: await pack('senMLImportLogs'),
21+
apiHealthCheck: await pack('apiHealthCheck'),
2122
})

cdk/resources/ConnectionInformationGeoLocation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
RemovalPolicy,
99
Stack,
1010
} from 'aws-cdk-lib'
11-
import { STACK_NAME } from '../stacks/stackConfig.js'
11+
import { STACK_NAME } from '../stackConfig.js'
1212
import {
1313
LambdaLogGroup,
1414
LambdaSource,

cdk/resources/CustomDevicesAPI.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from 'aws-cdk-lib'
99
import { Construct } from 'constructs'
1010
import type { BackendLambdas } from '../BackendLambdas.js'
11-
import { STACK_NAME } from '../stacks/stackConfig.js'
11+
import { STACK_NAME } from '../stackConfig.js'
1212
import type { PublicDevices } from './PublicDevices.js'
1313

1414
export class CustomDevicesAPI extends Construct {

0 commit comments

Comments
 (0)