メインコンテンツにスキップ

モジュール構成

ルートモジュールが1つだけの単純なOpenTofu構成では、フラットなリソースセットを作成し、OpenTofuの式構文を使用して、これらのリソース間の関係を記述します。

コードブロック
resource "aws_vpc" "example" {
cidr_block = "10.1.0.0/16"
}

resource "aws_subnet" "example" {
vpc_id = aws_vpc.example.id

availability_zone = "us-west-2b"
cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 4, 1)
}

moduleブロックを導入すると、構成はフラットではなく階層的になります。各モジュールには独自のリソースセットと、場合によっては独自の子モジュールが含まれており、潜在的にリソース構成の深くて複雑なツリーを作成できます。

ただし、ほとんどの場合、モジュールツリーはフラットに保ち、子モジュールのレベルは1つだけにすることを強くお勧めします。そして、上記の式を使用してモジュール間の関係を記述するのと同様の手法を使用します。

コードブロック
module "network" {
source = "./modules/aws-network"

base_cidr_block = "10.0.0.0/8"
}

module "consul_cluster" {
source = "./modules/aws-consul-cluster"

vpc_id = module.network.vpc_id
subnet_ids = module.network.subnet_ids
}

このフラットスタイルのモジュール使用法を*モジュール構成*と呼びます。これは、複数の構成可能なビルディングブロックモジュールを取得し、それらを組み立ててより大きなシステムを作成するためです。モジュールが依存関係を*埋め込み*、独自のコピーを作成および管理する代わりに、モジュールはルートモジュールから依存関係を*受信*します。そのため、ルートモジュールは同じモジュールを異なる方法で接続して異なる結果を生成できます。

このページの残りの部分では、OpenTofuを使用して大規模なシステムを記述する際に役立つ、より具体的な構成パターンについて説明します。

依存性逆転

上記の例では、AWS VPCネットワークで実行されているHashiCorp Consulサーバーのクラスターを記述すると思われる`consul_cluster`モジュールを見ました。そのため、引数としてVPC自体と、そのVPC内のサブネットの両方の識別子が必要です。

代替設計として、`consul_cluster`モジュールに*独自の*ネットワークリソースを記述させる方法がありますが、そのようにすると、Consulクラスターが同じネットワーク内の他のインフラストラクチャと共存することが難しくなります。そのため、可能な場合は、モジュールを比較的小さく保ち、依存関係を渡すことをお勧めします。

この依存性逆転アプローチは、将来のリファクタリングの柔軟性も向上させます。 `consul_cluster`モジュールは、呼び出し元のモジュールがこれらの識別子をどのように取得するかを知らない、または気にしないためです。将来のリファクタリングでは、ネットワークの作成を独自の構成に分離できる可能性があり、そのため、データソースからモジュールにこれらの値を渡す場合があります。

コードブロック
data "aws_vpc" "main" {
tags = {
Environment = "production"
}
}

data "aws_subnet_ids" "main" {
vpc_id = data.aws_vpc.main.id
}

module "consul_cluster" {
source = "./modules/aws-consul-cluster"

vpc_id = data.aws_vpc.main.id
subnet_ids = data.aws_subnet_ids.main.ids
}

オブジェクトの条件付き作成

同じモジュールが複数の環境で使用される状況では、必要なオブジェクトが一部の環境に既に存在するが、他の環境では作成する必要があることがよくあります。

たとえば、これは開発環境のシナリオで発生する可能性があります。コストの理由から、特定のインフラストラクチャが複数の開発環境で共有される場合がありますが、本番環境ではインフラストラクチャは一意であり、本番構成によって直接管理されます。

モジュール自体に何かが存在するかどうかを検出し、存在しない場合は作成するよう記述するのではなく、依存関係の逆転アプローチを適用することをお勧めします。つまり、入力変数を介して、モジュールが必要とするオブジェクトを引数として受け入れます。

たとえば、OpenTofuモジュールがディスクイメージに基づいてコンピューティングインスタンスをデプロイし、一部の環境では特殊なディスクイメージが利用可能で、他の環境では共通のベースディスクイメージを共有しているとします。モジュール自体にこれらの両方のシナリオを処理させるのではなく、ディスクイメージを表すオブジェクトの入力変数を宣言できます。 AWS EC2を例として、`aws_ami`リソースタイプとデータソーススキーマの共通のサブタイプを宣言できます。

