Terraformの標準モジュール構造でモジュールを管理しよう!

eyecatch-iac-terraform-module
目次

概要

Terraform

TerraformはHashiCorp社によって開発されたオープンソースのIaCツールになります。IaCツールの中でもよく利用されるツールの1つであり、IaCはITエンジニアであればどの職種であっても需要がある人気スキルになります。本記事ではTerraformによるインフラ管理をより簡単に行うためにStandard Module Structureをベースとしたモジュール管理の考え方を紹介したいと思います。

Terraformの基本については下記に基本的な仕組みをまとめましたのでよかったからご参考ください。

Standard Module Structure(標準モジュール構造)

Terraformでは管理するリソースが多くなることを想定し、リソースをどのように分割して管理するかがとても重要になります。その基本的な考え方として、TerraformではStandard Module Structureというベストプラクティスを提唱しており、多くのTerraformのコードがこの指針に従ったディレクトリ構成、ファイル構成を採用しています。

下記コードは公式ページから引用した最小限の標準モジュール構造です。Terraformでは最低限3行目から6行目にある4つのファイルを作成することを推奨しています。特にvariables.tfやoutputs.tfはシンプルなインフラ管理では利用しないこともありますが空でも作成することが提唱されています。

$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
Terraform, Standard Module Structure

下記のコードは公式ページから引用した標準モジュール構造です。簡単に解説すると、Terraformが提唱する3-7行目はインフラ全体に関わる内容を記述するファイルになり、8行目のmodules以下はまとまり(例えばサーバー、NW、CI/CDなど)ごとにインフラ設定を記述する構成となります。16行目以降のexampleは各モジュールを動かすためのコードを配置し、開発者や利用者がモジュールを理解する助けとなります。

$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
   ├── nestedA/
      ├── README.md
      ├── variables.tf
      ├── main.tf
      ├── outputs.tf
   ├── nestedB/
   ├── .../
├── examples/
   ├── exampleA/
      ├── main.tf
   ├── exampleB/
   ├── .../
Terraform, Standard Module Structure

モジュール管理

ここでは実際のモジュール管理の例として環境とリソースの観点を扱います。他にもプロバイダーごとに分離したりシステムの機能ごとに分離するなどの観点がありますが、多くのコードやブログ等で紹介されているこの2つの観点について説明します。

環境の観点

一般的にシステム開発では、開発を進めるための開発環境(dev環境)、本番に近い環境を用意しユーザーのテストや検証用に利用するステージング環境(stg環境)、本番運用を実現するための本番環境(prod環境)の3つを用意することが一般的です。

この考え方を反映したディレクトリの例を下記に示します。8行目のようにenvironmentsディレクトリを作成し、前述した3つの環境別にサブディレクトリを作成します。そして環境毎に必要な設定ファイルを作成するイメージとなります。もし共通設定がある場合はenvironments/commonのように共通ディレクトリを作成しするのもありです。

$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── environments/
   ├── dev
      ├── main.tf
      ├── variables.tf
      ├── outputs.tf
      ├── provider.tf
      ├── ...
   ├── staging
   ├── develop
   ├── ...
├── modules/

リソースの観点

続いての観点はリソースの観点です。NWに関するリソース、サーバーに関するリソース、CI/CDに関するリソースなど機能別にリソースを分類して管理することで見通しの良いインフラ管理ができるようになります。

下記はリソース観点でディレクトリを構成した例です。9行目のmodules以下には10行目のec2や14行目のvpcといったサブディレクトリが存在しており、リソース毎にインフラ設定用のmain.tfや変数設定用のvariables.tf、出力設定用のoutputs.tfといったファイルが用意されていることがわかります。

$ tree terraform-sample/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── environments/
└── modules/
    ├── ec2
       ├── main.tf
       ├── outputs.tf
       └── variables.tf
    └── vpc
        ├── main.tf
        └── outputs.tf

モジュール管理を意識したサンプルコード

前提

Terraform 1.3.8
EC2とVPCを作成

全体のディレクトリ構造

解説するサンプルコードのディレクトリ構造を下記に示します。今回のサンプルコードでは3行目のenvironmentsと14行目のmodulesの2つのディレクトリから構成されることがわかります。

ここで重要なのはdevのmain.tfもprodのmain.tfも同じmodulesのec2やvpcを指定しているということです。あくまで環境の用途(ここでは開発か本番か)に応じてdev直下でterraformコマンドを実行するか、prod直下でterraformコマンドを実行するかが決まります。

$ tree 
.
├── environments
   ├── dev
      ├── backend.tf
      ├── main.tf
      ├── outputs.tf
      └── provider.tf
   └── prod
       ├── backend.tf
       ├── main.tf
       ├── outputs.tf
       └── provider.tf
└── modules
    ├── ec2
       ├── main.tf
       ├── outputs.tf
       ├── user_data.sh
       └── variables.tf
    └── vpc
        ├── main.tf
        └── outputs.tf

environmentsディレクトリの中身

まずenvironmentsディレクトリの中身を確認します。dev、prod両方とも内容についてはほぼ同じですので、ここではdevの中身を解説していきます。

backedn.tfファイルの中身を確認します。ここではリソースの実態情報である.tfstateファイルの保存場所をS3の任意のバケット・キーに設定をしています。devとprodで別の.tfstateファイルを管理する場合は5行目のキーの値をprod/terraform.tfstateなどに変更するとよいでしょう。

