1import * as efs from "@distilled.cloud/aws/efs";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as Stream from "effect/Stream";
6import { Unowned } from "../../AdoptPolicy.ts";
7import { isResolved } from "../../Diff.ts";
8import { createPhysicalName } from "../../PhysicalName.ts";
9import * as Provider from "../../Provider.ts";
10import { Resource } from "../../Resource.ts";
11import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
12import { AWSEnvironment } from "../Environment.ts";
13import {
14 normalizePolicyDocument,
15 type PolicyDocument,
16 stringifyPolicyDocument,
17} from "../IAM/Policy.ts";
18import type { Providers } from "../Providers.ts";
19
20/**
21 * A single EFS lifecycle-management rule. EFS requires each rule to carry
22 * exactly one transition setting, so a full configuration is expressed as an
23 * array of single-setting policies.
24 */
25export interface FileSystemLifecyclePolicy {
26 /**
27 * Transition files to the Infrequent Access (IA) storage class after the
28 * given period without access, e.g. `"AFTER_30_DAYS"`.
29 */
30 transitionToIA?: efs.TransitionToIARules;
31 /**
32 * Transition files back to primary storage on first access, e.g.
33 * `"AFTER_1_ACCESS"`.
34 */
35 transitionToPrimaryStorageClass?: efs.TransitionToPrimaryStorageClassRules;
36 /**
37 * Transition files to the Archive storage class after the given period
38 * without access, e.g. `"AFTER_90_DAYS"`.
39 */
40 transitionToArchive?: efs.TransitionToArchiveRules;
41}
42
43export interface FileSystemProps {
44 /**
45 * Performance mode of the file system. `generalPurpose` is recommended for
46 * all workloads; `maxIO` is a previous-generation mode for highly
47 * parallelized workloads. Cannot be changed after creation (replacement).
48 * `maxIO` is not supported with `elastic` throughput or One Zone file
49 * systems.
50 * @default "generalPurpose"
51 */
52 performanceMode?: "generalPurpose" | "maxIO";
53 /**
54 * Whether the file system is encrypted at rest. Cannot be changed after
55 * creation (replacement).
56 * @default true
57 */
58 encrypted?: boolean;
59 /**
60 * The KMS key used to encrypt the file system. Only meaningful when
61 * `encrypted` is `true`. Cannot be changed after creation (replacement).
62 * @default the AWS-managed `/aws/elasticfilesystem` key
63 */
64 kmsKeyId?: string;
65 /**
66 * Throughput mode of the file system. `elastic` scales automatically and is
67 * recommended for spiky workloads; `provisioned` requires
68 * `provisionedThroughputInMibps`. Updatable in place.
69 * @default "bursting"
70 */
71 throughputMode?: "bursting" | "provisioned" | "elastic";
72 /**
73 * Provisioned throughput in MiB/s. Required when `throughputMode` is
74 * `provisioned`.
75 */
76 provisionedThroughputInMibps?: number;
77 /**
78 * Create a One Zone file system in this Availability Zone (e.g.
79 * `us-west-2a`). Cannot be changed after creation (replacement).
80 * @default regional (multi-AZ) file system
81 */
82 availabilityZoneName?: string;
83 /**
84 * Lifecycle management rules that transition files between storage
85 * classes. Omit (or pass an empty array) to disable lifecycle management.
86 */
87 lifecyclePolicies?: FileSystemLifecyclePolicy[];
88 /**
89 * Whether AWS Backup automatic backups are enabled for the file system.
90 * Updatable in place. When omitted, the AWS default is left alone
91 * (disabled for regional file systems, enabled for One Zone file
92 * systems).
93 * @default AWS default (regional: disabled; One Zone: enabled)
94 */
95 backup?: boolean;
96 /**
97 * Replication overwrite protection. `ENABLED` (the AWS default) makes the
98 * file system writable and blocks it from being used as a replication
99 * destination; `DISABLED` makes it read-only so an EFS replication
100 * configuration can overwrite it. Updatable in place. When omitted, the
101 * current protection setting is left alone.
102 * @default AWS default ("ENABLED")
103 */
104 replicationOverwriteProtection?: "ENABLED" | "DISABLED";
105 /**
106 * The file system policy — an IAM resource-based policy controlling client
107 * access to the file system (e.g. enforcing in-transit encryption or
108 * restricting mounts to access points). Either a structured
109 * {@link PolicyDocument} or a raw JSON string. Omitting the property
110 * removes any explicit policy, reverting to the default EFS policy.
111 */
112 policy?: PolicyDocument | string;
113 /**
114 * Tags to apply to the file system. Merged with internal Alchemy tags.
115 */
116 tags?: Record<string, string>;
117}
118
119export interface FileSystem extends Resource<
120 "AWS.EFS.FileSystem",
121 FileSystemProps,
122 {
123 /** The ID of the file system (e.g. `fs-0123456789abcdef0`). */
124 fileSystemId: string;
125 /** The ARN of the file system. */
126 fileSystemArn: string;
127 },
128 {},
129 Providers
130> {}
131
132/**
133 * An Amazon EFS file system — serverless, elastic, shared POSIX storage.
134 *
135 * The file system is created encrypted by default with a deterministic
136 * creation token derived from the app, stage, and logical ID, so retried
137 * creates are idempotent. Mount it into compute with
138 * {@link MountTarget} (per-subnet network endpoints) and
139 * {@link AccessPoint} (application-specific POSIX entry points — required
140 * for Lambda mounts).
141 * ### Creating File Systems
142 * **Example:** Default file system (encrypted, general purpose)
143 * ```typescript
144 * import * as AWS from "alchemy/AWS";
145 *
146 * const files = yield* AWS.EFS.FileSystem("Files");
147 * ```
148 *
149 * **Example:** Elastic throughput
150 * ```typescript
151 * const files = yield* AWS.EFS.FileSystem("Files", {
152 * throughputMode: "elastic",
153 * });
154 * ```
155 *
156 * ### Lifecycle Management
157 * **Example:** Tier cold files to Infrequent Access
158 * ```typescript
159 * const files = yield* AWS.EFS.FileSystem("Files", {
160 * lifecyclePolicies: [
161 * { transitionToIA: "AFTER_30_DAYS" },
162 * { transitionToPrimaryStorageClass: "AFTER_1_ACCESS" },
163 * ],
164 * });
165 * ```
166 *
167 * ### Backup and Protection
168 * **Example:** Enable AWS Backup automatic backups
169 * ```typescript
170 * const files = yield* AWS.EFS.FileSystem("Files", {
171 * backup: true,
172 * });
173 * ```
174 *
175 * **Example:** Allow the file system to be a replication destination
176 * ```typescript
177 * const files = yield* AWS.EFS.FileSystem("Files", {
178 * replicationOverwriteProtection: "DISABLED",
179 * });
180 * ```
181 *
182 * ### File System Policy
183 * **Example:** Enforce in-transit encryption with a typed PolicyDocument
184 * ```typescript
185 * const files = yield* AWS.EFS.FileSystem("Files", {
186 * policy: {
187 * Version: "2012-10-17",
188 * Statement: [
189 * {
190 * Sid: "DenyUnencryptedTransport",
191 * Effect: "Deny",
192 * Principal: { AWS: "*" },
193 * Action: ["elasticfilesystem:ClientMount"],
194 * Condition: { Bool: { "aws:SecureTransport": "false" } },
195 * },
196 * ],
197 * },
198 * });
199 * ```
200 *
201 * ### Mounting into Lambda
202 * Lambda mounts EFS through an access point; the function must be attached
203 * to a VPC that can reach a mount target.
204 *
205 * **Example:** File system + mount target + access point + Lambda
206 * ```typescript
207 * const files = yield* AWS.EFS.FileSystem("Files");
208 * const target = yield* AWS.EFS.MountTarget("FilesTarget", {
209 * fileSystemId: files.fileSystemId,
210 * subnetId,
211 * });
212 * const accessPoint = yield* AWS.EFS.AccessPoint("FilesAccess", {
213 * fileSystemId: files.fileSystemId,
214 * posixUser: { uid: 1000, gid: 1000 },
215 * rootDirectory: {
216 * path: "/lambda",
217 * creationInfo: { ownerUid: 1000, ownerGid: 1000, permissions: "750" },
218 * },
219 * });
220 * const fn = yield* AWS.Lambda.Function("Api", {
221 * main: "./src/handler.ts",
222 * vpc: { subnetIds: [subnetId], securityGroupIds: [securityGroupId] },
223 * fileSystemConfigs: [
224 * // pass the AccessPoint resource itself (or its ARN)
225 * { accessPoint, localMountPath: "/mnt/files" },
226 * ],
227 * // depend on the mount target so the function is created only after
228 * // the network endpoint is available
229 * env: { EFS_MOUNT_TARGET: target.mountTargetId },
230 * });
231 * ```
232 *
233 * **Example:** Host-agnostic mount binding (Lambda or ECS)
234 * `EFS.mount` wires the mount config + least-privilege IAM through the
235 * host's binding channel — the same code works inside a Lambda Function or
236 * an ECS Task body (provide `AWS.EFS.MountLive` on the host Effect).
237 * ```typescript
238 * export default class Api extends AWS.Lambda.Function<Api>()(
239 * "Api",
240 * { main: import.meta.url, vpc: { subnetIds, securityGroupIds } },
241 * Effect.gen(function* () {
242 * const files = yield* AWS.EFS.mount(accessPoint, { path: "/mnt/files" });
243 * return Effect.fn(function* (event: unknown) {
244 * // read/write under files.path at runtime
245 * return { mountedAt: files.path };
246 * });
247 * }).pipe(Effect.provide(AWS.EFS.MountLive)),
248 * ) {}
249 * ```
250 *
251 * @resource
252 */
253export const FileSystem = Resource<FileSystem>("AWS.EFS.FileSystem");
254
255const isMutableEfsTagKey = (key: string): boolean =>
256 !key.toLowerCase().startsWith("aws:");
257
258const mutableEfsTags = (tags: Record<string, string>): Record<string, string> =>
259 Object.fromEntries(
260 Object.entries(tags).filter(([key]) => isMutableEfsTagKey(key)),
261 );
262
263const efsTagsToRecord = (
264 tags: readonly efs.Tag[] | undefined,
265): Record<string, string> =>
266 Object.fromEntries(
267 (tags ?? [])
268 .filter((tag) => isMutableEfsTagKey(tag.Key))
269 .map((tag) => [tag.Key, tag.Value]),
270 );
271
272/**
273 * A file system briefly reports `creating`/`updating` before settling in
274 * `available`. Bounded poll (~60s), explicitly typed so declaration emit
275 * never widens the provider layer (see PATTERNS §7).
276 */
277const retryUntilFileSystemAvailable = <E extends { _tag: string }, R>(
278 self: Effect.Effect<efs.FileSystemDescription, E, R>,
279): Effect.Effect<efs.FileSystemDescription, E | FileSystemNotAvailable, R> =>
280 Effect.retry(
281 Effect.flatMap(self, (fs) =>
282 fs.LifeCycleState === "available"
283 ? Effect.succeed(fs)
284 : Effect.fail(
285 new FileSystemNotAvailable({
286 fileSystemId: fs.FileSystemId,
287 state: fs.LifeCycleState,
288 }),
289 ),
290 ),
291 {
292 while: (e) => e._tag === "FileSystemNotAvailable",
293 schedule: Schedule.max([
294 Schedule.fixed("2 seconds"),
295 Schedule.recurs(30),
296 ]),
297 },
298 );
299
300/**
301 * Deleting a file system whose mount targets are still tearing down (their
302 * ENIs release asynchronously) transiently fails with `FileSystemInUse`.
303 * Bounded retry (~60s), explicitly typed (PATTERNS §7).
304 */
305const retryWhileFileSystemInUse = <A, E extends { _tag: string }, R>(
306 self: Effect.Effect<A, E, R>,
307): Effect.Effect<A, E, R> =>
308 Effect.retry(self, {
309 while: (e) => e._tag === "FileSystemInUse",
310 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(20)]),
311 });
312
313/**
314 * Throughput updates are rejected with `IncorrectFileSystemLifeCycleState`
315 * while a previous modification is still applying. Bounded retry (~60s),
316 * explicitly typed (PATTERNS §7).
317 */
318const retryWhileFileSystemUpdating = <A, E extends { _tag: string }, R>(
319 self: Effect.Effect<A, E, R>,
320): Effect.Effect<A, E, R> =>
321 Effect.retry(self, {
322 while: (e) => e._tag === "IncorrectFileSystemLifeCycleState",
323 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(20)]),
324 });
325
326/**
327 * Internal marker error used to drive the bounded wait for a file system to
328 * reach the `available` lifecycle state.
329 */
330export class FileSystemNotAvailable extends Data.TaggedError(
331 "FileSystemNotAvailable",
332)<{ fileSystemId: string; state: string }> {}
333
334const toWireLifecyclePolicies = (
335 policies: FileSystemLifecyclePolicy[] | undefined,
336): efs.LifecyclePolicy[] =>
337 (policies ?? []).map((p) => ({
338 ...(p.transitionToIA !== undefined
339 ? { TransitionToIA: p.transitionToIA }
340 : {}),
341 ...(p.transitionToPrimaryStorageClass !== undefined
342 ? { TransitionToPrimaryStorageClass: p.transitionToPrimaryStorageClass }
343 : {}),
344 ...(p.transitionToArchive !== undefined
345 ? { TransitionToArchive: p.transitionToArchive }
346 : {}),
347 }));
348
349const canonicalPolicies = (policies: readonly efs.LifecyclePolicy[]): string =>
350 JSON.stringify(
351 policies
352 .map((p) => ({
353 TransitionToIA: p.TransitionToIA ?? null,
354 TransitionToPrimaryStorageClass:
355 p.TransitionToPrimaryStorageClass ?? null,
356 TransitionToArchive: p.TransitionToArchive ?? null,
357 }))
358 .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
359 );
360
361export const FileSystemProvider = () =>
362 Provider.effect(
363 FileSystem,
364 Effect.gen(function* () {
365 const createToken = Effect.fn(function* (id: string) {
366 return yield* createPhysicalName({ id, maxLength: 64 });
367 });
368
369 const fileSystemArnOf = Effect.fn(function* (fileSystemId: string) {
370 const { accountId, region } = yield* AWSEnvironment.current;
371 return `arn:aws:elasticfilesystem:${region}:${accountId}:file-system/${fileSystemId}`;
372 });
373
374 const findByToken = Effect.fn(function* (token: string) {
375 return yield* efs.describeFileSystems({ CreationToken: token }).pipe(
376 Effect.map((r) => r.FileSystems?.[0]),
377 Effect.catchTag("FileSystemNotFound", () =>
378 Effect.succeed(undefined),
379 ),
380 );
381 });
382
383 const findById = Effect.fn(function* (fileSystemId: string) {
384 return yield* efs
385 .describeFileSystems({ FileSystemId: fileSystemId })
386 .pipe(
387 Effect.map((r) => r.FileSystems?.[0]),
388 Effect.catchTag("FileSystemNotFound", () =>
389 Effect.succeed(undefined),
390 ),
391 );
392 });
393
394 return FileSystem.Provider.of({
395 stables: ["fileSystemId", "fileSystemArn"],
396
397 list: () =>
398 Effect.gen(function* () {
399 const systems = yield* efs.describeFileSystems.items({}).pipe(
400 Stream.runCollect,
401 Effect.map((chunk) => Array.from(chunk)),
402 );
403 return yield* Effect.forEach(systems, (fs) =>
404 Effect.gen(function* () {
405 return {
406 fileSystemId: fs.FileSystemId,
407 fileSystemArn:
408 fs.FileSystemArn ??
409 (yield* fileSystemArnOf(fs.FileSystemId)),
410 };
411 }),
412 );
413 }),
414
415 read: Effect.fn(function* ({ id, output }) {
416 const fs = output?.fileSystemId
417 ? yield* findById(output.fileSystemId)
418 : yield* findByToken(yield* createToken(id));
419 if (fs === undefined || fs.LifeCycleState === "deleted") {
420 return undefined;
421 }
422 const attrs = {
423 fileSystemId: fs.FileSystemId,
424 fileSystemArn:
425 fs.FileSystemArn ?? (yield* fileSystemArnOf(fs.FileSystemId)),
426 };
427 return (yield* hasAlchemyTags(id, efsTagsToRecord(fs.Tags)))
428 ? attrs
429 : Unowned(attrs);
430 }),
431
432 diff: Effect.fn(function* ({ news = {}, olds = {} }) {
433 if (!isResolved(news)) return undefined;
434 const replaced =
435 (news.performanceMode ?? "generalPurpose") !==
436 (olds.performanceMode ?? "generalPurpose") ||
437 (news.encrypted ?? true) !== (olds.encrypted ?? true) ||
438 news.kmsKeyId !== olds.kmsKeyId ||
439 news.availabilityZoneName !== olds.availabilityZoneName;
440 if (replaced) {
441 return { action: "replace" } as const;
442 }
443 // undefined → default update path (throughput/lifecycle/tags sync)
444 }),
445
446 reconcile: Effect.fn(function* ({ id, news = {}, output, session }) {
447 const token = yield* createToken(id);
448 const internalTags = yield* createInternalTags(id);
449
450 // 1. OBSERVE — the creation token is the deterministic identity;
451 // output.fileSystemId is only a cache and may be stale.
452 let fs = output?.fileSystemId
453 ? yield* findById(output.fileSystemId)
454 : yield* findByToken(token);
455 if (fs?.LifeCycleState === "deleted") {
456 fs = undefined;
457 }
458
459 // 2. ENSURE — create if missing. CreateFileSystem is idempotent on
460 // the creation token; a concurrent create surfaces the typed
461 // FileSystemAlreadyExists which we resolve by re-observing.
462 if (fs === undefined) {
463 fs = yield* efs
464 .createFileSystem({
465 CreationToken: token,
466 PerformanceMode: news.performanceMode,
467 Encrypted: news.encrypted ?? true,
468 KmsKeyId: news.kmsKeyId,
469 Backup: news.backup,
470 ThroughputMode: news.throughputMode,
471 ProvisionedThroughputInMibps: news.provisionedThroughputInMibps,
472 AvailabilityZoneName: news.availabilityZoneName,
473 Tags: Object.entries(
474 mutableEfsTags({ ...news.tags, ...internalTags }),
475 ).map(([Key, Value]) => ({ Key, Value })),
476 })
477 .pipe(
478 Effect.catchTag("FileSystemAlreadyExists", (e) =>
479 efs
480 .describeFileSystems({ FileSystemId: e.FileSystemId })
481 .pipe(Effect.map((r) => r.FileSystems![0])),
482 ),
483 );
484 }
485
486 const fileSystemId = fs.FileSystemId;
487
488 // File-system mutations (throughput, lifecycle) require the
489 // `available` state; creation settles in seconds.
490 fs = yield* retryUntilFileSystemAvailable(
491 efs
492 .describeFileSystems({ FileSystemId: fileSystemId })
493 .pipe(Effect.map((r) => r.FileSystems![0])),
494 );
495
496 // 3a. SYNC throughput — observed vs desired; only call the API on a
497 // real delta.
498 if (news.throughputMode !== undefined) {
499 const modeChanged = fs.ThroughputMode !== news.throughputMode;
500 const provisionedChanged =
501 news.throughputMode === "provisioned" &&
502 fs.ProvisionedThroughputInMibps !==
503 news.provisionedThroughputInMibps;
504 if (modeChanged || provisionedChanged) {
505 yield* retryWhileFileSystemUpdating(
506 efs.updateFileSystem({
507 FileSystemId: fileSystemId,
508 ThroughputMode: news.throughputMode,
509 ProvisionedThroughputInMibps:
510 news.throughputMode === "provisioned"
511 ? news.provisionedThroughputInMibps
512 : undefined,
513 }),
514 );
515 }
516 }
517
518 // 3b. SYNC lifecycle policies — a PUT with the full desired list;
519 // an empty list clears lifecycle management.
520 const observedPolicies = yield* efs
521 .describeLifecycleConfiguration({ FileSystemId: fileSystemId })
522 .pipe(Effect.map((r) => r.LifecyclePolicies ?? []));
523 const desiredPolicies = toWireLifecyclePolicies(
524 news.lifecyclePolicies,
525 );
526 if (
527 canonicalPolicies(observedPolicies) !==
528 canonicalPolicies(desiredPolicies)
529 ) {
530 yield* retryWhileFileSystemUpdating(
531 efs.putLifecycleConfiguration({
532 FileSystemId: fileSystemId,
533 LifecyclePolicies: desiredPolicies,
534 }),
535 );
536 }
537
538 // 3c. SYNC backup policy — observed vs desired; only call the API
539 // on a real delta. An in-flight transition (ENABLING/
540 // DISABLING) counts as its target state. When the prop is
541 // omitted the AWS default is left alone.
542 if (news.backup !== undefined) {
543 const observedBackupStatus = yield* efs
544 .describeBackupPolicy({ FileSystemId: fileSystemId })
545 .pipe(
546 Effect.map((r) => r.BackupPolicy?.Status),
547 Effect.catchTag("PolicyNotFound", () =>
548 Effect.succeed(undefined),
549 ),
550 );
551 const observedBackup =
552 observedBackupStatus === "ENABLED" ||
553 observedBackupStatus === "ENABLING";
554 if (observedBackup !== news.backup) {
555 yield* retryWhileFileSystemUpdating(
556 efs.putBackupPolicy({
557 FileSystemId: fileSystemId,
558 BackupPolicy: {
559 Status: news.backup ? "ENABLED" : "DISABLED",
560 },
561 }),
562 );
563 }
564 }
565
566 // 3d. SYNC replication overwrite protection — observed vs desired
567 // from the file system description. When the prop is omitted
568 // the current protection setting is left alone.
569 if (news.replicationOverwriteProtection !== undefined) {
570 const observedProtection =
571 fs.FileSystemProtection?.ReplicationOverwriteProtection ??
572 "ENABLED";
573 if (observedProtection !== news.replicationOverwriteProtection) {
574 yield* retryWhileFileSystemUpdating(
575 efs.updateFileSystemProtection({
576 FileSystemId: fileSystemId,
577 ReplicationOverwriteProtection:
578 news.replicationOverwriteProtection,
579 }),
580 );
581 }
582 }
583
584 // 3e. SYNC file system policy — compare the observed policy with
585 // the desired one after canonicalization (sorted keys, no
586 // whitespace) so a re-deploy of an equivalent document is a
587 // no-op API-wise.
588 const observedPolicy = yield* efs
589 .describeFileSystemPolicy({ FileSystemId: fileSystemId })
590 .pipe(
591 Effect.map((r) => r.Policy),
592 Effect.catchTag("PolicyNotFound", () =>
593 Effect.succeed(undefined),
594 ),
595 );
596 const desiredPolicy =
597 news.policy === undefined
598 ? undefined
599 : typeof news.policy === "string"
600 ? news.policy
601 : stringifyPolicyDocument(news.policy);
602 if (desiredPolicy === undefined) {
603 if (observedPolicy !== undefined) {
604 yield* retryWhileFileSystemUpdating(
605 efs.deleteFileSystemPolicy({ FileSystemId: fileSystemId }),
606 );
607 }
608 } else if (
609 observedPolicy === undefined ||
610 normalizePolicyDocument(observedPolicy) !==
611 normalizePolicyDocument(desiredPolicy)
612 ) {
613 yield* retryWhileFileSystemUpdating(
614 efs.putFileSystemPolicy({
615 FileSystemId: fileSystemId,
616 Policy: desiredPolicy,
617 }),
618 );
619 }
620
621 // 3f. SYNC tags — diff against OBSERVED cloud tags so adoption
622 // converges.
623 const observedTags = efsTagsToRecord(fs.Tags);
624 const desiredTags = mutableEfsTags({
625 ...news.tags,
626 ...internalTags,
627 });
628 const { upsert, removed } = diffTags(observedTags, desiredTags);
629 if (upsert.length > 0) {
630 yield* efs.tagResource({
631 ResourceId: fileSystemId,
632 Tags: upsert,
633 });
634 }
635 if (removed.length > 0) {
636 yield* efs.untagResource({
637 ResourceId: fileSystemId,
638 TagKeys: removed,
639 });
640 }
641
642 yield* session.note(fileSystemId);
643 return {
644 fileSystemId,
645 fileSystemArn:
646 fs.FileSystemArn ?? (yield* fileSystemArnOf(fileSystemId)),
647 };
648 }),
649
650 delete: Effect.fn(function* ({ output }) {
651 yield* retryWhileFileSystemInUse(
652 efs.deleteFileSystem({ FileSystemId: output.fileSystemId }),
653 ).pipe(Effect.catchTag("FileSystemNotFound", () => Effect.void));
654 }),
655 });
656 }),
657 );