Skip to content

Commit e493780

Browse files
authored
feat: Option to use last existing snapshot (#139)
Instead of creating a new snapshot, set `useExistingSnapshot` to `true` and use the latest available snapshot. This can speed up the process significantly, but may end up using older than desired snapshots. If you already have an automated snapshot, it might be good enough. ``` new RdsSanitizedSnapshotter(sfnStack, 'MySQL Cluster Snapshotter', { vpc, databaseCluster: mysqlDatabaseCluster, script: 'SELECT 1', snapshotPrefix: 'mysql-cluster-snapshot', useExistingSnapshot: true, // <==== }); ``` Resolves #1
1 parent 518c79e commit e493780

10 files changed

+189
-20
lines changed

.eslintrc.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitattributes

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitignore

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/files.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/tasks.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

API.md

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/find-snapshot-function.ts

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/find-snapshot.lambda.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import { DescribeDBClusterSnapshotsCommand, DescribeDBSnapshotsCommand, DescribeDBSnapshotsCommandOutput, RDSClient } from '@aws-sdk/client-rds';
3+
4+
const rds = new RDSClient();
5+
6+
interface Input {
7+
databaseIdentifier: string;
8+
isCluster: boolean;
9+
}
10+
11+
exports.handler = async function (input: Input) {
12+
let marker: string | undefined = undefined;
13+
let lastSnapshotId = '';
14+
let lastSnapshotTime = 0;
15+
16+
do {
17+
if (!input.isCluster) {
18+
const snapshots: DescribeDBSnapshotsCommandOutput = await rds.send(new DescribeDBSnapshotsCommand({
19+
DBInstanceIdentifier: input.databaseIdentifier,
20+
Marker: marker,
21+
}));
22+
for (const snapshot of snapshots.DBSnapshots ?? []) {
23+
if (snapshot.DBSnapshotIdentifier && snapshot.SnapshotCreateTime && snapshot.SnapshotCreateTime.getTime() > lastSnapshotTime) {
24+
lastSnapshotTime = snapshot.SnapshotCreateTime.getTime();
25+
lastSnapshotId = snapshot.DBSnapshotIdentifier;
26+
}
27+
}
28+
marker = snapshots.Marker;
29+
} else {
30+
const snapshots = await rds.send(new DescribeDBClusterSnapshotsCommand({
31+
DBClusterIdentifier: input.databaseIdentifier,
32+
Marker: marker,
33+
}));
34+
for (const snapshot of snapshots.DBClusterSnapshots ?? []) {
35+
if (snapshot.DBClusterSnapshotIdentifier && snapshot.SnapshotCreateTime && snapshot.SnapshotCreateTime.getTime() > lastSnapshotTime) {
36+
lastSnapshotTime = snapshot.SnapshotCreateTime.getTime();
37+
lastSnapshotId = snapshot.DBClusterSnapshotIdentifier;
38+
}
39+
}
40+
marker = snapshots.Marker;
41+
}
42+
} while (marker);
43+
44+
if (lastSnapshotId === '') {
45+
throw new Error('No snapshots found');
46+
}
47+
48+
return {
49+
id: lastSnapshotId,
50+
};
51+
};

src/index.ts

+67-20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'aws-cdk-lib';
1414
import { Construct } from 'constructs';
1515
import { DeleteOldFunction } from './delete-old-function';
16+
import { FindSnapshotFunction } from './find-snapshot-function';
1617
import { ParametersFunction } from './parameters-function';
1718
import { WaitFunction } from './wait-function';
1819

@@ -88,6 +89,15 @@ export interface IRdsSanitizedSnapshotter {
8889
*/
8990
readonly snapshotPrefix?: string;
9091

92+
/**
93+
* Use the latest available snapshot instead of taking a new one. This can be used to shorten the process at the cost of using a possibly older snapshot.
94+
*
95+
* This will use the latest snapshot whether it's an automatic system snapshot or a manual snapshot.
96+
*
97+
* @default false
98+
*/
99+
readonly useExistingSnapshot?: boolean;
100+
91101
/**
92102
* Prefix for all temporary snapshots and databases. The step function execution id will be added to it.
93103
*
@@ -130,6 +140,7 @@ export class RdsSanitizedSnapshotter extends Construct {
130140
private readonly fargateCluster: ecs.ICluster;
131141
private readonly sqlScript: string;
132142
private readonly reencrypt: boolean;
143+
private readonly useExistingSnapshot: boolean;
133144

134145
private readonly generalTags: {Key: string; Value: string}[];
135146
private readonly finalSnapshotTags: {Key: string; Value: string}[];
@@ -188,6 +199,7 @@ export class RdsSanitizedSnapshotter extends Construct {
188199
this.snapshotPrefix = props.snapshotPrefix ?? this.databaseIdentifier;
189200

190201
this.reencrypt = props.snapshotKey !== undefined;
202+
this.useExistingSnapshot = props.useExistingSnapshot ?? false;
191203

192204
this.dbClusterArn = cdk.Stack.of(this).formatArn({
193205
service: 'rds',
@@ -204,7 +216,7 @@ export class RdsSanitizedSnapshotter extends Construct {
204216
this.tempSnapshotArn = cdk.Stack.of(this).formatArn({
205217
service: 'rds',
206218
resource: this.isCluster ? 'cluster-snapshot' : 'snapshot',
207-
resourceName: `${this.tempPrefix}-*`,
219+
resourceName: this.useExistingSnapshot ? '*' : `${this.tempPrefix}-*`,
208220
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
209221
});
210222
this.tempDbClusterArn = cdk.Stack.of(this).formatArn({
@@ -242,14 +254,20 @@ export class RdsSanitizedSnapshotter extends Construct {
242254

243255
let c: stepfunctions.IChainable;
244256
let s: stepfunctions.INextable;
245-
s = c = this.createSnapshot('Create Temporary Snapshot', '$.databaseIdentifier', '$.tempSnapshotId', this.generalTags);
246-
s = s.next(this.waitForOperation('Wait for Snapshot', 'snapshot', '$.databaseIdentifier', '$.tempSnapshotId'));
257+
let originalSnapshotLocation = '$.tempSnapshotId';
258+
if (this.useExistingSnapshot) {
259+
originalSnapshotLocation = '$.latestSnapshot.id';
260+
s = c = this.findLatestSnapshot('Find Latest Snapshot', '$.databaseIdentifier');
261+
} else {
262+
s = c = this.createSnapshot('Create Temporary Snapshot', '$.databaseIdentifier', originalSnapshotLocation, this.generalTags);
263+
s = s.next(this.waitForOperation('Wait for Snapshot', 'snapshot', '$.databaseIdentifier', originalSnapshotLocation));
264+
}
247265
if (props.snapshotKey) {
248-
s = s.next(this.reencryptSnapshot(props.snapshotKey));
266+
s = s.next(this.reencryptSnapshot(originalSnapshotLocation, props.snapshotKey));
249267
s = s.next(this.waitForOperation('Wait for Re-encrypt', 'snapshot', '$.databaseIdentifier', '$.tempEncSnapshotId'));
250268
s = s.next(this.createTemporaryDatabase('$.tempEncSnapshotId'));
251269
} else {
252-
s = s.next(this.createTemporaryDatabase('$.tempSnapshotId'));
270+
s = s.next(this.createTemporaryDatabase(originalSnapshotLocation));
253271
}
254272
s = s.next(this.waitForOperation('Wait for Temporary Database', this.isCluster ? 'cluster' : 'instance', '$.tempDbId'));
255273
s = s.next(this.setPassword());
@@ -342,6 +360,30 @@ export class RdsSanitizedSnapshotter extends Construct {
342360
return parametersState;
343361
}
344362

363+
private findLatestSnapshot(id: string, databaseId: string) {
364+
const findFn = new FindSnapshotFunction(this, 'find-snapshot', {
365+
logRetention: logs.RetentionDays.ONE_MONTH,
366+
initialPolicy: [
367+
new iam.PolicyStatement({
368+
actions: ['rds:DescribeDBClusterSnapshots', 'rds:DescribeDBSnapshots'],
369+
resources: [this.dbClusterArn, this.dbInstanceArn],
370+
}),
371+
],
372+
});
373+
374+
let payload = {
375+
databaseIdentifier: stepfunctions.JsonPath.stringAt(databaseId),
376+
isCluster: this.isCluster,
377+
};
378+
379+
return new stepfunctions_tasks.LambdaInvoke(this, id, {
380+
lambdaFunction: findFn,
381+
payloadResponseOnly: true,
382+
payload: stepfunctions.TaskInput.fromObject(payload),
383+
resultPath: stepfunctions.JsonPath.stringAt('$.latestSnapshot'),
384+
});
385+
}
386+
345387
private createSnapshot(id: string, databaseId: string, snapshotId: string, tags: { Key: string; Value: string }[]) {
346388
return new stepfunctions_tasks.CallAwsService(this, id, {
347389
service: 'rds',
@@ -393,14 +435,14 @@ export class RdsSanitizedSnapshotter extends Construct {
393435
});
394436
}
395437

396-
private reencryptSnapshot(key: kms.IKey) {
438+
private reencryptSnapshot(snapshot: string, key: kms.IKey) {
397439
return new stepfunctions_tasks.CallAwsService(this, 'Re-encrypt Snapshot', {
398440
service: 'rds',
399441
action: this.isCluster ? 'copyDBClusterSnapshot' : 'copyDBSnapshot',
400442
parameters: {
401-
SourceDBClusterSnapshotIdentifier: this.isCluster ? stepfunctions.JsonPath.stringAt('$.tempSnapshotId') : undefined,
443+
SourceDBClusterSnapshotIdentifier: this.isCluster ? stepfunctions.JsonPath.stringAt(snapshot) : undefined,
402444
TargetDBClusterSnapshotIdentifier: this.isCluster ? stepfunctions.JsonPath.stringAt('$.tempEncSnapshotId') : undefined,
403-
SourceDBSnapshotIdentifier: this.isCluster ? undefined : stepfunctions.JsonPath.stringAt('$.tempSnapshotId'),
445+
SourceDBSnapshotIdentifier: this.isCluster ? undefined : stepfunctions.JsonPath.stringAt(snapshot),
404446
TargetDBSnapshotIdentifier: this.isCluster ? undefined : stepfunctions.JsonPath.stringAt('$.tempEncSnapshotId'),
405447
KmsKeyId: key.keyId,
406448
CopyTags: false,
@@ -711,18 +753,23 @@ export class RdsSanitizedSnapshotter extends Construct {
711753
// We retry everything because when any branch fails, all other branches are cancelled.
712754
// Retrying gives other branches an opportunity to start and hopefully at least run.
713755
const p = new stepfunctions.Parallel(this, 'Cleanup', { resultPath: stepfunctions.JsonPath.DISCARD });
714-
p.branch(
715-
new stepfunctions_tasks.CallAwsService(this, 'Temporary Snapshot', {
716-
service: 'rds',
717-
action: this.isCluster ? 'deleteDBClusterSnapshot' : 'deleteDBSnapshot',
718-
parameters: {
719-
DbClusterSnapshotIdentifier: this.isCluster ? stepfunctions.JsonPath.stringAt('$.tempSnapshotId') : undefined,
720-
DbSnapshotIdentifier: this.isCluster ? undefined : stepfunctions.JsonPath.stringAt('$.tempSnapshotId'),
721-
},
722-
iamResources: [this.tempSnapshotArn],
723-
resultPath: stepfunctions.JsonPath.DISCARD,
724-
}).addRetry({ maxAttempts: 5, interval: cdk.Duration.seconds(10) }),
725-
);
756+
if (!this.useExistingSnapshot) {
757+
p.branch(
758+
new stepfunctions_tasks.CallAwsService(this, 'Temporary Snapshot', {
759+
service: 'rds',
760+
action: this.isCluster ? 'deleteDBClusterSnapshot' : 'deleteDBSnapshot',
761+
parameters: {
762+
DbClusterSnapshotIdentifier: this.isCluster ? stepfunctions.JsonPath.stringAt('$.tempSnapshotId') : undefined,
763+
DbSnapshotIdentifier: this.isCluster ? undefined : stepfunctions.JsonPath.stringAt('$.tempSnapshotId'),
764+
},
765+
iamResources: [this.tempSnapshotArn],
766+
resultPath: stepfunctions.JsonPath.DISCARD,
767+
}).addRetry({
768+
maxAttempts: 5,
769+
interval: cdk.Duration.seconds(10),
770+
}),
771+
);
772+
}
726773
if (this.reencrypt) {
727774
p.branch(
728775
new stepfunctions_tasks.CallAwsService(this, 'Re-encrypted Snapshot', {

0 commit comments

Comments
 (0)