コードブロック
variable "ami" {
type = object({
# Declare an object using only the subset of attributes the module
# needs. OpenTofu will allow any object that has at least these
# attributes.
id = string
architecture = string
})
}

このモジュールの呼び出し元は、これがインラインで作成されるAMIなのか、他の場所から取得されるAMIなのかを直接表現できるようになりました。

コードブロック
# In situations where the AMI will be directly managed:

resource "aws_ami_copy" "example" {
name = "local-copy-of-ami"
source_ami_id = "ami-abc123"
source_ami_region = "eu-west-1"
}

module "example" {
source = "./modules/example"

ami = aws_ami_copy.example
}
コードブロック
# Or, in situations where the AMI already exists:

data "aws_ami" "example" {
owner = "9999933333"

tags = {
application = "example-app"
environment = "dev"
}
}

module "example" {
source = "./modules/example"

ami = data.aws_ami.example
}

これはOpenTofuの宣言型スタイルと一致しています。複雑な条件分岐を持つモジュールを作成するのではなく、既に存在する必要があるものと、OpenTofu自体に管理させたいものを直接記述します。

このパターンに従うことで、AMIが既に存在すると予想される状況とそうでない状況を明示的に示すことができます。設定を後から読む人は、リモートシステムの状態を最初に検査することなく、その意図を直接理解できます。

上記の例では、作成または読み取るオブジェクトは単一のリソースとしてインラインで指定できるほど単純ですが、依存関係自体が抽象化の恩恵を受けるほど複雑な状況では、このページの他の場所で説明されているように、複数のモジュールを組み合わせることもできます。

前提条件と保証

すべてのモジュールには、想定されるデータとコンシューマーに提供されるデータを定義する暗黙の前提条件と保証があります。

  • 前提条件:特定のリソースの設定を使用可能にするために真でなければならない条件。たとえば、aws_instance設定では、指定されたAMIが常にx86_64 CPUアーキテクチャ用に構成されているという前提条件を設定できます。
  • 保証:設定の残りの部分が依存できるオブジェクトの特性または動作。たとえば、aws_instance設定では、EC2インスタンスがプライベートDNSレコードを割り当てるネットワークで実行されているという保証を設定できます。

前提条件と保証を捕捉してテストするために、カスタム条件を使用することをお勧めします。これは、将来の保守担当者が設定の設計と意図を理解するのに役立ちます。また、カスタム条件は、エラーに関する有用な情報を早期にコンテキスト内で返し、コンシューマーが設定の問題をより簡単に診断するのに役立ちます。

次の例では、EC2インスタンスに暗号化されたルートボリュームがあるかどうかを確認する前提条件を作成します。

コードブロック
output "api_base_url" {
value = "https://${aws_instance.example.private_dns}:8433/"

# The EC2 instance must have an encrypted root volume.
precondition {
condition = data.aws_ebs_volume.example.encrypted
error_message = "The server's root volume is not encrypted."
}
}

マルチクラウド抽象化

OpenTofu自体は、異なるベンダーが提供する同様のサービスを抽象化しようとは意図的にしていません。これは、各オファリングの全機能を公開したいと考えているためであり、単一のインターフェースの背後で複数のオファリングを統合すると、「最小公分母」アプローチが必要になる傾向があるためです。

ただし、モジュールの構成を通じて、どのプラットフォーム機能が重要かについて独自のトレードオフを行うことで、独自の軽量マルチクラウド抽象化を作成できます。

このような抽象化の機会は、複数のベンダーが同じ概念、プロトコル、またはオープンスタンダードを実装しているあらゆる状況で発生します。たとえば、ドメインネームシステムの基本機能はすべてのベンダーで共通であり、一部のベンダーはジオロケーションやスマートロードバランシングなどの独自の機能で差別化していますが、ユースケースでは、複数のベンダーにわたる共通のDNS概念を抽象化するモジュールを作成する代わりに、これらの機能を放棄しても構わないと結論付ける場合があります。

コードブロック
module "webserver" {
source = "./modules/webserver"
}

