@infoxchange/make-it-so 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +16 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/cdk-constructs/IxCertificate.d.ts +16 -0
- package/dist/cdk-constructs/IxCertificate.d.ts.map +1 -0
- package/dist/cdk-constructs/IxCertificate.js +26 -0
- package/dist/cdk-constructs/IxDnsRecord.d.ts +20 -0
- package/dist/cdk-constructs/IxDnsRecord.d.ts.map +1 -0
- package/dist/cdk-constructs/IxDnsRecord.js +26 -0
- package/dist/cdk-constructs/IxNextjsSite.d.ts +19 -0
- package/dist/cdk-constructs/IxNextjsSite.d.ts.map +1 -0
- package/dist/cdk-constructs/IxNextjsSite.js +147 -0
- package/dist/cdk-constructs/IxVpcDetails.d.ts +13 -0
- package/dist/cdk-constructs/IxVpcDetails.d.ts.map +1 -0
- package/dist/cdk-constructs/IxVpcDetails.js +23 -0
- package/dist/cdk-constructs/index.d.ts +5 -0
- package/dist/cdk-constructs/index.d.ts.map +1 -0
- package/dist/cdk-constructs/index.js +4 -0
- package/dist/deployConfig.d.ts +18 -0
- package/dist/deployConfig.d.ts.map +1 -0
- package/dist/deployConfig.js +10 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +6 -0
- package/eslint.config.js +11 -0
- package/package.json +47 -0
- package/src/cdk-constructs/IxCertificate.ts +54 -0
- package/src/cdk-constructs/IxDnsRecord.ts +55 -0
- package/src/cdk-constructs/IxNextjsSite.ts +191 -0
- package/src/cdk-constructs/IxVpcDetails.ts +35 -0
- package/src/cdk-constructs/index.ts +4 -0
- package/src/deployConfig.ts +28 -0
- package/src/shared.ts +11 -0
- package/tsconfig.json +9 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
end_of_line = lf
|
|
5
|
+
charset = utf-8
|
|
6
|
+
|
|
7
|
+
[{*.js,*.json,*.ts,*.md,*.yml,*.yaml}]
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 2
|
|
10
|
+
trim_trailing_whitespace = true
|
|
11
|
+
insert_final_newline = true
|
|
12
|
+
|
|
13
|
+
[*.js]
|
|
14
|
+
block_comment_start = /**
|
|
15
|
+
block_comment = *
|
|
16
|
+
block_comment_end = */
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Callum Gare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Make It So
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@infoxchange/make-it-so)
|
|
4
|
+
|
|
5
|
+
A helpful little library that allows you to deploy apps on Infoxchange's (IX) infrastructure without having to specify all the implementation details that are specific to IX's deployment environment. You tell it what you want and it will worry about making it happen. Most of the heavily lifting is done by [SST](https://sst.dev/) which is extending to take care the IX related specifics.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
# NPM
|
|
11
|
+
npm --save-dev @infoxchange/make-it-so
|
|
12
|
+
# Yarn
|
|
13
|
+
yarn add --dev @infoxchange/make-it-so
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
### deployConfig
|
|
19
|
+
|
|
20
|
+
The IX pipeline provides certain information about the deployment currently in progress via environment variables. deployConfig gives you a friendly (and typed) way to access these details.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import deployConfig from "@infoxchange/make-it-so/deployConfig";
|
|
24
|
+
|
|
25
|
+
if (deployConfig.isIxDeploy) {
|
|
26
|
+
console.log(
|
|
27
|
+
`Deploying ${deployConfig.appName} into ${deployConfig.environment}`,
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`Not deploying via the IX deploy pipeline`);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Name | Description | Type for IX Deploy | Type for non-IX Deploy |
|
|
35
|
+
| ---------------- | ------------------------------------ | ------------------ | ---------------------- |
|
|
36
|
+
| isIxDeploy | Is deploying via IX pipeline or not | true | false |
|
|
37
|
+
| appName | Name of app being deployed | string | undefined |
|
|
38
|
+
| environment | Name of env app is being deployed to | string | undefined |
|
|
39
|
+
| workloadGroup | The workload group of the app | string | undefined |
|
|
40
|
+
| primaryAwsRegion | AWS Region used by IX | string | undefined |
|
|
41
|
+
| siteDomains | Domains to be used by the app | string[] | [] |
|
|
42
|
+
|
|
43
|
+
### CDK Construct - IxNextjsSite
|
|
44
|
+
|
|
45
|
+
Deploys a serverless instance of a Next.js. IxNextjsSite extends [SST's NextjsSite](https://docs.sst.dev/constructs/NextjsSite) and takes the exact same props.
|
|
46
|
+
|
|
47
|
+
It will automatically create certificates and DNS records for any custom domains given (including alternative domain names which SST doesn't currently do). If the props `customDomain` is not set the first site domain provided by the IX deployment pipeline will be used as the primary custom domain and if there is more than one domain the rest will be used as alternative domain names. Explicitly setting `customDomain` to `undefined` will
|
|
48
|
+
|
|
49
|
+
It will also automatically attach the site to the standard IX VPC created in each workload account (unless you explicitly pass other VPC details or set the VPC-related props (see the SST doco) to `undefined`).
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { IxNextjsSite } from "@infoxchange/make-it-so/cdk-constructs";
|
|
53
|
+
|
|
54
|
+
const site = new IxNextjsSite(stack, "site", {
|
|
55
|
+
environment: {
|
|
56
|
+
DATABASE_URL: process.env.DATABASE_URL || "",
|
|
57
|
+
SESSION_SECRET: process.env.SESSION_SECRET || "",
|
|
58
|
+
},
|
|
59
|
+
// Included by default:
|
|
60
|
+
// customDomain: {
|
|
61
|
+
// domainName: ixDeployConfig.siteDomains[0],
|
|
62
|
+
// alternateNames: ixDeployConfig.siteDomains.slice(1)
|
|
63
|
+
// },
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### CDK Construct - IxCertificate
|
|
68
|
+
|
|
69
|
+
Creates a new DNS validated ACM certificate for a domain managed by IX.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { IxCertificate } from "@infoxchange/make-it-so/cdk-constructs";
|
|
73
|
+
|
|
74
|
+
const domainCert = new IxCertificate(scope, "IxCertificate", {
|
|
75
|
+
domainName: "example.com",
|
|
76
|
+
subjectAlternativeNames: ["other-domain.com"],
|
|
77
|
+
region: "us-east-1",
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Description |
|
|
82
|
+
| ----------------------- | -------- | --------------------------------------------------------------- |
|
|
83
|
+
| domainName | string | Domain name for cert |
|
|
84
|
+
| subjectAlternativeNames | string[] | (optional) Any domains for the certs "Subject Alternative Name" |
|
|
85
|
+
| region | string | (optional) The AWS region to create the cert in |
|
|
86
|
+
|
|
87
|
+
### CDK Construct - IxDnsRecord
|
|
88
|
+
|
|
89
|
+
Creates a DNS record for a domain managed by IX. Route53 HostedZones for IX managed domains live in the dns-hosting AWS account so if a workload AWS account requires a DNS record to be created this must be done "cross-account". IxDnsRecord handles that part for you. Just give it the details for the DNS record itself and IxDnsRecord will worry about creating it.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { IxDnsRecord } from "@infoxchange/make-it-so/cdk-constructs";
|
|
93
|
+
|
|
94
|
+
new IxDnsRecord(scope, `DnsRecord-${domainNameLogicalId}`, {
|
|
95
|
+
type: "A",
|
|
96
|
+
name: "example.com",
|
|
97
|
+
value: "1.1.1.1",
|
|
98
|
+
ttl: 900,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Prop | Type | Description |
|
|
103
|
+
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| type | "A" \| "CNAME" \| "NS" \| "SOA" \| "ALIAS" | DNS record type |
|
|
105
|
+
| name | string | DNS record FQDN |
|
|
106
|
+
| value | string | DNS record value |
|
|
107
|
+
| ttl | number | (optional) TTL value for DNS record |
|
|
108
|
+
| hostedZoneId | string | (optional) The ID of the Route53 HostedZone belonging to the dns-hosting account in which to create the DNS record. If not given the correct HostedZone will be inferred from the domain in the "value" prop. |
|
|
109
|
+
| aliasZoneId | string | (only needed if type = "Alias") the Route53 HostedZone that the target of the alias record lives in. Generally this will be the well known ID of a HostedZone for a AWS service itself that is managed by AWS, not an end-user. |
|
|
110
|
+
|
|
111
|
+
### CDK Construct - IxVpcDetails
|
|
112
|
+
|
|
113
|
+
Fetches the standard VPC and subnets that exist in all IX workload aws accounts.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { IxVpcDetails } from "@infoxchange/make-it-so/cdk-constructs";
|
|
117
|
+
|
|
118
|
+
const vpcDetails = new IxVpcDetails(scope, id + "-IxVpcDetails");
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Prop | Type | Description |
|
|
122
|
+
| ----------------------- | -------- | --------------------------------------------------------------- |
|
|
123
|
+
| domainName | string | Domain name for cert |
|
|
124
|
+
| subjectAlternativeNames | string[] | (optional) Any domains for the certs "Subject Alternative Name" |
|
|
125
|
+
| region | string | (optional) The AWS region to create the cert in |
|
|
126
|
+
|
|
127
|
+
## Full Example
|
|
128
|
+
|
|
129
|
+
To deploy a Next.js based site you would include a `sst.config.ts` file at the root of repo with contents like this:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { SSTConfig } from "sst";
|
|
133
|
+
import { IxNextjsSite } from "@infoxchange/make-it-so/cdk-constructs";
|
|
134
|
+
import deployConfig from "@infoxchange/make-it-so/deployConfig";
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
config: () => ({
|
|
138
|
+
name: deployConfig.appName || "fallback-app-name",
|
|
139
|
+
region: deployConfig.primaryAwsRegion,
|
|
140
|
+
}),
|
|
141
|
+
stacks(app) {
|
|
142
|
+
app.stack(
|
|
143
|
+
({ stack }) => {
|
|
144
|
+
const site = new IxNextjsSite(stack, "site", {
|
|
145
|
+
environment: {
|
|
146
|
+
DATABASE_URL: process.env.DATABASE_URL || "",
|
|
147
|
+
SESSION_SECRET: process.env.SESSION_SECRET || "",
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
stack.addOutputs({
|
|
152
|
+
SiteUrl: site.primaryOrigin,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
{ stackName: `${app.name}-${app.stage}` }, // Use the same stack name format as our docker apps
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
} satisfies SSTConfig;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Then simply configure the IX pipeline to deploy that repo as a serverless app.
|
|
162
|
+
|
|
163
|
+
important that sst and aws lib version match those used in ix-deploy-support
|
|
164
|
+
|
|
165
|
+
# The Name
|
|
166
|
+
|
|
167
|
+
Honestly I've never seen Star Trek but I figured the name is appropriate since the goal of this library is to allow you, the user, to deploy applications by stating what you want and letting someone else handle the nitty gritty details of how to actually implement it.
|
|
168
|
+
|
|
169
|
+
# Contributing
|
|
170
|
+
|
|
171
|
+
Changes to the main branch automatically trigger the CI to build and publish to npm. We do this with [semantic-release](https://semantic-release.gitbook.io/) which uses commit messages to determine what the new version number should be.
|
|
172
|
+
|
|
173
|
+
Commit messages must be formatted in the [Conventional Commits](https://www.conventionalcommits.org) style to allow semantic-release to generate release notes based on the git history. To help with this the CLI tool for creating a commit with a valid commit message can be used via `npm run commit`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
4
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
5
|
+
type Props = {
|
|
6
|
+
domainName: string;
|
|
7
|
+
subjectAlternativeNames?: string[];
|
|
8
|
+
region?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class IxCertificate extends Construct {
|
|
11
|
+
acmCertificate: ICertificate;
|
|
12
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props);
|
|
13
|
+
private createCertificate;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=IxCertificate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IxCertificate.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/IxCertificate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAe,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAG/E,KAAK,cAAc,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,KAAK,WAAW,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9D,KAAK,KAAK,GAAG;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,aAAc,SAAQ,SAAS;IACnC,cAAc,EAAE,YAAY,CAAC;gBAExB,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;IAKhE,OAAO,CAAC,iBAAiB;CA+B1B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
+
import { CustomResource } from "aws-cdk-lib";
|
|
5
|
+
export class IxCertificate extends Construct {
|
|
6
|
+
acmCertificate;
|
|
7
|
+
constructor(scope, id, props) {
|
|
8
|
+
super(scope, id);
|
|
9
|
+
this.acmCertificate = this.createCertificate(scope, id, props);
|
|
10
|
+
}
|
|
11
|
+
createCertificate(scope, id, props) {
|
|
12
|
+
const certificateCreationLambdaArn = StringParameter.valueForStringParameter(scope, "/shared-services/acm/lambdaArn");
|
|
13
|
+
const certificateCustomResource = new CustomResource(scope, "CertificateCustomResource", {
|
|
14
|
+
resourceType: "Custom::CertIssuingLambda",
|
|
15
|
+
serviceToken: certificateCreationLambdaArn,
|
|
16
|
+
properties: {
|
|
17
|
+
DomainName: props.domainName,
|
|
18
|
+
...(props.subjectAlternativeNames && {
|
|
19
|
+
SubjectAlternativeNames: props.subjectAlternativeNames,
|
|
20
|
+
}),
|
|
21
|
+
...(props.region && { CertificateIssuingRegion: props.region }),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return Certificate.fromCertificateArn(scope, id + "-AwsCertificate", certificateCustomResource.ref);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
3
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
4
|
+
type Props = {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
ttl?: number;
|
|
8
|
+
hostedZoneId?: string;
|
|
9
|
+
} & ({
|
|
10
|
+
type: "A" | "CNAME" | "NS" | "SOA";
|
|
11
|
+
} | {
|
|
12
|
+
type: "ALIAS";
|
|
13
|
+
aliasZoneId: string;
|
|
14
|
+
});
|
|
15
|
+
export declare class IxDnsRecord extends Construct {
|
|
16
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props);
|
|
17
|
+
private createDnsRecord;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=IxDnsRecord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IxDnsRecord.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/IxDnsRecord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAKvC,KAAK,cAAc,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,KAAK,WAAW,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9D,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,CACA;IACE,IAAI,EAAE,GAAG,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;CACpC,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,CACJ,CAAC;AAEF,qBAAa,WAAY,SAAQ,SAAS;gBAC5B,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;IAKhE,OAAO,CAAC,eAAe;CAyBxB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { CustomResource } from "aws-cdk-lib";
|
|
4
|
+
import { remapKeys } from "../shared.js";
|
|
5
|
+
export class IxDnsRecord extends Construct {
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
this.createDnsRecord(scope, id, props);
|
|
9
|
+
}
|
|
10
|
+
createDnsRecord(scope, id, constructProps) {
|
|
11
|
+
const dnsRecordUpdaterLambdaArn = StringParameter.valueForStringParameter(scope, "/shared-services/route53/lambdaArn");
|
|
12
|
+
const lambdaProps = remapKeys(constructProps, {
|
|
13
|
+
name: "RecordFQDN",
|
|
14
|
+
value: "RecordValue",
|
|
15
|
+
ttl: "RecordTTL",
|
|
16
|
+
hostedZoneId: "HostedZoneId",
|
|
17
|
+
type: "RecordType",
|
|
18
|
+
aliasZoneId: "AliasZoneId",
|
|
19
|
+
});
|
|
20
|
+
new CustomResource(scope, id + "-CertificateCustomResource", {
|
|
21
|
+
resourceType: "Custom::DNSRecordUpdaterLambda",
|
|
22
|
+
serviceToken: dnsRecordUpdaterLambdaArn,
|
|
23
|
+
properties: lambdaProps,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextjsSite } from "sst/constructs";
|
|
2
|
+
type ConstructScope = ConstructorParameters<typeof NextjsSite>[0];
|
|
3
|
+
type ConstructId = ConstructorParameters<typeof NextjsSite>[1];
|
|
4
|
+
type ConstructProps = Exclude<ConstructorParameters<typeof NextjsSite>[2], undefined>;
|
|
5
|
+
export declare class IxNextjsSite extends NextjsSite {
|
|
6
|
+
constructor(scope: ConstructScope, id: ConstructId, props?: ConstructProps);
|
|
7
|
+
private static addVpcDetailsToProps;
|
|
8
|
+
private static setupCustomDomain;
|
|
9
|
+
private static setupCertificate;
|
|
10
|
+
private createDnsRecords;
|
|
11
|
+
get customDomains(): string[];
|
|
12
|
+
get primaryCustomDomain(): string | null;
|
|
13
|
+
get aliasDomain(): string | null;
|
|
14
|
+
get alternativeDomains(): string[];
|
|
15
|
+
primaryDomain: string | undefined;
|
|
16
|
+
primaryOrigin: string;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=IxNextjsSite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IxNextjsSite.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/IxNextjsSite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAO5C,KAAK,cAAc,GAAG,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAClE,KAAK,WAAW,GAAG,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/D,KAAK,cAAc,GAAG,OAAO,CAC3B,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAC3C,SAAS,CACV,CAAC;AAEF,qBAAa,YAAa,SAAQ,UAAU;gBAExC,KAAK,EAAE,cAAc,EACrB,EAAE,EAAE,WAAW,EACf,KAAK,GAAE,cAAmB;IAiB5B,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAqCnC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAiBhC,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAgC/B,OAAO,CAAC,gBAAgB;IAgBxB,IAAW,aAAa,IAAI,MAAM,EAAE,CASnC;IAED,IAAW,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAO9C;IAED,IAAW,WAAW,IAAI,MAAM,GAAG,IAAI,CAKtC;IAED,IAAW,kBAAkB,IAAI,MAAM,EAAE,CAKxC;IAEM,aAAa,qBACwD;IAErE,aAAa,SAAmC;CACxD"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NextjsSite } from "sst/constructs";
|
|
2
|
+
import { IxCertificate } from "./IxCertificate.js";
|
|
3
|
+
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
4
|
+
import { IxVpcDetails } from "./IxVpcDetails.js";
|
|
5
|
+
import { IxDnsRecord } from "./IxDnsRecord.js";
|
|
6
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
7
|
+
export class IxNextjsSite extends NextjsSite {
|
|
8
|
+
constructor(scope, id, props = {}) {
|
|
9
|
+
const isIxDeploy = !!process.env.IX_APP_NAME;
|
|
10
|
+
if (isIxDeploy) {
|
|
11
|
+
IxNextjsSite.addVpcDetailsToProps(scope, id, props);
|
|
12
|
+
IxNextjsSite.setupCustomDomain(scope, id, props);
|
|
13
|
+
}
|
|
14
|
+
super(scope, id, props);
|
|
15
|
+
if (isIxDeploy) {
|
|
16
|
+
this.createDnsRecords(scope);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// This must be static because we need to call it in the constructor before super
|
|
20
|
+
static addVpcDetailsToProps(scope, id, props) {
|
|
21
|
+
const vpcDetails = new IxVpcDetails(scope, id + "-IxVpcDetails");
|
|
22
|
+
if (!props.cdk?.server || !("vpc" in props.cdk.server)) {
|
|
23
|
+
props.cdk = props.cdk ?? {};
|
|
24
|
+
props.cdk.server = {
|
|
25
|
+
...props.cdk.server,
|
|
26
|
+
vpc: vpcDetails.vpc,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!props.cdk?.revalidation || !("vpc" in props.cdk.revalidation)) {
|
|
30
|
+
props.cdk = props.cdk ?? {};
|
|
31
|
+
props.cdk.revalidation = {
|
|
32
|
+
...props.cdk.revalidation,
|
|
33
|
+
vpc: vpcDetails.vpc,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!props.cdk?.server || !("vpcSubnets" in props.cdk.server)) {
|
|
37
|
+
props.cdk = props.cdk ?? {};
|
|
38
|
+
props.cdk.server = {
|
|
39
|
+
...props.cdk.server,
|
|
40
|
+
vpcSubnets: vpcDetails.vpcSubnets,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!props.cdk?.revalidation || !("vpcSubnets" in props.cdk.revalidation)) {
|
|
44
|
+
props.cdk = props.cdk ?? {};
|
|
45
|
+
props.cdk.revalidation = {
|
|
46
|
+
...props.cdk.revalidation,
|
|
47
|
+
vpcSubnets: vpcDetails.vpcSubnets,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// This must be static because we need to call it in the constructor before super
|
|
52
|
+
static setupCustomDomain(scope, id, props) {
|
|
53
|
+
// Default to using domains names passed in by the pipeline as the custom domain
|
|
54
|
+
if (ixDeployConfig.isIxDeploy && !("customDomain" in props)) {
|
|
55
|
+
props.customDomain = {
|
|
56
|
+
domainName: ixDeployConfig.siteDomains[0],
|
|
57
|
+
alternateNames: ixDeployConfig.siteDomains.slice(1),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
this.setupCertificate(scope, id, props);
|
|
61
|
+
}
|
|
62
|
+
// This must be static because we need to call it in the constructor before super
|
|
63
|
+
static setupCertificate(scope, id, props) {
|
|
64
|
+
if (!props?.customDomain)
|
|
65
|
+
return;
|
|
66
|
+
if (typeof props.customDomain === "string") {
|
|
67
|
+
props.customDomain = { domainName: props.customDomain };
|
|
68
|
+
}
|
|
69
|
+
const domainName = props.customDomain.domainName;
|
|
70
|
+
let subjectAlternativeNames = props.customDomain.alternateNames;
|
|
71
|
+
// If domainAlias is provided, ensure it's in the subjectAlternativeNames
|
|
72
|
+
if (props.customDomain.domainAlias) {
|
|
73
|
+
subjectAlternativeNames = subjectAlternativeNames ?? [];
|
|
74
|
+
if (!subjectAlternativeNames.includes(props.customDomain.domainAlias)) {
|
|
75
|
+
subjectAlternativeNames.push(props.customDomain.domainAlias);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const domainCert = new IxCertificate(scope, id + "-IxCertificate", {
|
|
79
|
+
domainName,
|
|
80
|
+
subjectAlternativeNames,
|
|
81
|
+
region: "us-east-1", // CloudFront will only use certificates in us-east-1
|
|
82
|
+
});
|
|
83
|
+
props.customDomain.isExternalDomain = true;
|
|
84
|
+
props.customDomain.cdk = props.customDomain.cdk ?? {};
|
|
85
|
+
props.customDomain.cdk.certificate = domainCert.acmCertificate;
|
|
86
|
+
}
|
|
87
|
+
createDnsRecords(scope) {
|
|
88
|
+
if (!this.cdk?.distribution)
|
|
89
|
+
return;
|
|
90
|
+
for (const domainName of this.customDomains) {
|
|
91
|
+
const domainNameLogicalId = convertToBase62Hash(domainName);
|
|
92
|
+
new IxDnsRecord(scope, `DnsRecord-${domainNameLogicalId}`, {
|
|
93
|
+
type: "ALIAS",
|
|
94
|
+
name: domainName,
|
|
95
|
+
value: this.cdk.distribution.distributionDomainName,
|
|
96
|
+
aliasZoneId: CloudFrontTarget.getHostedZoneId(scope),
|
|
97
|
+
ttl: 900,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
get customDomains() {
|
|
102
|
+
const domainNames = new Set();
|
|
103
|
+
if (this.primaryCustomDomain)
|
|
104
|
+
domainNames.add(this.primaryCustomDomain);
|
|
105
|
+
if (this.aliasDomain)
|
|
106
|
+
domainNames.add(this.aliasDomain);
|
|
107
|
+
if (this.alternativeDomains.length)
|
|
108
|
+
this.alternativeDomains.forEach((domain) => domainNames.add(domain));
|
|
109
|
+
return Array.from(domainNames);
|
|
110
|
+
}
|
|
111
|
+
get primaryCustomDomain() {
|
|
112
|
+
if (typeof this.props.customDomain === "string") {
|
|
113
|
+
return this.props.customDomain;
|
|
114
|
+
}
|
|
115
|
+
else if (typeof this.props.customDomain === "object") {
|
|
116
|
+
return this.props.customDomain.domainName ?? null;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
get aliasDomain() {
|
|
121
|
+
if (typeof this.props.customDomain === "object") {
|
|
122
|
+
return this.props.customDomain.domainAlias ?? null;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
get alternativeDomains() {
|
|
127
|
+
if (typeof this.props.customDomain === "object") {
|
|
128
|
+
return this.props.customDomain.alternateNames ?? [];
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
primaryDomain = this.primaryCustomDomain ?? this.cdk?.distribution.distributionDomainName;
|
|
133
|
+
primaryOrigin = `https://${this.primaryDomain}`;
|
|
134
|
+
}
|
|
135
|
+
function convertToBase62Hash(string) {
|
|
136
|
+
const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
137
|
+
let hash = "";
|
|
138
|
+
let num = 0;
|
|
139
|
+
for (let i = 0; i < string.length; i++) {
|
|
140
|
+
num += string.charCodeAt(i);
|
|
141
|
+
}
|
|
142
|
+
while (num > 0) {
|
|
143
|
+
hash = base62Chars[num % 62] + hash;
|
|
144
|
+
num = Math.floor(num / 62);
|
|
145
|
+
}
|
|
146
|
+
return hash;
|
|
147
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { IVpc, SubnetSelection } from "aws-cdk-lib/aws-ec2";
|
|
3
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
4
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
5
|
+
export declare class IxVpcDetails extends Construct {
|
|
6
|
+
vpc: IVpc;
|
|
7
|
+
vpcSubnets: SubnetSelection;
|
|
8
|
+
constructor(scope: ConstructScope, id: ConstructId);
|
|
9
|
+
private getVpc;
|
|
10
|
+
private getVpcSubnet;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=IxVpcDetails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IxVpcDetails.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/IxVpcDetails.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAO,IAAI,EAAE,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAG/E,KAAK,cAAc,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,KAAK,WAAW,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9D,qBAAa,YAAa,SAAQ,SAAS;IAClC,GAAG,EAAE,IAAI,CAAC;IACV,UAAU,EAAE,eAAe,CAAC;gBAEvB,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW;IAMlD,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,YAAY;CAWrB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { Vpc, SubnetFilter } from "aws-cdk-lib/aws-ec2";
|
|
4
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
5
|
+
export class IxVpcDetails extends Construct {
|
|
6
|
+
vpc;
|
|
7
|
+
vpcSubnets;
|
|
8
|
+
constructor(scope, id) {
|
|
9
|
+
super(scope, id);
|
|
10
|
+
this.vpc = this.getVpc(scope, id);
|
|
11
|
+
this.vpcSubnets = this.getVpcSubnet(scope);
|
|
12
|
+
}
|
|
13
|
+
getVpc(scope, id) {
|
|
14
|
+
const vpcId = StringParameter.valueFromLookup(scope, "/vpc/id");
|
|
15
|
+
return Vpc.fromLookup(scope, id + "-Vpc", { vpcId });
|
|
16
|
+
}
|
|
17
|
+
getVpcSubnet(scope) {
|
|
18
|
+
const vpcSubnetIds = [1, 2, 3].map((subnetNum) => StringParameter.valueFromLookup(scope, `/vpc/subnet/private-${ixDeployConfig.workloadGroup}/${subnetNum}/id`));
|
|
19
|
+
return {
|
|
20
|
+
subnetFilters: [SubnetFilter.byIds(vpcSubnetIds)],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type IxDeployConfig = {
|
|
2
|
+
isIxDeploy: true;
|
|
3
|
+
appName: string;
|
|
4
|
+
environment: string;
|
|
5
|
+
workloadGroup: string;
|
|
6
|
+
primaryAwsRegion: string;
|
|
7
|
+
siteDomains: string[];
|
|
8
|
+
} | {
|
|
9
|
+
isIxDeploy: false;
|
|
10
|
+
appName: undefined;
|
|
11
|
+
environment: undefined;
|
|
12
|
+
workloadGroup: undefined;
|
|
13
|
+
primaryAwsRegion: undefined;
|
|
14
|
+
siteDomains: [];
|
|
15
|
+
};
|
|
16
|
+
declare const _default: IxDeployConfig;
|
|
17
|
+
export default _default;
|
|
18
|
+
//# sourceMappingURL=deployConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployConfig.d.ts","sourceRoot":"","sources":["../src/deployConfig.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GACf;IACE,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,GACD;IACE,UAAU,EAAE,KAAK,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;IACnB,WAAW,EAAE,SAAS,CAAC;IACvB,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,WAAW,EAAE,EAAE,CAAC;CACjB,CAAC;;AAEN,wBASoB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
isIxDeploy: Boolean(process.env.IX_DEPLOYMENT),
|
|
3
|
+
appName: process.env.IX_APP_NAME,
|
|
4
|
+
environment: process.env.IX_ENVIRONMENT,
|
|
5
|
+
workloadGroup: process.env.IX_WORKLOAD_GROUP,
|
|
6
|
+
primaryAwsRegion: process.env.IX_PRIMARY_AWS_REGION,
|
|
7
|
+
siteDomains: (process.env.IX_SITE_DOMAINS || "")
|
|
8
|
+
.split(",")
|
|
9
|
+
.map((domain) => domain.trim()),
|
|
10
|
+
};
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;EAQ/B"}
|
package/dist/shared.js
ADDED
package/eslint.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
import eslintConfigPrettier from "eslint-config-prettier";
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{ languageOptions: { globals: globals.node } },
|
|
8
|
+
pluginJs.configs.recommended,
|
|
9
|
+
...tseslint.configs.recommended,
|
|
10
|
+
eslintConfigPrettier,
|
|
11
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@infoxchange/make-it-so",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Makes deploying services to IX infra easy",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "vitest",
|
|
9
|
+
"lint": "eslint . --fix && prettier . --write",
|
|
10
|
+
"prepare": "husky",
|
|
11
|
+
"commit": "lint-staged && commit"
|
|
12
|
+
},
|
|
13
|
+
"author": "Infoxchange Vic Dev Team <vicdevs@infoxchange.org>",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./cdk-constructs": "./dist/cdk-constructs/index.js",
|
|
17
|
+
"./deployConfig": "./dist/deployConfig.js"
|
|
18
|
+
},
|
|
19
|
+
"lint-staged": {
|
|
20
|
+
"**/*": [
|
|
21
|
+
"eslint --fix --no-warn-ignored",
|
|
22
|
+
"prettier --write --ignore-unknown"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@eslint/js": "^9.3.0",
|
|
27
|
+
"aws-cdk-lib": "2.142.1",
|
|
28
|
+
"constructs": "^10.3.0",
|
|
29
|
+
"eslint": "^8.57.0",
|
|
30
|
+
"eslint-config-prettier": "^9.1.0",
|
|
31
|
+
"sst": "2.42.0",
|
|
32
|
+
"typescript-eslint": "^7.11.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@commitlint/cli": "^19.3.0",
|
|
36
|
+
"@commitlint/config-conventional": "^19.2.2",
|
|
37
|
+
"@commitlint/prompt-cli": "^19.3.1",
|
|
38
|
+
"@tsconfig/node21": "^21.0.3",
|
|
39
|
+
"globals": "^15.3.0",
|
|
40
|
+
"husky": "^9.0.11",
|
|
41
|
+
"lint-staged": "^15.2.5",
|
|
42
|
+
"prettier": "3.2.5",
|
|
43
|
+
"semantic-release": "^23.1.1",
|
|
44
|
+
"typescript": "^5.4.5",
|
|
45
|
+
"vitest": "^1.6.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { Certificate, ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
+
import { CustomResource } from "aws-cdk-lib";
|
|
5
|
+
|
|
6
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
7
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
domainName: string;
|
|
11
|
+
subjectAlternativeNames?: string[];
|
|
12
|
+
region?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class IxCertificate extends Construct {
|
|
16
|
+
public acmCertificate: ICertificate;
|
|
17
|
+
|
|
18
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props) {
|
|
19
|
+
super(scope, id);
|
|
20
|
+
this.acmCertificate = this.createCertificate(scope, id, props);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private createCertificate(
|
|
24
|
+
scope: ConstructScope,
|
|
25
|
+
id: ConstructId,
|
|
26
|
+
props: Props,
|
|
27
|
+
): ICertificate {
|
|
28
|
+
const certificateCreationLambdaArn =
|
|
29
|
+
StringParameter.valueForStringParameter(
|
|
30
|
+
scope,
|
|
31
|
+
"/shared-services/acm/lambdaArn",
|
|
32
|
+
);
|
|
33
|
+
const certificateCustomResource = new CustomResource(
|
|
34
|
+
scope,
|
|
35
|
+
"CertificateCustomResource",
|
|
36
|
+
{
|
|
37
|
+
resourceType: "Custom::CertIssuingLambda",
|
|
38
|
+
serviceToken: certificateCreationLambdaArn,
|
|
39
|
+
properties: {
|
|
40
|
+
DomainName: props.domainName,
|
|
41
|
+
...(props.subjectAlternativeNames && {
|
|
42
|
+
SubjectAlternativeNames: props.subjectAlternativeNames,
|
|
43
|
+
}),
|
|
44
|
+
...(props.region && { CertificateIssuingRegion: props.region }),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
return Certificate.fromCertificateArn(
|
|
49
|
+
scope,
|
|
50
|
+
id + "-AwsCertificate",
|
|
51
|
+
certificateCustomResource.ref,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { CustomResource } from "aws-cdk-lib";
|
|
4
|
+
import { remapKeys } from "../shared.js";
|
|
5
|
+
|
|
6
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
7
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
ttl?: number;
|
|
13
|
+
hostedZoneId?: string;
|
|
14
|
+
} & (
|
|
15
|
+
| {
|
|
16
|
+
type: "A" | "CNAME" | "NS" | "SOA";
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
type: "ALIAS";
|
|
20
|
+
aliasZoneId: string;
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export class IxDnsRecord extends Construct {
|
|
25
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props) {
|
|
26
|
+
super(scope, id);
|
|
27
|
+
this.createDnsRecord(scope, id, props);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private createDnsRecord(
|
|
31
|
+
scope: ConstructScope,
|
|
32
|
+
id: ConstructId,
|
|
33
|
+
constructProps: Props,
|
|
34
|
+
): void {
|
|
35
|
+
const dnsRecordUpdaterLambdaArn = StringParameter.valueForStringParameter(
|
|
36
|
+
scope,
|
|
37
|
+
"/shared-services/route53/lambdaArn",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const lambdaProps = remapKeys(constructProps, {
|
|
41
|
+
name: "RecordFQDN",
|
|
42
|
+
value: "RecordValue",
|
|
43
|
+
ttl: "RecordTTL",
|
|
44
|
+
hostedZoneId: "HostedZoneId",
|
|
45
|
+
type: "RecordType",
|
|
46
|
+
aliasZoneId: "AliasZoneId",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
new CustomResource(scope, id + "-CertificateCustomResource", {
|
|
50
|
+
resourceType: "Custom::DNSRecordUpdaterLambda",
|
|
51
|
+
serviceToken: dnsRecordUpdaterLambdaArn,
|
|
52
|
+
properties: lambdaProps,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { NextjsSite } from "sst/constructs";
|
|
2
|
+
import { IxCertificate } from "./IxCertificate.js";
|
|
3
|
+
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
4
|
+
import { IxVpcDetails } from "./IxVpcDetails.js";
|
|
5
|
+
import { IxDnsRecord } from "./IxDnsRecord.js";
|
|
6
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
7
|
+
|
|
8
|
+
type ConstructScope = ConstructorParameters<typeof NextjsSite>[0];
|
|
9
|
+
type ConstructId = ConstructorParameters<typeof NextjsSite>[1];
|
|
10
|
+
type ConstructProps = Exclude<
|
|
11
|
+
ConstructorParameters<typeof NextjsSite>[2],
|
|
12
|
+
undefined
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export class IxNextjsSite extends NextjsSite {
|
|
16
|
+
constructor(
|
|
17
|
+
scope: ConstructScope,
|
|
18
|
+
id: ConstructId,
|
|
19
|
+
props: ConstructProps = {},
|
|
20
|
+
) {
|
|
21
|
+
const isIxDeploy = !!process.env.IX_APP_NAME;
|
|
22
|
+
|
|
23
|
+
if (isIxDeploy) {
|
|
24
|
+
IxNextjsSite.addVpcDetailsToProps(scope, id, props);
|
|
25
|
+
IxNextjsSite.setupCustomDomain(scope, id, props);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
super(scope, id, props);
|
|
29
|
+
|
|
30
|
+
if (isIxDeploy) {
|
|
31
|
+
this.createDnsRecords(scope);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// This must be static because we need to call it in the constructor before super
|
|
36
|
+
private static addVpcDetailsToProps(
|
|
37
|
+
scope: ConstructScope,
|
|
38
|
+
id: ConstructId,
|
|
39
|
+
props: ConstructProps,
|
|
40
|
+
): void {
|
|
41
|
+
const vpcDetails = new IxVpcDetails(scope, id + "-IxVpcDetails");
|
|
42
|
+
if (!props.cdk?.server || !("vpc" in props.cdk.server)) {
|
|
43
|
+
props.cdk = props.cdk ?? {};
|
|
44
|
+
props.cdk.server = {
|
|
45
|
+
...props.cdk.server,
|
|
46
|
+
vpc: vpcDetails.vpc,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!props.cdk?.revalidation || !("vpc" in props.cdk.revalidation)) {
|
|
50
|
+
props.cdk = props.cdk ?? {};
|
|
51
|
+
props.cdk.revalidation = {
|
|
52
|
+
...props.cdk.revalidation,
|
|
53
|
+
vpc: vpcDetails.vpc,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!props.cdk?.server || !("vpcSubnets" in props.cdk.server)) {
|
|
57
|
+
props.cdk = props.cdk ?? {};
|
|
58
|
+
props.cdk.server = {
|
|
59
|
+
...props.cdk.server,
|
|
60
|
+
vpcSubnets: vpcDetails.vpcSubnets,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!props.cdk?.revalidation || !("vpcSubnets" in props.cdk.revalidation)) {
|
|
64
|
+
props.cdk = props.cdk ?? {};
|
|
65
|
+
props.cdk.revalidation = {
|
|
66
|
+
...props.cdk.revalidation,
|
|
67
|
+
vpcSubnets: vpcDetails.vpcSubnets,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// This must be static because we need to call it in the constructor before super
|
|
73
|
+
private static setupCustomDomain(
|
|
74
|
+
scope: ConstructScope,
|
|
75
|
+
id: ConstructId,
|
|
76
|
+
props: ConstructProps,
|
|
77
|
+
): void {
|
|
78
|
+
// Default to using domains names passed in by the pipeline as the custom domain
|
|
79
|
+
if (ixDeployConfig.isIxDeploy && !("customDomain" in props)) {
|
|
80
|
+
props.customDomain = {
|
|
81
|
+
domainName: ixDeployConfig.siteDomains[0],
|
|
82
|
+
alternateNames: ixDeployConfig.siteDomains.slice(1),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.setupCertificate(scope, id, props);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// This must be static because we need to call it in the constructor before super
|
|
90
|
+
private static setupCertificate(
|
|
91
|
+
scope: ConstructScope,
|
|
92
|
+
id: ConstructId,
|
|
93
|
+
props: ConstructProps,
|
|
94
|
+
): void {
|
|
95
|
+
if (!props?.customDomain) return;
|
|
96
|
+
|
|
97
|
+
if (typeof props.customDomain === "string") {
|
|
98
|
+
props.customDomain = { domainName: props.customDomain };
|
|
99
|
+
}
|
|
100
|
+
const domainName = props.customDomain.domainName;
|
|
101
|
+
let subjectAlternativeNames = props.customDomain.alternateNames;
|
|
102
|
+
|
|
103
|
+
// If domainAlias is provided, ensure it's in the subjectAlternativeNames
|
|
104
|
+
if (props.customDomain.domainAlias) {
|
|
105
|
+
subjectAlternativeNames = subjectAlternativeNames ?? [];
|
|
106
|
+
|
|
107
|
+
if (!subjectAlternativeNames.includes(props.customDomain.domainAlias)) {
|
|
108
|
+
subjectAlternativeNames.push(props.customDomain.domainAlias);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const domainCert = new IxCertificate(scope, id + "-IxCertificate", {
|
|
113
|
+
domainName,
|
|
114
|
+
subjectAlternativeNames,
|
|
115
|
+
region: "us-east-1", // CloudFront will only use certificates in us-east-1
|
|
116
|
+
});
|
|
117
|
+
props.customDomain.isExternalDomain = true;
|
|
118
|
+
props.customDomain.cdk = props.customDomain.cdk ?? {};
|
|
119
|
+
props.customDomain.cdk.certificate = domainCert.acmCertificate;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private createDnsRecords(scope: ConstructScope) {
|
|
123
|
+
if (!this.cdk?.distribution) return;
|
|
124
|
+
|
|
125
|
+
for (const domainName of this.customDomains) {
|
|
126
|
+
const domainNameLogicalId = convertToBase62Hash(domainName);
|
|
127
|
+
|
|
128
|
+
new IxDnsRecord(scope, `DnsRecord-${domainNameLogicalId}`, {
|
|
129
|
+
type: "ALIAS",
|
|
130
|
+
name: domainName,
|
|
131
|
+
value: this.cdk.distribution.distributionDomainName,
|
|
132
|
+
aliasZoneId: CloudFrontTarget.getHostedZoneId(scope),
|
|
133
|
+
ttl: 900,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public get customDomains(): string[] {
|
|
139
|
+
const domainNames = new Set<string>();
|
|
140
|
+
|
|
141
|
+
if (this.primaryCustomDomain) domainNames.add(this.primaryCustomDomain);
|
|
142
|
+
if (this.aliasDomain) domainNames.add(this.aliasDomain);
|
|
143
|
+
if (this.alternativeDomains.length)
|
|
144
|
+
this.alternativeDomains.forEach((domain) => domainNames.add(domain));
|
|
145
|
+
|
|
146
|
+
return Array.from(domainNames);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public get primaryCustomDomain(): string | null {
|
|
150
|
+
if (typeof this.props.customDomain === "string") {
|
|
151
|
+
return this.props.customDomain;
|
|
152
|
+
} else if (typeof this.props.customDomain === "object") {
|
|
153
|
+
return this.props.customDomain.domainName ?? null;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public get aliasDomain(): string | null {
|
|
159
|
+
if (typeof this.props.customDomain === "object") {
|
|
160
|
+
return this.props.customDomain.domainAlias ?? null;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public get alternativeDomains(): string[] {
|
|
166
|
+
if (typeof this.props.customDomain === "object") {
|
|
167
|
+
return this.props.customDomain.alternateNames ?? [];
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public primaryDomain =
|
|
173
|
+
this.primaryCustomDomain ?? this.cdk?.distribution.distributionDomainName;
|
|
174
|
+
|
|
175
|
+
public primaryOrigin = `https://${this.primaryDomain}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function convertToBase62Hash(string: string): string {
|
|
179
|
+
const base62Chars =
|
|
180
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
181
|
+
let hash = "";
|
|
182
|
+
let num = 0;
|
|
183
|
+
for (let i = 0; i < string.length; i++) {
|
|
184
|
+
num += string.charCodeAt(i);
|
|
185
|
+
}
|
|
186
|
+
while (num > 0) {
|
|
187
|
+
hash = base62Chars[num % 62] + hash;
|
|
188
|
+
num = Math.floor(num / 62);
|
|
189
|
+
}
|
|
190
|
+
return hash;
|
|
191
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
3
|
+
import { Vpc, IVpc, SubnetSelection, SubnetFilter } from "aws-cdk-lib/aws-ec2";
|
|
4
|
+
import ixDeployConfig from "../deployConfig.js";
|
|
5
|
+
|
|
6
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
7
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
8
|
+
|
|
9
|
+
export class IxVpcDetails extends Construct {
|
|
10
|
+
public vpc: IVpc;
|
|
11
|
+
public vpcSubnets: SubnetSelection;
|
|
12
|
+
|
|
13
|
+
constructor(scope: ConstructScope, id: ConstructId) {
|
|
14
|
+
super(scope, id);
|
|
15
|
+
this.vpc = this.getVpc(scope, id);
|
|
16
|
+
this.vpcSubnets = this.getVpcSubnet(scope);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private getVpc(scope: ConstructScope, id: ConstructId): IVpc {
|
|
20
|
+
const vpcId = StringParameter.valueFromLookup(scope, "/vpc/id");
|
|
21
|
+
return Vpc.fromLookup(scope, id + "-Vpc", { vpcId });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private getVpcSubnet(scope: ConstructScope): SubnetSelection {
|
|
25
|
+
const vpcSubnetIds = [1, 2, 3].map((subnetNum) =>
|
|
26
|
+
StringParameter.valueFromLookup(
|
|
27
|
+
scope,
|
|
28
|
+
`/vpc/subnet/private-${ixDeployConfig.workloadGroup}/${subnetNum}/id`,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
subnetFilters: [SubnetFilter.byIds(vpcSubnetIds)],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type IxDeployConfig =
|
|
2
|
+
| {
|
|
3
|
+
isIxDeploy: true;
|
|
4
|
+
appName: string;
|
|
5
|
+
environment: string;
|
|
6
|
+
workloadGroup: string;
|
|
7
|
+
primaryAwsRegion: string;
|
|
8
|
+
siteDomains: string[];
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
isIxDeploy: false;
|
|
12
|
+
appName: undefined;
|
|
13
|
+
environment: undefined;
|
|
14
|
+
workloadGroup: undefined;
|
|
15
|
+
primaryAwsRegion: undefined;
|
|
16
|
+
siteDomains: [];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
isIxDeploy: Boolean(process.env.IX_DEPLOYMENT),
|
|
21
|
+
appName: process.env.IX_APP_NAME,
|
|
22
|
+
environment: process.env.IX_ENVIRONMENT,
|
|
23
|
+
workloadGroup: process.env.IX_WORKLOAD_GROUP,
|
|
24
|
+
primaryAwsRegion: process.env.IX_PRIMARY_AWS_REGION,
|
|
25
|
+
siteDomains: (process.env.IX_SITE_DOMAINS || "")
|
|
26
|
+
.split(",")
|
|
27
|
+
.map((domain) => domain.trim()),
|
|
28
|
+
} as IxDeployConfig;
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function remapKeys(
|
|
2
|
+
object: Record<string, unknown>,
|
|
3
|
+
keyMap: Record<string, string>,
|
|
4
|
+
) {
|
|
5
|
+
return Object.fromEntries(
|
|
6
|
+
Object.entries(object).map(([key, value]) => {
|
|
7
|
+
const newKey = keyMap[key] ?? key;
|
|
8
|
+
return [newKey, value];
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
}
|