1/**
2 * The `aws-eks` {@link ClusterAdapter}: everything platform-specific about
3 * running `Kubernetes.*` workloads on Amazon EKS.
4 *
5 * - **connect** — SigV4 bearer tokens (STS presign) against the cluster
6 * endpoint, re-describing the cluster when the connection doesn't carry
7 * endpoint/CA (or they've gone stale).
8 * - **identity** — EKS Pod Identity: a generated IAM role +
9 * `PodIdentityAssociation`, with `policyStatements` host bindings landed
10 * as the role's inline policy.
11 * - **registry** — a per-workload ECR repository; `main` programs are
12 * bundled, `context` Dockerfiles built, and `image` refs mirrored into
13 * it.
14 * - **bootstrap** — generated container entries wiring the AWS credential
15 * chain so Pod Identity's container-credentials endpoint resolves inside
16 * the pod.
17 * - **loadBalancerDefaults** — EKS Auto Mode's `loadBalancerClass` and the
18 * internet-facing NLB scheme.
19 *
20 * Registered by `AWS.providers()`; resolved dynamically by the
21 * `Kubernetes.*` providers via the connection's `auth.kind`.
22 */
23import { Credentials } from "@distilled.cloud/aws/Credentials";
24import { Region, type RegionName } from "@distilled.cloud/aws/Region";
25import * as ecr from "@distilled.cloud/aws/ecr";
26import * as eks from "@distilled.cloud/aws/eks";
27import * as SigV4 from "@distilled.cloud/aws/SigV4";
28import * as Effect from "effect/Effect";
29import * as FileSystem from "effect/FileSystem";
30import * as Layer from "effect/Layer";
31import * as Path from "effect/Path";
32import * as Redacted from "effect/Redacted";
33import type { HttpClient } from "effect/http/HttpClient";
34import {
35 ClusterAdapter,
36 ClusterNotFoundError,
37 type ClusterAdapterService,
38 type ClusterTransport,
39 type ImageRegistryResolveOptions,
40 type WorkloadIdentityReconcileOptions,
41} from "../../Kubernetes/ClusterAdapter.ts";
42import type { Connection } from "../../Kubernetes/Connection.ts";
43import { createPhysicalName } from "../../PhysicalName.ts";
44import { Self } from "../../Self.ts";
45import { AWSEnvironment } from "../Environment.ts";
46import { makeImageSource, type ImageSourceLike } from "../ECR/ImageSource.ts";
47import type { PolicyStatement } from "../IAM/Policy.ts";
48import {
49 attachPolicyStatements,
50 deleteAssociation,
51 deletePodRole,
52 ensureAssociation,
53 ensurePodRole,
54} from "./internal/podIdentity.ts";
55
56declare module "../../Kubernetes/Connection.ts" {
57 interface AuthRegistry {
58 /**
59 * Authenticate against an Amazon EKS cluster with SigV4 bearer tokens
60 * minted from the ambient AWS credentials. Contributed by
61 * `AWS.providers()` — `AWS.EKS.Cluster` attributes expose a ready-made
62 * `connection` carrying this descriptor.
63 */
64 "aws-eks": {
65 /** The EKS cluster name. */
66 clusterName: string;
67 /**
68 * The cluster's region.
69 * @default the ambient AWS region
70 */
71 region?: string;
72 };
73 }
74}
75
76declare module "../../Kubernetes/ClusterAdapter.ts" {
77 interface IdentityStateRegistry {
78 /** EKS Pod Identity: generated IAM role + association. */
79 "aws-pod-identity": {
80 /** The ARN of the IAM role pods assume via Pod Identity. */
81 roleArn: string;
82 /** The name of the IAM role pods assume via Pod Identity. */
83 roleName: string;
84 /** The ARN of the Pod Identity association binding the role. */
85 associationArn: string;
86 /** The ID of the Pod Identity association binding the role. */
87 associationId: string;
88 };
89 }
90 interface RegistryStateRegistry {
91 /** The per-workload ECR repository holding the image. */
92 "aws-ecr": {
93 /** The name of the ECR repository holding the image. */
94 repositoryName: string;
95 /** The URI of the ECR repository holding the image. */
96 repositoryUri: string;
97 };
98 }
99 interface WorkloadIdentityOptions {
100 /**
101 * Managed policy ARNs attached to the generated pod-identity role in
102 * addition to the inline policy synthesized from bindings (EKS only).
103 */
104 managedPolicyArns?: string[];
105 }
106 interface WorkloadBindingContract {
107 /**
108 * IAM policy statements landed on the workload's pod-identity role
109 * (EKS only) — the same host-binding channel as `AWS.Lambda.Function`
110 * and `AWS.ECS.Task`.
111 */
112 policyStatements?: PolicyStatement[];
113 }
114 interface WorkloadServicesRegistry {
115 /** AWS credential-chain services ambient inside EKS workload pods. */
116 aws: Credentials | Region | AWSEnvironment;
117 }
118}
119
120/** Services the adapter's methods close over at layer build. */
121type EksAdapterDeps =
122 | Credentials
123 | HttpClient
124 | Region
125 | AWSEnvironment
126 | FileSystem.FileSystem
127 | Path.Path;
128
129/** Mint an EKS SigV4 bearer token (STS `GetCallerIdentity` presign). */
130const mintEksToken = Effect.fn(function* (clusterName: string, region: string) {
131 const credentials = yield* yield* Credentials;
132
133 const presigned = yield* SigV4.sign({
134 method: "GET",
135 url: `https://sts.${region}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`,
136 headers: { "x-k8s-aws-id": clusterName },
137 accessKeyId: Redacted.value(credentials.accessKeyId),
138 secretAccessKey: credentials.secretAccessKey,
139 sessionToken: credentials.sessionToken,
140 service: "sts",
141 region,
142 signQuery: true,
143 allHeaders: true,
144 });
145
146 return `k8s-aws-v1.${Buffer.from(presigned.url).toString("base64url")}`;
147});
148
149/**
150 * Build a {@link ClusterTransport} for an EKS cluster from known
151 * endpoint/CA. Captures the ambient AWS services so the per-request token
152 * mint is self-contained — used by the adapter and by the
153 * `AWS.EKS.Cluster` provider's own kubernetes-object binding channel.
154 */
155export const makeEksTransport = Effect.fn(function* (options: {
156 clusterName: string;
157 region?: string | undefined;
158 endpoint: string;
159 certificateAuthorityData: string;
160}) {
161 const context = yield* Effect.context<Credentials | AWSEnvironment>();
162 const region = options.region ?? (yield* AWSEnvironment.current).region;
163 return {
164 endpoint: options.endpoint,
165 certificateAuthorityData: options.certificateAuthorityData,
166 headers: mintEksToken(options.clusterName, region).pipe(
167 Effect.map((token) => ({ Authorization: `Bearer ${token}` })),
168 Effect.provideContext(context),
169 ),
170 } satisfies ClusterTransport;
171});
172
173/**
174 * The `Kubernetes.Connection` of an EKS cluster — stamped on
175 * `AWS.EKS.Cluster` attributes so the cluster resource can be passed
176 * directly as any `Kubernetes.*` workload's `cluster`.
177 */
178export const eksConnectionOf = (options: {
179 clusterName: string;
180 region: string;
181 endpoint?: string | undefined;
182 certificateAuthorityData?: string | undefined;
183}): Connection => ({
184 endpoint: options.endpoint,
185 certificateAuthorityData: options.certificateAuthorityData,
186 auth: {
187 kind: "aws-eks",
188 clusterName: options.clusterName,
189 region: options.region,
190 },
191});
192
193const narrowEksAuth = (connection: Connection) =>
194 connection.auth.kind === "aws-eks"
195 ? Effect.succeed(connection.auth)
196 : Effect.die(
197 new Error(
198 `aws-eks adapter received auth kind '${connection.auth.kind}'`,
199 ),
200 );
201
202/**
203 * Provide a per-connection region override so a cluster in another region
204 * than the ambient one describes correctly.
205 */
206const withRegion = (region: string | undefined) => {
207 return <A, E, R>(self: Effect.Effect<A, E, R>) =>
208 region === undefined
209 ? self
210 : Effect.provideService(
211 self,
212 Region,
213 Effect.succeed(region as RegionName),
214 );
215};
216
217const createRoleName = (id: string) =>
218 createPhysicalName({ id: `${id}-pod-role`, maxLength: 64 });
219
220const createPolicyName = (id: string) =>
221 createPhysicalName({ id: `${id}-pod-policy`, maxLength: 128 });
222
223const createRepositoryName = (id: string) =>
224 createPhysicalName({
225 id: `${id}-repo`,
226 maxLength: 256,
227 lowercase: true,
228 });
229
230/**
231 * Generated container entry for an Effect-native EKS server: resolves the
232 * program's registered runners and serves the returned `{ fetch }` handler
233 * on `PORT`. Credentials use the full chain so EKS Pod Identity's
234 * container-credentials endpoint resolves inside the pod.
235 */
236export const makeEksServerBootstrap =
237 (handler: string) =>
238 (importPath: string): string =>
239 `
240import { BunServices } from "@effect/platform-bun";
241import { BunHttpServer } from "alchemy/Http";
242import { Stack } from "alchemy/Stack";
243import { Stage } from "alchemy/Stage";
244import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
245import { provideProcessTelemetry } from "alchemy/Telemetry";
246import * as Context from "effect/Context";
247import * as Config from "effect/Config";
248import * as ConfigProvider from "effect/ConfigProvider";
249import * as Credentials from "@distilled.cloud/aws/Credentials";
250import * as Effect from "effect/Effect";
251import * as FetchHttpClient from "effect/http/FetchHttpClient";
252import * as Layer from "effect/Layer";
253import * as Logger from "effect/Logger";
254import * as Region from "@distilled.cloud/aws/Region";
255
256import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
257
258// Normalize the entrypoint export: an inline-effect class default export is
259// an Effect resolving the platform instance, while the tagged form
260// (X.make(props, impl)) exports a Layer providing the Self tag. Both fold
261// into a Layer via makeEntrypointLayer (same pattern as the ECS/Lambda/
262// Cloudflare Container bridges).
263const tag = Context.Service("${Self.key}");
264const layer = makeEntrypointLayer(tag, entrypoint);
265
266const platform = Layer.mergeAll(
267 BunServices.layer,
268 FetchHttpClient.layer,
269 Logger.layer([Logger.consolePretty()]),
270);
271
272// Resolve the bundled program (the runners registered via host.run / serve)
273// and run it with a Bun HTTP server bound to PORT, so a returned { fetch }
274// handler is served and host.run loops stay alive. Credentials use the full
275// chain so EKS Pod Identity's container-credentials endpoint
276// (AWS_CONTAINER_CREDENTIALS_FULL_URI + token file) resolves inside the pod.
277const program = tag.pipe(
278 // Process-lifetime telemetry: built once into the root scope; exporters
279 // batch on their intervals and flush when the scope closes on graceful
280 // shutdown.
281 Effect.flatMap((host) =>
282 host.RuntimeContext.exports.pipe(
283 Effect.flatMap((exports) => exports.program),
284 provideProcessTelemetry(host.RuntimeContext),
285 ),
286 ),
287 Effect.provide(
288 layer.pipe(Layer.provideMerge(Layer.mergeAll(
289 Layer.effect(
290 Stack,
291 Effect.all([
292 Config.String("ALCHEMY_STACK_NAME"),
293 Config.String("ALCHEMY_STAGE")
294 ]).pipe(
295 Effect.map(([name, stage]) => ({
296 name,
297 stage,
298 bindings: {},
299 resources: {}
300 }))
301 )
302 ),
303 // Module-scope declarations shared with the Stack may read the stage.
304 Layer.effect(Stage, Config.String("ALCHEMY_STAGE")),
305 )),
306 Layer.provideMerge(Credentials.fromChain()),
307 Layer.provideMerge(Region.fromEnv()),
308 Layer.provideMerge(BunHttpServer()),
309 Layer.provideMerge(platform),
310 Layer.provideMerge(
311 Layer.succeed(
312 ConfigProvider.ConfigProvider,
313 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
314 )
315 ),
316 )
317 ),
318 Effect.scoped
319);
320
321console.log(\`EKS Deployment bootstrap starting on port \${process.env.PORT ?? 3000}...\`);
322await Effect.runPromise(program).catch((err) => {
323 console.error("EKS Deployment bootstrap failed:", err);
324 process.exit(1);
325});
326`;
327
328/**
329 * Generated container entry for an Effect-native EKS Job: resolves the
330 * program's `run` effect, executes it to completion, and exits. No HTTP
331 * server is started. Credentials use the full chain so EKS Pod Identity's
332 * container-credentials endpoint resolves inside the pod.
333 */
334export const makeEksJobBootstrap =
335 (handler: string) =>
336 (importPath: string): string =>
337 `
338import { BunServices } from "@effect/platform-bun";
339import { Stack } from "alchemy/Stack";
340import { Stage } from "alchemy/Stage";
341import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
342import { provideProcessTelemetry } from "alchemy/Telemetry";
343import * as Context from "effect/Context";
344import * as Config from "effect/Config";
345import * as ConfigProvider from "effect/ConfigProvider";
346import * as Credentials from "@distilled.cloud/aws/Credentials";
347import * as Effect from "effect/Effect";
348import * as FetchHttpClient from "effect/http/FetchHttpClient";
349import * as Layer from "effect/Layer";
350import * as Logger from "effect/Logger";
351import * as Region from "@distilled.cloud/aws/Region";
352
353import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
354
355// Normalize the entrypoint export (see the server bootstrap).
356const tag = Context.Service("${Self.key}");
357const layer = makeEntrypointLayer(tag, entrypoint);
358
359const platform = Layer.mergeAll(
360 BunServices.layer,
361 FetchHttpClient.layer,
362 Logger.layer([Logger.consolePretty()]),
363);
364
365// Resolve the bundled program's registered one-shot runners (the shape's
366// \`run\` effect and any host.run work) and execute them to completion.
367const program = tag.pipe(
368 // Process-lifetime telemetry: built once into the root scope; exporters
369 // batch on their intervals and flush when the scope closes as the
370 // one-shot program completes.
371 Effect.flatMap((host) =>
372 host.RuntimeContext.exports.pipe(
373 Effect.flatMap((exports) => exports.program),
374 provideProcessTelemetry(host.RuntimeContext),
375 ),
376 ),
377 Effect.provide(
378 layer.pipe(Layer.provideMerge(Layer.mergeAll(
379 Layer.effect(
380 Stack,
381 Effect.all([
382 Config.String("ALCHEMY_STACK_NAME"),
383 Config.String("ALCHEMY_STAGE")
384 ]).pipe(
385 Effect.map(([name, stage]) => ({
386 name,
387 stage,
388 bindings: {},
389 resources: {}
390 }))
391 )
392 ),
393 // Module-scope declarations shared with the Stack may read the stage.
394 Layer.effect(Stage, Config.String("ALCHEMY_STAGE")),
395 )),
396 Layer.provideMerge(Credentials.fromChain()),
397 Layer.provideMerge(Region.fromEnv()),
398 Layer.provideMerge(platform),
399 Layer.provideMerge(
400 Layer.succeed(
401 ConfigProvider.ConfigProvider,
402 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
403 )
404 ),
405 )
406 ),
407 Effect.scoped
408);
409
410console.log("EKS Job bootstrap starting...");
411await Effect.runPromise(program).catch((err) => {
412 console.error("EKS Job bootstrap failed:", err);
413 process.exit(1);
414});
415// Run-to-completion semantics: exit 0 explicitly so lingering handles
416// (sockets, timers) never keep the pod alive after the work is done.
417process.exit(0);
418`;
419
420/**
421 * The `aws-eks` cluster adapter layer. Provided (merged) by
422 * `AWS.providers()` so the `Kubernetes.*` workload providers can resolve
423 * it from the stack context.
424 */
425export const EksKubernetesAdapter = () =>
426 Layer.effect(
427 ClusterAdapter("aws-eks"),
428 Effect.gen(function* () {
429 const imageSource = yield* makeImageSource;
430 const context = yield* Effect.context<EksAdapterDeps>();
431
432 // Discharge the layer-captured AWS services; per-resource engine
433 // services (InstanceId/Stack/Stage) stay ambient — they are provided
434 // by the invoking lifecycle operation, never captured here.
435 const withAws =
436 (region: string | undefined) =>
437 <A, E, R>(self: Effect.Effect<A, E, R>) =>
438 Effect.provideContext(withRegion(region)(self), context);
439
440 /** Describe the cluster; NotFound / DELETING → ClusterNotFoundError. */
441 const describeLiveCluster = Effect.fn(function* (auth: {
442 clusterName: string;
443 region?: string | undefined;
444 }) {
445 const described = yield* eks
446 .describeCluster({ name: auth.clusterName })
447 .pipe(
448 Effect.catchTag("ResourceNotFoundException", () =>
449 Effect.succeed(undefined),
450 ),
451 withAws(auth.region),
452 );
453 const cluster = described?.cluster;
454 if (!cluster || cluster.status === "DELETING") {
455 return yield* Effect.fail(
456 new ClusterNotFoundError({
457 message: `EKS cluster '${auth.clusterName}' no longer exists`,
458 }),
459 );
460 }
461 return cluster;
462 });
463
464 const connect = Effect.fn(function* (connection: Connection) {
465 const auth = yield* narrowEksAuth(connection);
466 let endpoint = connection.endpoint;
467 let certificateAuthorityData = connection.certificateAuthorityData;
468 if (!endpoint || !certificateAuthorityData) {
469 // Re-describe for a fresh endpoint + CA (persisted attributes
470 // may predate them, and EKS can rotate the endpoint DNS on
471 // recreate-with-same-name).
472 const cluster = yield* describeLiveCluster(auth);
473 endpoint = cluster.endpoint;
474 certificateAuthorityData = cluster.certificateAuthority?.data;
475 if (!endpoint || !certificateAuthorityData) {
476 return yield* Effect.fail(
477 new Error(
478 `EKS cluster '${auth.clusterName}' has no endpoint or ` +
479 "certificate authority data yet (still creating?)",
480 ),
481 );
482 }
483 }
484 return yield* makeEksTransport({
485 clusterName: auth.clusterName,
486 region: auth.region,
487 endpoint,
488 certificateAuthorityData,
489 }).pipe(withAws(auth.region));
490 });
491
492 const identityReconcile = Effect.fn(function* (
493 options: WorkloadIdentityReconcileOptions,
494 ) {
495 const auth = yield* narrowEksAuth(options.connection);
496 return yield* Effect.gen(function* () {
497 const state = options.state;
498 const roleName =
499 typeof state?.roleName === "string"
500 ? state.roleName
501 : yield* createRoleName(options.id);
502 const policyName = yield* createPolicyName(options.id);
503
504 // Ensure IAM role + inline policy from bindings.
505 const roleArn =
506 typeof state?.roleArn === "string"
507 ? state.roleArn
508 : yield* ensurePodRole({
509 id: options.id,
510 roleName,
511 managedPolicyArns: options.options?.managedPolicyArns,
512 });
513 yield* attachPolicyStatements({
514 roleName,
515 policyName,
516 bindings: options.bindings,
517 });
518
519 // Ensure the pod identity association wires the role to the SA.
520 const { associationArn, associationId } = yield* ensureAssociation({
521 id: options.id,
522 clusterName: auth.clusterName,
523 namespace: options.namespace,
524 serviceAccount: options.serviceAccount,
525 roleArn,
526 });
527
528 // Unlike ECS/Fargate (whose agent injects `AWS_REGION`), EKS pods
529 // get no region env var — inject it so the bootstrap's
530 // `Region.fromEnv()` resolves inside the pod.
531 const { region } = yield* AWSEnvironment.current;
532
533 return {
534 env: { AWS_REGION: auth.region ?? region },
535 state: {
536 kind: "aws-pod-identity" as const,
537 roleArn,
538 roleName,
539 associationArn,
540 associationId,
541 },
542 };
543 }).pipe(withAws(auth.region));
544 });
545
546 const identityDelete = Effect.fn(function* (options: {
547 connection: Connection | undefined;
548 state: Record<string, unknown> | undefined;
549 }) {
550 const state = options.state;
551 const auth =
552 options.connection !== undefined
553 ? yield* narrowEksAuth(options.connection)
554 : undefined;
555 yield* Effect.gen(function* () {
556 // The association is cluster-scoped: skip it when the cluster is
557 // gone or DELETING (it dies with the cluster; EKS also rejects
558 // the delete with `InvalidRequestException: Cluster is in
559 // invalid state` mid-teardown).
560 if (auth !== undefined && typeof state?.associationId === "string") {
561 const cluster = yield* describeLiveCluster(auth).pipe(
562 Effect.catchTag("Kubernetes.ClusterNotFoundError", () =>
563 Effect.succeed(undefined),
564 ),
565 );
566 if (cluster) {
567 yield* deleteAssociation({
568 clusterName: auth.clusterName,
569 associationId: state.associationId,
570 }).pipe(
571 Effect.catchTag("InvalidRequestException", () => Effect.void),
572 );
573 }
574 }
575 if (typeof state?.roleName === "string") {
576 yield* deletePodRole(state.roleName);
577 }
578 }).pipe(withAws(auth?.region));
579 });
580
581 const registryResolve = Effect.fn(function* (
582 options: ImageRegistryResolveOptions,
583 ) {
584 const state = options.state;
585 const repositoryName =
586 typeof state?.repositoryName === "string"
587 ? state.repositoryName
588 : // Physical-name generation reads the resource's ambient
589 // InstanceId/Stack/Stage — do not shadow them with the
590 // layer-captured context.
591 yield* createRepositoryName(options.id);
592 const repositoryUri =
593 typeof state?.repositoryUri === "string" &&
594 state.repositoryName === repositoryName
595 ? state.repositoryUri
596 : undefined;
597 const resolved = yield* imageSource
598 .resolve({
599 id: options.id,
600 source: options.source as ImageSourceLike,
601 repositoryName,
602 repositoryUri,
603 tags: options.tags,
604 platform: options.platform,
605 port: options.port,
606 isExternal: options.isExternal,
607 bootstrap: options.bootstrap,
608 session: options.session,
609 })
610 .pipe(Effect.provideContext(context));
611 return {
612 imageUri: resolved.imageUri,
613 codeHash: resolved.codeHash,
614 state: {
615 kind: "aws-ecr" as const,
616 repositoryName: resolved.repositoryName,
617 repositoryUri: resolved.repositoryUri,
618 },
619 };
620 });
621
622 const registryHash = Effect.fn(function* (options: {
623 source: ImageSourceLike;
624 platform: string;
625 port?: number | undefined;
626 isExternal?: boolean | undefined;
627 bootstrap: (importPath: string) => string;
628 }) {
629 return yield* imageSource
630 .hash({
631 source: options.source,
632 platform: options.platform,
633 port: options.port,
634 isExternal: options.isExternal,
635 bootstrap: options.bootstrap,
636 })
637 .pipe(Effect.provideContext(context));
638 });
639
640 const registryDelete = Effect.fn(function* (options: {
641 state: Record<string, unknown> | undefined;
642 }) {
643 if (typeof options.state?.repositoryName !== "string") return;
644 yield* ecr
645 .deleteRepository({
646 repositoryName: options.state.repositoryName,
647 force: true,
648 })
649 .pipe(
650 Effect.catchTag("RepositoryNotFoundException", () => Effect.void),
651 Effect.provideContext(context),
652 );
653 });
654
655 const loadBalancerDefaults = Effect.fn(function* (options: {
656 connection: Connection;
657 }) {
658 const auth = yield* narrowEksAuth(options.connection);
659 // EKS Auto Mode's built-in load balancer controller only
660 // reconciles `LoadBalancer` Services whose `spec.loadBalancerClass`
661 // is `eks.amazonaws.com/nlb` (there is no in-tree cloud provider on
662 // Auto Mode), and it defaults new NLBs to the *internal* scheme.
663 // Detect the Auto Mode LB capability from the live cluster and
664 // default to an internet-facing NLB so the returned `url` is
665 // actually reachable.
666 const cluster = yield* describeLiveCluster(auth).pipe(
667 Effect.catchTag("Kubernetes.ClusterNotFoundError", () =>
668 Effect.succeed(undefined),
669 ),
670 );
671 return {
672 loadBalancerClass: cluster?.kubernetesNetworkConfig
673 ?.elasticLoadBalancing?.enabled
674 ? "eks.amazonaws.com/nlb"
675 : undefined,
676 annotations: {
677 "service.beta.kubernetes.io/aws-load-balancer-scheme":
678 "internet-facing",
679 },
680 };
681 });
682
683 return {
684 kind: "Kubernetes.ClusterAdapter" as const,
685 connect,
686 identity: {
687 reconcile: identityReconcile,
688 delete: identityDelete,
689 },
690 registry: {
691 resolve: registryResolve,
692 hash: registryHash,
693 delete: registryDelete,
694 },
695 bootstrap: {
696 server: makeEksServerBootstrap,
697 job: makeEksJobBootstrap,
698 },
699 loadBalancerDefaults,
700 } satisfies ClusterAdapterService;
701 }),
702 );