dcl-ops-lib 9.2.0 → 9.3.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/acceptNlb.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import * as aws from "@pulumi/aws";
2
+ /** Makes a given security group accessible by the shared supra NLB on the reserved port range (7700-7799) */
3
+ export declare function makeSecurityGroupAccessibleFromSharedNlb(securityGroup: aws.ec2.SecurityGroup, ruleName?: string): void;
package/acceptNlb.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeSecurityGroupAccessibleFromSharedNlb = void 0;
4
+ const aws = require("@pulumi/aws");
5
+ const utils_1 = require("./utils");
6
+ const values_1 = require("./values");
7
+ /** Makes a given security group accessible by the shared supra NLB on the reserved port range (7700-7799) */
8
+ function makeSecurityGroupAccessibleFromSharedNlb(securityGroup, ruleName = "") {
9
+ const nlbSgId = (0, values_1.getEnvConfiguration)().then(($) => {
10
+ if (!$.nlbSecurityGroupId) {
11
+ throw new Error("NLB security group not found.");
12
+ }
13
+ return $.nlbSecurityGroupId;
14
+ });
15
+ new aws.ec2.SecurityGroupRule((0, utils_1.withRuleName)("accept-nlb-udp-ingress-rule", ruleName), {
16
+ securityGroupId: securityGroup.id,
17
+ sourceSecurityGroupId: nlbSgId,
18
+ description: `Allow UDP 7700-7799 from the supra NLB`,
19
+ fromPort: 7700,
20
+ toPort: 7799,
21
+ protocol: "udp",
22
+ type: "ingress",
23
+ }, { deleteBeforeReplace: true });
24
+ new aws.ec2.SecurityGroupRule((0, utils_1.withRuleName)("accept-nlb-tcp-ingress-rule", ruleName), {
25
+ securityGroupId: securityGroup.id,
26
+ sourceSecurityGroupId: nlbSgId,
27
+ description: `Allow TCP 7700-7799 from the supra NLB`,
28
+ fromPort: 7700,
29
+ toPort: 7799,
30
+ protocol: "tcp",
31
+ type: "ingress",
32
+ }, { deleteBeforeReplace: true });
33
+ }
34
+ exports.makeSecurityGroupAccessibleFromSharedNlb = makeSecurityGroupAccessibleFromSharedNlb;
35
+ //# sourceMappingURL=acceptNlb.js.map
@@ -1,6 +1,7 @@
1
1
  import * as aws from "@pulumi/aws";
2
2
  import * as pulumi from "@pulumi/pulumi";
3
3
  import { ExtraExposedServiceOptions } from "./exposePublicService";
4
+ import { NLBMapping } from "./exposeNlbService";
4
5
  import { HealthCheck } from "@pulumi/aws/ecs";
5
6
  import { Team } from "./fargateHelpers";
6
7
  export declare const getDefaultLogs: (serviceName: string, logGroup: aws.cloudwatch.LogGroup) => aws.ecs.LogConfiguration;
