摘要
对于亚马逊云科技的初学者来说,高效管理Amazon Route 53托管区域记录集面临不少挑战。当托管区域含有一条记录后,控制台“导入区域文件”按钮变得不可用,增加了批量导入的难度。此外,记录的关联资源(如实例,负载均衡器等)发生变化时,记录本身并不会联动,导致信息不一致,增加维护难度。本文探讨如何自动化管理及维护托管区域记录集的问题。主要思路是跟踪相关资源的变化事件(创建资源、删除资源、添加标签、删除标签等),自动修改托管区域记录并与资源保持一致,减少运维压力,提高效率与准确性。
目标读者
本文预期读者需要掌握以下技术的基础知识:
亚马逊云科技相关服务,包括CloudTrail, DynamoDB, EC2, ELB, EventBridge, IAM, Lambda, Route53等
亚马逊云科技云开发工具包
Javascript 语言及亚马逊云科技软件开发包第二版
开放源代码
本文所述解决方案源代码开放并置于以下代码库:
https://github.com/aws-samples/amazon-route-53-record-auto-linker
问题描述
首先是批量添加记录集的问题。目前,在托管区域添加任意记录后,控制台中的“导入区域文件”按钮会变为不可用。其后只能就单个记录进行添加或者修改,颇为不便。如下图所示:
如果是简单记录值,如实例的内网地址,需要在界面依次输入记录名称、类型、缓存存续时间(TTL)值、记录值以及选择路由策略,共五步。如果是其他亚马逊云科技服务,例如负载均衡器别名记录值,则需要在界面依次输入记录名称、类型、路由策略、评估目标运行状况,然后选择别名目标的对应均衡器,也差不多五步。当记录值数量不多时,在图形化控制台利用手工操作基本可以完成。但是对于一个中小型系统而言,记录值的数量可以轻松达到几十到上百个。此外一一录入内网地址,或者点选均衡器,耗时费力程度不容忽视,且容易出错。
再来说修改的问题。当终止实例,或者删除均衡器后,其对应的记录并不会连带删除,需要手动确认Route53和相关资源的对应关系,无形中增加运维负担。从删除资源的关联性可以推导出,当资源新建时,如果有需要,最好也可以自动新增记录,而无需人工干预。所以,本文需要解决的问题可以归纳为,如何建立资源和托管区域记录之间的联动机制。
????想要了解更多亚马逊云科技最新技术发布和实践创新,敬请关注在上海、北京、深圳三地举办的2021亚马逊云科技中国峰会!点击图片报名吧~
解决方案
利用亚马逊云科技的服务和无服务器设施,可以解决上述问题。概括来讲,利用Amazon CloudTrail打开资源应用接口的调用跟踪,利用Amazon EventBridge匹配相关资源的相关事件,利用Amazon Lambda读取资源信息并对Route 53托管区域记录集进行修改,同时利用Amazon DynamoDB记录必要信息以应对资源和标签删除事件,即可完成任务。
架构图
架构图如下所示。实例和均衡器并不需要在同一虚拟网中。事件的大致触发机制为:当相关资源(实例,负载均衡器)的相关事件触发时,该事件被跟踪并触发Lambda,从而读取资源信息、记录信息到DynamoDB表中并修改Route 53 到记录,完成操作。此外在DynamoDB表中,可以一览资源与记录的关联关系,一目了然。
假定资源和记录有一一对应关系,又假设域名为example.com。要记录资源对应的二级域名关系,并尽可能减少运维压力,元数据标签是顺理成章的选择。当资源拥有一个特殊键值对时,程序即自动为其维护资源与记录的对应关系。此处标签的值,定义为二级域名。通过二级域名可以算出顶级域名,从而找到其在Route53的托管区域,完成后续操作。例如以下标签键值对:
1{
2 "key": "route53:record",
3 "value": "a.example.com"
4}
5JSON
以实例和负载均衡器为例,跟踪特定事件。对实例来说,跟踪以下事件:
启动实例 RunInstances
终止实例 TerminateInstances
创建标签 CreateTags
删除标签 DeleteTags
对均衡器来说,跟踪以下事件:
新建负载均衡器 CreateLoadBalancer
删除负载均衡器 DeleteLoadBalancer
创建标签 AddTags
删除标签 RemoveTags
需要特别注意的是,当删除资源时,资源的相关信息随即失效,不能再读取。例如所有的标签、实例内网地址、均衡器的DNS名称等。删除资源时,唯一可用的信息唯有资源编号。所以,为了自动化删除记录,需要事先在其他地方保存资源信息,以留后用。这里选择DynamoDB来做持久保存。
利用域名查找托管区域的类。如果域名正确且存在,则会找到唯一的结果。
1class HostedZone {
2 zone;
3 constructor(dnsName) { this.dnsName = dnsName; }
4
5 async load() {
6 const data = await route53.listHostedZonesByName({DNSName: this.dnsName}).promise();
7 this.zone = data.HostedZones[0];
8 }
9
10 get id() { return this.zone.Id; }
11}
12JavaScript
保存资源相关信息的持久层类。表名是事先指定的。表主键直接使用资源编号,可以确保唯一性。额外记录二级域名和资源信息即可。当资源删除时,可以在表中读取资源删除前的属性。
1class RecordTable {
2 static TABLE_NAME = "Route53Records";
3 async getItem(id) {
4 const item = await dynamodb.get({
5 TableName: RecordTable.TABLE_NAME,
6 Key: {"id": id}
7 }).promise();
8 return item ? item.Item : null;
9 }
10
11 async update(id, alias, object) {
12 await dynamodb.put({
13 TableName: RecordTable.TABLE_NAME,
14 Item: {"id": id, "alias": alias, "object": object}}).promise();
15 };
16
17 async remove(id) {
18 await dynamodb.delete({
19 TableName: RecordTable.TABLE_NAME,
20 Key: {"id": id}}).promise();
21 }
22}
23JavaScript
实例管理
限于篇幅,这里假定启动或删除实例时只启动一台实例。启动或删除多台实例的情况可以照例类推处理,在此从略。修改标签时也假定仅对一台实例操作。分析实例跟踪事件可以发现,在删除标签的事件中,标签的值也附在其中。所以创建标签和删除标签事件可以合并处理,仅以事件名称来区分即可。
1function getDomain(url) {
2 return url.substring(url.indexOf(".") + 1);
3}
4
5function findEntry(tags) {
6 return tags ? tags.filter(tag => tag.key == TAG_KEY).pop() : null;
7}
8
9async function processEc2(detail) {
10 const parameters = detail.requestParameters;
11 var zone;
12 var entry;
13 var handler;
14 var instanceId;
15
16 switch (detail.eventName) {
17 case "RunInstances":
18 if (!parameters.tagSpecificationSet) { return; }
19
20 entry = findEntry(parameters.tagSpecificationSet.items[0].tags);
21 if (!entry) { return; }
22
23 instanceId = detail.responseElements.instancesSet.items[0].instanceId;
24 handler = new Ec2Handler(instanceId);
25 await handler.load();
26
27 zone = new HostedZone(getDomain(entry.value));
28 await zone.load();
29 await handler.updateRecords(zone.id, entry.value, true);
30 break;
31
32 case "TerminateInstances":
33 instanceId = parameters.instancesSet.items[0].instanceId;
34 const item = await table.getItem(instanceId);
35 if (!item) { return; }
36
37 zone = new HostedZone(getDomain(item.alias));
38 await zone.load();
39
40 handler = new Ec2Handler(item.object.InstanceId);
41 handler.instance = item.object;
42 await handler.updateRecords(zone.id, item.alias, false);
43 break;
44
45 case "CreateTags":
46 case "DeleteTags":
47 entry = findEntry(parameters.tagSet.items);
48 if (!entry) { return; }
49
50 instanceId = parameters.resourcesSet.items[0].resourceId;
51 handler = new Ec2Handler(instanceId);
52 await handler.load();
53
54 zone = new HostedZone(getDomain(entry.value));
55 await zone.load();
56 await handler.updateRecords(zone.id, entry.value, detail.eventName == "CreateTags");
57 break;
58 }
59}
60JavaScript
具体到实例记录的修改,单独抽象为一个类。首先通过实例ID读取实例信息,然后根据增删改操作,在相应的持久层表中添加或者删除相应记录,并对托管区域记录值进行修改。代码为:
1class Ec2Handler {
2 instance;
3 constructor(instanceId) { this.instanceId = instanceId; }
4
5 async load() {
6 const data = await ec2.describeInstances({InstanceIds: [this.instanceId]}).promise();
7 this.instance = data.Reservations[0].Instances[0];
8 }
9
10 async updateRecords(zoneId, alias, upsert) {
11 const ttl = DEFAULT_TTL;
12 const changeBatch = [{
13 Action: upsert ? "UPSERT" : "DELETE",
14 ResourceRecordSet: {
15 Type: "A",
16 TTL: ttl,
17 Name: alias,
18 ResourceRecords: [{ Value: this.instance.PrivateIpAddress }]
19 }}];
20
21 if (upsert) {
22 await table.update(this.instance.InstanceId, alias, this.instance);
23 } else {
24 await table.remove(this.instance.InstanceId);
25 }
26
27 const data = await route53.changeResourceRecordSets({
28 HostedZoneId: zoneId,
29 ChangeBatch: {Changes: changeBatch}
30 }).promise();
31 console.log("Change ID: " + data.ChangeInfo.Id);
32 }
33}
34
35JavaScript
负载均衡器管理
和实例不同,负载均衡器的创建与删除不能成批进行,略为简单。但是就标签操作而言,与实例跟踪事件比起来,负载均衡器稍微复杂一些。分析均衡器跟踪事件可以发现,在删除标签的事件中,标签的值没有附在其中。所以创建标签和删除标签事件需分开处理。
1function findKey(tags) {
2 return tags ? tags.filter(tag => tag == TAG_KEY).pop() : null;
3}
4
5async function processElb(detail) {
6 const parameters = detail.requestParameters;
7 var zone;
8 var entry;
9 var handler;
10 var lbArn;
11 var item;
12
13 switch (detail.eventName) {
14 case "CreateLoadBalancer":
15 entry = findEntry(parameters.tags);
16 if (!entry) { return; }
17
18 lbArn = detail.responseElements.loadBalancers[0].loadBalancerArn;
19 handler = new ElbHandler(lbArn);
20 await handler.load();
21
22 zone = new HostedZone(getDomain(entry.value));
23 await zone.load();
24 await handler.updateRecords(zone.id, entry.value, true);
25 break;
26
27 case "DeleteLoadBalancer":
28 lbArn = parameters.loadBalancerArn;
29 item = await table.getItem(lbArn);
30 if (!item) { return; }
31
32 zone = new HostedZone(getDomain(item.alias));
33 await zone.load();
34
35 handler = new ElbHandler(lbArn);
36 handler.lb = item.object;
37 await handler.updateRecords(zone.id, item.alias, false);
38 break;
39
40 case "AddTags":
41 entry = findEntry(parameters.tags);
42 if (!entry) { return; }
43
44 lbArn = parameters.resourceArns[0];
45 handler = new ElbHandler(lbArn);
46 await handler.load();
47
48 zone = new HostedZone(getDomain(entry.value));
49 await zone.load();
50 await handler.updateRecords(zone.id, entry.value, true);
51 break;
52
53 case "RemoveTags":
54 entry = findKey(parameters.tagKeys);
55 if (!entry) { return; }
56
57 lbArn = parameters.resourceArns[0];
58 item = await table.getItem(lbArn);
59 zone = new HostedZone(getDomain(item.alias));
60 await zone.load();
61
62 handler = new ElbHandler(lbArn);
63 handler.lb = item.object;
64 await handler.updateRecords(zone.id, item.alias, false);
65 break;
66 }
67}
68JavaScript
具体到均衡器别名记录的修改,单独抽象为一个类。首先通过均衡器ARN读取均衡器信息,然后根据增删改操作,在相应的持久层表中添加或者删除相应记录,并对托管区域记录值进行修改。此外如果是非网络均衡器,需要对其DNS 名称添加前缀。这里也一并处理。代码为:
1class ElbHandler {
2 lb;
3 constructor(lbArn) { this.lbArn = lbArn; }
4
5 async load() {
6 const data = await elbv2.describeLoadBalancers({LoadBalancerArns: [this.lbArn]}).promise();
7 this.lb = data.LoadBalancers[0];
8 }
9
10 async updateRecords(zoneId, alias, upsert) {
11 const changeBatch = [{
12 Action: upsert ? "UPSERT" : "DELETE",
13 ResourceRecordSet: {
14 Type: "A",
15 Name: alias,
16 AliasTarget: {
17 HostedZoneId: this.lb.CanonicalHostedZoneId,
18 DNSName: (this.lb.Type == "application" ? "dualstack." : "") + this.lb.DNSName + ".",
19 EvaluateTargetHealth: true
20 }
21 }
22 }];
23
24 if (upsert) {
25 await table.update(this.lb.LoadBalancerArn, alias, this.lb);
26 } else {
27 await table.remove(this.lb.LoadBalancerArn);
28 }
29
30 console.log("Change LB batch: " + JSON.stringify(changeBatch));
31 const data = await route53.changeResourceRecordSets({
32 HostedZoneId: zoneId,
33 ChangeBatch: {Changes: changeBatch}
34 }).promise();
35 console.log("Change ID: " + data.ChangeInfo.Id);
36 }
37}
38
39JavaScript
最后Lambda函数的处理方式就很直观了,根据跟踪事件导航到不同的函数即可,如下所示。
1exports.handler = async function(event) {
2 switch (event.source) {
3 case "aws.ec2":
4 await processEc2(event.detail);
5 break;
6
7 case "aws.elasticloadbalancing":
8 await processElb(event.detail);
9 break;
10 }
11}
12JavaScript
部署资源
资源记录联动机制的程序虽然只有短短数百行,但因为涉及的服务多(还有没有列出来的IAM等服务)、相互依赖关系深,故要正确部署各服务、配置访问授权并顺利运行该工具,仍是个不大不小的挑战。利用Amazon CDK,可以一键部署运行该程序的所涉资源。部署完成后,亚马逊云科技即开始跟踪相关资源(实例,均衡器)的相关事件(四个),并通过触发Lambda按设计预期进行操作,实现资源与记录之间的联动机制,无需额外人工干预。
资源自动化部署方面,选择Amazon CDK的方式。其特点是代码简洁,易读易用。具体如下:
1class R53RecordsStack extends Stack {
2 constructor(scope) {
3 super(scope, "R53-Records");
4
5 this.bucket = this.bucket();
6 this.dynamodb();
7 this.cloudtrail();
8 this.events(this.lambda());
9 }
10
11 bucket() {
12 return new Bucket(this, "Bucket", {
13 autoDeleteObjects: true,
14 removalPolicy: RemovalPolicy.DESTROY
15 });
16 }
17
18 dynamodb() {
19 return new Table(this, "Table", {
20 tableName: "Route53Records",
21 removalPolicy: RemovalPolicy.DESTROY,
22 partitionKey: {
23 name: "id",
24 type: AttributeType.STRING
25 }
26 });
27 }
28
29 cloudtrail() {
30 new Trail(this, "Trail", {
31 bucket: this.bucket,
32 s3KeyPrefix: "trail",
33 isMultiRegionTrail: false,
34 });
35 }
36
37 events(updateFunction) {
38 new Rule(this, "ec2", {
39 ruleName: "R53-Records-EC2",
40 description: "Register a record to its private IP address",
41 eventPattern: {
42 source: [ "aws.ec2" ],
43 detailType: [ "AWS API Call via CloudTrail" ],
44 detail: {
45 "eventSource": [ "ec2.amazonaws.com" ],
46 "eventName": [
47 "RunInstances",
48 "TerminateInstances",
49 "CreateTags",
50 "DeleteTags"
51 ]
52 }
53 },
54 targets: [ new LambdaFunction(updateFunction) ]
55 });
56
57 new Rule(this, "elb", {
58 ruleName: "R53-Records-ELB",
59 description: "Register an alias record to its DNS name",
60 eventPattern: {
61 source: [ "aws.elasticloadbalancing" ],
62 detailType: [ "AWS API Call via CloudTrail" ],
63 detail: {
64 "eventSource": [ "elasticloadbalancing.amazonaws.com" ],
65 "eventName": [
66 "CreateLoadBalancer",
67 "DeleteLoadBalancer",
68 "AddTags",
69 "RemoveTags"
70 ]
71 }
72 },
73 targets: [ new LambdaFunction(updateFunction) ]
74 });
75 }
76
77 lambda() {
78 const role = new Role(this, "Role", {
79 assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
80 managedPolicies: [
81 ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
82 ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess"),
83 ManagedPolicy.fromAwsManagedPolicyName("AmazonEc2ReadOnlyAccess"),
84 ManagedPolicy.fromAwsManagedPolicyName("AmazonRoute53FullAccess"),
85 ManagedPolicy.fromAwsManagedPolicyName("ElasticLoadBalancingReadOnly")
86 ]
87 });
88
89 return new Function(this, "Function", {
90 functionName: "R53-UpdateRecords",
91 handler: "update-records.handler",
92 role: role,
93 runtime: Runtime.NODEJS_12_X,
94 timeout: Duration.minutes(5),
95 logRetention: RetentionDays.ONE_MONTH,
96 description: "Update Route53 records.",
97 code: Code.fromAsset("../lambda/r53")
98 });
99 }
100}
101JavaScript
扩展工作
可以扩展的工作包括对其他支持的亚马逊云科技服务添加Route53记录的联动机制支持。相信即便前述“导入区域文件”按钮恢复可用以后,本文所描述的联动机制工具,凭借其轻量、自动化的特性,仍然有独特的用武之地。
参考资料
亚马逊云科技软件开发包(JavaScript )第二版:
https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/welcome.html
如何使用Amazon CLI在Amazon Route 53中创建简单的资源记录集:
https://aws.amazon.com/cn/premiumsupport/knowledge-center/simple-resource-record-route53-cli/
本篇作者
袁文俊
亚马逊云科技解决方案架构师
曾在亚马逊美国西雅图总部工作多年,就职于 Amazon Relational Database Service (RDS) 核心服务团队,拥有丰富的后端开发、运维经验。现负责业务持续性及可扩展性运行、企业应用及数据库上云迁移、云上灾难恢复管理系统等架构咨询、方案设计及项目实施等工作。他拥有复旦大学理学学士学位。
李沐
亚马逊云科技首席科学家
刘育新
亚马逊云科技 ProServe 团队高级顾问
长期从事企业客户入云解决方案的制定和项目的实施工作。
听说,点完下面4个按钮
就不会碰到bug了!