问题描述
使用Terraform部署基础架构,目前的架构由一个由负载均衡器前端的AutoScalingGroup和一个指向该负载均衡器的Route53 DNS记录组成。用户希望扩展架构,包括一个指向负载均衡器的Cloudfront分发,并将Route53 DNS记录指向CDN。
用户希望能够:
– 在不导致任何停机时间的情况下,启动Cloudfront并更改DNS记录以指向CDN;
– 在不导致任何停机时间的情况下,关闭CDN并更改DNS记录以指向负载均衡器。
当前的配置如下:
resource "aws_lb" "alb" {
name = "${var.alb-name}"
...
}
resource "aws_cloudfront_distribution" "cdn" {
count = "${var.use_cloudfront != "false" ? 1 : 0}"
...
}
resource "aws_route53_record" "www" {
count = "${var.domain-name != "" ? 1 : 0}"
depends_on = ["aws_lb.alb", "aws_cloudfront_distribution.cdn"]
zone_id = "${data.aws_route53_zone.primary.zone_id}"
name = "${var.domain-name}"
type = "A"
alias = {
name = "${var.use_cloudfront == "true" ? element(concat(aws_cloudfront_distribution.cdn.*.domain_name, list("")), 0) : aws_lb.alb.dns_name}"
zone_id = "${var.use_cloudfront == "true" ? element(concat(aws_cloudfront_distribution.cdn.*.hosted_zone_id, list("")), 0) : aws_lb.alb.zone_id}"
evaluate_target_health = true
}
}
当前的行为如下:
– 启动CDN,然后将DNS记录重定向到CDN,不会导致任何停机时间;
– 关闭CDN,然后将DNS记录重定向到负载均衡器,会导致停机时间。
用户意识到在Route53记录上使用depends_on = ["aws_lb.alb", "aws_cloudfront_distribution.cdn"]
可能不是创建所需行为的最佳选择。然而,用户不确定如何使用Terraform实现所需的行为。
用户还提供了Terraform输出的Gist文件:https://gist.github.com/rafaelmarques7/03ec6e576faff81ef36d7fc878e85230
用户想知道是否有人对实现所需行为有任何建议。如果提供的信息不足,请告知。感谢您的帮助,非常感谢!
解决方案
请注意以下操作注意版本差异及修改前做好备份。
在Terraform中,我们推荐使用模块组合来解决这个问题。这意味着将问题拆分为多个较小的模块,然后由调用模块来组合这些模块以实现所需的结果。
在这种情况下,看起来有三个不同的构建块:负载均衡器、可选的Cloudfront分发和DNS记录。我将展示使用Terraform 0.12功能的模块组合示例。如果您仍在使用Terraform 0.11,则仍然可以遵循类似的模式,但是Terraform 0.11的表达式处理能力更有限,因此细节会有所不同。
在解决这个问题时,我的第一步是确定这些组件之间需要流动的信息。在这种情况下:
– 负载均衡器具有自己的一些参数,但完全不依赖于其他组件。
– 可选的Cloudfront分发需要知道负载均衡器的主机名。
– Route53记录需要记录名称和区域ID以填充alias
块;它们可以是负载均衡器或Cloudfront分发的,具体取决于调用模块遵循的两种架构之一。
在使用Cloudfront的完整架构中,这些模块可能如下组合:
module "load_balancer" {
source = "./modules/load-balancer"
# ... 负载均衡器特定参数
}
module "cloudfront" {
source = "./modules/cloudfront"
target = module.load_balancer.dns_name
}
module "dns" {
source = "./modules/route53-record"
alias = module.cloudfront.route53_alias
}
在仅使用负载均衡器的架构中,我们删除cloudfront
模块,并直接将DNS连接到负载均衡器:
module "load_balancer" {
source = "./modules/load-balancer"
# ... 负载均衡器特定参数
}
module "dns" {
source = "./modules/route53-record"
alias = module.load_balancer.route53_alias
}
Terraform 0.12允许我们在dns
模块上定义alias
变量,具有特定的对象类型,以便将别名设置作为单个对象值在模块之间轻松传递:
# (在route53-record模块中)
variable "alias" {
type = object({
name = string
zone_id = string
})
}
resource "aws_route53_record" "example" {
# (其他参数与您的示例相同)
alias {
name = var.alias.name
zone_id = var.alias.zone_id
evaluate_target_health = true
}
}
这里的想法是,其他两个模块都遵循这种常规结构,以便route53-record
模块可以与任何一个模块兼容,而不必关心使用哪个模块。这将与每个其他模块中的输出遵循相同的结构相对应:
# 在load-balancer模块中
output "route53_alias" {
value = {
name = aws_alb.example.dns_name
zone_id = aws_alb.example.zone_id
}
}
# 在cloudfront模块中
output "route53_alias" {
value = {
name = aws_cloudfront_distribution.example.domain_name
zone_id = aws_cloudfront_distribution.example.hosted_zone_id
}
}
通过这种不同的设计,我们可以通过让调用模块的作者来控制在这两个模型之间切换的方式来解决您的原始用例。为了避免从Cloudfront切换回负载均衡器时出现停机时间,我们需要首先修改上面的第一个示例,将DNS记录指向负载均衡器,同时保留Cloudfront分发:
module "load_balancer" {
source = "./modules/load-balancer"
# ... 负载均衡器特定参数
}
module "cloudfront" {
source = "./modules/cloudfront"
target = module.load_balancer.dns_name
}
module "dns" {
source = "./modules/route53-record"
alias = module.load_balanccer.route53_alias
}
然后,一旦Cloudfront分发变得空闲,可以随时完全删除该模块并再次应用以清理它。
在Terraform中,组合是我们建模关系的主要方式,因此在使用模块时,最自然的方式是使用组合,而不是构建具有大量条件的大型模块来处理每种情况。组合允许在将来对基础架构进行更改时灵活地进行决策,以便顶级模块的维护者可以决定要进行的权衡,例如进行较小的更改以避免停机时间。
我在上面使用了模块,因为原始问题是关于模块的,但我还要注意,将这些分解为单独的模块后,我们每个资源都留下了一个资源类型的模块,该模块的名称是资源类型,这是《何时编写模块?》中的一个“嗅探测试”之一。
如果将其分解为单独的模块后,最终只创建了围绕单个资源块的薄包装器模块,通常最好完全消除模块,直接使用资源类型。
但是,如果其中一个模块仍然提高了抽象级别(例如,通过为大多数资源参数提供硬编码的良好值,并仅公开一小部分参数进行自定义),则仍然可能需要该特定模块。