Terraformを学び始めた頃、最初は同じディレクトリにすべてのtfファイルを置いて、Resourceブロックの書き方を中心に学び、作っては簡単に削除できる"全能感"がとても楽しかったのを覚えています。
でも、しばらくするとDRYに書きたくなって、Moduleという機能を知りました。
プログラミングでいうところの関数(Function)のようなものですね。
今でこそModuleをある程度読み書きできるようになりましたが、昔はプログラミングの関数のようなものだ、という例えも知らずイメージが全く掴めませんでしたし、DRYに書きたいのに変数や出力の記述が冗長になってしまったりして、使いこなすのが難しいイメージをずっと持っていました。
Moduleを使いこなすには、ディレクトリ構造とか、モジュール化の粒度など、設計の要素も大切なのですが、そういった話はいったん置いておいて、Moduleに変数を渡す、出力を受け取る、といったInput/Outputまわりの仕組みについて整理してみます。
インフラ構成
サンプルのインフラ構成(AWS)はvpcとsubnet1つだけの超シンプル構成です。Moduleの仕組み理解が目的なので、余計なコードは省いてModuleとの受け渡しにフォーカスして書いていきます。
図解してみた
変数を渡すInputの流れが水色の矢印。
出力を受け取るOutputの流れが紫の矢印。
ディレクトリ構造
envディレクトリで環境ごとのフォルダを分けるよくあるパターン。
Moduleのディレクトリは機能別にrootに作成。vpcとsubnetを分けることは普通はありませんが、今回はあくまでサンプルということで。
ソースコードはGithubに置いています。
https://github.com/m-oka-system/terraform-module-sample
.
├── env
│ ├── dev
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ ├── terraform.tfvars
│ │ └── variables.tf
│ └── prd
│ :(省略)
├── subnet
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── vpc
├── main.tf
├── outputs.tf
└── variables.tf
Root Moduleの変数
/env/dev/ をRootModuleとして開発環境のVPCを構築するケースで考えます。
まず、変数部分。環境ごとに異なる変数はterraform.tfvars
に記述します。
variable "region" {
type = string
default = "ap-northeast-1"
}
variable "env" {
type = string
}
variable "cidr" {
type = string
}
env = "dev"
cidr = "172.16.0.0/16"
モジュールの呼び出し部分
main.tf
でvpcとsubnetのModuleを呼んでいます。
source
はModuleの置いてあるディレクトリを相対パスで記述ですね。
後述しますが、Moduleの中では環境ごとに異なる値を変数で抽象化しているので、Moduleを呼ぶ時に変数の値を渡してあげる必要があります。module.vpc.vpc_id
は、VPCモジュールのOutputから受け取った値です。他のModuleで作ったリソースの値を利用したい場合、Outputして受け取る必要があります。プログラミングでいうところの戻り値(Return)みたいなもんですかね。
module "vpc" {
source = "../../vpc"
env = var.env
cidr = var.cidr
}
module "subnet" {
source = "../../subnet"
vpc_id = module.vpc.vpc_id
cidr = var.cidr
}
VPCモジュール
Module側にはリソースを作成するためのResourceブロックを記述します。
繰り返しになりますが、cidr_block
やtag:Name
など、環境ごとに異なる値を変数化しているので、RootModuleから受け取る変数の値によって動的にリソースを作成できるわけですね。
resource "aws_vpc" "vpc" {
cidr_block = var.cidr
tags = {
Name = "${var.env}-${var.cidr}"
}
}
変数はModule側でも宣言しておく必要があります。
variable "env" {}
variable "cidr" {}
VPCIDはサブネットの作成に必要なので、OutputでRootModuleのmain.tf
に返してあげます。
output "vpc_id" {
value = aws_vpc.vpc.id
}
Subnetモジュール
VPCモジュールでOutputしたVPCのIDをここで使っています。
本筋からは少し逸れますが、cidrsubnet 関数を使って、VPCのCIDRブロックを元にサブネットのCIDRを求めています。
resource "aws_subnet" "public" {
vpc_id = var.vpc_id
cidr_block = cidrsubnet(var.cidr, 8, 11) # 172.16.0.0/16 -> 172.16.11.0/24
}
Moduleで使う変数の宣言をお忘れなく。
variable "vpc_id" {}
variable "cidr" {}
ここでは、SubnetIDを出力して、RootModuleのmain.tf
に返しています。
output "subnet_id" {
value = aws_subnet.public.id
}
Root Moduleの出力
ターミナルの画面に出力したいものはRootModule側でOutputします。サンプルで出力しているのがSubnetのIDですが、これ自体に意味はありません。
output "public_subnet_id" {
value = module.subnet.subnet_id
}
ここで意識すべきなのは、次の2通りの使い方があるという点だと思います。
- Moduleからの戻り値として呼び出し元(main.tf)に返すOutput
- ターミナルの画面に出力させるためのOutput
まとめ
Moduleを駆使してリソースを使いまわしできるような設計を学習中ですが、Module化しようとした時にだいたい引っかかるのが
- 変数の宣言し忘れ
- 出力のし忘れ
なんですよね。なので、冒頭のイメージ図を頭に入れながらInput/Outputの流れを意識しつつ、コーディングするように心がけたいと思います!