# environments/dev/backend.tf
terraform {
  backend "s3" {
    bucket = "terraform-s3-7897583108"
    key    = "dev/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

main.tfの中身を確認します。ここではsourceを指定してmodulesのvpcやec2を紐づけています。また8、9行目で記載されているallow_sshとsubnet_idはmodules/ec2/variables.tfに記載されています。モジュールに記載されている変数はenvironmetsにあるmainに記載することで9行目のようにvpcのサブネットIDをec2のsubnet_idに渡すことができます。

# environments/dev/main.tf
module "vpc" {
  source = "../../modules/vpc"
}

module "ec2" {
  source    = "../../modules/ec2"
  allow_ssh = true
  subnet_id = module.vpc.public_subnet_ids[0]
}

outputs.tfの中身を確認します。outputsはターミナルや変数に値を出力させるための機能です。ここではec2.urlの値を受け取り、ターミナルにurlを出力します。

# environments/dev/outputs.tf
output "url" {
  value = module.ec2.url
}

provider.tfの中身を確認します。ここでは連携先のクラウドサービスを登録します。

# environments/dev/provider.tf
terraform {
  required_version = "1.3.8"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.54.0"
    }

    random = {
      source  = "hashicorp/random"
      version = "3.4.3"
    }
  }
}

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

modules/ec2ディレクトリの中身

main.tfの中身を確認します。ここでは2-16行目でEC2の基本設定、18-20行目でランダムな8桁の数字を生成する設定、22-24行目でvpcから渡ってきたサブネットIDの格納(environments/dev/main.tfを参照)、26-58行目でセキュリティグループの設定を行っています。

# modules/ec2/main.tf
resource "aws_instance" "this" {
  ami           = "ami-0b828c1c5ac3f13ee"
  instance_type = "t2.micro"

  subnet_id                   = var.subnet_id
  vpc_security_group_ids      = [aws_security_group.this.id]
  associate_public_ip_address = true

  user_data                   = file("${path.module}/user_data.sh")
  user_data_replace_on_change = true

  tags = {
    Name = "terraform-ec2"
  }
}

resource "random_id" "this" {
  byte_length = 8
}

data "aws_subnet" "this" {
  id = var.subnet_id
}

resource "aws_security_group" "this" {
  name   = "terraform-ec2-sg-${random_id.this.hex}"
  vpc_id = data.aws_subnet.this.vpc_id
}

resource "aws_security_group_rule" "ssh" {
  count = var.allow_ssh ? 1 : 0

  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.this.id
}

resource "aws_security_group_rule" "http" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.this.id
}

resource "aws_security_group_rule" "egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.this.id
}

variables.tfの中身を確認します。ここでec2モジュールの中で利用する変数を宣言します。

# modules/ec2/variables.tf
variable "allow_ssh" {
  type    = bool
  default = false
}

variable "subnet_id" {
  type = string
}

user_data.shの中身を確認します。ユーザーデータはEC2生成時に1度だけ実行されるスクリプトになります。ここではnginxをインストールするコマンドが記述されています。

# modules/ec2/user_data.sh
#!/bin/bash

sudo apt update
sudo apt install -y nginx

outputs.tfの中身を確認します。ここでは生成されたEC2インスタンスのパブリックIPを含むURLの値を返却する設定を宣言しています。

# modules/ec2/outputs.tf
output "url" {
  value = "http://${aws_instance.this.public_ip}"
}

modules/vpcディレクトリの中身

main.tfの中身を確認します。ここではVPCに関連する設定が記述されています。

# modules/vpc/main.tf
locals {
  subnet_args = {
    "ap-northeast-1a" = "10.0.0.0/24"
    "ap-northeast-1c" = "10.0.1.0/24"
    "ap-northeast-1d" = "10.0.2.0/24"
  }
}

resource "aws_vpc" "this" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "terraform-vpc"
  }
}

resource "aws_subnet" "public" {
  for_each = local.subnet_args

  vpc_id            = aws_vpc.this.id
  cidr_block        = each.value
  availability_zone = each.key
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }
}

resource "aws_route_table_association" "public" {
  for_each = aws_subnet.public

  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

outputs.tfの中身を確認します。ここではmodules/ec2/main.tfに記述されているaws_subnet.publicからsubnet.idを取得している処理になります。

# modules/vpc/outputs.tf
output "public_subnet_ids" {
  value = [
    for subnet in aws_subnet.public : subnet.id
  ]
}

上記の4行目は下記のようにfor文を繰り返し実行していると考えるとわかりやすいでしょう。

for subnet in aws_subnet.public : 
  subnet.id

補足 Terraformの実行

上記のディレクトリ構造でのTerraform実行については下記の記事を参考に確認頂けます。

まとめ

本記事ではTerraformのモジュール化の考え方を解説しました。実務では本記事よりも複雑なディレクトリ構造でコードが管理されているかもしれませんが、基本となる考え方は本記事の内容になるかと思います。ぜひ本記事で紹介したコードを実際に動かしながら基礎を理解頂き、実務で応用してみてくださいね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Hack Luck Labの管理人hakula(ハクラ)です。2012年にSIerに新卒入社し、SE、新規事業、情シスを担当。その後、ITコンサルを経て、現在はバックエンドエンジニア。過去にはC#、SQL Server、JavaScriptで開発を行い、現在はPython、Rest Framework、Postgresql、Linux、AWSなどを使用しています。ノーコードツールやDX関連も興味あり。「技術は価値を生むために使う」ことが信条で、顧客や組織への貢献を重視しています。

目次