カスタムTerraformモジュールの構築方法

著者は、無料かつオープンソースファンドを、寄付のためのライティングプログラムの一環として受け取るために選択しました。

はじめに

Terraformモジュールを使用すると、インフラストラクチャの異なるリソースを1つの統合されたリソースにグループ化できます。これにより、リソースの定義を繰り返すことなく、後で可能なカスタマイズと共に再利用できます。これは、大規模で複雑な構造のプロジェクトに有益です。入力変数を使用してモジュールのインスタンスをカスタマイズし、出力を使用して情報を抽出することができます。独自のカスタムモジュールを作成するだけでなく、Terraform Registryで公開されている事前に作成されたモジュールも使用できます。開発者は、これらのモジュールを入力変数を使用してカスタマイズすることができますが、そのソースコードはクラウドに保存され、取得されます。

このチュートリアルでは、複数のドロップレットをロードバランサーの背後に設定するTerraformモジュールを作成します。また、Hashicorp Configuration Language(HCL)のfor_eachおよびcountループ機能を使用して、同時に複数のカスタマイズされたモジュールのインスタンスを展開します。

前提条件

注意: このチュートリアルは、Terraform 1.1.3 で特にテストされています。

モジュールの構造と利点

このセクションでは、モジュールがもたらす利点、プロジェクト内で通常どこに配置されるか、およびどのように構造化されるべきかについて学びます。

カスタムTerraformモジュールは、よくある大規模プロジェクトで頻繁に使用され、デプロイされる連結されたコンポーネントをカプセル化するために作成されます。これらは自己完結型であり、必要なリソース、変数、およびプロバイダーのみをバンドルします。

モジュールは通常、プロジェクトのルートにある中央フォルダに格納され、それぞれがその下にある専用のサブフォルダに配置されます。モジュール間のクリーンな分離を維持するために、常に単一の目的を持つように設計し、決してサブモジュールを含めないようにします。

リソーススキームからモジュールを作成するのは便利です。そのリソースを頻繁にカスタマイズして繰り返す場合。単一のリソースをモジュールとしてパッケージ化することは不要であり、全体的なアーキテクチャの単純さを徐々に失います。

小規模の開発およびテストプロジェクトでは、モジュールを組み込む必要はありません。それらの場合にはあまり改善をもたらしません。モジュールはカスタマイズ可能であり、複雑な構造のプロジェクトの構築要素です。開発者は、コードの重複を回避するための重要な利点のために、大規模なプロジェクトでモジュールを使用します。モジュールはまた、定義が1か所でのみ修正される必要があるため、インフラストラクチャ全体にそれが伝播されるという利点も提供します。

次に、Terraformプロジェクトでモジュールを定義し、使用し、カスタマイズします。

モジュールの作成

このセクションでは、複数のDropletsとロードバランサーをTerraformリソースとして定義し、それらをモジュールにパッケージ化します。また、モジュールの結果をモジュール入力を使用してカスタマイズします。

モジュールをdroplet-lbというディレクトリに格納します。そして、modulesというディレクトリの下にそれを置きます。前提条件として作成したterraform-modulesディレクトリ内にいると仮定して、以下を実行して一度に両方を作成します:

  1. mkdir -p modules/droplet-lb

-p引数は、mkdirに対して指定されたパス内のすべてのディレクトリを作成するように指示します。

それに移動します:

  1. cd modules/droplet-lb

前のセクションで指摘されたように、モジュールには使用するリソースや変数の定義が含まれます。Terraform 0.13からは、使用するプロバイダの定義も含める必要があります。モジュールは、プロジェクトのルートディレクトリであっても、TerraformはHCLコードを含むすべてのディレクトリをモジュールと見なします。

モジュールで定義された変数は、その入力として公開され、リソースの定義で使用できます。作成するモジュールには、作成するDropletsの数とそのグループの名前の2つの入力があります。変数を格納するためのvariables.tfという名前のファイルを作成して編集します。

  1. nano variables.tf

以下の行を追加してください:

modules/droplet-lb/variables.tf
variable "droplet_count" {}
variable "group_name" {}

ファイルを保存して閉じます。

ドロップレットの定義をdroplets.tfという名前のファイルに保存します。これを作成して開きます:

  1. nano droplets.tf

以下の行を追加してください:

modules/droplet-lb/droplets.tf
resource "digitalocean_droplet" "droplets" {
  count  = var.droplet_count
  image  = "ubuntu-20-04-x64"
  name   = "${var.group_name}-${count.index}"
  region = "fra1"
  size   = "s-1vcpu-1gb"
}

