CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

Terraformのmoduleを使ってみました!

Terraformのmoduleを使ってみました!

こんにちは、CCCMKホールディングス TECH LAB三浦です。

小学生になる子どもと、算数の問題の解き方を一緒に考えることが増えてきました。学年を重ねるごとにどんどん問題が難しくなっていきます・・・。大人になった今の視点から算数の勉強を見ていると、問題の設定から法則を見つけてより分かりやすい問題に置き換える、抽象化のテクニックを算数の勉強を通じて教えようとしているのかな、と感じます。正しく物事を抽象化することは大人になってからも求められることが多いので、算数を通じて学んだことは多かったんだな、と思いました。

前回IaC(Infrastructure as Code)ツール"Terraform"を使い始めた話を書かせて頂きました。

techblog.cccmkhd.co.jp

以降も色々な実装例を見ながら使い方を調べています。Terraformを使うとクラウド上にリソースをすぐに作ることが出来ますが、リソースを作ることでコストが発生する可能性もあるため、何か試すときもローカルでアプリを作る時よりも慎重になりながら進めています。

コードを使ってリソースを定義していると、リソースを作るために必要な設定項目の意味をこれまで以上に意識するようになりました。Azure Portalで作る時は画面上に設定が必要な項目が提示されている状態ですが、コードで書く場合は自分でそれらの設定項目と値を指定します。そうすると「今書いたこの設定項目ってどういう意味があるんだろう?」と気になるようになり、その都度リファレンスを参照するようになりました。最初は時間がかかるのですが、クラウドサービスについてこれまで以上に勉強するいいきっかけになったように感じています。

ところでGithubなどに公開されているTerraformを使った実装を見ていると、一つのインフラを構築するのに複数の"module"を組み合わせていることがほとんどのようです。Terraformにはmoduleという他のプログラミング言語におけるライブラリに該当する機能があり、まとまった処理をカプセル化したり再利用可能にするのに活用することが出来ます。

Terraformを使ってインフラを構築するにあたり、moduleの利用は避けて通ることが出来ないといえます。そこで今回は簡単なTerraformのmoduleを作り、そのmoduleを呼び出してリソースを作る方法や、module化する際に意識する基準などを調べてみたのでまとめてみたいと思います。

Terraformのmoduleについて

Terraformにおけるmoduleとは、1つのディレクトリにまとめられた設定ファイル(tfファイル)の集まりのことを指します。terraform initなどのコマンドを実行する最上位のディレクトリもmoduleの1種で、"root module"と呼ばれます。

1つのmoduleは基本的に以下のようなファイルで構成されます。

.
└── module_directory
    ├── LICENSE
    ├── README.md
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

あるmoduleから別のmoduleを呼び出して使用することが出来ます。呼び出すには呼び出し側のmoduleのmain.tfなどの設定ファイルにmoduleブロックを記述します。たとえば以下のような形です。

