aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.56k stars 3.87k forks source link

aws_route53: Allow updates to existing records in a single operation #26754

Open christow-aws opened 1 year ago

christow-aws commented 1 year ago

Describe the feature

Currently, in order to update a Route53 record's routing policy using CDK (which uses an underlying lambda to process events), we have to do multiple passes at a record's fields over the course of multiple deployments. This operation is supported in the console with 1 click. For example, we have to do the following:

(initial code - uses simple routing policy)

const aRecord = new ARecord(this, 'RecordSet', {
    target: RecordTarget.fromAlias(new ApiGatewayDomain(domain)),
    zone: hostedZone,
})

(first pass - changing routing policy to weighted)

// Delete this for round 2 of deployments
const aRecord = new ARecord(this, 'RecordSet', {
    target: RecordTarget.fromAlias(new ApiGatewayDomain(domain)),
    zone: hostedZone,
})

const aRecord2 = new ARecord(this, 'RecordSet2', {
    target: RecordTarget.fromAlias(new ApiGatewayDomain(domain)),
    zone: hostedZone,
    deleteExisting: true, // Delete this for round 2 of deployments
})
const recordSet = aRecord2.node.defaultChild as CfnRecordSet
recordSet.weight = 100
recordSet.setIdentifier = 'API-Gateway-A-Record-weighted'
aRecord2.node.addDependency(aRecord)

(second pass - cleanup)

const aRecord2 = new ARecord(this, 'RecordSet2', {
    target: RecordTarget.fromAlias(new ApiGatewayDomain(domain)),
    zone: hostedZone,
})
const recordSet = aRecord2.node.defaultChild as CfnRecordSet
recordSet.weight = 100
recordSet.setIdentifier = 'API-Gateway-A-Record-weighted'

Use Case

Modifying record routing policies.

Proposed Solution

Involves updating the custom resource lambda, of which I haven't poked around in.

Other Information

We're also required to use the deleteExisting field, which theoretically isn't necessary (but is super dangerous). R53 appears to allow the entire operation to be done at once (rm old, insert new) with validation at API-call-time that the new, to-be-inserted is valid before deleting the old one. This means that there shouldn't be a case where the existing record is deleted and the new one cannot be inserted.

Acknowledgements

CDK version used

2, TypeScript

Environment details (OS name and version, etc.)

AL2 on x86

pahud commented 1 year ago

Yes we probably should create some patterns in aws-route53-patterns to address that.

Check out this workaround if you need to create RecordSets for weighted ARecords.

Let me know if this sample works for you.

export class DemoStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const hostedZone = new route53.HostedZone(this, 'Zone', { zoneName: 'pahuddemo.com' });

    const recordWeight: { ip: string, weight: number }[] = [
      {ip: '1.2.3.4', weight: 10},
      {ip: '5.6.7.8', weight: 20},
    ]

    recordWeight.forEach(rs => {
      this.createdWeightedRecordSet(rs.ip, rs.weight, hostedZone, 'test');
    })
  }
  private createdWeightedRecordSet(ip: string, weight: number, hostedZone: route53.HostedZone, recordName: string) {
    const arecord = new route53.ARecord(this, `RecordSet${weight}`, {
      target: route53.RecordTarget.fromIpAddresses(ip),
      zone: hostedZone,
      recordName,
    })
    const cfnRecordSet = arecord.node.tryFindChild('Resource') as route53.CfnRecordSet
    cfnRecordSet.addPropertyOverride('Weight', weight);
    cfnRecordSet.addPropertyOverride('SetIdentifier', `RecordSet${weight}`);
  }
}
christow-aws commented 1 year ago

Sorry, the example given isn't really a workaround for this issue. Or, if it is, I don't get it. It seems to be a minimal example of how to create a weighted record, which is well understood and not the purpose of this issue. (Also, for future reference, SetIdentifier needs to be unique per record of same type and routing policy, and the way the example has it seeded means that there couldn't be two records with the same weight, even if they pointed to different resources. That's perfectly legal, and normal as far as use cases go. Pretty sure the ARecord node name when new is called would also cause problems due to repeats.)

Back to the topic at hand... This won't work in cases that a record of the same type with the same name already exists, and you want to change the routing policy. For example, going from simple routing to weighted.

The custom resource lambda seems to try to create the new record in one action, and delete the old one in another. I say "try" because this doesn't work in practice. R53 barks that a record of the same type, name, and routing policy already exists, and there can't be another that differs in routing policies. This behavior should actually be all-at-once. Deleting a record in one operation and recreating in another introduces an availability risk, even if it's done within the TTL of the record. Some clients may expire during the cutover, or there may be new clients that don't have the DNS record cached.

In the console, an API call appears to be made that deletes the old record AND creates the new one in one go. So, unlike the lambda which has atomic operations (one thing at a time, for sure), the console uses an atomic transaction (do everything I ask, or none of it). This removes the availability risk, as the work is done all at once, so any resolved DNS calls either point to the old resource before the update, or the new one after it is complete. Yay for atomic transactions!