1. ホーム
  2. 記事一覧
  3. 【Terraformハンズオン】Terraformでモジュールを作成してみよう

2024.10.17

【Terraformハンズオン】Terraformでモジュールを作成してみよう

IaC(Infrastructure as Code)ツールであるTerraformは、インフラを効率的にコードで管理できるため、インフラエンジニアにとって欠かせない技術です。中でも、「モジュール」はコードの再利用性やメンテナンス性を向上させ、複数環境でのインフラ構築を楽にしてくれます。

この記事では、Terraformのモジュールに焦点を当て、記事前半で基本を解説し、後半でEC2、VPCモジュールを作成するハンズオンを行います。

Terraformの環境構築がまだの方は、以下の記事で環境構築が可能です。

【実践】TerraformのinstallからAWS EC2の作成〜ssh接続までを実践してみた

モジュールを活用することで、さらにインフラ構築の効率化が可能になりますので、この記事を参考にモジュールの使い方をマスターしましょう。

Terraformモジュールとは

Terraformモジュールとは、再利用が可能なコードの集まりです。

たとえば、開発環境・本番環境など複数の環境でEC2を構築する場合、EC2のリソースをモジュールとして定義することで、共通部分のコードを一度書くだけで再利用することができます。

リソースを作成する際の「コードのテンプレート」を作成するのがモジュールと考えると理解しやすいかもしれません。

モジュールを使うメリット

ここから、実際にモジュールを作成することでどんなメリットがあるのかを解説します。

再利用性が高くなる

モジュールとしてコードを一度作成すれば、同じコードを複数の環境やプロジェクトで繰り返し利用することが可能です。モジュールを作成し使いまわすことで、何度も同じリソースのコードを書く作業を省略することができます。

例えば、AWS上でEC2インスタンスをprod、stgで分けて作成する必要がある場合、同じ設定を毎回記述するのは面倒です。

こういった時にモジュールを呼び出し、環境に合わせて異なるパラメータを渡すことでprod、stgを分けて簡単にリソースを作成することができます。

以下、Terraformの公式ドキュメントになります。

Creating Modules

モジュールの基本的な構成

Terraformのモジュールは、関連するリソースを1つのディレクトリにまとめたものです。一般的にmoduleディレクトリを作成し、その中にリソースをまとめます。

1つのモジュールディレクトリを構成するには、主にリソース、パラメータ、出力する値、この3つが必要になります。

1つのファイルで完結させることも可能ですが、今回は3つのファイルに分けた場合で紹介します。

main.tf

main.tfでは、実際に作成するリソースブロックを定義します。ここで定義するリソースは、VPC、EC2インスタンス、セキュリティグループなど、Terraformで管理したいAWSリソースです。

モジュールとして作成する場合、値を変数として定義することで呼び出し側から柔軟に値を渡すことができます。

こうすることで、モジュールの再利用性が高まります。

# 例 EC2 moduleの作成
resource "aws_instance" "web-server" {
  ami                    = var.ami
  instance_type          = var.instance_type
  vpc_security_group_ids = var.security_group_ids
  subnet_id              = var.subnet_id
  tags = {
    Name = var.name
  }
}

variables.tf

variables.tfは、モジュールが受け取るパラメータを定義するファイルになります。

ここで定義された変数が、main.tfで使用される値になります。

# variables.tfの例
variable "ami" {
  description = "AMI ID"
  type        = string
}

variable "instance_type" {
  description = "Instance type"
  type        = string
}

variable "name" {
  description = "Instance name"
  type        = string
}

variable "security_group_ids" {
  description = "List of security group IDs"
  type        = list(string)
}

variable "subnet_id" {
  description = "Subnet ID"
  type        = string
}

Terraformのinput変数、variableに関しては、以下の記事で詳しく解説しています。

Terraformのinput変数の基本的な定義方法 Variableとは

【IaC】Terraformで定義したinput変数への値の渡し方について解説

outputs.tf

outputs.tfは、リソース作成時にモジュールが出力する値を定義するファイルです。

ここで定義された出力値は、モジュールを「呼び出した側」で参照することができます。

例えば、EC2インスタンスのインスタンスIDやIPアドレスなど、他のリソースを作成する際に必要な情報を出力値として設定することが一般的です。

# outputs.tfの例
output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web-server.id
}

モジュールの呼び出し方

基本的なモジュールの呼び出し方は次のようになります。

module "<識別子>" {
  source          = "モジュールのパス"
	変数名           = 値
}

"<識別子>"のところは、任意の名前を付与することができます。VPCであればvpc、EC2であればec2など、リソースの用途に合わせて分かりやすい名前を設定します。

