Terraformモジュールとテンプレートを使用して再利用可能なインフラストラクチャを作成する方法

提供:Dev Guides
移動先:案内検索

著者は、 Write for DOnations プログラムの一環として、 Free and Open SourceFundを選択して寄付を受け取りました。

序章

Infrastructure as Code(IAC)の主な利点の1つは、定義されたインフラストラクチャの一部を再利用することです。 Terraformでは、モジュールを使用して、論理的に接続されたコンポーネントを1つのエンティティにカプセル化し、定義した入力変数を使用してそれらをカスタマイズできます。 モジュールを使用してインフラストラクチャを高レベルで定義することにより、同じモジュールに異なる値を渡すだけで、開発、ステージング、および本番環境を分離できます。これにより、コードの重複が最小限に抑えられ、簡潔さが最大化されます。

カスタムモジュールのみを使用することに限定されません。 TerraformレジストリはTerraformに統合されており、required_providersセクションで定義することにより、プロジェクトにすぐに組み込むことができるモジュールとプロバイダーを一覧表示します。 パブリックモジュールを参照すると、ワークフローが高速化され、コードの重複が減ります。 便利なモジュールがあり、それを世界と共有したい場合は、他の開発者が使用できるようにレジストリに公開することを検討できます。

このチュートリアルでは、Terraformプロジェクトでコードを定義して再利用する方法のいくつかを探ります。 Terraformレジストリからモジュールを参照し、モジュールを使用して開発環境と本番環境を分離し、テンプレートとその使用方法について学習し、depends_onメタ引数を使用してリソースの依存関係を明示的に指定します。

前提条件

  • DigitalOceanパーソナルアクセストークン。DigitalOceanコントロールパネルから作成できます。 手順については、DigitalOcean製品ドキュメントパーソナルアクセストークンの作成方法を参照してください。
  • ローカルマシンにインストールされたTerraformと、DigitalOceanプロバイダーでセットアップされたプロジェクト。 DigitalOcean チュートリアルでTerraformを使用する方法のステップ1およびステップ2を完了し、プロジェクトフォルダーにterraform-reusabilityではなく名前を付けてください。 loadbalanceステップ2の間、pvt_key変数とSSHキーリソースを含めないでください。
  • terraform-reusabilitymodulesで利用可能なdroplet-lbモジュール。 カスタムモジュールの作成方法チュートリアルに従い、droplet-lbモジュールが機能的に完了するまでそれを実行します。 (つまり、モジュールの作成セクションのcd ../..コマンドまで。)
  • Terraformプロジェクトの構造化アプローチに関する知識。 詳細については、チュートリアルTerraformプロジェクトを構築する方法を参照してください。
  • (オプション)ネームサーバーがレジストラのDigitalOceanを指す2つの別個のドメイン。 ドメインはまだDigitalOceanアカウントに追加されてはなりません。 これを設定するには、共通ドメインレジストラからDigitalOceanネームサーバーを指定する方法チュートリアルを参照してください。 このチュートリアルで作成するプロジェクトをデプロイする予定がない場合は、これを行う必要がないことに注意してください。

注:このチュートリアルは、Terraform1.0.2を使用してテストされています。


開発環境と本番環境の分離

このセクションでは、モジュールを使用してターゲットのデプロイメント環境を分離します。 より複雑なプロジェクトの構造に従ってこれらを配置します。 2つのモジュールでプロジェクトを作成します。1つはドロップレットとロードバランサーを定義し、もう1つはDNSドメインレコードを設定します。 その後、同じモジュールを呼び出す2つの異なる環境(devprod)の構成を記述します。

dns-recordsモジュールの作成

前提条件の一部として、terraform-reusabilityの下に初期プロジェクトを設定し、modulesの下の独自のサブディレクトリにdroplet-lbモジュールを作成しました。 次に、dns-recordsと呼ばれる、変数、出力、およびリソース定義を含む2番目のモジュールをセットアップします。 terraform-reusabilityディレクトリから、次のコマンドを実行してdns-recordsを作成します。

mkdir modules/dns-records

そこに移動します:

cd modules/dns-records

このモジュールには、ドメインの定義と、後でロードバランサーを指すDNSレコードが含まれます。 最初に変数を定義します。これは、このモジュールが公開する入力になります。 それらをvariables.tfというファイルに保存します。 編集用に作成します。

nano variables.tf

次の変数定義を追加します。

terraform-reusability / modules / dns-records / variables.tf

variable "domain_name" {}
variable "ipv4_address" {}

ファイルを保存して閉じます。 ここで、ドメインとそれに付随するAおよびCNAMEレコードをrecords.tfという名前のファイルで定義します。 次のコマンドを実行して、編集用に作成して開きます。

nano records.tf

次のリソース定義を追加します。

terraform-reusability / modules / dns-records / records.tf

