🙌

TerraformでGoogle Cloud上にJMeter分散実行環境をサクッと構築しよう!

に公開

Google Cloud上に負荷テスト実行環境を構築する

この記事では、負荷テストツールとして広く使われているApache JMeterの実行環境を、Google Cloud (旧GCP) 上にTerraformを使って自動で構築する方法を、初心者の方にも分かりやすく解説します。

Webサービスやアプリケーションを公開する前には、多くのユーザーが同時にアクセスした際にどの程度の負荷に耐えられるかを確認する「負荷テスト」が欠かせません。JMeterは、この負荷テストを実施するための代表的なオープンソースツールです。

しかし、大規模な負荷テストを行う場合、1台のマシンだけでは十分な負荷を生成できないことがあります。そこで、複数のマシンを連携させて大量の負荷を発生させる「分散実行」という手法が用いられます。JMeterでは、司令塔となる「マスター」ノードと、実際に負荷をかける「スレーブ」ノードを連携させることで、この分散実行を実現します。

このマスターとスレーブからなる環境を、手動で一台ずつ構築するのは非常に手間がかかります。そこで役立つのが、今回ご紹介するTerraformです。Terraformは「Infrastructure as Code (IaC)」を実現するツールで、インフラの構成をコードとして記述することで、誰が実行しても同じ環境を、何度でも、素早く、そして自動で構築・管理できるようになります。

この記事を読めば、Google Cloud上にJMeterの分散実行環境をTerraformで構築する一連の流れを理解し、ご自身のプロジェクトでも応用できるようになるでしょう。

構成図

構築する環境のアーキテクチャ

今回構築する環境は、以下のコンポーネントで構成されます。

  • VPCネットワーク: JMeterインスタンス専用のVPC(Virtual Private Cloud)とサブネットを作成し、セキュアなネットワーク環境を構築します。
  • JMeterマスター: 司令塔となる単一のCompute Engine (GCE) インスタンスです。テストの実行指示や結果の集約を行います。
  • JMeterスレーブ: 実際にリクエストを送信して負荷をかける複数のGCEインスタンス群です。スレーブの数は設定ファイルで自由に変更できます。
  • スタートアップスクリプト: GCEインスタンスの起動時に自動で実行されるスクリプトです。JavaやJMeterのインストール、必要な設定変更などを自動で行い、手作業でのセットアップを不要にします。

それでは、実際に構築を始めるための準備から見ていきましょう。


準備するもの (Prerequisites)

構築を始める前に、以下のツールがお手元の環境にインストールされ、設定済みであることを確認してください。

  • Google Cloud SDK (gcloud): Google Cloudをコマンドラインから操作するためのツールです。
  • Terraform: 今回の主役であるIaCツールです。
  • Google Cloudプロジェクト: 課金が有効になっているGoogle Cloudプロジェクトが必要です。また、プロジェクトでCompute Engine APIが有効になっている必要があります。
  • Google Cloudへの認証情報: TerraformがGoogle Cloudリソースを作成・変更できるよう、認証情報が設定済みである必要があります。(例: gcloud auth application-default login を実行)

コードの全体像

今回使用するTerraformのコードは、役割ごとにディレクトリやファイルが整理されています。まずは全体の構造を把握しましょう。
GitHub

.
├── LICENSE
├── README.md
└── terraform
    ├── environments
    │   └── pord       # 本番環境用の設定ファイル群
    │       ├── api.tf
    │       ├── host_server_script.tpl
    │       ├── local.tf
    │       ├── main.tf
    │       ├── output.tf
    │       ├── start_script.tpl
    │       └── terraform.tf
    └── modules          # 再利用可能な部品(モジュール)群
        ├── compute_engine # GCEインスタンスを作成するモジュール
        │   ├── main.tf
        │   └── variables.tf
        └── vpc            # VPCネットワークを作成するモジュール
            ├── main.tf
            ├── output.tf
            └── variables.tf
  • terraform/environments/pord:
    • このディレクトリが、実際にterraform applyを実行する場所です。
    • main.tfは、モジュールを呼び出して全体のインフラを定義する中心的なファイルです。
    • local.tfでは、スレーブインスタンスの数やプロジェクト名など、環境固有の変数を定義します。
    • terraform.tfでは、Terraformのバージョンや、使用するプロバイダ(今回はGoogle Cloud)の情報を設定します。
    • *.tpl ファイルは、GCEインスタンスの起動時に実行されるスクリプトのテンプレートです。
  • terraform/modules:
    • インフラの構成要素を再利用可能な「モジュール」として定義しています。
    • vpcモジュールは、VPCネットワーク、サブネット、IPアドレス、ファイアウォールルールを作成する責務を持ちます。
    • compute_engineモジュールは、GCEインスタンス(マスター、スレーブ)を作成する責務を持ちます。