sourceの値は、モジュールのコードが存在するパスを指定します。ローカルに保存されたモジュールであれば相対パス、リモートから取得する場合はTerraform RegistryのURLやGitリポジトリのパスを指定できます。

変数名では、モジュールで定義した変数に対して、設定したい値を渡します。variables.tfで定義した変数に対応して、必要な値を指定します。

VPCを作成する際の例

以下の例では、ローカルにあるvpcモジュールを呼び出します。

module "vpc" {
  source                   = "../module/vpc"
  vpc_cidr_block           = "10.0.0.0/16"
  subnet_cidr_block        = "10.0.10.0/24"
  security_group_name      = "prod-web-server-sg"
  security_group_cidr_ipv4 = "0.0.0.0/0"
}

この例では、モジュール識別子としてvpcを使用し、sourceにはローカルのvpcモジュールが配置されている相対パスを指定しています。また、vpc_cidr_blocksubnet_cidr_blockなどの変数に対して、実際に利用する値を設定しています

ローカルモジュールとリモートモジュール

Terraformで扱うモジュールには、大きく分けて「ローカルモジュール」と「リモートモジュール」があります。

ローカルモジュール

この記事で紹介している「プロジェクト内に作成したモジュール」のことで、呼び出す場合はファイルシステム上のパスを使って参照します。

モジュールのソースコードがローカルに存在するため、プロジェクトごとにカスタマイズしたい場合や、プロジェクト内で限定的に使用する場合に適しています。

リモートモジュール

リモートモジュールは、Terraform Registryなどから取得して利用するモジュールです。

リモートモジュールを使うことで、複数のプロジェクトやチーム間でモジュールを共有することができます。

モジュールのソースコードはリモートのリポジトリに保存されているため、そのリポジトリのパスを使って参照します。

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.13.0"
}

Terraformモジュールを作成する

ここからは、ローカルモジュールを作成し、モジュールとはどのようなものかを手を動かしながら理解していきましょう。

今回作成するモジュールは、VPCとEC2を作成します。

ディレクトリ構成

今回はprodとstg環境でモジュールを呼び出すよう、以下のディレクトリ構成を作成します。

.
├── module
│   ├── vpc
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── web-server
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── prod
│   └── main.tf
└── stg
    └── main.tf

VPCモジュールの作成

module/vpc/main.tfは次のように作成します。

ここでは、VPC、サブネット、セキュリティグループ、セキュリティグループルールを定義します。

# VPCの作成
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block
}

# サブネットの作成
resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block
}

# セキュリティグループの作成
resource "aws_security_group" "main" {
  name   = var.security_group_name
  vpc_id = aws_vpc.main.id
}

# セキュリティグループのアウトバウンドルール
resource "aws_vpc_security_group_egress_rule" "egress" {
  security_group_id = aws_security_group.main.id
  cidr_ipv4         = var.security_group_cidr_ipv4
  ip_protocol       = var.security_group_ip_protocol
}

モジュールとして作成するため、各リソースに渡す値は変数として定義しています。

module/vpc/variables.tfでは、モジュールの値で定義した変数を記述します。

variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "subnet_cidr_block" {
  description = "CIDR block for the subnet"
  type        = string
}

variable "security_group_name" {
  description = "Name of the security group"
  type        = string
}

variable "security_group_cidr_ipv4" {
  description = "CIDR block for the security group egress rule"
  type        = string
}

variable "security_group_ip_protocol" {
  description = "IP protocol for the security group egress rule"
  type        = string
  default     = "-1"
}

module/vpc/outputs.tfは、モジュールで作成したリソースのIDを出力します。outputすることで、モジュールの呼び出し先でこのIDを利用することができます。

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "subnet_id" {
  description = "ID of the subnet"
  value       = aws_subnet.main.id
}

output "security_group_id" {
  description = "ID of the security group"
  value       = aws_security_group.main.id
}

web-serverモジュールの作成

module/web-server/main.tfでは、EC2インスタンスリソースを定義します。

もっとパラメータを定義することは可能ですが、今回はシンプルな構成で定義します。

# EC2 moduleの作成
resource "aws_instance" "web-server" {
  ami                    = var.ami
  instance_type          = var.instance_type
  vpc_security_group_ids = var.security_group_ids
  subnet_id              = var.subnet_id
  tags = {
    Name = var.name
  }
}

module/web-server/outputs.tfでは、インスタンスIDを出力します。

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web-server.id
}

module/web-server/variables.tfで、EC2インスタンス作成時の変数を定義します。

variable "ami" {
  description = "AMI ID"
  type        = string
}

variable "instance_type" {
  description = "Instance type"
  type        = string
}

variable "name" {
  description = "Instance name"
  type        = string
}