resource "digitalocean_domain" "domain" {
  name = var.domain_name
}

resource "digitalocean_record" "domain_A" {
  domain = digitalocean_domain.domain.name
  type   = "A"
  name   = "@"
  value  = var.ipv4_address
}

resource "digitalocean_record" "domain_CNAME" {
  domain = digitalocean_domain.domain.name
  type   = "CNAME"
  name   = "www"
  value  = "@"
}

まず、DigitalOceanアカウントにドメイン名を追加します。 クラウドは、3つのDigitalOceanネームサーバーをNSレコードとして自動的に追加します。 Terraformに指定するドメイン名は、DigitalOceanアカウントにまだ存在していない必要があります。存在していないと、インフラストラクチャの作成中にTerraformにエラーが表示されます。

次に、ドメインのAレコードを定義し、変数として指定されたIPアドレスにルーティングします(@valueはサブドメインなしの真のドメイン名を意味します)。 ipv4_address。 モジュールのインスタンスを初期化すると、実際のIPアドレスが渡されます。 完全を期すために、次のCNAMEレコードは、wwwサブドメインも同じドメインを指す必要があることを指定しています。 完了したら、ファイルを保存して閉じます。

次に、このモジュールの出力を定義します。 出力には、作成されたレコードのFQDN(完全修飾ドメイン名)が表示されます。 outputs.tfを作成して開き、編集します。

nano outputs.tf

次の行を追加します。

terraform-reusability / modules / dns-records / outputs.tf

output "A_fqdn" {
  value = digitalocean_record.domain_A.fqdn
}

output "CNAME_fqdn" {
  value = digitalocean_record.domain_CNAME.fqdn
}

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

変数、DNSレコード、および出力を定義したら、最後に指定する必要があるのは、このモジュールのプロバイダー要件です。 dns-recordsモジュールがprovider.tfというファイルでdigitaloceanプロバイダーを必要とすることを指定します。 編集のために作成して開きます。

nano provider.tf

次の行を追加します。

terraform-再利用性/モジュール/dns-records/provider.tf

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

完了したら、ファイルを保存して閉じます。 digitaloceanプロバイダーが定義されたので、dns-recordsモジュールは機能的に完成しました。

さまざまな環境の作成

terraform-reusabilityプロジェクトの現在の構造は、次のようになります。

terraform-reusability/
├─ modules/
│  ├─ dns-records/
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ records.tf
│  │  ├─ variables.tf
│  ├─ droplet-lb/
│  │  ├─ droplets.tf
│  │  ├─ lb.tf
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ variables.tf
├─ provider.tf

これまでのところ、プロジェクトには2つのモジュールがあります。1つは作成したばかり(dns-records)で、もう1つは前提条件の一部として作成したもの(droplet-lb)です。

さまざまな環境を容易にするために、devおよびprod環境構成ファイルをenvironmentsというディレクトリに保存します。このディレクトリは、プロジェクトのルートにあります。 どちらの環境も同じ2つのモジュールを呼び出しますが、パラメーター値は異なります。 これの利点は、モジュールが将来内部的に変更されたときに、渡す値を更新するだけでよいことです。

まず、次のコマンドを実行して、プロジェクトのルートに移動します。

cd ../..

次に、environmentsの下にdevディレクトリとprodディレクトリを同時に作成します。

mkdir -p environments/dev && mkdir environments/prod

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

最初にその環境を構成するので、devディレクトリに移動します。

cd environments/dev

コードはmain.tfという名前のファイルに保存するので、編集用に作成します。

nano main.tf

次の行を追加します。

terraform-再利用性/環境/dev/main.tf

module "droplets" {
  source   = "../../modules/droplet-lb"

  droplet_count = 2
  group_name    = "dev"
}

module "dns" {
  source   = "../../modules/dns-records"

  domain_name   = "your_dev_domain"
  ipv4_address  = module.droplets.lb_ip
}

ここでは、droplet-lbdns-recordsの2つのモジュールを呼び出して構成します。これにより、2つのドロップレットが作成されます。 それらの前面にはロードバランサーがあり、提供されたドメインのDNSレコードは、そのロードバランサーを指すように設定されています。 your_dev_domaindev環境の目的のドメイン名に置き換えてから、ファイルを保存して閉じてください。

次に、DigitalOceanプロバイダーを構成し、前提条件の一部として作成した個人用アクセストークンを受け入れることができるように、プロバイダーの変数を作成します。 編集のために、provider.tfという名前の新しいファイルを開きます。

nano provider.tf

次の行を追加します。

terraform-再利用性/環境/dev/provider.tf

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

variable "do_token" {}

provider "digitalocean" {
  token = var.do_token
}

このコードでは、digitaloceanプロバイダーが使用可能であり、do_token変数をそのインスタンスに渡す必要があります。 ファイルを保存して閉じます。

