Terraform Client VPN 路由:目标 CIDR 多于关联子网时
关联、路由和授权是三个独立对象。把每个目标 CIDR 映射到已关联的子网,而不是去索引一份更短的子网列表。
笔记本连上了 Client VPN 终端节点,terraform apply 却仍在某个路由资源上失败。配置里的目标 CIDR 通常比关联子网多,而路由块用索引去挑目标。这假定两个集合是平行的。它们并不平行,于是失败在应用时才暴露,而不是在评审时。
先指出失败的是哪个对象
涉及三个资源,各自回答不同的问题。关联把终端节点的网络接口放进某个子网。路由说明哪个目标前缀被投递到目标子网。授权规则说明哪些用户可以到达哪个前缀。指向从未关联过的子网的路由会失败,即使两个对象都存在,因为目标必须属于这个终端节点。
先列出关联:aws ec2 describe-client-vpn-target-networks --client-vpn-endpoint-id cvpn-endpoint-0123456789abcdef0。
别忘了 VPC 的本地路由
第一个关联会为整个 VPC 增加一条本地路由,因此该 VPC 里的每个子网无需路由资源就已可达。额外的路由是给对等 VPC、Transit Gateway 连接以及 VPC 并不拥有的本地前缀准备的。这就是为什么只有三个关联的终端节点可以合法地携带七条目标条目。
在添加路由之前,先数一数落在 VPC 之外的目标。如果这个数目小于 CIDR 列表,那份列表描述的是根本不需要路由的子网。
用映射替换索引
把索引包进取模运算符只会让报错安静下来,却保留了建模错误。无法通过所选目标到达的目标依然不承载流量,无论计划应用得多干净。改为存储这层关系,让 Terraform 在创建任何东西之前先做检查。下面的块仅作示例,使用的是提供方的终端节点路由参数。
variable "client_vpn_destinations" {
description = "Destination CIDR mapped to an associated subnet key"
type = map(string)
}
resource "aws_ec2_client_vpn_route" "this" {
for_each = var.client_vpn_destinations
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.this.id
destination_cidr_block = each.key
target_vpc_subnet_id = aws_subnet.private[each.value].id
lifecycle {
precondition {
condition = contains(keys(aws_subnet.private), each.value)
error_message = "Route target must be a subnet key in the associated map."
}
}
}
当映射摆在关联列表旁边时,添加一个目标就必须决定由哪个子网来承载它。这正是索引一直藏起来的那场对话。
另一种失败产生的是无意义的值,而不是索引错误。在期望高层接口的地方传入低层构造,会让 API 读取的字段留空,得到的 CIDR 也无法使用。当字符串出现在期望对象的位置时,Terraform 显示的是同一种形态。去查提供方的 schema,确认参数类型。
从客户端证明它
在应用之前,先运行 terraform validate 和针对真实状态的计划。之后,把终端节点的目标网络列表与映射对照,而不是相信一次成功的 apply。
验收测试是:已连接的客户端能到达每一个预期前缀。
授权规则是许多配置过早停下的地方。它的范围限定在一条目标 CIDR 和一个组上,而没有匹配授权的路由会产生一个连上了却到不了该前缀的客户端。为每个前缀都检查这两个对象,因为创建了路由却忘了规则的 apply 看起来是完整的。用一个组内用户和一个组外用户去测试。
当某个前缀失败时,把它的路由条目与授权规则对照。缺少授权和缺少路由在客户端日志里看起来很相似,却要在不同的资源里修复。