variable "security_group_ids" {
  description = "List of security group IDs"
  type        = list(string)
}

variable "subnet_id" {
  description = "Subnet ID"
  type        = string
}

prodディレクトリでのモジュールの呼び出し

prod環境と仮定して、prodディレクトリで作成したモジュールを呼び出します。main.tfを作成し、その中でモジュールを呼び出します。

# main.tf

provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.26.0"
    }
  }
}

# ami idの取得
data "aws_ami" "amazon-linux-2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
  }
}

# vpcモジュールの呼び出し
module "vpc" {
  source                   = "../module/vpc"
  vpc_cidr_block           = "10.0.0.0/16"
  subnet_cidr_block        = "10.0.10.0/24"
  security_group_name      = "prod-web-server-sg"
  security_group_cidr_ipv4 = "0.0.0.0/0"
}

# web-serverモジュールの呼び出し
module "web-server" {
  source             = "../module/web-server"
  name               = "prod-web-server"
  ami                = data.aws_ami.amazon-linux-2.id
  instance_type      = "t3.nano"
  security_group_ids = [module.vpc.security_group_id]
  subnet_id          = module.vpc.subnet_id
}

# local変数で参照するためのoutput
output "web_server_instance_id" {
  description = "ID of the web server instance"
  value       = module.web-server.instance_id
}

output "vpc_id" {
  description = "ID of the VPC"
  value       = module.vpc.vpc_id
}

output "subnet_id" {
  description = "ID of the subnet"
  value       = module.vpc.subnet_id
}

output "security_group_id" {
  description = "ID of the security group"
  value       = module.vpc.security_group_id
}

VPC、web-server各モジュールに対して、モジュール側で定義した変数に対して値を割り当てていることが分かります。このように、モジュール側では値を変数として定義しておき、呼び出す側で任意の値を渡してあげることができます。

こうすることで、呼び出す側では任意の値を渡せるようになるため、すでにあるモジュールを柔軟に利用することができ、環境ごとに異なる値を渡してリソースを作成することが可能です。

注意点

筆者がハマったポイントとして、outputの使い方です。

モジュール側、呼び出す側の両方でoutputを定義しますが、モジュール側でのみoutputを定義した場合には、その値は利用できません。

呼び出し側でも必ずoutputを定義しないと、その値を利用できないため、モジュール、呼び出しの両方でoutputを定義することは忘れないようにしましょう。

stgディレクトリでのモジュールの呼び出し

prod同様、stgディレクトリを作成し、モジュールを呼び出します。

provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.26.0"
    }
  }
}

data "aws_ami" "amazon-linux-2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
  }
}

module "vpc" {
  source                   = "../module/vpc"
  vpc_cidr_block           = "10.0.0.0/16"
  subnet_cidr_block        = "10.0.10.0/24"
  security_group_name      = "stg-web-server-sg"
  security_group_cidr_ipv4 = "0.0.0.0/0"
}

module "web-server" {
  source             = "../module/web-server"
  name               = "stg-web-server"
  ami                = data.aws_ami.amazon-linux-2.id
  instance_type      = "t3.nano"
  security_group_ids = [module.vpc.security_group_id]
  subnet_id          = module.vpc.subnet_id
}

# local変数で参照するためのoutput
output "web_server_instance_id" {
  description = "ID of the web server instance"
  value       = module.web-server.instance_id
}

output "vpc_id" {
  description = "ID of the VPC"
  value       = module.vpc.vpc_id
}

output "subnet_id" {
  description = "ID of the subnet"
  value       = module.vpc.subnet_id
}

output "security_group_id" {
  description = "ID of the security group"
  value       = module.vpc.security_group_id
}

こちらでは、セキュリティグループ名、インスタンス名をstgに変更しています。

このように、モジュールを呼び出す側で変数に渡す値を自由に変更することができます。

今回の場合だと、prodとstgでインスタンスクラスを違うものにする、といった変更も可能になります。

リソースの作成

モジュールの作成、モジュールの内容を変更した場合、呼び出す側ではTerraformの初期化をする必要があります。

そのため、initコマンドで初期化を行います。

terraform init

initコマンドを実行後、plan、applyを実行しリソースを作成します。

terraform plan

terraform apply

まとめ

この記事では、Terraformのモジュールについて基本を解説し、実際にVPCとEC2のモジュールを作成するハンズオンを行いました。

モジュールを使うことで、同じコードを何度も書かずに済み、インフラ管理が効率化されます。また、チームで共有することでインフラ構築の標準化や保守性の向上にもつながります。

今回紹介した内容は、ロードバランサーなど他のリソースにも応用することができます。

この記事をきっかけに様々なモジュール化を試していただき、モジュールの理解を深めていただければ幸いです。

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事