@@ -39,6 +40,7 @@ export type FargateTaskOptions = {
39
40
  forceNewDeployment?: boolean;
40
41
  extraPortMappings?: aws.ecs.PortMapping[];
41
42
  extraALBMappings?: ALBMapping[];
43
+ nlbMappings?: NLBMapping[];
42
44
  executionRolePolicies?: Record<string, pulumi.Input<string> | aws.iam.Policy>;
43
45
  taskRolePolicies?: Record<string, pulumi.Input<string> | aws.iam.Policy>;
44
46
  secrets?: aws.ecs.Secret[];
@@ -4,9 +4,11 @@ exports.createInternalService = exports.createFargateTask = exports.getFargateTa
4
4
  const aws = require("@pulumi/aws");
5
5
  const pulumi = require("@pulumi/pulumi");
6
6
  const acceptAlb_1 = require("./acceptAlb");
7
+ const acceptNlb_1 = require("./acceptNlb");
7
8
  const acceptBastion_1 = require("./acceptBastion");
8
9
  const domain_1 = require("./domain");
9
10
  const exposePublicService_1 = require("./exposePublicService");
11
+ const exposeNlbService_1 = require("./exposeNlbService");
10
12
  const network_1 = require("./network");
11
13
  const vpc_1 = require("./vpc");
12
14
  const supra_1 = require("./supra");
@@ -102,7 +104,7 @@ exports.getFargateTaskRole = getFargateTaskRole;
102
104
  * @param options.appAutoscaling Configuration for autoscaling
103
105
  */
104
106
  async function createFargateTask(serviceName, dockerImage, dockerListeningPort, environment, hostname, options) {
105
- let { healthCheck, healthCheckContainer, essential, dontExpose, securityGroups, cluster, memoryReservation, command, version, ephemeralStorageInGB, desiredCount, cpuReservation, extraPortMappings, extraALBMappings, executionRolePolicies, taskRolePolicies, ignoreServiceDiscovery, secrets, metrics, forceNewDeployment, dontAssignPublicIp, dependsOn, volumes, deregistrationDelay, mountPoints, repositoryCredentials, team, appAutoscaling, enableExecuteCommand, } = options;
107
+ let { healthCheck, healthCheckContainer, essential, dontExpose, securityGroups, cluster, memoryReservation, command, version, ephemeralStorageInGB, desiredCount, cpuReservation, extraPortMappings, extraALBMappings, nlbMappings, executionRolePolicies, taskRolePolicies, ignoreServiceDiscovery, secrets, metrics, forceNewDeployment, dontAssignPublicIp, dependsOn, volumes, deregistrationDelay, mountPoints, repositoryCredentials, team, appAutoscaling, enableExecuteCommand, } = options;
106
108
  if (undefined === essential) {
107
109
  essential = true;
108
110
  }
@@ -127,6 +129,9 @@ async function createFargateTask(serviceName, dockerImage, dockerListeningPort,
127
129
  if (undefined === extraALBMappings) {
128
130
  extraALBMappings = [];
129
131
  }
132
+ if (undefined === nlbMappings) {
133
+ nlbMappings = [];
134
+ }
130
135
  if (undefined === dependsOn) {
131
136
  dependsOn = [];
132
137
  }
@@ -235,7 +240,13 @@ async function createFargateTask(serviceName, dockerImage, dockerListeningPort,
235
240
  endpoint: "not exposed",
236
241
  };
237
242
  }
238
- const exposed = await (0, exposePublicService_1.exposePublicService)(`${serviceName}-${version}`, hostname, dockerListeningPort, healthCheck, vpc.id, options.extraExposedServiceOptions, deregistrationDelay);
243
+ // unified-dns mode: if any NLB mapping omits separateDomain, the NLB owns
244
+ // the public Cloudflare record, so tell the ALB to skip creating one.
245
+ const nlbOwnsCloudflareRecord = nlbMappings.some((m) => !m.separateDomain);
246
+ const albExtraOptions = nlbOwnsCloudflareRecord
247
+ ? { ...options.extraExposedServiceOptions, skipCloudflareRecord: true }
248
+ : options.extraExposedServiceOptions;
249
+ const exposed = await (0, exposePublicService_1.exposePublicService)(`${serviceName}-${version}`, hostname, dockerListeningPort, healthCheck, vpc.id, albExtraOptions, deregistrationDelay);
239
250
  targetGroups.push(exposed.targetGroup);
240
251
  for (let extraALBMapping of extraALBMappings) {
241
252
  const exposedExtra = await (0, exposePublicService_1.exposePublicService)(`${serviceName}-${extraALBMapping.dockerListeningPort}-${version}`, extraALBMapping.domain, extraALBMapping.dockerListeningPort, extraALBMapping.healthCheck, vpc.id, extraALBMapping.extraExposedServiceOptions);
@@ -248,12 +259,39 @@ async function createFargateTask(serviceName, dockerImage, dockerListeningPort,
248
259
  });
249
260
  }
250
261
  }
262
+ // NLB mappings: expose UDP/TCP ports via the shared Network Load Balancer
263
+ for (let nlbMapping of nlbMappings) {
264
+ const exposedNlb = await (0, exposeNlbService_1.exposeNlbService)(`${serviceName}-${nlbMapping.port}-${version}`, hostname, nlbMapping.port, nlbMapping.protocol, vpc.id, nlbMapping.healthCheck, nlbMapping.separateDomain);
265
+ targetGroups.push(exposedNlb.targetGroup);
266
+ if (!extraPortMappings.find((p) => p.hostPort === nlbMapping.port)) {
267
+ extraPortMappings.push({
268
+ containerPort: nlbMapping.port,
269
+ hostPort: nlbMapping.port,
270
+ protocol: nlbMapping.protocol,
271
+ });
272
+ }
273
+ // Allow inbound traffic on the specific port from anywhere
274
+ // (NLB preserves client source IPs for UDP)
275
+ new aws.ec2.SecurityGroupRule(`${serviceName}-nlb-${nlbMapping.protocol}-${nlbMapping.port}`, {
276
+ securityGroupId: taskSecurityGroup.id,
277
+ type: "ingress",
278
+ fromPort: nlbMapping.port,
279
+ toPort: nlbMapping.port,
280
+ protocol: nlbMapping.protocol,
281
+ cidrBlocks: ["0.0.0.0/0"],
282
+ description: `Allow ${nlbMapping.protocol.toUpperCase()} ${nlbMapping.port} from NLB (client IP preserved)`,
283
+ });
284
+ }
251
285
  const portMapping = {
252
286
  containerPort: dockerListeningPort,
253
287
  hostPort: dockerListeningPort,
254
288
  };
