Terraform連載 第5回:module(モジュール)の紹介
Terraform連載企画!前回ご紹介したfor_eachの続きとして、今回はmodule(モジュール)を紹介します
はじめに
こんにちは、NTTテクノクロスの岩瀨です。
Terraformの紹介ブログ5回目です。前回はfor_eachを使った効率的な書き方を紹介しましたが、今回はその続きとしてmodule(モジュール)を紹介します。
またモジュールの活用例として、複数のリージョンにリソースを作成する方法についても紹介します。
関連記事:
「Terraform連載 第1回:いまさら聞けない、IaCってなに?~Terraform、IaSQLの紹介~」
「Terraform連載 第2回:Terraform v1.5の紹介&活用方法について考えてみた」
「Terraform連載 第3回:Terraform v1.6のtestコマンドについてご紹介」
「Terraform連載 第4回:for_eachの使い方」
「Terraform連載 第5回:module(モジュール)の紹介」
「Terraform連載 第6回:Terraform v1.7 removedブロックの紹介」
「Terraform連載 第7回:importブロックはmoduleのリソースも取り込めるか?」
「Terraform連載 第8回:Terraformの管理スコープとしてiDRACをimportしてみた」
モジュールとは
一言でいえば、複数のリソースをまとめたものです。
https://developer.hashicorp.com/terraform/language/modules/syntax
※公式ページでは「container」の語が使われていますが、仮想化技術のコンテナではなく、単なる容器・入れ物といった意味と思われます。
利用シーンとしては、コードを共通化して同じものを複数の環境にデプロイ出来るようにする、といった使い方が挙げられます。例えばサービスを構成する一連のリソース(VPC、サブネット、EC2など)をモジュール化して、複数のリージョンやアカウントに同じものをデプロイするといった形です。
モジュール化の粒度は利用者が任意で決めることができます。例えばVPC1個でもモジュール化できますし、多数のリソースを1つのモジュールに収めることもできます(ただし粒度が大きすぎると再利用性が下がる可能性があるので注意しましょう)。
モジュールの種類について
モジュールを使ったコードを書く前に、モジュールの種類について説明します。モジュールには以下の2種類があります。
- - root module (ルートモジュール)
- - child module (子モジュール)
.tfファイルのある作業ディレクトリのことを「ルートモジュール」と呼称します。過去の記事で.tfファイルを格納してterraformコマンドを実行していたディレクトリがルートモジュールにあたります。そしてルートモジュールから呼び出されるモジュールが「子モジュール」となります。
モジュールを使ったコードを書いてみる
では実際にモジュールを使ったコードを書いてみます。
前回、for_eachを使ってサブネットを複数作成するコードを書きましたので、これとVPCをあわせて子モジュール「networkモジュール」として実装し、ルートモジュールから呼び出すようにしてみます。
検証環境のディレクトリ構成
今回は以下のようなディレクトリ構成を取ってみようと思います。
├── environments
│ └── test-root
│ ├── main_tokyo.tf
│ └── providers.tf
└── modules
└── network
├── providers.tf
├── variables.tf
└── vpc-subnet.tf
「test-root」がルートモジュールのディレクトリ、「network」が今回作成するnetworkモジュールのディレクトリとなります。「modules」は複数のモジュールを格納するための親ディレクトリとなります。
子モジュール「network」のコード
まずは子モジュール「network」のコードです。各ファイルについて記載内容を説明していきます。
[terraform:vpc-subnet.tf]
# vpc-subnet.tf
resource "aws_subnet" "main" {
for_each = { for i in var.subnet_map_list : i.name => i }
vpc_id = aws_vpc.main.id
availability_zone = each.value.az_name
cidr_block = each.value.cidr
tags = {
Name = join("-", [var.system_name, var.environment, each.value.name])
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = false
enable_dns_support = true
enable_network_address_usage_metrics = false
instance_tenancy = "default"
tags = {
Name = join("-", [var.system_name, var.environment])
}
}
vpc-subnet.tfはVPCとサブネットを作成するresourceブロックを記載したものです。
サブネット作成に使っているfor_each文は[Terraform連載 第4回:for_eachの使い方]で紹介していますので、もしよければこちらの記事もお読みください。
VPCおよびサブネットのNameタグは、後述するモジュールの入力変数(variable)で指定された値をjoin関数で連結したものを指定しています。join関数の使い方は以下を参照してください。
https://developer.hashicorp.com/terraform/language/functions/join
[terraform:variables.tf]
# variables.tf
variable "system_name" {
type = string
description = "システム名称:本システムの名称を指定。一意な名称にすること"
}
variable "environment" {
type = string
description = "環境面の名前:dev, stg, prdなどを指定。一意な名称にすること"
}
variable "vpc_cidr" {
type = string
description = "VPCのCIDRブロック"
}
variable "subnet_map_list" {
type = list(map(string))
description = "サブネットのMAP"
}
variables.tfでは、このモジュールの入力変数を定義しています。
https://developer.hashicorp.com/terraform/language/values/variables
system_nameおよびenvironmentは環境を識別するための名称・識別子として利用する文字列、vpc_cidrはVPCのCIDRブロックを指定する文字列、subnet_map_listは作成するサブネットの名称、アベイラビリティゾーン、CIDRブロックを指定するリストとなっています。
variableはモジュール化する際の重要なポイントで、モジュールを使って複数環境面をデプロイする際に、重複が許されない値を変更するために利用できます。今回はVPCのCIDRブロックをvariableにしていますが、他の代表的な例としてはS3バケット名が重複しないようにする、などがあります。
[terraform:providers.tf]
# providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
providers.tfではモジュールが使うプロバイダーを明示的に指定しています。
今回は2つのリージョンにリソースをデプロイするので、モジュール側で利用するプロバイダーを明示しています
https://developer.hashicorp.com/terraform/language/providers/requirements#requiring-providers
なおデプロイ先のリージョンが1つのみの場合は、子モジュール側でのプロバイダー定義は不要です。暗黙的にルートモジュールのプロバイダー定義が継承されるためです。
https://developer.hashicorp.com/terraform/language/modules/develop/providers#implicit-provider-inheritance
ルートモジュールのコード
次にルートモジュール「test-root」のコードです。
[terraform:main_tokyo.tf]
# main_tokyo.tf
module "network_tokyo" {
source = "../../modules/network"
environment = "dev"
system_name = "tokyo"
vpc_cidr = "172.16.0.0/16"
subnet_map_list = [
{ "name" = "public-1a", "cidr" = "172.16.0.0/26", "az_name" = "ap-northeast-1a" },
{ "name" = "private-1a", "cidr" = "172.16.0.64/26", "az_name" = "ap-northeast-1a" },
{ "name" = "public-1c", "cidr" = "172.16.0.128/26", "az_name" = "ap-northeast-1c" },
{ "name" = "private-1c", "cidr" = "172.16.0.192/26", "az_name" = "ap-northeast-1c" },
]
}
main_tokyo.tfのmodule{}ブロックでnetworkモジュールの呼び出しを定義しています。
- -
source
は呼び出すモジュールのソースコードの場所を定義しています(ここではルートディレクトリからの相対パスを指定しています) - -
environment
、system_name
、vpc_cidr
、subnet_map_list
の4つは、それぞれ「network」モジュールのvariableブロックで定義した入力値を指定しています。
[terraform:providers.tf]
# provider.tf
# default provider(tokyo)
provider "aws" {
region = "ap-northeast-1"
}
terraform {
backend "s3" {
bucket = "terraform-state-management-tx"
key = "tf-test-04"
region = "ap-northeast-1"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
providers.tfは過去の連載でも登場したプロバイダー定義のブロックとなっています。(いつもの通りAWSプロバイダー+東京リージョンを使い、tfstateファイルをS3に格納する定義をしています。)
terraform applyでリソースを作成する
このコードを使ってリソースを作成してみます。ルートモジュールをカレントディレクトリにして、terraform init
、terraform plan
、terraform apply
を実行します。
[bash]
$ cd environments/test-root
$ terraform init
(略)
$ terraform plan
(略)
$ terraform apply
(略)
Plan: 5 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
module.network_tokyo.aws_vpc.main: Creating...
module.network_tokyo.aws_vpc.main: Creation complete after 1s [id=vpc-0d40d51439ca61b00]
module.network_tokyo.aws_subnet.main["public-1c"]: Creating...
module.network_tokyo.aws_subnet.main["private-1c"]: Creating...
module.network_tokyo.aws_subnet.main["public-1a"]: Creating...
module.network_tokyo.aws_subnet.main["private-1a"]: Creating...
module.network_tokyo.aws_subnet.main["public-1a"]: Creation complete after 1s [id=subnet-083da759797bc32b0]
module.network_tokyo.aws_subnet.main["private-1a"]: Creation complete after 1s [id=subnet-0ccb0c8771b51dab8]
module.network_tokyo.aws_subnet.main["private-1c"]: Creation complete after 1s [id=subnet-0592542c0974609e7]
module.network_tokyo.aws_subnet.main["public-1c"]: Creation complete after 1s [id=subnet-06a045ac801dea28f]
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
これでnetworkモジュールで定義したリソースを東京リージョンにデプロイできました。
次節からはモジュールを使った活用例をいくつか紹介してみます。
Terraform有償版について詳しく知りたい方へ
モジュールの活用例1:同じリソースを複数リージョンに作成する
最初に説明した通り、モジュールは複数のリソースをまとめたもので、同じものを複数デプロイすることができます。
例えば1つのモジュールを「開発環境」「ステージング環境」「本番環境」など複数の環境面用にデプロイする、複数のリージョン(例えば東京リージョンと大阪リージョン)やAWSアカウントにデプロイしてディザスタリカバリー対策にする、などの使い方も可能です。
ここでは先程デプロイした東京リージョンに加えて、大阪リージョンにもデプロイしてみます。
大阪リージョン用のコード
子モジュール「modules/network」配下のコードの変更はありません。
ルートモジュール側には、大阪リージョンにデプロイするためのprovider定義およびモジュール呼び出しのコードを追加します。
まずはproviders.tfのプロバイダー定義です。
[terraform:providers.tf]
# provider.tf
# default provider(tokyo)
provider "aws" {
region = "ap-northeast-1"
}
# ADDED alternative provider(osaka)
provider "aws" {
alias = "osaka"
region = "ap-northeast-3"
}
terraform {
backend "s3" {
bucket = "terraform-state-management-tx"
key = "tf-test-04"
region = "ap-northeast-1"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
ADDEDというコメント箇所が追加したプロバイダー定義です。大阪リージョン(ap-northeast-3
)のproviderブロックを追加し、エイリアスとしてosaka
を設定しています。
[terraform:main_osaka.tf]
# main_osaka.tf
module "network_osaka" {
providers = {
aws = aws.osaka
}
source = "../../modules/network"
environment = "dev"
system_name = "osaka"
vpc_cidr = "172.17.0.0/16"
subnet_map_list = [
{ "name" = "public-3a", "cidr" = "172.17.0.0/26", "az_name" = "ap-northeast-3a" },
{ "name" = "private-3a", "cidr" = "172.17.0.64/26", "az_name" = "ap-northeast-3a" },
{ "name" = "public-3c", "cidr" = "172.17.0.128/26", "az_name" = "ap-northeast-3c" },
{ "name" = "private-3c", "cidr" = "172.17.0.192/26", "az_name" = "ap-northeast-3c" },
]
}
今回はmain_osaka.tfファイルを追加し、大阪リージョン用へリソースを作るコードを記述しました。
モジュール呼び出しのパラメータsource
、environment
、system_name
、vpc_cidr
、subnet_map_list
の4つに加え、使用するプロバイダーをproviders
パラメータで指定しています。
なお今回は東京リージョンと大阪リージョンでVPC CIDRのアドレスを変えていますが、同一でも問題ありません(ただし同一の場合、VPCピアリングを構成できないなどの制約も発生しますので、要件に合わせて判断する必要があります)。
最終的なディレクトリ構成は以下のようになります。
├── environments
│ └── test-root
│ ├── main_osaka.tf
│ ├── main_tokyo.tf
│ └── providers.tf
└── modules
└── network
├── providers.tf
├── variables.tf
└── vpc-subnet.tf
コード追加後、terraform init
、terraform plan
、terraform apply
を実行することでリソースをデプロイできます。
新たに大阪リージョン分のproviderブロックを追加しましたので、プロバイダーの初期化のためterraform init
から再度実行する必要がある点に注意してください。
モジュールの活用例2:モジュールで作成したリソースのIDをoutputで返却する
先程紹介したnetworkモジュールはVPCおよびサブネットを作るモジュールですが、実際の運用ではサブネット配下にEC2を作ったり、VPC配下にセキュリティグループを作ったりするなど、配下に様々なリソースを作ることになります。
これらを一つのモジュール内にすべて実装することも可能ですが、それではモジュールが肥大化してしまいメンテナンス性が低下してしまう恐れもあるため、モジュールはある程度の大きさで分割するほうが望ましいと言えます。
よってモジュールで作成したVPCのVPC IDやサブネットのサブネットIDを別のモジュールやルートモジュールでも利用できるようになると、モジュールの粒度を小さくする事ができ、柔軟な構成を取ることができるようになります。
Terraformにはoutputという設定があり、これを使うことでモジュールの呼び出し元へ値を返すことができるようになります(プログラムでいう戻り値に似た機能です)。
https://developer.hashicorp.com/terraform/language/values/outputs
今回はモジュールで作成したサブネットのサブネットIDをoutputを用いて返却し、ルートモジュールで利用する例を書いてみます。
outputを設定するコード
outputはモジュール側で設定します。ここではoutputs.tfというファイルをnetworkモジュールに追加します。
[terraform:outputs.tf]
# outputs.tf
output "aws_subnet_id" {
value = { for i in var.subnet_map_list : i.name => aws_subnet.main[i.name].id }
}
output "vpc_id" {
value = aws_vpc.main.id
}
この例では「aws_subnet_id」の名前でサブネット名とサブネットIDの対応MAP、「vpc_id」という名前でVPC IDを返却しています。ちなみにforで生成しているaws_subnet_idのMAPは、以下のようなデータ構造になっています。
{
private-3a = "subnet-xxxxxxxxxxxxxxxxx"
private-3c = "subnet-xxxxxxxxxxxxxxxxx"
public-3a = "subnet-xxxxxxxxxxxxxxxxx"
public-3c = "subnet-xxxxxxxxxxxxxxxxx"
}
ルートモジュール側は大阪リージョンのコードmain_osaka.tfを以下のように修正します。ADDEDのコメントより下の部分が追加したコードです。
[terraform:main_osaka.tf]
# main_osaka.tf
module "network_osaka" {
providers = {
aws = aws.osaka
}
source = "../../modules/network"
environment = "dev"
system_name = "osaka"
vpc_cidr = "172.17.0.0/16"
subnet_map_list = [
{ "name" = "public-3a", "cidr" = "172.17.0.0/26", "az_name" = "ap-northeast-3a" },
{ "name" = "private-3a", "cidr" = "172.17.0.64/26", "az_name" = "ap-northeast-3a" },
{ "name" = "public-3c", "cidr" = "172.17.0.128/26", "az_name" = "ap-northeast-3c" },
{ "name" = "private-3c", "cidr" = "172.17.0.192/26", "az_name" = "ap-northeast-3c" },
]
}
# ADDED
data "aws_ami" "al2023_arm64_osaka" {
provider = aws.osaka
most_recent = true
owners = ["amazon"]
filter {
name = "architecture"
values = ["arm64"]
}
filter {
name = "name"
values = ["al2023-ami-2023*"]
}
}
resource "aws_instance" "osaka01" {
provider = aws.osaka
ami = data.aws_ami.al2023_arm64_osaka.id
instance_type = "t4g.micro"
subnet_id = module.network_osaka.aws_subnet_id.private-3a
root_block_device {
volume_type = "gp3"
volume_size = 10
iops = 3000
}
tags = {
Name = "osaka01"
}
}
この例では大阪リージョンのアベイラビリティゾーンap-northeast-3cに作成したサブネットにEC2インスタンスを1台作成するコードを書いています。
aws_instance
リソースブロックのsubnet_id
の値に、モジュールから返却されたサブネットIDを利用していることに注目してください。今回はルートモジュール内に直接リソースブロックを書いていますが、新しいモジュールでEC2を作るようにしてモジュールのvariableでサブネットIDを渡す、といった構成にすることも可能です。
まとめ
今回はモジュールを使ってリソースをまとめ、モジュール単位でリソースをデプロイする方法を紹介してみました。
モジュールを使うメリットは紹介した通り、複数の環境面へ同一のリソースをデプロイしたり、複数のリージョン・複数のAWSアカウントにデプロイする事ができるようになります。
他にもコードの独立性(疎結合性)を意識してモジュールを分割することで、再利用性を高め、別のシステムへの移植をしやすくなるなどの利点に繋がる可能性もあります。
このように様々な良い点があるので、Terraformのコードを開発するときはぜひモジュール化も考慮して見てください。
なおHashicorp社のサイトにて、モジュール構成のベストプラクティスの情報もありますので、こちらも参考にすると良さそうです。
https://developer.hashicorp.com/terraform/language/modules/develop/composition
お読みいただきありがとうございました。
IOWNデジタルツイン事業部では、Terraform EnterpriseおよびTerraform Cloud Plusに関する、日本語による保守サポートをご提供しております。Terraformの導入にあたってお悩みの方は、ぜひお気軽にご相談ください。
Terraformの導入にあたってお悩みの方へ
NTTテクノクロス株式会社
ⅠOWNデジタルツイン事業部
第三ビジネスユニット