countパラメーターでは、作成するリソースのインスタンス数を指定します。これにはdroplet_count変数を渡します。その値は、メインプロジェクトコードからモジュールが呼び出されたときに指定されます。展開される各ドロップレットの名前は異なりますが、これは現在のドロップレットのインデックスを提供されたグループ名に追加することで実現します。ドロップレットの展開はfra1リージョンで行われ、Ubuntu 20.04を実行します。

完了したら、ファイルを保存して閉じます。

これでドロップレットが定義されたので、ロードバランサーの作成に移ります。そのリソース定義をlb.tfという名前のファイルに保存します。次のコマンドを実行して、ファイルを作成して開きます:

  1. nano lb.tf

リソースの定義を追加してください:

modules/droplet-lb/lb.tf
resource "digitalocean_loadbalancer" "www-lb" {
  name   = "lb-${var.group_name}"
  region = "fra1"

  forwarding_rule {
    entry_port     = 80
    entry_protocol = "http"

    target_port     = 80
    target_protocol = "http"
  }

  healthcheck {
    port     = 22
    protocol = "tcp"
  }

  droplet_ids = [
    for droplet in digitalocean_droplet.droplets:
      droplet.id
  ]
}

ロードバランサーの名前にグループ名を含めて定義します。これにより、ロードバランサーを識別できます。ドロップレットと同じfra1リージョンに展開します。次の2つのセクションでは、対象ポートと監視ポート、プロトコルが指定されます。

ハイライトされたdroplet_idsブロックは、ロードバランサーによって管理されるべきDropletsのIDを受け取ります。複数のDropletsがあり、その数が事前にわかっていないため、forループを使用してDropletsのコレクション(digitalocean_droplet.droplets)を走査し、それらのIDを取得します。結果として得られるコレクションがリストになるように、forループを角かっこ([])で囲みます。

ファイルを保存して閉じます。

これで、Droplet、ロードバランサー、およびモジュールの変数を定義しました。モジュールが使用するプロバイダー要件を定義する必要があります。モジュールが使用するプロバイダー、そのバージョン、および位置を明示的に指定する必要があります。Terraformの0.13以降、モジュールは使用する非Hashicorpメンテナンスプロバイダーのソースを明示的に定義する必要があります。これは、親プロジェクトからそれらを継承しないためです。

プロバイダー要件をprovider.tfという名前のファイルに保存します。次のコマンドを実行して編集用に作成します:

  1. nano provider.tf

digitaloceanプロバイダーを要求するために次の行を追加します:

modules/droplet-lb/provider.tf
terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

編集が完了したら、ファイルを保存して閉じます。これでdroplet-lbモジュールはdigitaloceanプロバイダーを必要とします。

モジュールは出力もサポートしており、リソースの状態に関する内部情報を取得するために使用できます。ロードバランサーのIPアドレスを公開する出力を定義し、outputs.tfという名前のファイルに保存します。次のコマンドを実行して編集用に作成します:

  1. nano outputs.tf

次の定義を追加します:

modules/droplet-lb/outputs.tf
output "lb_ip" {
  value = digitalocean_loadbalancer.www-lb.ip
}

この出力はロードバランサーのIPアドレスを取得します。ファイルを保存して閉じます。

droplet-lbモジュールは現在、機能的に完了し、展開の準備ができています。メインコードから呼び出し、プロジェクトのルートに保存します。まず、ファイルディレクトリを2回上に移動して移動してください:

  1. cd ../..

次に、main.tfというファイルを作成し、編集するために開いてください:

  1. nano main.tf

以下の行を追加してください:

main.tf
module "groups" {
  source = "./modules/droplet-lb"

  droplet_count = 3
  group_name    = "group1"
}

output "loadbalancer-ip" {
  value = module.groups.lb_ip
}

この宣言では、sourceとして指定されたディレクトリにあるdroplet-lbモジュールを呼び出します。提供される入力であるdroplet_countgroup_nameを設定し、後でインスタンスを区別できるようにgroup1に設定します。

ロードバランサーのIP出力がモジュールで定義されているため、プロジェクトを適用すると自動的に表示されません。これに対する解決策は、その値を取得する別の出力(loadbalancer_ip)を作成することです。

作業が完了したら、ファイルを保存して閉じてください。

次のコマンドを実行してモジュールを初期化してください:

  1. terraform init

出力は次のようになります:

Output
Initializing modules... - groups in modules/droplet-lb Initializing the backend... Initializing provider plugins... - Finding digitalocean/digitalocean versions matching "~> 2.0"... - Installing digitalocean/digitalocean v2.19.0... - Installed digitalocean/digitalocean v2.19.0 (signed by a HashiCorp partner, key ID F82037E524B9C0E8) ... Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.