このように、環境定義 (environments)部品 (modules) を分離することで、コードの見通しが良くなり、再利用性も高まります。


コード解説

それでは、主要なコードが何をしているのかを具体的に見ていきましょう。

1. ネットワークの構築 (modules/vpc/main.tf)

まず、JMeterインスタンスが動作するための土台となるネットワーク(VPC)を定義します。

# VPCネットワークの作成
resource "google_compute_network" "vpc" {
  name                    = var.vpc_name
  auto_create_subnetworks = false # サブネットは手動で定義
  routing_mode            = var.routing_mode
}

# サブネットの作成
resource "google_compute_subnetwork" "subnet" {
  for_each                = var.subnet_works
  name                    = each.value["name"]
  ip_cidr_range           = each.value["ip_cidr_range"]
  network                 = google_compute_network.vpc.id
  region                  = each.value["region"]
}

# グローバルIPアドレス(外部IP)の予約
resource "google_compute_address" "default" {
  count        = var.ip_number
  name         = "stress-test-vm-global-ip${count.index}"
  address_type = "EXTERNAL"
}

# 内部IPアドレスの予約
resource "google_compute_address" "intarnal" {
  count        = var.ip_number
  name         = "stress-test-vm-intarnal-ip${count.index}"
  address_type = "INTERNAL"
  subnetwork   = google_compute_subnetwork.subnet["subnet1"].id
  region       = "asia-northeast1"
}

