はじめに
こんにちは、Terraform2年目の人です。
Terraformにどっぷりつかっている今日この頃。今取り組んでいるプロジェクトでドハマりした内容をご紹介します。
こんな人向け
-
アカウントやクラウドをまたいでTerraformでコード管理している人
-
Module化したコード上でMultiple Provider Configurationsを見てもどうやってもTerraform plan/applyが通らない人
事の始まり
現在参画中のプロジェクトはAWS+自社データセンター(オンプレミス)というハイブリッドクラウドアーキテクチャとなっています。
要件よりオンプレミスサーバからのリクエストを処理するシステムをAWS上でECS+NLBで構築することとなりました。
オンプレミスサーバからNLBへの接続のため、NLBのプライベートDNS名の名前解決を行うRoute53プライベートホストゾーンにエイリアスレコードを追加する必要があります。
ただ、すでに別のAWSアカウントにてRoute53プライベートホストゾーン+Resolverというハイブリッドクラウドを念頭に置いて構築されている環境がありました。
という訳でプロジェクトとして
「可能な限りリソースは相乗りでよろ。あと、プライベートホストゾーンは1箇所にまとめてね!だってDirect connectやResolverって高いじゃん!!(超訳)」
ということで、マルチアカウントでのプライベートホストゾーン構築という方針で進めることになりました。
また、両環境とも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を使用し、プライベートホストゾーンへ名前解決できるようにしています。
②では①の名前解決の結果、Account B上のNLBに紐づくプライベートIPアドレスが解決できた場合の接続経路となります。
なお、図には表現されていませんが、Account B上にはEC2インスタンスも存在しており、オンプレミス上のサーバと同様にNLBへ通信を行いますので、VPCピアリングやプライベートホストゾーンのクロスアカウント参照も行っています。
ただ、本筋から離れますのでここでは割愛しています。
さて、ここで問題となってくるのがプライベートホストゾーンに登録するプライベートDNS名をどう反映させるかです。
エイリアスレコードはNLBのプライベートDNS名が登録されます。NLBのプライベートDNS名は作成時に一意の値が割り当てられるため、利用者側で特定することができません。
したがって、予めエイリアスレコードを作成することはできません。
また、例えばAccount Bに対するTerraform apply後にNLBのプライベートDNSを確認し、Account A用Terraformコードに反映させ、Terraform applyを実施、では実運用には到底耐えられません。
Multi Provider Configurationsを活用する
ということでTerraform の Multiple Provider Configurationsを使ってAWSクロスアカウントの環境構築を楽ちんにの記事を参考にしながらMultiple Provider Configurationsに記載のあるマルチプロバイダー設定というコード構造を採用することになります。
Multi Provider Configurationsとは、ざっくりいうとAccount A用とAccount B用のProvider(リージョンや接続用Profileを定義したもの)を用意し、Terraformコード内で必要に応じて使い分けることになります。(間違っていたらご指摘ください)
Module化したコードではどうかけばいい?
記事は出てくるのですが、単にこう書けばよい、という説明に留まっており、Module化されたコード構造となっている場合はどのように書けばよいかまで言及されていません。
今回対象となるAccount B用のTerraformコードは以下のような構造となっています。
基本的に環境依存パラメータとModule化されたコード(以下、Moduleコード)とで厳格に分離されており、今回追加すべきコードはModuleコード側となります。
■改修前のファイル・ディレクトリ構成
|-- 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
provider "aws" {
profile = "<Account B用AWS Profile名>" ※後述するAWS Profile例では"accountB"
region = "ap-northeast-1"
}
module "loadbalancer" {
source = "../../modules/loadbalancer"
:
}
■envs/<環境>/version.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
}
}
required_version = "1.1.7"
}
■module/loadbalancer/lb.tf
resource "aws_lb" "nlb" {
:
}
プロバイダーとして登録すべきパラメータは環境ごとに異なる設定を行う必要があったため、/envs配下の環境面ごとの.tfに定義することになります。
したがって、環境依存パラメータ側で定義したプロバイダーをModuleコードに引き渡すにはどうすればよいか、という課題がありました。
こうすればよかった
前置きが長くなりましたが、先に動作確認済みコードを提示します。
動作確認済み環境は以下となります。
Terraform:4.35.0
aws-provider: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/<環境>/resource.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
}
■modules/dns/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
configuration_aliases = [ aws.accountA ]
}
}
required_version = "1.1.7"
}
■modules/dns/variable.tf
variable "lb_alias_name" {}
variable "lb_alias_zone_id" {}
■modules/dns/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
}
}
■modules/loadbalancer/output.tf
output "accountB_lb_dns_name" {
value = nlb.lb_dns_name
}
output "accountB_lb_dns_name" {
value = nlb.lb_zone_id
}
以下、解説を入れていきます。
・envs/<環境>/resource.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"
}
Account Aに関するプロバイダーを追加して定義しています。
Account Aにはalias
を指定していますが、これは後述するprovider
を指定しない場合のデフォルトのProviderをAccount Bに設定するためです。
今回はすでに存在しているコードへの追加となるため、既存コードへの影響を小さくするため、明示的にprovider
を定義しない場合は従来のコード通り、Account BのProviderが暗黙的に選択されるようにしています。
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コードを呼び出しています。
今回の肝その1となります。
ModuleコードにProvider設定を引き渡すためにproviders
内でAccount A、Account B用それぞれのProviderを引き渡しています。
Multi Provider Configurationsを読みこむと以下のように書くのが正しいと思いがちですが、今回のコードではplanが通りませんでした。
module "dns" {
source = "../../modules/dns"
providers = {
aws = aws.accountA ※ダメな書き方
}
さらにNLBデプロイ用Moduleコードで取得したプライベートDNS名およびHostedZoneIdをそれぞれmodule.loadbalancer.accountB_lb_dns_name
およびmodule.loadbalancer.accountB_lb_dns_name
として呼び出し、変数lb_alias_name
、lb_alias_zone_id
としてModuleコード側で使用できるようにしています。
・modules/dns/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.35.0"
configuration_aliases = [ aws.accountA ]
}
}
required_version = "1.1.7"
}
今回の肝その2です。
Multi Provider Configurationsで言及されているconfiguration_aliases
は試行錯誤の結果、Moduleコード側で定義する必要がありました。
「To declare a configuration alias within a module in order to receive an alternate provider configuration from the parent module, add the argument to that provider's entry. 」と明記はされていたので、振り返れば確かにその通りだなとは思いますが、試行錯誤中はそこまで至らずに、envs/<環境>/version.tf
にconfiguration_aliases
を追加してしまい、ドツボにはまりました。
・modules/dns/dns.tf
data "aws_route53_zone" "accountA_zone" {
provider = aws.accountA
name = "account_a.internal.jp"
private_zone = true
}
Account A用のProviderを指定して、指定したプライベートホストゾーンの情報を取得します。
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
として定義、alias
内のname
やzone_id
としてNLBデプロイ用Moduleコードでoutput
定義により取得したプライベートDNS名およびHostedZoneIdを指定し、エイリアスレコードを作成するようにします。
留意点
Terraformコマンドを実行する環境ですが、Account AおよびAccount Bへのアクセス権が必要となります。
ですので、Terraform plan/applyを実行するAWS CLI環境で両環境とも変更権限のあるProfileを設定しておく必要があります。
以下、AWS Profileの設定例となりますが、上記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によるMulti Provider Configurationsは最適解を提供してくれるのではないでしょうか。