次のコマンドを実行して構成を初期化します。

terraform init

次の出力が表示されます。

OutputInitializing modules...
- dns in ../../modules/dns-records
- droplets in ../../modules/droplet-lb

Initializing the backend...

Initializing provider plugins...
- Finding digitalocean/digitalocean versions matching "~> 2.0"...
- Installing digitalocean/digitalocean v2.10.1...
- Installed digitalocean/digitalocean v2.10.1 (signed by a HashiCorp partner, key ID F82037E524B9C0E8)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.

prod環境の構成も同様です。 次のコマンドを実行して、そのディレクトリに移動します。

cd ../prod

main.tfを作成して開き、編集します。

nano main.tf

次の行を追加します。

terraform-再利用性/環境/prod/main.tf

module "droplets" {
  source   = "../../modules/droplet-lb"

  droplet_count = 5
  group_name    = "prod"
}

module "dns" {
  source   = "../../modules/dns-records"

  domain_name   = "your_prod_domain"
  ipv4_address  = module.droplets.lb_ip
}

これとdevコードの違いは、5つのドロップレットがデプロイされることです。 さらに、prodドメイン名に置き換える必要のあるドメイン名は異なります。 完了したら、ファイルを保存して閉じます。

次に、devからプロバイダー構成をコピーします。

cp ../dev/provider.tf .

この構成も初期化します。

terraform init

このコマンドの出力は、前回実行したときと同じになります。