# ファイアウォールルールの作成
resource "google_compute_firewall" "default" {
  name    = "stress-test-vm-allow-tcp"
  network = google_compute_network.vpc.name

  allow {
    protocol = "tcp"
    ports    = ["22", "80", "443", "26000", "26001", "25000"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["stress-test-vm"]
}

  • google_compute_network: stress-test-asia-ne1-prod という名前のVPCを作成します。
  • google_compute_subnetwork: VPC内にサブネットを作成します。for_eachを使い、複数のサブネットを定義できるようになっています。
  • google_compute_address: GCEインスタンスに割り当てるための外部IP内部IPを、必要な数だけ静的に予約します。これにより、インスタンスを再起動してもIPアドレスが変わりません。
  • google_compute_firewall: ファイアウォールルールを定義します。JMeterマスターとスレーブ間の通信(ポート 26000 など)や、SSH接続(ポート 22)を許可しています。target_tagsstress-test-vm を指定し、このタグが付与されたインスタンスにルールが適用されるようにしています。

2. GCEインスタンスの定義 (modules/compute_engine/main.tf)

次に、マスターとスレーブになるGCEインスタンスを定義するモジュールです。

resource "google_compute_instance" "instance" {
  count        = var.instance_count
  name         = var.instance_name == "slave-group" ? "${var.instance_name}-${count.index + 1}" : var.instance_name
  machine_type = var.machine_type
  zone         = var.zone
  tags         = var.tags # ファイアウォールルール適用のため

  boot_disk {
    initialize_params {
      image = var.disk_image
    }
  }

  network_interface {
    network    = var.network
    subnetwork = var.subnetwork

    # 外部IPの設定
    access_config {
      nat_ip = length(var.global_ip) > 0 ? element(var.global_ip, count.index + 1) : null
    }
  }

  # スタートアップスクリプトの設定
  metadata_startup_script = length(var.startup_script) > 0 ? element(var.startup_script, count.index) : templatefile("${path.module}/../../environments/pord/start_script.tpl", {
    rmi_port    = 26000 + count.index
    server_host = length(var.internal_ip) > 0 ? element(var.internal_ip, count.index) : ""
    pj_name     = var.pj_name
  })

  # ... 省略 ...
}

  • count = var.instance_count: このモジュールを呼び出す際にinstance_countという変数を渡すことで、同じ設定のインスタンスを複数台(スレーブの数だけ)作成できるようにしています。

  • name: インスタンス名を設定します。スレーブの場合は slave-group-1, slave-group-2 のように連番が付与されます。

  • tags: 前述のファイアウォールルールを適用するため、stress-test-vm タグを付与しています。

  • network_interface: どのVPCとサブネットに所属するか、また、予約した外部IP (nat_ip) を割り当てる設定です。

  • metadata_startup_script: このインスタンスの核となる部分です。インスタンス起動時に一度だけ実行されるスクリプトを指定します。後ほど詳しく解説しますが、ここでJMeterのインストールや設定が自動的に行われます。テンプレートファイル (.tpl) を使用し、インスタンスごとに異なる値(ポート番号など)を埋め込めるようになっています。

3. 全体の組み立てとインスタンスの作成 (environments/pord/main.tf)

environments/pord/main.tf で、これまで見てきたVPCとCompute Engineのモジュールを呼び出し、全体を組み立てます。

# VPCモジュールの呼び出し
module "vpc" {
  source       = "../../modules/vpc"
  vpc_name     = "stress-test-asia-ne1-prod"
  # ...
  ip_number = local.instance_count + 1 #マスター1台 + スレーブの台数分
}

# JMeterマスター用のCompute Engineモジュール呼び出し
module "host_compute_engine" {
  source                = "../../modules/compute_engine"
  instance_name         = "master-server"
  machine_type          = "f1-micro"
  zone                  = "asia-northeast1-b"
  tags                  = ["stress-test-vm"]
  network               = module.vpc.vpc_name
  subnetwork            = module.vpc.subnetwork_id["subnet1"]
  global_ip             = [module.vpc.ip_address[0]] # 予約したIPの1番目をマスターに
  internal_ip           = [module.vpc.internal_ip_address[0]]
  startup_script = [templatefile("./host_server_script.tpl", {
    # スレーブの内部IPリストを渡す
    remote_server_internal_ip_list = join(",", [for i in range(local.instance_count) : "${module.vpc.internal_ip_address[i]}:${26000 + i}"])
    pj_name                        = local.pj_name
  })]
}

# JMeterスレーブ用のCompute Engineモジュール呼び出し
module "compute_engine" {
  instance_count        = local.instance_count # local.tfで定義した数だけ作成
  source                = "../../modules/compute_engine"
  instance_name         = "slave-group"
  machine_type          = "f1-micro"
  zone                  = "asia-northeast1-b"
  provisioning_model    = "SPOT" # コスト削減のためスポットVMを利用
  tags                  = ["stress-test-vm"]
  network               = module.vpc.vpc_name
  subnetwork            = module.vpc.subnetwork_id["subnet1"]
  global_ip             = module.vpc.ip_address
  internal_ip           = module.vpc.internal_ip_address
  pj_name               = local.pj_name
}
  • module "vpc": VPCモジュールを呼び出します。ここで作成されたVPC名やサブネットID、IPアドレスなどの情報は、module.vpc.vpc_name のようにして他のモジュールから参照できます。
  • module "host_compute_engine": JMeterマスターを1台作成します。startup_scripthost_server_script.tpl を指定し、引数としてスレーブの内部IPアドレスのリストを渡している点が重要です。これにより、マスターはどのスレーブに接続すればよいかを認識できます。
  • module "compute_engine": JMeterスレーブを作成します。instance_countlocal.tf で定義した値(このサンプルでは3)が設定され、3台のスレーブが作成されます。また、コストを抑えるために provisioning_model = "SPOT" を指定し、安価なスポットVMを利用しています。

4. 自動セットアップの心臓部!スタートアップスクリプト

インスタンス起動時に実行されるスクリプトの中身も見てみましょう。これはマスター用の host_server_script.tpl です。

#!/bin/bash

# パッケージリストの更新
sudo apt update
# JMeterのダウンロードと展開
sudo wget [https://dlcdn.apache.org/jmeter/binaries/apache-jmeter-5.6.3.tgz](https://dlcdn.apache.org/jmeter/binaries/apache-jmeter-5.6.3.tgz)
tar -xzvf apache-jmeter-5.6.3.tgz
# Javaのインストール
sudo apt install openjdk-11-jre -y

# JMeterの設定ファイルを書き換え
# RMIのSSLを無効化
sudo echo 'server.rmi.ssl.disable=true' >> /apache-jmeter-5.6.3/bin/jmeter.properties
# マスターが接続しにいくスレーブのIPリストを設定
sudo sed -i 's/remote_hosts=127.0.0.1/remote_hosts=${remote_server_internal_ip_list}/g' /apache-jmeter-5.6.3/bin/jmeter.properties

# ... メモリ割り当ての変更など ...

このスクリプトがインスタンス起動時に自動で実行されることで、

  1. 必要なソフトウェア(Java, JMeter)がインストールされる
  2. JMeterの設定ファイル (jmeter.properties) が書き換えられ、分散実行に必要な設定(マスターがスレーブを認識するための remote_hosts など)が自動で投入される

という流れが実現され、手作業でのセットアップが一切不要になります。スレーブ側の start_script.tpl も同様に、JMeterサーバーとして起動するための設定を自動で行います。


使い方

それでは、実際にこのTerraformコードを使ってGoogle Cloud上に環境を構築してみましょう。手順は非常にシンプルです。

  1. リポジトリのクローン:
    リポジトリをクローンし、作成されたディレクトリに移動します。
git clone <リポジトリのURL>
cd terraform-google-cloud-jmeter
  1. 作業ディレクトリへの移動:
    Terraformの実行ファイルが置かれている環境ディレクトリへ移動します。
cd terraform/environments/pord
  1. 設定のカスタマイズ (任意):
    local.tf ファイルを開き、スレーブの数やプロジェクト名を変更できます。
    また、マシンタイプなどを変更したい場合は、main.tf 内のモジュール呼び出し部分を編集します。
locals {
  instance_count = 3  // スレーブの数を変更
  pj_name        = "my-google-cloud-project" // あなたのGoogle CloudプロジェクトID
}
  1. Terraformの初期化:
    Terraformがプロバイダのプラグインなどをダウンロードします。
terraform init
  1. Terraformの適用:
    インフラの構築を実行します。どのようなリソースが作成されるかのプレビュー(plan)が表示されるので、内容を確認して yes と入力します。
    applyが完了すると、output.tf で定義されたマスターやスレーブのIPアドレスが表示されます。このIPアドレスを使ってインスタンスにSSH接続し、JMeterのテストを実行できます。
terraform apply
  1. インフラの破棄:
    負荷テストが完了したら、コマンド一つで、今回作成したすべてのリソース(GCEインスタンス、VPC、IPアドレスなど)を安全に削除できます。これにより、不要なコストがかかり続けるのを防げます。
terraform destroy

まとめ

いかがでしたでしょうか。この記事では、Terraformを使ってGoogle Cloud上にJMeterの分散実行環境を自動で構築する方法を解説しました。

手動での作業は時間がかかり、ミスも発生しがちですが、Terraform (IaC) を活用することで、「コード」という形でインフラを管理でき、以下のような多くのメリットが得られます。

  • 自動化による効率化: コマンド一つで、複雑な環境を数分で構築できます。
  • 再現性の担保: コードに基づいているため、誰が実行しても、何度実行しても、全く同じ環境を再現できます。
  • 構成管理の容易さ: 構成の変更(例: スレーブを3台から10台に増やす)も、コードの数値を変更して apply するだけで完了します。変更履歴はGitで管理できます。
  • コスト管理: 不要なリソースを確実に削除できるため、無駄なコストの発生を防ぎます。

今回紹介したコードは、あくまで基本的なサンプルですが、これをベースにご自身の要件に合わせてマシンタイプを変更したり、ディスクサイズを調整したりと、自由にカスタマイズが可能です。

ぜひこのTerraformプロジェクトを、あなたのサービスの品質向上のための負荷テスト基盤として活用してみてください!

Discussion