プロジェクトのアクションをTerraformがどのように取るかを確認するためにプロジェクトの計画を試すことができます。次のコマンドを実行してください:

  1. terraform plan -var "do_token=${DO_PAT}"

出力は次のようになります:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # モジュールのgroups.digitalocean_droplet.droplets[0]が作成されます + resource "digitalocean_droplet" "droplets" { ... + name = "group1-0" ... } # モジュールのgroups.digitalocean_droplet.droplets[1]が作成されます + resource "digitalocean_droplet" "droplets" { ... + name = "group1-1" ... } # モジュールのgroups.digitalocean_droplet.droplets[2]が作成されます + resource "digitalocean_droplet" "droplets" { ... + name = "group1-2" ... } # モジュールのgroups.digitalocean_loadbalancer.www-lbが作成されます + resource "digitalocean_loadbalancer" "www-lb" { ... + name = "lb-group1" ... } Plan: 4 to add, 0 to change, 0 to destroy. ...

この出力は、Terraformがgroup1-0group1-1group1-2という名前の3つのDropletを作成し、また3つのDroplet間のトラフィックを管理するgroup1-lbという名前のロードバランサも作成することを詳細に示しています。

プロジェクトをクラウドに適用してみることができます。次のコマンドを実行してください:

  1. terraform apply -var "do_token=${DO_PAT}"

プロンプトが表示されたらyesを入力してください。出力にはすべてのアクションとロードバランサのIPアドレスも表示されます:

Output
module.groups.digitalocean_droplet.droplets[1]: Creating... module.groups.digitalocean_droplet.droplets[0]: Creating... module.groups.digitalocean_droplet.droplets[2]: Creating... ... Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: loadbalancer-ip = ip_address

カスタマイズ可能な数のDropletとそれらのトラフィックを自動的に管理するロードバランサを含むモジュールを作成しました。

展開されたリソースの名前変更

前のセクションで、定義したモジュールをデプロイし、groupsと呼びました。名前を変更したい場合は、単純にモジュール呼び出しを名前変更するだけでは期待される結果は得られません。呼び出しの名前を変更すると、Terraform がリソースを破棄して再作成し、過度のダウンタイムを引き起こします。

例えば、次のコマンドを実行して main.tf を編集します:

  1. nano main.tf

ハイライトされているように、groupsモジュールをgroups_renamedに名前変更します:

main.tf
module "groups_renamed" {
  source = "./modules/droplet-lb"

  droplet_count = 3
  group_name    = "group1"
}

output "loadbalancer-ip" {
  value = module.groups_renamed.lb_ip
}

ファイルを保存して閉じます。次に、プロジェクトを再初期化します:

  1. terraform init

プロジェクトを計画できるようになります:

  1. terraform plan -var "do_token=${DO_PAT}"

出力は長くなりますが、次のようになります:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create - destroy Terraform will perform the following actions: # module.groups.digitalocean_droplet.droplets[0] will be destroyed ... # module.groups_renamed.digitalocean_droplet.droplets[0] will be created ...

Terraform は既存のインスタンスを破棄して新しいものを作成するように促します。これは破壊的で不要であり、望ましくないダウンタイムを引き起こす可能性があります。

代わりに、movedブロックを使用して、Terraform に古いリソースを新しい名前の下に移動するように指示できます。 main.tfを編集し、ファイルの末尾に次の行を追加します:

moved {
  from = module.groups
  to   = module.groups_renamed
}

作業が完了したら、ファイルを保存して閉じます。

プロジェクトを再度計画できるようになります:

  1. terraform plan -var "do_token=${DO_PAT}"

main.tfmovedブロックが存在する状態で計画すると、Terraform はリソースを再作成する代わりに移動しようとします:

Output
Terraform will perform the following actions: # module.groups.digitalocean_droplet.droplets[0] は module.groups_renamed.digitalocean_droplet.droplets[0] に移動しました ... # module.groups.digitalocean_droplet.droplets[1] は module.groups_renamed.digitalocean_droplet.droplets[1] に移動しました ...

リソースを移動すると、Terraform ステート内でその位置が変更されます。つまり、実際のクラウドリソースは変更されず、破壊されたり再作成されたりしません。

次のステップで構成を大幅に変更する予定なので、展開されたリソースを破壊するには、次のコマンドを実行します:

  1. terraform destroy -var "do_token=${DO_PAT}"

プロンプトが表示されたら、yes を入力します。出力は次のようになります:

Output
... Destroy complete! Resources: 4 destroyed.

このセクションでは、Terraform プロジェクト内のリソースの名前を変更することなく、それらをデプロイします。次に、同じコードを使用してモジュールの複数のインスタンスを展開します。

複数のモジュールインスタンスのデプロイ

このセクションでは、countfor_each を使用して、カスタマイズされた状態で droplet-lb モジュールを複数回展開します。

