-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(route53): replace existing record sets (#20416)
Add a `deleteExisting` prop to `RecordSet` to delete an existing record set before deploying the new one. This is useful if you want to minimize downtime and avoid "manual" actions while deploying a stack with a record set that already exists. This is typically the case for record sets that are not already "owned" by CloudFormation or "owned" by another stack or construct that is going to be deleted (migration). ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
17 changed files
with
1,128 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
packages/@aws-cdk/aws-route53/lib/delete-existing-record-set-handler/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Route53 } from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies | ||
|
||
interface ResourceProperties { | ||
HostedZoneId: string; | ||
RecordName: string; | ||
RecordType: string; | ||
} | ||
|
||
export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { | ||
const resourceProps = event.ResourceProperties as unknown as ResourceProperties; | ||
|
||
// Only delete the existing record when the new one gets created | ||
if (event.RequestType !== 'Create') { | ||
return; | ||
} | ||
|
||
const route53 = new Route53({ apiVersion: '2013-04-01' }); | ||
|
||
const listResourceRecordSets = await route53.listResourceRecordSets({ | ||
HostedZoneId: resourceProps.HostedZoneId, | ||
StartRecordName: resourceProps.RecordName, | ||
StartRecordType: resourceProps.RecordType, | ||
}).promise(); | ||
|
||
const existingRecord = listResourceRecordSets.ResourceRecordSets | ||
.find(r => r.Name === resourceProps.RecordName && r.Type === resourceProps.RecordType); | ||
|
||
if (!existingRecord) { | ||
// There is no existing record, we can safely return | ||
return; | ||
} | ||
|
||
const changeResourceRecordSets = await route53.changeResourceRecordSets({ | ||
HostedZoneId: resourceProps.HostedZoneId, | ||
ChangeBatch: { | ||
Changes: [{ | ||
Action: 'DELETE', | ||
ResourceRecordSet: { | ||
Name: existingRecord.Name, | ||
Type: existingRecord.Type, | ||
// changeResourceRecordSets does not correctly handle undefined values | ||
// https://github.com/aws/aws-sdk-js/issues/3506 | ||
...existingRecord.TTL ? { TTL: existingRecord.TTL } : {}, | ||
...existingRecord.AliasTarget ? { AliasTarget: existingRecord.AliasTarget } : {}, | ||
...existingRecord.ResourceRecords ? { ResourceRecords: existingRecord.ResourceRecords } : {}, | ||
}, | ||
}], | ||
}, | ||
}).promise(); | ||
|
||
await route53.waitFor('resourceRecordSetsChanged', { Id: changeResourceRecordSets.ChangeInfo.Id }).promise(); | ||
|
||
return { | ||
PhysicalResourceId: `${existingRecord.Name}-${existingRecord.Type}`, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
packages/@aws-cdk/aws-route53/test/delete-existing-record-set-handler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
const mockListResourceRecordSetsResponse = jest.fn(); | ||
const mockChangeResourceRecordSetsResponse = jest.fn(); | ||
|
||
const mockRoute53 = { | ||
listResourceRecordSets: jest.fn().mockReturnValue({ | ||
promise: mockListResourceRecordSetsResponse, | ||
}), | ||
changeResourceRecordSets: jest.fn().mockReturnValue({ | ||
promise: mockChangeResourceRecordSetsResponse, | ||
}), | ||
waitFor: jest.fn().mockReturnValue({ | ||
promise: jest.fn().mockResolvedValue({}), | ||
}), | ||
}; | ||
|
||
jest.mock('aws-sdk', () => { | ||
return { | ||
Route53: jest.fn(() => mockRoute53), | ||
}; | ||
}); | ||
|
||
import { handler } from '../lib/delete-existing-record-set-handler'; | ||
|
||
const event: AWSLambda.CloudFormationCustomResourceEvent & { PhysicalResourceId?: string } = { | ||
RequestType: 'Create', | ||
ServiceToken: 'service-token', | ||
ResponseURL: 'response-url', | ||
StackId: 'stack-id', | ||
RequestId: 'request-id', | ||
LogicalResourceId: 'logical-resource-id', | ||
ResourceType: 'Custom::DeleteExistingRecordSet', | ||
ResourceProperties: { | ||
ServiceToken: 'service-token', | ||
HostedZoneId: 'hosted-zone-id', | ||
RecordName: 'dev.cdk.aws.', | ||
RecordType: 'A', | ||
}, | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test('create request with existing record', async () => { | ||
mockListResourceRecordSetsResponse.mockResolvedValueOnce({ | ||
ResourceRecordSets: [ | ||
{ | ||
Name: 'dev.cdk.aws.', | ||
Type: 'A', | ||
TTL: 900, | ||
}, | ||
{ | ||
Name: 'dev.cdk.aws.', | ||
Type: 'AAAA', | ||
TTL: 900, | ||
}, | ||
], | ||
}); | ||
|
||
mockChangeResourceRecordSetsResponse.mockResolvedValueOnce({ | ||
ChangeInfo: { | ||
Id: 'change-id', | ||
}, | ||
}); | ||
|
||
await handler(event); | ||
|
||
expect(mockRoute53.listResourceRecordSets).toHaveBeenCalledWith({ | ||
HostedZoneId: 'hosted-zone-id', | ||
StartRecordName: 'dev.cdk.aws.', | ||
StartRecordType: 'A', | ||
}); | ||
|
||
expect(mockRoute53.changeResourceRecordSets).toHaveBeenCalledWith({ | ||
HostedZoneId: 'hosted-zone-id', | ||
ChangeBatch: { | ||
Changes: [ | ||
{ | ||
Action: 'DELETE', | ||
ResourceRecordSet: { | ||
Name: 'dev.cdk.aws.', | ||
TTL: 900, | ||
Type: 'A', | ||
}, | ||
}, | ||
], | ||
}, | ||
}); | ||
|
||
expect(mockRoute53.waitFor).toHaveBeenCalledWith('resourceRecordSetsChanged', { | ||
Id: 'change-id', | ||
}); | ||
}); | ||
|
||
test('create request with non existing record', async () => { | ||
mockListResourceRecordSetsResponse.mockResolvedValueOnce({ | ||
ResourceRecordSets: [ | ||
{ | ||
Name: 'www.cdk.aws.', | ||
Type: 'A', | ||
TTL: 900, | ||
}, | ||
{ | ||
Name: 'dev.cdk.aws.', | ||
Type: 'MX', | ||
TTL: 900, | ||
}, | ||
], | ||
}); | ||
|
||
await handler(event); | ||
|
||
expect(mockRoute53.changeResourceRecordSets).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('update request', async () => { | ||
await handler({ | ||
...event, | ||
RequestType: 'Update', | ||
PhysicalResourceId: 'id', | ||
OldResourceProperties: {}, | ||
}); | ||
|
||
expect(mockRoute53.changeResourceRecordSets).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('delete request', async () => { | ||
await handler({ | ||
...event, | ||
RequestType: 'Delete', | ||
PhysicalResourceId: 'id', | ||
}); | ||
|
||
expect(mockRoute53.changeResourceRecordSets).not.toHaveBeenCalled(); | ||
}); |
Oops, something went wrong.