module "network" {
  source                  = "./modules/network"
  resource_group_name     = azurerm_resource_group.rg.name
  resource_group_location = azurerm_resource_group.rg.location
...

sourceの引数で参照するmoduleのディレクトリのパスを指定しています。さらに参照するmodule内で定義されたvariableに値をセットすることで参照先moduleのresourceブロックで定義されたリソースをまとめて作ることが出来ます。

moduleを使ってみる

手を動かしてmoduleの使い方を理解しよう、ということで、AzureのVirtual NetworkとSubnetをまとめたnetwork moduleを作り、root moduleから呼び出す方法を試してみました。

全体のファイル構成は以下の様になります。

.
└── root
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── modules
        └── network
            ├── main.tf
            ├── variables.tf
            └── outputs.tf

network moduleを構成するmain.tf, variables.tf, outputs.tfはそれぞれ以下の様に記述しました。

main.tf

1つのVirtual Networkとその中に複数のSubnetを作成します。複数のSubnetを一度に作成するため、Subnetを定義するresourceブロックではfor_eachという特別な引数を使用しています。

resource "azurerm_virtual_network" "vnet" {
    name = var.virtual_network.name
    resource_group_name = var.resource_group_name
    location = var.resource_group_location
    address_space = var.virtual_network.address_space
}

resource "azurerm_subnet" "subnets" {
    for_each = { for subnet in var.subnets : subnet.name => subnet }
    name = each.key
    resource_group_name = var.resource_group_name
    virtual_network_name = azurerm_virtual_network.vnet.name
    address_prefixes = each.value.address_prefixes
}

network moduleのvariables.tf

network moduleを呼び出すときの引数を定義します。

variable resource_group_name {
    description = "Resource Group Name"
    type = string
}

variable resource_group_location {
  description = "Resource Group location"
  type        = string
  default     = "Japan East"
}

variable virtual_network {
    description = "Virtual Network Config"
    type        = object({
        name = string
        address_space = list(string)
    })
}

variable subnets {
    description = "Subnet Configs"
    type        = list(object({
        name = string
        address_prefixes = list(string)
    }))
}

network moduleのoutputs.tf

outputブロックに値を指定することで、network moduleを呼び出したmoduleからnetwork moduleで作成されたリソースの情報を参照することが出来るようになります。

output virtual_network_name {
    description = "The name of the virtual network"
    value = azurerm_virtual_network.vnet.name
}

output subnet_names {
    description = "The names of the subnets"
    value = [ for subnet in azurerm_subnet.subnets: subnet.name ]
}

network moduleを呼び出す

最後にroot moduleのmain.tfに、次のようなnetwork moduleを呼び出すためのmoduleブロックを記述します。

module "network" {
  source                  = "./modules/network"
  resource_group_name     = azurerm_resource_group.rg.name
  resource_group_location = azurerm_resource_group.rg.location
  virtual_network = {
    name : var.virtual_network_name
    address_space : var.virtual_network_address_space
  }
  subnets = [
    {
      name : var.frontend_subnet_name
      address_prefixes : var.frontend_subnet_address_prefixes
    },
    {
      name : var.backend_subnet_name
      address_prefixes : var.backend_subnet_address_prefixes
    }
  ]
}

以上がTerraformのmoduleの作り方とその呼び出し方です。

module化する判断基準

どこまでのリソースを1つのmoduleとしてまとめるかという判断は、構築するインフラ全体の要件や運営する組織体によって変わるため、常にこうすべきという具体的な答えはないようです。しかし、module化する際に考慮すべき観点はあります。

Terraformの開発者向けのチュートリアル"Module creation - recommended pattern"に記述されています。

developer.hashicorp.com

ドキュメントによると、Encapsulation(カプセル化), Privileges(権限の範囲),Volatility(変更頻度)の3つをmodule化の際に意識する必要があります。

  • カプセル化

いつも一緒にデプロイする必要があるリソースはmoduleとしてまとめる。まとめればまとめるほど1つの操作で複数のリソースをデプロイできるのでインフラ自体のデプロイは容易になるのですが、そのmoduleの役割や要件が理解できなくなってしまいます。

  • 権限の範囲

一つのmoduleの中に色々な組織が権限を持つリソースが含まれていると、そのmoduleを使うことで想定外の権限の侵害が発生してしまう恐れがあります。そのためリソースに対する権限の範囲を意識してmoduleを作る必要があります。たとえば権限周りを管理するリソースは一部の特権ユーザーのみが利用するべきなので、それらは他のmoduleには含めず、1つのmoduleにまとめておくなどです。

  • 変更頻度

リソースには頻繁に変更を行うものと、あまり変更を行わないものがあります。それらを同一のmoduleに含めてしまうと、変更頻度が多いリソースの更新に合わせ、本来変更する必要がないリソースにまで予期せぬ変更を加えてしまう恐れが発生します。そのため変更頻度が同じリソースをmoduleに含めるようにします。

この基準を考慮すると、それほど変更が発生せず、かつ一部のユーザーだけが管理すべきネットワーク周りのリソースはnetwork moduleとしてまとめ、アプリのバージョンアップに伴う更新作業が頻繁に発生すると思われるアプリケーション実行環境はapplication moduleのようにまとめた方が良いと言えそうです。

まとめ

今回はTerraformのmoduleについて調べた内容をまとめました。特にローカルで開発するmoduleに焦点を絞ってまとめたのですが、TerraformではTerraform Registryを通じてHashiCorp社やサードパーティが開発したmoduleをダウンロードして使用することも可能です。上手に活用し、効率的にインフラの開発を進められるようになりたいと思います。