用多云Terraform代码进行模块化的多账户 “与时俱进的多云”
首先
你好,我是 Terraform 的二年级用户。
最近我沉迷于使用Terraform。现在我将介绍我正在进行的项目中所遇到的问题。
适合这种人
アカウントやクラウドをまたいでTerraformでコード管理している人
Module化したコード上でMultiple Provider Configurationsを見てもどうやってもTerraform plan/applyが通らない人
事情的开始
目前参与的项目采用了AWS+公司自有数据中心(本地环境)的混合云架构。
我們決定在AWS上使用ECS+NLB來構建能夠處理來自本地伺服器的請求的系統。
为了从本地服务器连接到NLB,您需要在Route53私有主机区域中添加别名记录来解析NLB的私有DNS名称。
然而,已经存在一个以在另一个AWS账户中构建为导向的Route53私有区域+解析器的混合云环境。
因此,作为一个项目
尽量共享资源吧。另外,请把私有主机区域集中到一个地方!因为直接连接和解析器真是太贵了!(超级翻译)
因此,我们决定采用多帐户设置的方式来构建私有主机区域。
此外,由于两个环境都使用了基于Terraform的IaC来进行配置管理,因此也需要以Terraform代码来进行智能化管理。那么,我们该如何处理呢?
请以中文将以下句子表达出来,仅需要一个选项:
我想实现的目标。
首先将要实现的事情概括出来。
作为要件,以下的事项如下。
-
- 構築済みAWSアカウント(以下、Account A)上で構築されているRoute53プライベートホストゾーン上に、今回新しく構築するAWSアカウント(以下、Account B)のNLBに紐づくプライベートDNS名のエイリアスレコードを作成する。
-
- オンプレミス環境からの名前解決およびRoute53 Resolverのコストを考慮し、Route53プライベートホストゾーンはAccount A上のものを使用する。
- オンプレミスからAccount Bへの接続経路はコストを考慮し、Acount Aにて敷設済みDirect connect Gatewayを経由する。
①成为从本地服务器进行名称解析的路径。
我們使用Route53 Resolver來解析名稱,使其能夠解析至私有主機區域。
如果在步骤①中,名字解析能够成功并且得到与帐户B上的NLB相关联的私有IP地址,那么它将成为连接路径。
另外,尽管图中没有显示,B账户上也存在着EC2实例,并且与私有云服务器一样,会与NLB进行通信,因此还会进行VPC对等连接和私有主机域的跨账户引用。
不过,由于偏离了主题,在这里将其省略了。
现在,问题在于如何将要注册到私有主机区域的私有DNS名称反映出来。
Alias记录将被注册到NLB的私有DNS名称中。由于在创建时会分配唯一的值给NLB的私有DNS名称,因此无法由使用者来确定。
因此,无法事先创建别名记录。
另外,例如在对于Account B进行Terraform apply之后,检查NLB的私有DNS并将其反映到Account A的Terraform代码中,然后再进行Terraform apply的做法绝对无法在实际运营中长久地承受。
利用多供应商配置
在使用Terraform的Multiple Provider Configurations中,参考了一篇名为”使用Terraform的Multiple Provider Configurations轻松构建AWS跨账户环境”的文章后,决定采用Multiple Provider Configurations中提到的代码结构,进行AWS跨账户的环境配置。
Multi Provider Configurations 是指为账户 A 和账户 B 准备不同的提供者(定义了区域和连接配置的)并在 Terraform 代码中根据需要进行切换。(如果有错误,请指正)
在已经进行模块化的代码中,需要怎样书写呢?
有关这一主题已经有了一些文章,但是它们只是停留在“只需要这样写”的解释上,并没有提及如何处理模块化的代码结构。
以下是本次针对Account B所用的Terraform代码的结构。
基本上,环境相关参数和模块化代码(以下简称模块代码)被严格地分离开来,此次需要添加的代码将属于模块代码一侧。
■ 修改之前的文件目录结构
|-- envs ★環境面ごとのパラメータを定義
| |-- prd
| | |-- resource.tf ★このファイルが起点となり、modules配下のModuleコードを呼び出す
| | `-- versions.tf
| |-- stg1
| | |-- :
| | `-- versions.tf
| |-- stg2
| | |-- :
| | `-- versions.tf
| `-- stgX
| |-- :
| `-- versions.tf
|
`-- modules ★Moduleコードを定義
|-- compute
|-- container
|-- datasync
|-- loadbalancer
| |-- lb.tf ★NLBに関するコードを格納
| `-- variable.tf
`-- :
■envs//resource.tf
■envs/<环境>/resource.tf = ■envs//resource.tf.
provider "aws" {
profile = "<Account B用AWS Profile名>" ※後述するAWS Profile例では"accountB"
region = "ap-northeast-1"
}
module "loadbalancer" {
source = "../../modules/loadbalancer"
:
}
■ envs/<环境>/version.tf
■ 环境/<环境>/版本.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
}
}
required_version = "1.1.7"
}
■模块/负载均衡器/lb.tf
resource "aws_lb" "nlb" {
:
}
由于作为提供者进行注册的参数需要根据每个环境进行不同的设置,因此需要在/envs目录下的每个环境面进行定义。
因此,在环境依赖参数中定义的提供者需要传递给模块代码,我们面临了一个问题。
如果这样做就好了。
话虽说得有点长,但是我会先给出已经经过测试的代码。
确认过的环境如下所示。
Terraform:4.35.0
AWS提供商:1.1.7版本
■文件和目录结构经过修改后
|-- envs
| |-- prd
| | |-- resource.tf <-- Change !!
| | `-- versions.tf
| |-- stg1
| | |-- :
| | `-- versions.tf
| |-- stg2
| | |-- :
| | `-- versions.tf
| `-- stgX
| |-- :
| `-- versions.tf
|
`-- modules ★Moduleコードを定義
|-- compute
|-- container
|-- datasync
|-- dns <--New !!
| |-- dns.tf <--New !!
| |-- providers.tf <--New !!
| `-- variable.tf <--New !!
|-- loadbalancer
| |-- lb.tf
| |-- output.tf <-- New !!
| `-- variable.tf
|-- :
`-- :
■envs/<环境>/资源.tf
# Provider for Account B
provider "aws" {
profile = "<Account B用AWS Profile名>" ※後述するAWS Profile例では"accountB"
region = "ap-northeast-1"
}
# Provider for Account A
provider "aws" {
alias = "accountA"
profile = "<Account A用AWS Profile名>" ※後述するAWS Profile例では"accountA"
region = "ap-northeast-1"
}
module "loadbalancer" {
source = "../../modules/loadbalancer"
:
}
module "dns" {
source = "../../modules/dns"
providers = {
aws = aws
aws.accoutA = aws.accountA
}
lb_alias_name = module.loadbalancer.accountB_lb_dns_name
lb_alias_zone_id = module.loadbalancer.accountB_lb_dns_name
}
■模块/dns/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
configuration_aliases = [ aws.accountA ]
}
}
required_version = "1.1.7"
}
■模块/域名系统/变量.tf
variable "lb_alias_name" {}
variable "lb_alias_zone_id" {}
■模块/域名系统/dns.tf
data "aws_route53_zone" "accountA_zone" {
provider = aws.accountA
name = "account_a.internal.jp"
private_zone = true
}
resource "aws_route53_record" "log-aggregator" {
provider = aws.accountA
zone_id = data.aws_route53_zone.accountA.zone_id
name = "account_b_nlb"
type = "A"
alias {
name = var.lb_alias_name
zone_id = var.lb_alias_zone_id
evaluate_target_health = true
}
}
■模块/负载均衡器/output.tf
output "accountB_lb_dns_name" {
value = nlb.lb_dns_name
}
output "accountB_lb_dns_name" {
value = nlb.lb_zone_id
}
以下,我将加入解释说明。
・envs/<环境>/资源.tf
# Provider for Account B
provider "aws" {
profile = "<Account B用AWS Profile名>" ※後述するAWS Profile例では"accountB"
region = "ap-northeast-1"
}
# Provider for Account A
provider "aws" {
alias = "accountA"
profile = "<Account A用AWS Profile名>" ※後述するAWS Profile例では"accountA"
region = "ap-northeast-1"
}
我正在添加和定义与帐户A相关的提供者。
如果没有指定后续提供者,则在Account B中设置默认提供者,同时Account A已经指定了别名。
为了减小对现有代码的影响,本次将对现有代码进行添加。如果在不明确定义提供者的情况下,我们将按照传统代码的方式选择暗示地选择帐户B的提供者。
module "dns" {
source = "../../modules/dns"
providers = {
aws = aws
aws.accountA = aws.accountA
}
lb_alias_name = module.loadbalancer.accountB_lb_dns_name
lb_alias_zone_id = module.loadbalancer.accountB_lb_dns_name
}
这里正在调用与私有托管区域相关的模块代码。
这次将成为关键的部分一。
在Module代码中,将Provider的设置传递给Account A和Account B,分别在providers内传递各自的Provider。
当阅读Multi Provider Configurations时,你可能会认为应该按照以下方式写出它,但是在本次的代码中,计划却没有通过。
module "dns" {
source = "../../modules/dns"
providers = {
aws = aws.accountA ※ダメな書き方
}
在NLB部署模块代码中获取私有DNS名称和HostedZoneId,并将其分别命名为module.loadbalancer.accountB_lb_dns_name和module.loadbalancer.accountB_lb_dns_name,以便在模块代码中将它们作为变量lb_alias_name和lb_alias_zone_id使用。
・模块/域名解析器/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
configuration_aliases = [ aws.accountA ]
}
}
required_version = "1.1.7"
}
这是这次的重点话题之二。
在Multi Provider Configurations中提到的configuration_aliases是根据试错的结果,在Module代码中需要进行定义。
在模块中声明一个配置别名,以便从父模块接收替代提供者配置,只需将该提供者的条目添加到该参数即可。
由于明确规定了此事,回过头来看,确实是如此。但在不断尝试的过程中,我没有达到这一步骤,而是错误地将配置别名添加到envs/<环境>/version.tf中,导致陷入困境。
【modules/dns/dns.tf】的翻译选项如下:
– 模块/域名解析/域名解析.tf
data "aws_route53_zone" "accountA_zone" {
provider = aws.accountA
name = "account_a.internal.jp"
private_zone = true
}
选择Account A的提供商,获取指定的私有主机区域的信息。
resource "aws_route53_record" "log-aggregator" {
provider = aws.accountA
zone_id = data.aws_route53_zone.accountA.zone_id
name = "account_b_nlb"
type = "A"
alias {
name = var.lb_alias_name
zone_id = var.lb_alias_zone_id
evaluate_target_health = true
}
}
使用`data “aws_route53_zone” “accountA_zone”`获取的私有主机区域ID将被定义为`zone_id`,在NLB部署用Module代码中,通过输出定义获取私有DNS名称和HostedZoneId,并指定为别名的名称和`zone_id`,以创建别名记录。
请注意
执行Terraform命令需要访问Account A和Account B的权限。
因此,在执行AWS CLI环境中的Terraform plan/apply之前,需要设置具有变更权限的Profile来适用于两个环境。
以下是AWS配置文件的示例设置,这样可以在上述Terraform代码中进行引用。
■~/.aws/credentials(例如)
[accountA_iam]
aws_access_key_id = xxxxxxxxxxxxxxxxxxxx
aws_secret_access_key = ***************************************
[accountB_iam]
aws_access_key_id = yyyyyyyyyyyyyyyyyyy
aws_secret_access_key = ***************************************
~/.aws/config(示例)
[profile accountA]
region = ap-northeast-1
role_arn = arn:aws:iam::123456789012:role/DeveloperRole
source_profile = accountA_iam
output = json
[profile accountB]
region = ap-northeast-1
role_arn = arn:aws:iam::123456789013:role/DeveloperRole
source_profile = accountB_iam
output = json
最后
我們在這個使用情境中使用多供應商配置 (Multi Provider Configurations) 來管理同一個雲端內的多個帳號資源,並且使用單一的 Terraform 代碼。
Multi Provider Configurations明确指出,不仅可以在多个地区进行管理,而且可以在多云环境下使用单一的Terraform代码进行管理。
在理论上,我们认为可以组合使用AWS+ Azure,Azure+OCI。
也许Terraform的多供应商配置能够提供一个解决方案,满足希望采用多云架构却担心基础设施管理成本的需求。
在这篇文章中参考的文件
Provider Configuration – Configuration Language | Terraform | HashiCorp Developer
Terraform の Multiple Provider Configurationsを使ってAWSクロスアカウントの環境構築を楽ちんに