TerraformのModuleの仕組みを図解してみた

Terraformを学び始めた頃、最初は同じディレクトリにすべてのtfファイルを置いて、Resourceブロックの書き方を中心に学び、作っては簡単に削除できる"全能感"がとても楽しかったのを覚えています。

でも、しばらくするとDRYに書きたくなって、Moduleという機能を知りました。
プログラミングでいうところの関数(Function)のようなものですね。

今でこそModuleをある程度読み書きできるようになりましたが、昔はプログラミングの関数のようなものだ、という例えも知らずイメージが全く掴めませんでしたし、DRYに書きたいのに変数や出力の記述が冗長になってしまったりして、使いこなすのが難しいイメージをずっと持っていました。

Moduleを使いこなすには、ディレクトリ構造とか、モジュール化の粒度など、設計の要素も大切なのですが、そういった話はいったん置いておいて、Moduleに変数を渡す、出力を受け取る、といったInput/Outputまわりの仕組みについて整理してみます。

# インフラ構成
サンプルのインフラ構成(AWS)はvpcとsubnet1つだけの超シンプル構成です。Moduleの仕組み理解が目的なので、余計なコードは省いてModuleとの受け渡しにフォーカスして書いていきます。
![infra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/d3041470-95db-b712-2211-4dff29584e80.png)

# 図解してみた
変数を渡すInputの流れが水色の矢印。
出力を受け取るOutputの流れが紫の矢印。
![module.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/cc09d66c-2b19-92e6-c594-db78a39e4aaa.png)

# ディレクトリ構造
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`に記述します。

```terraform:./env/dev/variable.tf
variable "region" {
  type    = string
  default = "ap-northeast-1"
}

variable "env" {
  type = string
}

variable "cidr" {
  type = string
}
```

```terraform:./env/dev/terraform.tfvars
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）みたいなもんですかね。

```terraform:./env/dev/main.tf
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から受け取る変数の値によって動的にリソースを作成できるわけですね。

```terraform:./vpc/main.tf
resource "aws_vpc" "vpc" {
  cidr_block = var.cidr

  tags = {
    Name = "${var.env}-${var.cidr}"
  }
}
```

変数はModule側でも宣言しておく必要があります。

```terraform:./vpc/variables.tf
variable "env" {}
variable "cidr" {}
```

VPCIDはサブネットの作成に必要なので、OutputでRootModuleの`main.tf`に返してあげます。

```terraform:./vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.vpc.id
}
```

# Subnetモジュール
VPCモジュールでOutputしたVPCのIDをここで使っています。
本筋からは少し逸れますが、[cidrsubnet](https://www.terraform.io/docs/language/functions/cidrsubnet.html) 関数を使って、VPCのCIDRブロックを元にサブネットのCIDRを求めています。

```terraform:./subnet/main.tf
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で使う変数の宣言をお忘れなく。

```terraform:./subnet/variables.tf
variable "vpc_id" {}
variable "cidr" {}
```

ここでは、SubnetIDを出力して、RootModuleの`main.tf`に返しています。

```terraform:./subnet/outputs.tf
output "subnet_id" {
  value = aws_subnet.public.id
}
```

# Root Moduleの出力
ターミナルの画面に出力したいものはRootModule側でOutputします。サンプルで出力しているのがSubnetのIDですが、これ自体に意味はありません。

```terraform:./env/dev/outputs.tf
output "public_subnet_id" {
  value = module.subnet.subnet_id
}
```

ここで意識すべきなのは、次の2通りの使い方があるという点だと思います。

- Moduleからの戻り値として呼び出し元(main.tf)に返すOutput
- ターミナルの画面に出力させるためのOutput

# まとめ
Moduleを駆使してリソースを使いまわしできるような設計を学習中ですが、Module化しようとした時にだいたい引っかかるのが

- 変数の宣言し忘れ
- 出力のし忘れ

なんですよね。なので、冒頭のイメージ図を頭に入れながらInput/Outputの流れを意識しつつ、コーディングするように心がけたいと思います！