count の使用

同じモジュールの複数のインスタンスを一度に展開する方法の一つは、countパラメータにいくつ展開するかを渡すことです。このパラメータは各モジュールで自動的に利用できます。編集するためにmain.tfを開いてください:

  1. nano main.tf

以下のように修正し、既存の出力定義とmovedブロックを削除してください:

main.tf
module "groups" {
  source = "./modules/droplet-lb"

  count  = 3

  droplet_count = 3
  group_name    = "group1-${count.index}"
}

count3に設定することで、Terraformに対してモジュールを3回展開するように指示します。編集が完了したら、ファイルを保存して閉じてください。

次のコマンドを実行して展開を計画してください:

  1. terraform plan -var "do_token=${DO_PAT}"

出力は長く、次のようになります:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: モジュール.groups[0].digitalocean_droplet.droplets[0]が作成されます ... # モジュール.groups[0].digitalocean_droplet.droplets[1]が作成されます ... # モジュール.groups[0].digitalocean_droplet.droplets[2]が作成されます ... # モジュール.groups[0].digitalocean_loadbalancer.www-lbが作成されます ... # モジュール.groups[1].digitalocean_droplet.droplets[0]が作成されます ... # モジュール.groups[1].digitalocean_droplet.droplets[1]が作成されます ... # モジュール.groups[1].digitalocean_droplet.droplets[2]が作成されます ... # モジュール.groups[1].digitalocean_loadbalancer.www-lbが作成されます ... # モジュール.groups[2].digitalocean_droplet.droplets[0]が作成されます ... # モジュール.groups[2].digitalocean_droplet.droplets[1]が作成されます ... # モジュール.groups[2].digitalocean_droplet.droplets[2]が作成されます ... # モジュール.groups[2].digitalocean_loadbalancer.www-lbが作成されます ... Plan: 12 to add, 0 to change, 0 to destroy. ...

3つのモジュールインスタンスそれぞれが、それぞれ3つのDropletと関連付けられた1つのロードバランサーを持つことに関する詳細を出力します。

for_eachの使用

for_eachを使用すると、モジュールがより複雑なインスタンスのカスタマイズまたはインスタンスの数がコードを書いている間にはわからないサードパーティのデータ(多くの場合はマップとして提示される)に依存する場合に使用できます。

これで、グループ名とDropletの数をペアにしたマップを定義し、droplet-lbのインスタンスを展開します。これにより、main.tfを編集するために次を実行します:

  1. nano main.tf

ファイルを次のように変更します:

main.tf
variable "group_counts" {
  type    = map
  default = {
    "group1" = 1
    "group2" = 3
  }
}

module "groups" {
  source   = "./modules/droplet-lb"
  for_each = var.group_counts

  droplet_count = each.value
  group_name    = each.key
}

まず、指定されたグループがどれだけのDropletを持つかを含むgroup_countsという名前のマップを定義します。その後、モジュールdroplet-lbを呼び出しますが、for_eachループが直前に定義したマップvar.group_counts上で動作するように指定します。droplet_countは、現在のペアの値であるeach.value、つまり現在のグループのDropletの数を取ります。group_nameにはグループの名前が入ります。

完了したらファイルを保存して閉じます。

次のコマンドを実行して構成を適用してみてください:

  1. terraform plan -var "do_token=${DO_PAT}"

出力には、Terraformが2つのグループとそれらのDropletとロードバランサーを作成するために実行するアクションの詳細が表示されます。

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # モジュール.groups["group1"].digitalocean_droplet.droplets[0] が作成されます ... # モジュール.groups["group1"].digitalocean_loadbalancer.www-lb が作成されます ... # モジュール.groups["group2"].digitalocean_droplet.droplets[0] が作成されます ... # モジュール.groups["group2"].digitalocean_droplet.droplets[1] が作成されます ... # モジュール.groups["group2"].digitalocean_droplet.droplets[2] が作成されます ... # モジュール.groups["group2"].digitalocean_loadbalancer.www-lb が作成されます ...

このステップでは、countfor_each を使用して、同じコードから同じモジュールの複数のカスタマイズされたインスタンスを展開しました。

結論

このチュートリアルでは、Terraformモジュールを作成して展開しました。モジュールを使用して論理的に関連するリソースをグループ化し、中央のコード定義から複数の異なるインスタンスを展開するためにそれらをカスタマイズしました。また、モジュールに含まれるリソースの属性を表示するために出力を使用しました。

Terraformについてもっと学びたい場合は、Terraformでインフラストラクチャを管理する方法 シリーズをご覧ください。

Source:
https://www.digitalocean.com/community/tutorials/how-to-build-a-custom-terraform-module