@appliance.sh/infra 1.17.0 → 1.19.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.19.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.19.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, ApplianceStackMetadata, 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, metadata?: ApplianceStackMetadata) {
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
+ metadata,
61
60
  config: this.baseConfig,
62
61
  },
63
62
  {
@@ -74,8 +73,8 @@ export class ApplianceDeploymentService {
74
73
  };
75
74
  }
76
75
 
77
- private async getOrCreateStack(stackName: string): Promise<auto.Stack> {
78
- const program = this.inlineProgram();
76
+ private async getOrCreateStack(stackName: string, metadata?: ApplianceStackMetadata): Promise<auto.Stack> {
77
+ const program = this.inlineProgram(stackName, metadata);
79
78
  const envVars: Record<string, string> = {
80
79
  AWS_REGION: this.region,
81
80
  };
@@ -113,8 +112,8 @@ export class ApplianceDeploymentService {
113
112
  return auto.Stack.createOrSelect(stackName, ws);
114
113
  }
115
114
 
116
- async deploy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
117
- const stack = await this.getOrCreateStack(stackName);
115
+ async deploy(stackName: string, metadata?: ApplianceStackMetadata): Promise<PulumiResult> {
116
+ const stack = await this.getOrCreateStack(stackName, metadata);
118
117
  const result = await stack.up({ onOutput: (m) => console.log(m) });
119
118
  const changes = result.summary.resourceChanges || {};
120
119
  const totalChanges = Object.entries(changes)
@@ -130,7 +129,7 @@ export class ApplianceDeploymentService {
130
129
  };
131
130
  }
132
131
 
133
- async destroy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
132
+ async destroy(stackName: string): Promise<PulumiResult> {
134
133
  try {
135
134
  const stack = await this.selectExistingStack(stackName);
136
135
  await stack.destroy({ onOutput: (m) => console.log(m) });
@@ -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,10 +1,51 @@
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
+ }
37
+
38
+ export interface ApplianceStackMetadata {
39
+ projectId: string;
40
+ projectName: string;
41
+ environmentId: string;
42
+ environmentName: string;
43
+ deploymentId: string;
44
+ stackName: string;
45
+ }
5
46
 
6
47
  export interface ApplianceStackArgs {
7
- tags?: Record<string, string>;
48
+ metadata?: ApplianceStackMetadata;
8
49
  config: ApplianceBaseConfig;
9
50
  }
10
51
 
@@ -25,17 +66,32 @@ export class ApplianceStack extends pulumi.ComponentResource {
25
66
  constructor(name: string, args: ApplianceStackArgs, opts: ApplianceStackOpts) {
26
67
  super('appliance:aws:ApplianceStack', name, args, opts);
27
68
 
69
+ // Short ID for AWS resource names (subject to 64-char limits)
70
+ const rid = toResourceId(name);
71
+ // DNS-safe label (max 63 chars per label)
72
+ const dnsLabel = toDnsLabel(name);
73
+
28
74
  const defaultOpts = { parent: this, provider: opts.provider };
29
75
  const defaultNativeOpts = { parent: this, provider: opts.nativeProvider };
30
- const defaultTags = { stack: name, managed: 'appliance', ...args.tags };
76
+ const defaultTags: Record<string, string> = {
77
+ 'appliance:managed': 'true',
78
+ 'appliance:stack-name': name,
79
+ };
80
+ if (args.metadata) {
81
+ defaultTags['appliance:project-id'] = args.metadata.projectId;
82
+ defaultTags['appliance:project-name'] = args.metadata.projectName;
83
+ defaultTags['appliance:environment-id'] = args.metadata.environmentId;
84
+ defaultTags['appliance:environment-name'] = args.metadata.environmentName;
85
+ defaultTags['appliance:deployment-id'] = args.metadata.deploymentId;
86
+ }
31
87
 
32
- this.lambdaRole = new aws.iam.Role(`${name}-role`, {
88
+ this.lambdaRole = new aws.iam.Role(`${rid}-role`, {
33
89
  path: `/appliance/${name}/`,
34
90
  assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: 'lambda.amazonaws.com' }),
35
91
  tags: defaultTags,
36
92
  });
37
93
 
38
- this.lambdaRolePolicy = new aws.iam.Policy(`${name}-policy`, {
94
+ this.lambdaRolePolicy = new aws.iam.Policy(`${rid}-policy`, {
39
95
  path: `/appliance/${name}/`,
40
96
  policy: {
41
97
  Version: '2012-10-17',
@@ -43,13 +99,13 @@ export class ApplianceStack extends pulumi.ComponentResource {
43
99
  },
44
100
  });
45
101
 
46
- new aws.iam.RolePolicyAttachment(`${name}-role-policy-attachment`, {
102
+ new aws.iam.RolePolicyAttachment(`${rid}-role-policy-attachment`, {
47
103
  role: this.lambdaRole.name,
48
104
  policyArn: this.lambdaRolePolicy.arn,
49
105
  });
50
106
 
51
107
  this.lambda = new aws.lambda.CallbackFunction(
52
- `${name}-handler`,
108
+ `${rid}-handler`,
53
109
  {
54
110
  runtime: 'nodejs22.x',
55
111
  callback: async () => {
@@ -62,7 +118,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
62
118
 
63
119
  // lambda url
64
120
  this.lambdaUrl = new aws.lambda.FunctionUrl(
65
- `${name}-url`,
121
+ `${rid}-url`,
66
122
  {
67
123
  functionName: this.lambda.name,
68
124
  authorizationType: args.config.aws.cloudfrontDistributionId ? 'AWS_IAM' : 'NONE',
@@ -70,11 +126,12 @@ export class ApplianceStack extends pulumi.ComponentResource {
70
126
  defaultOpts
71
127
  );
72
128
 
73
- this.dnsRecord = pulumi.interpolate`${name}.${args.config.domainName ?? ''}`;
129
+ // DNS uses the full stack name, not the truncated resource ID
130
+ this.dnsRecord = pulumi.interpolate`${dnsLabel}.${args.config.domainName ?? ''}`;
74
131
 
75
132
  if (args.config.aws.cloudfrontDistributionId) {
76
133
  new aws.lambda.Permission(
77
- `${name}-url-invoke-url-permission`,
134
+ `${rid}-cf-invoke-url`,
78
135
  {
79
136
  function: this.lambda.name,
80
137
  action: 'lambda:InvokeFunctionUrl',
@@ -92,7 +149,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
92
149
  // The edge router role is the execution role of the Lambda@Edge function that signs requests
93
150
  if (args.config.aws.edgeRouterRoleArn) {
94
151
  new aws.lambda.Permission(
95
- `${name}-invoke-url-edge-router-permission`,
152
+ `${rid}-edge-invoke-url`,
96
153
  {
97
154
  function: this.lambda.name,
98
155
  action: 'lambda:InvokeFunctionUrl',
@@ -104,7 +161,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
104
161
  );
105
162
 
106
163
  new awsNative.lambda.Permission(
107
- `${name}-invoke-edge-router-permission`,
164
+ `${rid}-edge-invoke`,
108
165
  {
109
166
  action: 'lambda:InvokeFunction',
110
167
  principal: args.config.aws.edgeRouterRoleArn,
@@ -116,7 +173,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
116
173
  }
117
174
  } else {
118
175
  new aws.lambda.Permission(
119
- `${name}-url-invoke-url-permission`,
176
+ `${rid}-public-invoke-url`,
120
177
  {
121
178
  function: this.lambda.name,
122
179
  action: 'lambda:InvokeFunctionUrl',
@@ -130,7 +187,7 @@ export class ApplianceStack extends pulumi.ComponentResource {
130
187
 
131
188
  if (args.config.aws.cloudfrontDistributionId && args.config.aws.cloudfrontDistributionDomainName) {
132
189
  new awsNative.lambda.Permission(
133
- `${name}-url-invoke-lambda-native-permission`,
190
+ `${rid}-cf-invoke`,
134
191
  {
135
192
  action: 'lambda:InvokeFunction',
136
193
  principal: 'cloudfront.amazonaws.com',
@@ -144,10 +201,10 @@ export class ApplianceStack extends pulumi.ComponentResource {
144
201
  );
145
202
 
146
203
  new aws.route53.Record(
147
- `${name}-cname-record`,
204
+ `${rid}-cname`,
148
205
  {
149
206
  zoneId: args.config.aws.zoneId,
150
- name: pulumi.interpolate`${name}.${args.config.domainName ?? ''}`,
207
+ name: pulumi.interpolate`${dnsLabel}.${args.config.domainName ?? ''}`,
151
208
  type: 'CNAME',
152
209
  ttl: 60,
153
210
  records: [args.config.aws.cloudfrontDistributionDomainName],
@@ -156,10 +213,10 @@ export class ApplianceStack extends pulumi.ComponentResource {
156
213
  );
157
214
 
158
215
  new aws.route53.Record(
159
- `${name}-txt-record`,
216
+ `${rid}-txt`,
160
217
  {
161
218
  zoneId: args.config.aws.zoneId,
162
- name: pulumi.interpolate`origin.${name}.${args.config.domainName ?? ''}`,
219
+ name: pulumi.interpolate`origin.${dnsLabel}.${args.config.domainName ?? ''}`,
163
220
  type: 'TXT',
164
221
  ttl: 60,
165
222
  records: [this.lambdaUrl.functionUrl],