Terraform 基礎 #8 モジュール基礎 — リソースのまとまりの再利用とレジストリモジュール
バケット 1 つで始まったコードが、シリーズを進むうちにインスタンス、サブネット、セキュリティグループへと膨らみました。このまま実務のインフラを詰め込めば main.tf は数百行になり、「開発用と本番用に同じ構成を 2 セット」という要件の前では、結局コピー&ペーストが始まります。関数なしにスクリプト 1 ファイルで耐えるプログラミングと同じ状況です。Terraform で関数に当たるのがモジュール(module)です。リソースのまとまりに名前を付けて再利用する単位で、この記事では自分で作り、レジストリから持ってきて使い、分ける基準まで整理します。
すべてのディレクトリはすでにモジュールです #
モジュールは特別なファイル形式ではなく、.tf ファイルが入ったディレクトリです。だからこれまで作業してきたディレクトリもすでにモジュールで、Terraform はこれをルートモジュール(root module)と呼びます。モジュールのインターフェースも、すでに学んだ内容です。#3 の variable がモジュールの入力で、output がモジュールの出力です。あのときは人が値を入れて読み取る通り道でしたが、モジュール同士がつながると、呼び出す側が variable に値を渡して output で結果を受け取るという関数シグネチャになります。新しく学ぶことは呼び出しの文法 1 つだけです。
子モジュールを作る #
静的 Web サイト用のバケット構成(バケット + バージョニング + パブリックブロック)がプロジェクトごとに繰り返されるとします。このまとまりをモジュールに切り出します。慣例として modules/ の下に置きます。
modules/
└── static-bucket/
├── variables.tf # 入力
├── main.tf # リソース
└── outputs.tf # 出力# modules/static-bucket/variables.tf
variable "bucket_name" {
type = string
description = "作成するバケット名"
}
# modules/static-bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# modules/static-bucket/outputs.tf
output "bucket_arn" {
value = aws_s3_bucket.this.arn
}モジュールの中でリソース名を this にするのはコミュニティの慣例です。モジュール名がすでに役割を語っているので、中のリソースに重ねて役割の名前を付けない、という考え方です。呼び出しは module ブロックで行います。
# ルートモジュールの main.tf
module "assets" {
source = "./modules/static-bucket"
bucket_name = "my-assets-prod"
}
module "logs" {
source = "./modules/static-bucket"
bucket_name = "my-logs-prod"
}
output "assets_arn" {
value = module.assets.bucket_arn
}source がモジュールの場所で、残りの引数がモジュールの variable に入ります。同じモジュールを別の値で 2 回呼び出して、バケット構成が 2 セットできました。モジュールの出力は module.名前.出力名 で参照します。手順上の注意点が 1 つあります。モジュールを追加したり source を変えたりしたら、terraform init をもう一度実行する必要があるということです。init がモジュールをインストールする段階だからで、「Module not installed」エラーの答えはほとんどの場合 init です。
レジストリモジュール — 作る前に探します #
すべてのモジュールを自分で作る必要はありません。Terraform Registry にはコミュニティが磨いてきた公開モジュールがあり、特に terraform-aws-modules 系列は AWS 構成の事実上の標準コレクションです。VPC が代表例です。
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 6.0"
name = "main"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
}サブネット、ルートテーブル、NAT ゲートウェイまで数十個のリソースを作る構成が、入力数行で終わります。レジストリモジュールで守るべきルールは version の固定です。プロバイダーと違い、モジュールは version を省略しても動きますが、そうすると他人が公開した新バージョンが、予告なしにチームのインフラの plan を変えてしまいます。実践シリーズで VPC を扱うときは、動作原理を理解するためにまず素のリソースで作ってみてから、モジュールと比較します。便利さにはブラックボックスというコストが付き、基礎を知る人だけがそのコストをコントロールできるからです。
モジュールを分ける基準 #
モジュールを学ぶと何でもモジュールにしたくなりますが、過剰な分割は繰り返しと同じくらい有害です。リソース 1 つを包んだだけのモジュールは引数を素通しするだけの包み紙で、コードを増やすだけです。逆にインフラ全体を 1 つの巨大モジュールに入れると、再利用の単位が消えます。実用に耐える基準は 2 つです。
- 繰り返しが実際に 2 回以上現れたとき、そのまとまりをモジュールにします。将来の再利用を想像して先回りで作るモジュールは、たいていインターフェースを外します。
- 一緒に作って一緒に消す単位で切ります。「静的サイトのバケット一式」「サービス 1 つのネットワーク」のようにライフサイクルを共にするまとまりが良いモジュールで、寿命が違うもの(VPC とその上のアプリケーション)を 1 つのモジュールに入れると、小さな変更でも大きな plan が出ます。
この基準で、実践シリーズではインフラ全体をモジュールに構造化するリファクタリングを実際にやってみます。
まとめ #
この記事で扱った内容です。
- モジュールは
.tfファイルの入ったディレクトリで、これまでの作業ディレクトリもルートモジュールです。variable が入力、output が出力です - 子モジュールは
moduleブロックで呼び出し、module.名前.出力で結果を受け取ります。モジュール追加後は init が必要です - モジュール内部のリソース名は
thisにするのが慣例です - レジストリの terraform-aws-modules 系列は実績ある標準コレクションで、モジュールには必ず version を固定します
- モジュール分割は、繰り返しが実際に現れたときに、ライフサイクルを共にする単位で行います
次の記事(#9 リモート state とチーム協業)は基礎シリーズの最終回です。#5 で先送りした宿題、つまりノート PC のローカルディスクにある state をチームで共有する S3 バックエンドへ移し、ロックまでかけて協業の準備を仕上げます。