Open History to see the last commit
1import * as rds from "@distilled.cloud/aws/rds";
2import { isResolved } from "alchemy/Diff";
3import * as Provider from "alchemy/Provider";
4import { Resource } from "alchemy/Resource";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Stream from "effect/Stream";
8import type { Providers } from "../Providers.ts";
9
10export class DBProxyTargetGroupUnexpectedResponse extends Data.TaggedError(
11 "DBProxyTargetGroupUnexpectedResponse",
12)<{
13 readonly message: string;
14 readonly cause?: unknown;
15}> {}
16
17export interface DBProxyTargetGroupProps {
18 /**
19 * Proxy that owns the target group.
20 */
21 dbProxyName: string;
22 /**
23 * Target group name.
24 * @default "default"
25 */
26 targetGroupName?: string;
27 /**
28 * Cluster targets registered with the proxy.
29 */
30 dbClusterIdentifiers?: string[];
31 /**
32 * Instance targets registered with the proxy.
33 */
34 dbInstanceIdentifiers?: string[];
35 /**
36 * Connection pool configuration.
37 */
38 connectionPoolConfig?: rds.ConnectionPoolConfiguration;
39}
40
41export interface DBProxyTargetGroup extends Resource<
42 "AWS.RDS.DBProxyTargetGroup",
43 DBProxyTargetGroupProps,
44 {
45 /**
46 * Proxy that owns the target group.
47 */
48 dbProxyName: string;
49 /**
50 * Name of the target group (`default` unless overridden).
51 */
52 targetGroupName: string;
53 /**
54 * ARN of the target group.
55 */
56 targetGroupArn: string | undefined;
57 /**
58 * Status of the target group (e.g. `available`).
59 */
60 status: string | undefined;
61 /**
62 * Whether this is the proxy's default target group.
63 */
64 isDefault: boolean | undefined;
65 /**
66 * Observed connection pool configuration.
67 */
68 connectionPoolConfig: rds.ConnectionPoolConfigurationInfo | undefined;
69 /**
70 * Cluster targets registered with the proxy.
71 */
72 dbClusterIdentifiers: string[];
73 /**
74 * Instance targets registered with the proxy.
75 */
76 dbInstanceIdentifiers: string[];
77 },
78 never,
79 Providers
80> {}
81
82/**
83 * The proxy target group that registers Aurora clusters or instances behind an
84 * RDS Proxy.
85 *
86 * Every `DBProxy` has exactly one `default` target group; this resource
87 * adopts it, tunes its connection pool, and reconciles the registered
88 * cluster/instance targets. Deleting it deregisters the targets rather than
89 * deleting the group itself.
90 * ### Registering Targets
91 * **Example:** Register a Cluster Behind a Proxy
92 * ```typescript
93 * const targets = yield* DBProxyTargetGroup("ProxyTargets", {
94 * dbProxyName: proxy.dbProxyName,
95 * dbClusterIdentifiers: [cluster.dbClusterIdentifier],
96 * });
97 * ```
98 *
99 * **Example:** Tune the Connection Pool
100 * ```typescript
101 * const targets = yield* DBProxyTargetGroup("ProxyTargets", {
102 * dbProxyName: proxy.dbProxyName,
103 * dbClusterIdentifiers: [cluster.dbClusterIdentifier],
104 * connectionPoolConfig: {
105 * MaxConnectionsPercent: 90,
106 * MaxIdleConnectionsPercent: 10,
107 * },
108 * });
109 * ```
110 *
111 * @resource
112 */
113export const DBProxyTargetGroup = Resource<DBProxyTargetGroup>("AWS.RDS.DBProxyTargetGroup");
114
115const toTargetGroupName = (props: DBProxyTargetGroupProps) => props.targetGroupName ?? "default";
116
117export const DBProxyTargetGroupProvider = () =>
118 Provider.effect(
119 DBProxyTargetGroup,
120 Effect.gen(function* () {
121 const readGroup = Effect.fn(function* ({
122 dbProxyName,
123 targetGroupName,
124 }: {
125 dbProxyName: string;
126 targetGroupName: string;
127 }) {
128 const response = yield* rds
129 .describeDBProxyTargetGroups({
130 DBProxyName: dbProxyName,
131 TargetGroupName: targetGroupName,
132 })
133 .pipe(Effect.catchTag("DBProxyTargetGroupNotFoundFault", () => Effect.undefined));
134 return response?.TargetGroups?.[0];
135 });
136
137 const toAttrs = ({
138 group,
139 props,
140 }: {
141 group: rds.DBProxyTargetGroup;
142 props: DBProxyTargetGroupProps;
143 }): DBProxyTargetGroup["Attributes"] => ({
144 dbProxyName: group.DBProxyName ?? props.dbProxyName,
145 targetGroupName: group.TargetGroupName ?? toTargetGroupName(props),
146 targetGroupArn: group.TargetGroupArn,
147 status: group.Status,
148 isDefault: group.IsDefault,
149 connectionPoolConfig: group.ConnectionPoolConfig,
150 dbClusterIdentifiers: props.dbClusterIdentifiers ?? [],
151 dbInstanceIdentifiers: props.dbInstanceIdentifiers ?? [],
152 });
153
154 const readTargets = Effect.fn(function* ({
155 dbProxyName,
156 targetGroupName,
157 }: {
158 dbProxyName: string;
159 targetGroupName: string;
160 }) {
161 const response = yield* rds
162 .describeDBProxyTargets({
163 DBProxyName: dbProxyName,
164 TargetGroupName: targetGroupName,
165 })
166 .pipe(
167 Effect.catchTags({
168 DBProxyTargetNotFoundFault: () => Effect.undefined,
169 DBProxyTargetGroupNotFoundFault: () => Effect.undefined,
170 DBProxyNotFoundFault: () => Effect.undefined,
171 }),
172 );
173 const targets = response?.Targets ?? [];
174 const observedClusters: string[] = [];
175 const observedInstances: string[] = [];
176 for (const target of targets) {
177 const id = target.RdsResourceId;
178 if (!id) continue;
179 if (target.Type === "TRACKED_CLUSTER") {
180 observedClusters.push(id);
181 } else if (target.Type === "RDS_INSTANCE") {
182 observedInstances.push(id);
183 }
184 }
185 return { observedClusters, observedInstances };
186 });
187
188 return {
189 stables: ["dbProxyName", "targetGroupArn", "targetGroupName"],
190 // Target groups are proxy-scoped: `describeDBProxyTargetGroups`
191 // requires a `DBProxyName`, so there is no account-wide enumeration.
192 // Fan out — enumerate every proxy via the paginated `describeDBProxies`,
193 // then list each proxy's target groups and hydrate registered targets
194 // (cluster/instance identifiers) via `describeDBProxyTargets` so each
195 // element matches `read`'s `Attributes` shape exactly. A proxy/target
196 // group deleted between describes surfaces a typed not-found per item
197 // and is dropped.
198 list: () =>
199 Effect.gen(function* () {
200 const proxies = yield* rds.describeDBProxies.pages({}).pipe(
201 Stream.runCollect,
202 Effect.map((chunk) => Array.from(chunk).flatMap((page) => page.DBProxies ?? [])),
203 );
204 const perProxy = yield* Effect.forEach(
205 proxies,
206 (proxy) =>
207 Effect.gen(function* () {
208 const dbProxyName = proxy.DBProxyName;
209 if (!dbProxyName) return [];
210 const groups = yield* rds.describeDBProxyTargetGroups
211 .pages({ DBProxyName: dbProxyName })
212 .pipe(
213 Stream.runCollect,
214 Effect.map((chunk) =>
215 Array.from(chunk).flatMap((page) => page.TargetGroups ?? []),
216 ),
217 Effect.catchTags({
218 DBProxyNotFoundFault: () => Effect.succeed([] as rds.DBProxyTargetGroup[]),
219 DBProxyTargetGroupNotFoundFault: () =>
220 Effect.succeed([] as rds.DBProxyTargetGroup[]),
221 }),
222 );
223 return yield* Effect.forEach(
224 groups,
225 (group) =>
226 Effect.gen(function* () {
227 const targetGroupName = group.TargetGroupName ?? "default";
228 const { observedClusters, observedInstances } = yield* readTargets({
229 dbProxyName,
230 targetGroupName,
231 });
232 return {
233 dbProxyName: group.DBProxyName ?? dbProxyName,
234 targetGroupName,
235 targetGroupArn: group.TargetGroupArn,
236 status: group.Status,
237 isDefault: group.IsDefault,
238 connectionPoolConfig: group.ConnectionPoolConfig,
239 dbClusterIdentifiers: observedClusters,
240 dbInstanceIdentifiers: observedInstances,
241 } satisfies DBProxyTargetGroup["Attributes"];
242 }),
243 { concurrency: 10 },
244 );
245 }),
246 { concurrency: 10 },
247 );
248 return perProxy.flat();
249 }),
250 diff: Effect.fn(function* ({ olds, news }) {
251 if (!isResolved(news)) return undefined;
252 if (olds?.dbProxyName !== news.dbProxyName) {
253 return { action: "replace" } as const;
254 }
255 if (toTargetGroupName(olds ?? news) !== toTargetGroupName(news)) {
256 return { action: "replace" } as const;
257 }
258 }),
259 read: Effect.fn(function* ({ olds, output }) {
260 const dbProxyName = output?.dbProxyName ?? olds?.dbProxyName;
261 // State saved before the props resolved has no proxy name to look up.
262 if (!dbProxyName) return undefined;
263 const props = {
264 dbProxyName,
265 targetGroupName: output?.targetGroupName ?? olds?.targetGroupName,
266 dbClusterIdentifiers: output?.dbClusterIdentifiers ?? olds?.dbClusterIdentifiers,
267 dbInstanceIdentifiers: output?.dbInstanceIdentifiers ?? olds?.dbInstanceIdentifiers,
268 connectionPoolConfig: output?.connectionPoolConfig ?? olds?.connectionPoolConfig,
269 } satisfies DBProxyTargetGroupProps;
270 const group = yield* readGroup({
271 dbProxyName: props.dbProxyName,
272 targetGroupName: toTargetGroupName(props),
273 });
274 if (!group?.TargetGroupName) {
275 return undefined;
276 }
277 return toAttrs({ group, props });
278 }),
279 reconcile: Effect.fn(function* ({ news, output, session }) {
280 const targetGroupName = toTargetGroupName(news);
281
282 // Observe — the "default" target group is created automatically
283 // by RDS when the parent DBProxy is created, so we don't `Ensure`
284 // a target group ourselves. We just describe it to confirm the
285 // proxy is ready and to read connection-pool config.
286 let observedGroup = yield* readGroup({
287 dbProxyName: news.dbProxyName,
288 targetGroupName,
289 });
290
291 // Sync connection pool config — push desired shape if provided.
292 // `modifyDBProxyTargetGroup` is idempotent for unchanged config.
293 if (news.connectionPoolConfig) {
294 yield* rds.modifyDBProxyTargetGroup({
295 DBProxyName: news.dbProxyName,
296 TargetGroupName: targetGroupName,
297 ConnectionPoolConfig: news.connectionPoolConfig,
298 });
299 }
300
301 // Sync registered targets — diff observed live targets against
302 // desired and apply only the delta. We trust live cloud state
303 // over `output`/`olds` so adoption and drift converge.
304 const { observedClusters, observedInstances } = yield* readTargets({
305 dbProxyName: news.dbProxyName,
306 targetGroupName,
307 });
308 const desiredClusters = new Set(news.dbClusterIdentifiers ?? []);
309 const desiredInstances = new Set(news.dbInstanceIdentifiers ?? []);
310 const observedClusterSet = new Set(observedClusters);
311 const observedInstanceSet = new Set(observedInstances);
312
313 const addClusters = [...desiredClusters].filter((id) => !observedClusterSet.has(id));
314 const removeClusters = [...observedClusterSet].filter((id) => !desiredClusters.has(id));
315 const addInstances = [...desiredInstances].filter((id) => !observedInstanceSet.has(id));
316 const removeInstances = [...observedInstanceSet].filter(
317 (id) => !desiredInstances.has(id),
318 );
319
320 if (addClusters.length > 0 || addInstances.length > 0) {
321 yield* rds.registerDBProxyTargets({
322 DBProxyName: news.dbProxyName,
323 TargetGroupName: targetGroupName,
324 DBClusterIdentifiers: addClusters.length > 0 ? addClusters : undefined,
325 DBInstanceIdentifiers: addInstances.length > 0 ? addInstances : undefined,
326 });
327 }
328
329 if (removeClusters.length > 0 || removeInstances.length > 0) {
330 yield* rds.deregisterDBProxyTargets({
331 DBProxyName: news.dbProxyName,
332 TargetGroupName: targetGroupName,
333 DBClusterIdentifiers: removeClusters.length > 0 ? removeClusters : undefined,
334 DBInstanceIdentifiers: removeInstances.length > 0 ? removeInstances : undefined,
335 });
336 }
337
338 // Re-read so returned attrs reflect post-sync state. If the
339 // target group still isn't visible, surface the failure (the
340 // parent DBProxy must be up).
341 observedGroup = yield* readGroup({
342 dbProxyName: news.dbProxyName,
343 targetGroupName,
344 });
345 if (!observedGroup?.TargetGroupName) {
346 return yield* new DBProxyTargetGroupUnexpectedResponse({
347 message: `DB proxy target group '${targetGroupName}' not found`,
348 });
349 }
350 yield* session.note(
351 observedGroup.TargetGroupArn ?? output?.targetGroupArn ?? observedGroup.TargetGroupName,
352 );
353 return toAttrs({ group: observedGroup, props: news });
354 }),
355 delete: Effect.fn(function* ({ output }) {
356 if (output.dbClusterIdentifiers.length > 0 || output.dbInstanceIdentifiers.length > 0) {
357 yield* rds
358 .deregisterDBProxyTargets({
359 DBProxyName: output.dbProxyName,
360 TargetGroupName: output.targetGroupName,
361 DBClusterIdentifiers:
362 output.dbClusterIdentifiers.length > 0 ? output.dbClusterIdentifiers : undefined,
363 DBInstanceIdentifiers:
364 output.dbInstanceIdentifiers.length > 0
365 ? output.dbInstanceIdentifiers
366 : undefined,
367 })
368 .pipe(Effect.catchTag("DBProxyTargetGroupNotFoundFault", () => Effect.void));
369 }
370 }),
371 };
372 }),
373 );