構成を計画して、Terraformが次のコマンドを実行して作成するリソースを確認できます。

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

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

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:

  # module.dns.digitalocean_domain.domain will be created
  + resource "digitalocean_domain" "domain" {
      + id   = (known after apply)
      + name = "your_prod_domain"
      + urn  = (known after apply)
    }

  # module.dns.digitalocean_record.domain_A will be created
  + resource "digitalocean_record" "domain_A" {
      + domain = "your_prod_domain"
      + fqdn   = (known after apply)
      + id     = (known after apply)
      + name   = "@"
      + ttl    = (known after apply)
      + type   = "A"
      + value  = (known after apply)
    }

  # module.dns.digitalocean_record.domain_CNAME will be created
  + resource "digitalocean_record" "domain_CNAME" {
      + domain = "your_prod_domain"
      + fqdn   = (known after apply)
      + id     = (known after apply)
      + name   = "www"
      + ttl    = (known after apply)
      + type   = "CNAME"
      + value  = "@"
    }

  # module.droplets.digitalocean_droplet.droplets[0] will be created
  + resource "digitalocean_droplet" "droplets" {
...
      + name                 = "prod-0"
...
    }

  # module.droplets.digitalocean_droplet.droplets[1] will be created
  + resource "digitalocean_droplet" "droplets" {
...
      + name                 = "prod-1"
...
    }

  # module.droplets.digitalocean_droplet.droplets[2] will be created
  + resource "digitalocean_droplet" "droplets" {
...
      + name                 = "prod-2"
...
    }

  # module.droplets.digitalocean_droplet.droplets[3] will be created
  + resource "digitalocean_droplet" "droplets" {
...
      + name                 = "prod-3"
...
    }

  # module.droplets.digitalocean_droplet.droplets[4] will be created
  + resource "digitalocean_droplet" "droplets" {
...
      + name                 = "prod-4"
...
    }

  # module.droplets.digitalocean_loadbalancer.www-lb will be created
  + resource "digitalocean_loadbalancer" "www-lb" {
...
      + name                     = "lb-prod"
...

Plan: 9 to add, 0 to change, 0 to destroy.
...

これにより、ロードバランサーを使用して5つのドロップレットがデプロイされます。 また、ロードバランサーを指す2つのDNSレコードで指定したprodドメインを作成します。 dev環境の構成を計画することもできます。2つのドロップレットの展開が計画されていることに注意してください。

注:次のコマンドを使用して、devおよびprod環境にこの構成を適用できます。

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

破棄するには、次のコマンドを実行し、プロンプトが表示されたらyesと入力します。

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

以下は、このプロジェクトをどのように構成したかを示しています。

terraform-reusability/
├─ environments/
│  ├─ dev/
│  │  ├─ main.tf
│  │  ├─ provider.tf
│  ├─ prod/
│  │  ├─ main.tf
│  │  ├─ provider.tf
├─ modules/
│  ├─ dns-records/
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ records.tf
│  │  ├─ variables.tf
│  ├─ droplet-lb/
│  │  ├─ droplets.tf
│  │  ├─ lb.tf
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ variables.tf
├─ provider.tf

追加されたのはenvironmentsディレクトリで、devおよびprod環境のコードを保持します。

このアプローチの利点は、モジュールへのさらなる変更がプロジェクトのすべての領域に自動的に伝播することです。 モジュール入力の可能なカスタマイズを除いて、このアプローチは反復的ではなく、展開環境全体でさえ、可能な限り再利用性を促進します。 全体として、これにより混乱が減り、バージョン管理システムを使用して変更を追跡できるようになります。

このチュートリアルの最後の2つのセクションでは、depends_onメタ引数とtemplatefile関数を確認します。

インフラストラクチャを順番に構築するための依存関係の宣言

アクションを計画している間、Terraformは既存の依存関係を自動的に識別し、それらを依存関係グラフに組み込みます。 検出できる主な依存関係は明確な参照です。 たとえば、モジュールの出力値が別のリソースのパラメーターに渡された場合です。 このシナリオでは、モジュールは最初に展開を完了して出力値を提供する必要があります。

Terraformが検出できない依存関係は隠されています。それらには、コードから推測できない副作用と相互参照があります。 この例は、オブジェクトが存在ではなく別のオブジェクトの動作に依存し、コードからその属性にアクセスしない場合です。 これを克服するには、depends_onを使用して、明示的な方法で依存関係を手動で指定できます。 Terraform 0.13以降、モジュールでdepends_onを使用して、モジュール自体をデプロイする前に、リストされたリソースを強制的に完全にデプロイすることもできます。 depends_onメタ引数をすべてのリソースタイプで使用できます。 depends_onは、指定されたリソースが依存する他のリソースのリストも受け入れます。

depends_onは、他のリソースへの参照のリストを受け入れます。 その構文は次のようになります。

resource "resource_type" "res" {
  depends_on = [...] # List of resources

  # Parameters...
}

最後の手段としてdepends_onのみを使用する必要があることに注意してください。 リソースが依存する動作がすぐにはわからない場合があるため、使用する場合は、十分に文書化しておく必要があります。

このチュートリアルの前のステップでは、depends_onを使用して明示的な依存関係を指定していません。これは、作成したリソースには、コードから推測できない副作用がないためです。 Terraformは、作成したコードから作成された参照を検出し、それに応じてリソースをデプロイするようにスケジュールします。

カスタマイズのためのテンプレートの使用

Terraformでは、テンプレート化とは、リソースに属性値を設定したり、文字列を作成したりする場合など、適切な場所で式の結果を置き換えることです。 これは、前の手順とチュートリアルの前提条件で使用して、ドロップレット名とその他のパラメーター値を動的に生成しました。

文字列に値を代入する場合、値は指定され、${}で囲まれます。 テンプレート置換は、作成されたリソースのカスタマイズを容易にするためにループでよく使用されます。 また、リソース属性の入力を置き換えることにより、モジュールのカスタマイズも可能です。

Terraformは、templatefile関数を提供します。この関数は、読み取るディスクからのファイルと、それらの値とペアになっている変数のマップの2つの引数を受け入れます。 返される値は、プロジェクトを計画または適用するときにTerraformが通常行うように、式が置換されてレンダリングされたファイルの内容です。 関数は依存関係グラフの一部ではないため、プロジェクトの別の部分からファイルを動的に生成することはできません。

droplets.tmplというテンプレートファイルの内容が次のようになっているとします。

%{ for address in addresses ~}
${address}:80
%{ endfor ~}

forおよびendfor宣言の場合と同様に、長い宣言は%{}で囲む必要があります。これは、それぞれforループの開始と終了を示します。 。 droplets変数の内容とタイプは、関数が呼び出されて実際の値が提供されるまで、次のようにわかりません。

templatefile("${path.module}/droplets.tmpl", { addresses = ["192.168.0.1", "192.168.1.1"] })

このtemplatefile呼び出しは、次の値を返します。

Output192.168.0.1:80
192.168.1.1:80

この関数にはユースケースがありますが、それらは一般的ではありません。 たとえば、構成の一部が独自の形式で存在する必要があるが、残りの値に依存し、動的に生成する必要がある場合に使用できます。 ほとんどの場合、可能であれば、すべての構成パラメーターをTerraformコードで直接指定することをお勧めします。

結論

この記事では、Terraformプロジェクトの例でコードの再利用を最大化しました。 主な方法は、頻繁に使用する機能と構成をカスタマイズ可能なモジュールとしてパッケージ化し、必要なときにいつでも使用することです。 そうすることで、基礎となるコードを複製せず(エラーが発生しやすくなる可能性があります)、変更を導入するために必要なのはモジュールの変更だけであるため、所要時間を短縮できます。

自分のモジュールに限定されません。 これまで見てきたように、 Terraform Registry は、プロジェクトに組み込むことができるサードパーティのモジュールとプロバイダーを提供します。

このチュートリアルは、Terraformシリーズでインフラストラクチャを管理する方法の一部です。 このシリーズでは、Terraformの初めてのインストールから複雑なプロジェクトの管理まで、Terraformの多くのトピックを取り上げています。