Terraform 基礎 #8 モジュール基礎 — リソースのまとまりの再利用とレジストリモジュール

読了 5分

バケット 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
# 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
# ルートモジュールの 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 が代表例です。

main.tf
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 バックエンドへ移し、ロックまでかけて協業の準備を仕上げます。

X