255
289
  // make the service accesible by the ALB
256
290
  (0, acceptAlb_1.makeSecurityGroupAccessibleFromSharedAlb)(taskSecurityGroup);
291
+ // make the service accessible by the NLB (if any NLB mappings exist)
292
+ if (nlbMappings.length > 0) {
293
+ (0, acceptNlb_1.makeSecurityGroupAccessibleFromSharedNlb)(taskSecurityGroup, serviceName);
294
+ }
257
295
  const service = await createInternalService({
258
296
  serviceName,
259
297
  cluster,
@@ -0,0 +1,36 @@
1
+ export type NLBMapping = {
2
+ /** The port to expose on the NLB (must be in 7700-7799 range) */
3
+ port: number;
4
+ protocol: "udp" | "tcp";
5
+ /** Health check config — NLB uses HTTP/TCP health checks even for UDP targets */
6
+ healthCheck?: {
7
+ port: string;
8
+ path?: string;
9
+ protocol?: "HTTP" | "TCP";
10
+ };
11
+ /**
12
+ * split-dns mode: provide a dedicated subdomain for NLB traffic
13
+ * (e.g. "pulse-udp." + publicDomain). The main hostname stays proxied via ALB.
14
+ *
15
+ * unified-dns mode (default): omit this — the main hostname's Cloudflare record
16
+ * becomes unproxied and points to the NLB instead of the ALB.
17
+ */
18
+ separateDomain?: string;
19
+ };
20
+ /**
21
+ * Expose a service port via the shared NLB. Creates an NLB target group,
22
+ * listener, and Cloudflare DNS record.
23
+ *
24
+ * @param name unique resource name prefix
25
+ * @param domain the service's main hostname (e.g. "pulse-server.decentraland.zone")
26
+ * @param port the port to expose (e.g. 7777)
27
+ * @param protocol "udp" or "tcp"
28
+ * @param vpcId VPC ID for the target group
29
+ * @param healthCheck health check config for the NLB target group
30
+ * @param separateDomain if provided, creates DNS on this domain instead of the main one
31
+ */
32
+ export declare function exposeNlbService(name: string, domain: string, port: number, protocol: "udp" | "tcp", vpcId: string, healthCheck?: NLBMapping["healthCheck"], separateDomain?: string): Promise<{
33
+ targetGroup: import("@pulumi/aws/lb/targetGroup").TargetGroup;
34
+ listener: import("@pulumi/aws/lb/listener").Listener;
35
+ cloudflareRecord: import("@pulumi/cloudflare/record").Record | undefined;
36
+ }>;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exposeNlbService = void 0;
4
+ const aws = require("@pulumi/aws");
5
+ const domain_1 = require("./domain");
6
+ const nlb_1 = require("./nlb");
7
+ const getDomainAndSubdomain_1 = require("./getDomainAndSubdomain");
8
+ const cloudflare_1 = require("./cloudflare");
9
+ const DEFAULT_NLB_HEALTHCHECK = {
10
+ protocol: "HTTP",
11
+ path: "/health",
12
+ port: "5000",
13
+ interval: 30,
14
+ healthyThreshold: 3,
15
+ unhealthyThreshold: 3,
16
+ };
17
+ /**
18
+ * Expose a service port via the shared NLB. Creates an NLB target group,
19
+ * listener, and Cloudflare DNS record.
20
+ *
21
+ * @param name unique resource name prefix
22
+ * @param domain the service's main hostname (e.g. "pulse-server.decentraland.zone")
23
+ * @param port the port to expose (e.g. 7777)
24
+ * @param protocol "udp" or "tcp"
25
+ * @param vpcId VPC ID for the target group
26
+ * @param healthCheck health check config for the NLB target group
27
+ * @param separateDomain if provided, creates DNS on this domain instead of the main one
28
+ */
29
+ async function exposeNlbService(name, domain, port, protocol, vpcId, healthCheck, separateDomain) {
30
+ if (port < 7700 || port > 7799) {
31
+ throw new Error(`NLB port ${port} is outside the allowed range (7700-7799). Update the NLB security group in supra to allow a wider range if needed.`);
32
+ }
33
+ const nlbResult = await (0, nlb_1.getNlb)();
34
+ if (!nlbResult) {
35
+ throw new Error("NLB is not available in this environment. NLB is only deployed in dev and prd.");
36
+ }
37
+ const { nlb } = nlbResult;
38
+ const healthCheckConfig = {
39
+ ...DEFAULT_NLB_HEALTHCHECK,
40
+ ...healthCheck,
41
+ };
42
+ // AWS target group names max 32 chars (base + dash + 7 random = base ≤ 24).
43
+ // "ntg-" (4) + last 20 chars of name = 24 max.
44
+ const tgSlug = name.slice(-20);
45
+ const targetGroup = new aws.lb.TargetGroup(`ntg-${tgSlug}`, {
46
+ port,
47
+ protocol: protocol.toUpperCase(),
48
+ targetType: "ip",
49
+ vpcId,
50
+ healthCheck: {
51
+ protocol: healthCheckConfig.protocol,
52
+ port: healthCheckConfig.port,
53
+ path: healthCheckConfig.protocol === "HTTP" ? healthCheckConfig.path : undefined,
54
+ interval: healthCheckConfig.interval,
55
+ healthyThreshold: healthCheckConfig.healthyThreshold,
56
+ unhealthyThreshold: healthCheckConfig.unhealthyThreshold,
57
+ },
58
+ });
59
+ const listener = new aws.lb.Listener(`nlb-ls-${name}`, {
60
+ loadBalancerArn: nlb.arn,
61
+ port,
62
+ protocol: protocol.toUpperCase(),
63
+ defaultActions: [
64
+ {
65
+ type: "forward",
66
+ targetGroupArn: targetGroup.arn,
67
+ },
68
+ ],
69
+ });
70
+ // DNS: create an unproxied Cloudflare CNAME pointing to the NLB
71
+ const dnsDomain = separateDomain || domain;
72
+ const domainParts = (0, getDomainAndSubdomain_1.getDomainAndSubdomain)(dnsDomain);
73
+ let cloudflareRecord;
74
+ if (dnsDomain.endsWith(`.${domain_1.publicDomain}`)) {
75
+ cloudflareRecord = await (0, cloudflare_1.setRecord)({
76
+ recordName: domainParts.subdomain,
77
+ type: "CNAME",
78
+ value: nlb.dnsName,
79
+ proxied: false,
80
+ ttl: 300,
81
+ });
82
+ }
83
+ return {
84
+ targetGroup,
85
+ listener,
86
+ cloudflareRecord,
87
+ };
88
+ }
89
+ exports.exposeNlbService = exposeNlbService;
90
+ //# sourceMappingURL=exposeNlbService.js.map
@@ -11,6 +11,7 @@ export type UnproxiedCloudflareDomain = {
11
11
  export type CloudflareDomainOptions = ProxiedCloudflareDomain | UnproxiedCloudflareDomain | {};
12
12
  export type ExtraExposedServiceOptions = CloudflareDomainOptions & {
13
13
  skipInternalDomain?: boolean;
14
+ skipCloudflareRecord?: boolean;
14
15
  targetGroupConditions?: pulumi.Input<aws.types.input.alb.ListenerRuleCondition>[];
15
16
  };
16
17
  /**
@@ -34,7 +34,8 @@ async function exposePublicService(name, domain, port, healthCheck = {}, vpcId,
34
34
  const healthCheckValue = Object.assign({}, DEFAULT_HEALTHCHECK_VALUES, healthCheck);
35
35
  const isProxied = isProxiedDomain(extraOptions);
36
36
  const isUnproxied = isUnProxiedDomain(extraOptions);
37
- const createCloudflareRecord = isProxied || isUnproxied || domain.endsWith(`.${domain_1.publicDomain}`);
37
+ const skipCloudflare = (extraOptions && extraOptions.skipCloudflareRecord) || false;
38
+ const createCloudflareRecord = !skipCloudflare && (isProxied || isUnproxied || domain.endsWith(`.${domain_1.publicDomain}`));
38
39
  const onlyCloudflare = (extraOptions && extraOptions.skipInternalDomain) || false;
39
40
  const createInternalDomain = !onlyCloudflare;
40
41
  const certificate = (0, certificate_1.getCertificateFor)(domain);
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./acceptAlb";
2
+ export * from "./acceptNlb";
2
3
  export * from "./acceptBastion";
3
4
  export * from "./acceptDb";
4
5
  export * from "./accessTheInternet";
@@ -14,11 +15,13 @@ export * from "./createScheduledFargateTask";
14
15
  export * from "./createSQSTriggeredFargateTask";
15
16
  export * from "./domain";
16
17
  export * from "./exposePublicService";
18
+ export * from "./exposeNlbService";
17
19
  export * from "./getAmi";
18
20
  export * from "./getDomainAndSubdomain";
19
21
  export * from "./getImageRegistryAndCredentials";
20
22
  export * from "./lambda";
21
23
  export * from "./network";
24
+ export * from "./nlb";
22
25
  export * from "./prometheus";
23
26
  export * from "./scheduledTaskBase";
24
27
  export * from "./secrets";
package/index.js CHANGED
@@ -17,6 +17,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.withCache = void 0;
19
19
  __exportStar(require("./acceptAlb"), exports);
20
+ __exportStar(require("./acceptNlb"), exports);
20
21
  __exportStar(require("./acceptBastion"), exports);
21
22
  __exportStar(require("./acceptDb"), exports);
22
23
  __exportStar(require("./accessTheInternet"), exports);
@@ -32,11 +33,13 @@ __exportStar(require("./createScheduledFargateTask"), exports);
32
33
  __exportStar(require("./createSQSTriggeredFargateTask"), exports);
33
34
  __exportStar(require("./domain"), exports);
34
35
  __exportStar(require("./exposePublicService"), exports);
36
+ __exportStar(require("./exposeNlbService"), exports);
35
37
  __exportStar(require("./getAmi"), exports);
36
38
  __exportStar(require("./getDomainAndSubdomain"), exports);
37
39
  __exportStar(require("./getImageRegistryAndCredentials"), exports);
38
40
  __exportStar(require("./lambda"), exports);
39
41
  __exportStar(require("./network"), exports);
42
+ __exportStar(require("./nlb"), exports);
40
43
  __exportStar(require("./prometheus"), exports);
41
44
  __exportStar(require("./scheduledTaskBase"), exports);
42
45
  __exportStar(require("./secrets"), exports);
package/nlb.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import * as aws from "@pulumi/aws";
2
+ export declare const getNlb: () => Promise<{
3
+ nlb: aws.lb.GetLoadBalancerResult;
4
+ } | undefined>;
package/nlb.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNlb = void 0;
4
+ const aws = require("@pulumi/aws");
5
+ const supra_1 = require("./supra");
6
+ const withCache_1 = require("./withCache");
7
+ exports.getNlb = (0, withCache_1.default)(async () => {
8
+ const nlbInstance = await supra_1.supra.getOutputValue("nlbInstance");
9
+ if (!nlbInstance) {
10
+ return undefined;
11
+ }
12
+ const nlb = await aws.lb.getLoadBalancer({ arn: nlbInstance.arn });
13
+ return { nlb };
14
+ });
15
+ //# sourceMappingURL=nlb.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcl-ops-lib",
3
- "version": "9.2.0",
3
+ "version": "9.3.1",
4
4
  "scripts": {
5
5
  "build": "tsc && cp bin/* . && node test.js",
6
6
  "clean": "rm *.d.ts *.js *.js.map"
package/values.d.ts CHANGED
@@ -10,5 +10,6 @@ export type EnvironmentValues = {
10
10
  dbSecurity: string;
11
11
  albSecurityGroupId: string;
12
12
  bastionSecurityGroupId: string;
13
+ nlbSecurityGroupId?: string;
13
14
  };
14
15
  export declare const getEnvConfiguration: () => Promise<EnvironmentValues>;
package/values.js CHANGED
@@ -15,6 +15,7 @@ exports.getEnvConfiguration = (0, withCache_1.default)(async function () {
15
15
  dbSecurity: await supra_1.supra.getOutputValue("acceptDbSecurityGroupId"),
16
16
  albSecurityGroupId: await supra_1.supra.getOutputValue("albSecurityGroupId"),
17
17
  bastionSecurityGroupId: await supra_1.supra.getOutputValue("bastionSecurityGroupId"),
18
+ nlbSecurityGroupId: (await supra_1.supra.getOutputValue("nlbSecurityGroupId")) || undefined,
18
19
  };
19
20
  });
20
21
  //# sourceMappingURL=values.js.map