locals {
fixed_recordsets = [
{
name = "www"
type = "CNAME"
ttl = 3600
records = [
"webserver01",
"webserver02",
"webserver03",
]
},
]
server_recordsets = [
for i, addr in module.webserver.public_ip_addrs : {
name = format("webserver%02d", i)
type = "A"
records = [addr]
}
]
}

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

route53_zone_id = var.route53_zone_id
recordsets = concat(local.fixed_recordsets, local.server_recordsets)
}

上記の例では、「レコードセット」オブジェクトの形式で軽量の抽象化を作成しました。これには、どのDNSプロバイダーにもマッピングできるDNSレコードセットの一般的な概念を記述する属性が含まれています。

次に、その抽象化の具体的な*実装*をモジュールとしてインスタンス化します。この場合は、レコードセットをAmazon Route53にデプロイします。

後で別のDNSプロバイダーに切り替える場合は、dns_recordsモジュールをそのプロバイダーをターゲットとする新しい実装に置き換えるだけで済みます。*レコードセット定義を*生成*するすべての設定は変更されないままで済みます。

関連する概念を表すOpenTofuオブジェクトタイプを定義し、これらのオブジェクトタイプをモジュールの入力変数に使用するこで、このような軽量の抽象化を作成できます。この場合、すべての「DNSレコード」実装で次の変数が宣言されます。

コードブロック
variable "recordsets" {
type = list(object({
name = string
type = string
ttl = number
records = list(string)
}))
}

DNSは単純な例ですが、ベンダー間で共通の要素を活用する機会は他にもたくさんあります。より複雑な例はKubernetesです。現在、多くの異なるベンダーがホストされたKubernetesクラスターを提供しており、Kubernetesを自分で実行する方法もさらに増えています。

これらのすべての実装に共通する機能がニーズを満たしている場合、特定のKubernetesクラスター実装を記述し、すべてクラスターのホスト名を出力値としてエクスポートするという共通の特性を持つ、異なるモジュールを実装することを選択できます。

コードブロック
output "hostname" {
value = azurerm_kubernetes_cluster.main.fqdn
}

次に、入力としてKubernetesクラスターのホスト名のみを想定する*他の*モジュールを作成し、それらをKubernetesクラスターモジュールと交換して使用できます。

コードブロック
module "k8s_cluster" {
source = "modules/azurerm-k8s-cluster"

# (Azure-specific configuration arguments)
}

module "monitoring_tools" {
source = "modules/monitoring_tools"

cluster_hostname = module.k8s_cluster.hostname
}

データのみのモジュール

ほとんどのモジュールにはresourceブロックが含まれており、作成および管理されるインフラストラクチャが記述されています。新しいインフラストラクチャをまったく記述せずに、データソースを使用して他の場所で作成された既存のインフラストラクチャに関する情報を取得するだけのモジュールを作成すると便利な場合があります。

従来のモジュールと同様に、この手法は、モジュールが何らかの方法で抽象化レベルを上げる場合、この場合はデータの取得方法を正確にカプセル化する場合にのみ使用することをお勧めします。

この手法の一般的な用途は、システムが複数のサブシステム構成に分解されているが、共通のIPネットワークなど、すべてのサブシステムで共有される特定のインフラストラクチャがある場合です。この場合、AWSにデプロイされたときに共有ネットワークに関する情報が必要な構成で呼び出すことができるjoin-network-awsと呼ばれる共有モジュールを作成できます。

コードブロック
module "network" {
source = "./modules/join-network-aws"

environment = "production"
}

module "k8s_cluster" {
source = "./modules/aws-k8s-cluster"

subnet_ids = module.network.aws_subnet_ids
}

networkモジュール自体は、このデータをさまざまな方法で取得できます。aws_vpcおよびaws_subnet_idsデータソースを使用してAWS APIを直接クエリすることも、consul_keysを使用してConsulクラスターから保存された情報を読み取ることも、terraform_remote_stateを使用してネットワークを管理する構成の状態から出力を直接読み取ることもできます。

このアプローチの主な利点は、この情報のソースを、それに依存するすべての構成を更新することなく、時間の経過とともに変更できることです。さらに、対応する管理モジュールと同様の出力セットを使用してデータのみのモジュールを設計すると、リファクタリング時に2つのモジュールを比較的簡単に交換できます。