@appliance.sh/infra 1.17.0 → 1.18.0

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@appliance.sh/infra",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Deploy the Appliance Infrastructure",
5
5
  "repository": "https://github.com/appliance-sh/appliance.sh",
6
6
  "license": "MIT",
7
7
  "author": "Eliot Lim",
8
- "main": "dist/lib/index.js",
8
+ "main": "src/index.ts",
9
9
  "types": "dist/lib/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
@@ -17,12 +17,13 @@
17
17
  "scripts": {
18
18
  "test": "echo \"Error: no test specified\" && exit 1",
19
19
  "build": "tsc",
20
+ "clean": "rm -rf ./dist/",
20
21
  "deploy:entrypoint": "ts-node src/index.ts",
21
- "deploy": "pulumi up",
22
+ "deploy": "npm run clean && pulumi up && npm run build",
22
23
  "dev:setup": "npm link"
23
24
  },
24
25
  "dependencies": {
25
- "@appliance.sh/sdk": "1.17.0",
26
+ "@appliance.sh/sdk": "1.18.0",
26
27
  "@pulumi/aws": "^7.16.0",
27
28
  "@pulumi/aws-native": "^1.48.0",
28
29
  "@pulumi/awsx": "^3.1.0",
@@ -1,7 +1,7 @@
1
1
  import * as auto from '@pulumi/pulumi/automation';
2
2
  import * as aws from '@pulumi/aws';
3
3
  import * as awsNative from '@pulumi/aws-native';
4
- import { ApplianceStack } from './aws/ApplianceStack';
4
+ import { ApplianceStack, toResourceId } from './aws/ApplianceStack';
5
5
  import { applianceBaseConfig, ApplianceBaseConfig } from '@appliance.sh/sdk';
6
6
 
7
7
  export type PulumiAction = 'deploy' | 'destroy';
@@ -32,32 +32,31 @@ export class ApplianceDeploymentService {
32
32
  this.region = this.baseConfig?.aws.region || 'us-east-1';
33
33
  }
34
34
 
35
- private inlineProgram() {
35
+ private inlineProgram(stackName: string) {
36
36
  return async () => {
37
- const name = 'appliance';
38
-
39
37
  if (!this.baseConfig) {
40
38
  throw new Error('Missing base config');
41
39
  }
42
40
 
43
- const regionalProvider = new aws.Provider(`${name}-regional`, {
41
+ const rid = toResourceId(stackName);
42
+ const regionalProvider = new aws.Provider(`${rid}-regional`, {
44
43
  region: (this.baseConfig?.aws.region as aws.Region) ?? 'ap-southeast-1',
45
44
  });
46
- const globalProvider = new aws.Provider(`${name}-global`, {
45
+ const globalProvider = new aws.Provider(`${rid}-global`, {
47
46
  region: 'us-east-1',
48
47
  });
49
- const nativeRegionalProvider = new awsNative.Provider(`${name}-native-regional`, {
48
+ const nativeRegionalProvider = new awsNative.Provider(`${rid}-native-regional`, {
50
49
  region: (this.baseConfig?.aws.region as awsNative.Region) ?? 'ap-southeast-1',
51
50
  });
52
51
 
53
- const nativeGlobalProvider = new awsNative.Provider(`${name}-native-global`, {
52
+ const nativeGlobalProvider = new awsNative.Provider(`${rid}-native-global`, {
54
53
  region: 'us-east-1',
55
54
  });
56
55
 
57
56
  const applianceStack = new ApplianceStack(
58
- `${name}-stack`,
57
+ stackName,
59
58
  {
60
- tags: { project: name },
59
+ tags: { project: stackName },
61
60
  config: this.baseConfig,
62
61
  },
63
62
  {
@@ -75,7 +74,7 @@ export class ApplianceDeploymentService {
75
74
  }
76
75
 
77
76
  private async getOrCreateStack(stackName: string): Promise<auto.Stack> {
78
- const program = this.inlineProgram();
77
+ const program = this.inlineProgram(stackName);
79
78
  const envVars: Record<string, string> = {
80
79
  AWS_REGION: this.region,
81
80
  };
@@ -20,6 +20,7 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
20
20
  public readonly certificateArn?: pulumi.Output<string>;
21
21
  public readonly cloudfrontDistribution?: aws.cloudfront.Distribution;
22
22
 
23
+ public readonly dataBucket: aws.s3.Bucket;
23
24
  public readonly config;
24
25
 
25
26
  constructor(name: string, args: ApplianceBaseAwsPublicArgs, opts?: ApplianceBaseAwsPublicOpts) {
@@ -123,6 +124,33 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
123
124
  { parent: this, provider: opts?.provider }
124
125
  );
125
126
 
127
+ this.dataBucket = new aws.s3.Bucket(
128
+ `${name}-data`,
129
+ {
130
+ acl: 'private',
131
+ forceDestroy: true,
132
+ },
133
+ { parent: this, provider: opts?.provider }
134
+ );
135
+
136
+ new aws.s3.BucketVersioning(
137
+ `${name}-data-versioning`,
138
+ {
139
+ bucket: this.dataBucket.bucket,
140
+ versioningConfiguration: { status: 'Enabled' },
141
+ },
142
+ { parent: this, provider: opts?.provider }
143
+ );
144
+
145
+ new aws.s3.BucketServerSideEncryptionConfiguration(
146
+ `${name}-data-sse`,
147
+ {
148
+ bucket: this.dataBucket.bucket,
149
+ rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }],
150
+ },
151
+ { parent: this, provider: opts?.provider }
152
+ );
153
+
126
154
  const lambdaOrigin = new aws.lambda.CallbackFunction(
127
155
  `${name}-origin`,
128
156
  {
@@ -458,6 +486,7 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
458
486
  cloudfrontDistributionId: this.cloudfrontDistribution.id,
459
487
  cloudfrontDistributionDomainName: this.cloudfrontDistribution.domainName,
460
488
  edgeRouterRoleArn: edgeRouterRole.arn,
489
+ dataBucketName: this.dataBucket.bucket,
461
490
  },
462
491
  };
463
492
 
@@ -1,7 +1,39 @@
1
1
  import * as pulumi from '@pulumi/pulumi';
2
2
  import * as aws from '@pulumi/aws';
3
3
  import * as awsNative from '@pulumi/aws-native';
4
- import { ApplianceBaseConfig } from '@appliance.sh/sdk';
4
+ import type { ApplianceBaseConfig } from '@appliance.sh/sdk';
5
+ import { createHash } from 'crypto';
6
+
7
+ // AWS resource name limits (IAM roles, Lambda functions) are 64 chars.
8
+ // Pulumi appends an 8-char suffix (-xxxxxxx). The longest resource
9
+ // suffix we add is "-handler" (8 chars). Budget: 64 - 8 - 8 = 48.
10
+ const MAX_RESOURCE_ID_LENGTH = 48;
11
+
12
+ // DNS labels (each segment between dots) are limited to 63 chars.
13
+ const MAX_DNS_LABEL_LENGTH = 63;
14
+
15
+ function truncateWithHash(name: string, maxLength: number): string {
16
+ if (name.length <= maxLength) return name;
17
+ const hash = createHash('sha256').update(name).digest('hex').slice(0, 7);
18
+ return `${name.slice(0, maxLength - 8)}-${hash}`;
19
+ }
20
+
21
+ /**
22
+ * Derive a short, deterministic resource ID from a stack name.
23
+ * If the name fits within the limit it is returned as-is.
24
+ * Otherwise it is truncated and a 7-char hash suffix is appended
25
+ * to preserve uniqueness.
26
+ */
27
+ export function toResourceId(name: string): string {
28
+ return truncateWithHash(name, MAX_RESOURCE_ID_LENGTH);
29
+ }
30
+
31
+ /**
32
+ * Derive a DNS-safe label from a stack name (max 63 chars).
33
+ */
34
+ export function toDnsLabel(name: string): string {
35
+ return truncateWithHash(name, MAX_DNS_LABEL_LENGTH);
36
+ }
5
37
 
6
38
  export interface ApplianceStackArgs {
7
39
  tags?: Record<string, string>;
@@ -25,17 +57,22 @@ export class ApplianceStack extends pulumi.ComponentResource {
25
57
  constructor(name: string, args: ApplianceStackArgs, opts: ApplianceStackOpts) {
26
58
  super('appliance:aws:ApplianceStack', name, args, opts);
27
59
 
60
+ // Short ID for AWS resource names (subject to 64-char limits)
61
+ const rid = toResourceId(name);
62
+ // DNS-safe label (max 63 chars per label)
63
+ const dnsLabel = toDnsLabel(name);
64
+
28
65
  const defaultOpts = { parent: this, provider: opts.provider };
29
66
  const defaultNativeOpts = { parent: this, provider: opts.nativeProvider };
30
67
  const defaultTags = { stack: name, managed: 'appliance', ...args.tags };
31
68
 
32
- this.lambdaRole = new aws.iam.Role(`${name}-role`, {
69
+ this.lambdaRole = new aws.iam.Role(`${rid}-role`, {
33
70
  path: `/appliance/${name}/`,
34
71
  assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: 'lambda.amazonaws.com' }),
35
72
  tags: defaultTags,
36
73
  });
37
74
 
38
- this.lambdaRolePolicy = new aws.iam.Policy(`${name}-policy`, {
75
+ this.lambdaRolePolicy = new aws.iam.Policy(`${rid}-policy`, {
39
76
  path: `/appliance/${name}/`,
40
77
  policy: {
41
78
  Version: '2012-10-17',
@@ -43,13 +80,13 @@ export class ApplianceStack extends pulumi.ComponentResource {
43
80
  },
44
81
  });
45
82
 
46
- new aws.iam.RolePolicyAttachment(`${name}-role-policy-attachment`, {
83
+ new aws.iam.RolePolicyAttachment(`${rid}-role-policy-attachment`, {
47
84
  role: this.lambdaRole.name,
48
85
  policyArn: this.lambdaRolePolicy.arn,
49
86
  });
50
87
 
51
88
  this.lambda = new aws.lambda.CallbackFunction(
52
- `${name}-handler`,
89
+ `${rid}-handler`,
53
90
  {
54
91
  runtime: 'nodejs22.x',
55
92
  callback: async () => {
@@ -62,7 +99,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
62
99
 
63
100
  // lambda url
64
101
  this.lambdaUrl = new aws.lambda.FunctionUrl(
65
- `${name}-url`,
102
+ `${rid}-url`,
66
103
  {
67
104
  functionName: this.lambda.name,
68
105
  authorizationType: args.config.aws.cloudfrontDistributionId ? 'AWS_IAM' : 'NONE',
@@ -70,11 +107,12 @@ export class ApplianceStack extends pulumi.ComponentResource {
70
107
  defaultOpts
71
108
  );
72
109
 
73
- this.dnsRecord = pulumi.interpolate`${name}.${args.config.domainName ?? ''}`;
110
+ // DNS uses the full stack name, not the truncated resource ID
111
+ this.dnsRecord = pulumi.interpolate`${dnsLabel}.${args.config.domainName ?? ''}`;
74
112
 
75
113
  if (args.config.aws.cloudfrontDistributionId) {
76
114
  new aws.lambda.Permission(
77
- `${name}-url-invoke-url-permission`,
115
+ `${rid}-cf-invoke-url`,
78
116
  {
79
117
  function: this.lambda.name,
80
118
  action: 'lambda:InvokeFunctionUrl',
@@ -92,7 +130,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
92
130
  // The edge router role is the execution role of the Lambda@Edge function that signs requests
93
131
  if (args.config.aws.edgeRouterRoleArn) {
94
132
  new aws.lambda.Permission(
95
- `${name}-invoke-url-edge-router-permission`,
133
+ `${rid}-edge-invoke-url`,
96
134
  {
97
135
  function: this.lambda.name,
98
136
  action: 'lambda:InvokeFunctionUrl',
@@ -104,7 +142,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
104
142
  );
105
143
 
106
144
  new awsNative.lambda.Permission(
107
- `${name}-invoke-edge-router-permission`,
145
+ `${rid}-edge-invoke`,
108
146
  {
109
147
  action: 'lambda:InvokeFunction',
110
148
  principal: args.config.aws.edgeRouterRoleArn,
@@ -116,7 +154,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
116
154
  }
117
155
  } else {
118
156
  new aws.lambda.Permission(
119
- `${name}-url-invoke-url-permission`,
157
+ `${rid}-public-invoke-url`,
120
158
  {
121
159
  function: this.lambda.name,
122
160
  action: 'lambda:InvokeFunctionUrl',
@@ -130,7 +168,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
130
168
 
131
169
  if (args.config.aws.cloudfrontDistributionId && args.config.aws.cloudfrontDistributionDomainName) {
132
170
  new awsNative.lambda.Permission(
133
- `${name}-url-invoke-lambda-native-permission`,
171
+ `${rid}-cf-invoke`,
134
172
  {
135
173
  action: 'lambda:InvokeFunction',
136
174
  principal: 'cloudfront.amazonaws.com',
@@ -144,10 +182,10 @@ export class ApplianceStack extends pulumi.ComponentResource {
144
182
  );
145
183
 
146
184
  new aws.route53.Record(
147
- `${name}-cname-record`,
185
+ `${rid}-cname`,
148
186
  {
149
187
  zoneId: args.config.aws.zoneId,
150
- name: pulumi.interpolate`${name}.${args.config.domainName ?? ''}`,
188
+ name: pulumi.interpolate`${dnsLabel}.${args.config.domainName ?? ''}`,
151
189
  type: 'CNAME',
152
190
  ttl: 60,
153
191
  records: [args.config.aws.cloudfrontDistributionDomainName],
@@ -156,10 +194,10 @@ export class ApplianceStack extends pulumi.ComponentResource {
156
194
  );
157
195
 
158
196
  new aws.route53.Record(
159
- `${name}-txt-record`,
197
+ `${rid}-txt`,
160
198
  {
161
199
  zoneId: args.config.aws.zoneId,
162
- name: pulumi.interpolate`origin.${name}.${args.config.domainName ?? ''}`,
200
+ name: pulumi.interpolate`origin.${dnsLabel}.${args.config.domainName ?? ''}`,
163
201
  type: 'TXT',
164
202
  ttl: 60,
165
203
  records: [this.lambdaUrl.functionUrl],