新手指南|利用跟踪事件建立资源和托管区域记录之间的联动机制

本文介绍了一种自动化方法,通过Amazon CloudTrail、EventBridge、Lambda和DynamoDB,实现在资源变更时自动同步Amazon Route 53托管区域记录,解决批量导入难题和资源记录维护问题,提高运维效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要

对于亚马逊云科技的初学者来说,高效管理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了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值