Spot VM を作成して使用する


このページでは、Spot VM の作成方法と管理方法について説明します。内容は次のとおりです。

  • Spot VM を作成、起動、識別する方法
  • Spot VM のプリエンプションを検出、処理、テストする方法
  • Spot VM のベスト プラクティス

Spot VM は、スポット プロビジョニング モデルを備えた仮想マシン(VM)インスタンスです。Spot VM は、標準 VM の料金と比較して最大 60~91% 割引でご利用いただけます。ただし、Compute Engine はいつでも Spot VM をプリエンプトすることでリソースを再利用する可能性があります。Spot VM は、VM プリエンプションに耐えることができるフォールト トレラント アプリケーションでのみ推奨されています。Spot VM を作成する前に、ご使用のアプリケーションでプリエンプションの処理が可能であることを確認してください。

始める前に

  • Spot VM のコンセプト ドキュメントを確認します。
  • まだ設定していない場合は、認証を設定します。認証とは、Google Cloud サービスと API にアクセスするために ID を確認するプロセスです。ローカル開発環境からコードまたはサンプルを実行するには、次のように Compute Engine に対する認証を行います。

    このページのサンプルをどのように使うかに応じて、タブを選択してください。

    コンソール

    Google Cloud コンソールを使用して Google Cloud サービスと API にアクセスする場合、認証を設定する必要はありません。

    gcloud

    1. Install the Google Cloud CLI, then initialize it by running the following command:

      gcloud init
    2. デフォルトのリージョンとゾーンを設定します

    Terraform

    このページの Terraform サンプルをローカル開発環境から使用するには、gcloud CLI をインストールして初期化し、自身のユーザー認証情報を使用してアプリケーションのデフォルト認証情報を設定してください。

    1. Install the Google Cloud CLI.
    2. To initialize the gcloud CLI, run the following command:

      gcloud init
    3. If you're using a local shell, then create local authentication credentials for your user account:

      gcloud auth application-default login

      You don't need to do this if you're using Cloud Shell.

    詳細については、 ローカル開発環境の認証の設定 をご覧ください。

    REST

    このページの REST API サンプルをローカル開発環境で使用するには、gcloud CLI に指定した認証情報を使用します。

      Install the Google Cloud CLI, then initialize it by running the following command:

      gcloud init

Spot VM を作成する

Spot VM を作成するには、Google Cloud コンソール、gcloud CLI、または Compute Engine API を使用します。Spot VM は、スポット プロビジョニング モデルを使用するように構成された VM です。

  • Google Cloud コンソールで、[VM プロビジョニング モデル] を [Spot] に設定します。
  • gcloud CLI 内の --provisioning-model=SPOT
  • Compute Engine API 内の "provisioningModel": "SPOT"

コンソール

  1. Google Cloud コンソールで、[インスタンスの作成] ページに移動します。

    [インスタンスの作成] に移動

  2. 次に、以下の操作を行います。

    1. [可用性ポリシー] セクションで、[VM プロビジョニング モデル] リストから [スポット] を選択します。これを選択すると、VM の自動再起動とホスト メンテナンス オプションが無効になり、終了アクション オプションが有効になります。
    2. 省略可: [VM の終了時] リストで、Compute Engine が VM をプリエンプトしたときの動作を選択します。
      • プリエンプション中に VM を停止するには、[停止] を選択します(デフォルト)。
      • VM を削除するには、[削除] をクリックします。
  3. 省略可: その他の VM オプションを指定します。詳しくは、VM インスタンスの作成と起動をご覧ください。

  4. VM を作成して起動するには、[作成] をクリックします。

gcloud

gcloud CLI で VM を作成するには、gcloud compute instances create コマンドを使用します。Spot VM を作成するには、--provisioning-model=SPOT フラグを使用する必要があります。必要に応じて、--instance-termination-action フラグも指定して Spot VM の終了アクションを指定することもできます。

gcloud compute instances create VM_NAME \
    --provisioning-model=SPOT \
    --instance-termination-action=TERMINATION_ACTION

次のように置き換えます。

  • VM_NAME: 新しい VM の名前
  • TERMINATION_ACTION: 省略可。Compute Engine が VM をプリエンプトするときに実行するアクション。STOP(デフォルト)または DELETE を指定します。

VM の作成時に指定できるオプションの詳細については、VM インスタンスの作成と起動をご覧ください。たとえば、指定したマシンタイプとイメージを使用して Spot VM を作成するには、次のコマンドを使用します。

gcloud compute instances create VM_NAME \
    --provisioning-model=SPOT \
    [--image=IMAGE | --image-family=IMAGE_FAMILY] \
    --image-project=IMAGE_PROJECT \
    --machine-type=MACHINE_TYPE \
    --instance-termination-action=TERMINATION_ACTION

次のように置き換えます。

  • VM_NAME: 新しい VM の名前
  • IMAGE: 次のいずれかを指定します。
    • IMAGE: 公開イメージの特定のバージョンまたはイメージ ファミリー。たとえば、特定のイメージは --image=debian-10-buster-v20200309 です。
    • イメージ ファミリー。 これにより、非推奨ではない最新の OS イメージから VM が作成されます。たとえば、--image-family=debian-10 を指定すると、Compute Engine は Debian 10 イメージ ファミリーの OS イメージの最新バージョンから VM を作成します。
  • IMAGE_PROJECT: イメージを含むプロジェクト。たとえば、イメージ ファミリーとして debian-10 を指定する場合は、イメージ プロジェクトとして debian-cloud を指定します。
  • MACHINE_TYPE: 新しい VM の事前定義またはカスタム マシンタイプ。
  • TERMINATION_ACTION: 省略可。Compute Engine が VM をプリエンプトするときに実行するアクション。STOP(デフォルト)または DELETE を指定します。

    ゾーンで使用できるマシンタイプのリストを取得するには、--zones フラグを指定して gcloud compute machine-types list コマンドを使用します。

Terraform

Terraform リソースで、scheduling ブロックを使用して、Spot インスタンスを作成できます。


resource "google_compute_instance" "spot_vm_instance" {
  name         = "spot-instance-name"
  machine_type = "f1-micro"
  zone         = "us-central1-c"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  scheduling {
    preemptible                 = true
    automatic_restart           = false
    provisioning_model          = "SPOT"
    instance_termination_action = "STOP"
  }

  network_interface {
    # A default network is created for all GCP projects
    network = "default"
    access_config {
    }
  }
}

REST

Compute Engine API で VM を作成するには、instances.insert メソッドを使用します。この場合は、VM のマシンタイプと名前を指定する必要があります。必要に応じて、ブートディスクのイメージを指定することもできます。

Spot VM を作成するには、"provisioningModel": spot フィールドを含める必要があります。必要に応じて、"instanceTerminationAction" フィールドを追加して、Spot VM の終了アクションを指定することもできます。

POST https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances
{
 "machineType": "zones/ZONE/machineTypes/MACHINE_TYPE",
 "name": "VM_NAME",
 "disks": [
   {
     "initializeParams": {
       "sourceImage": "projects/IMAGE_PROJECT/global/images/IMAGE"
     },
     "boot": true
   }
 ]
 "scheduling":
 {
     "provisioningModel": "SPOT",
     "instanceTerminationAction": "TERMINATION_ACTION"
 },
 ...
}

次のように置き換えます。

  • PROJECT_ID: VM を作成するプロジェクトの ID
  • ZONE: VM を作成するゾーン。ゾーンは、新しい VM に使用するマシンタイプもサポートしている必要があります。
  • MACHINE_TYPE: 新しい VM の事前定義またはカスタム マシンタイプ。
  • VM_NAME: 新しい VM の名前
  • IMAGE_PROJECT: イメージを含むプロジェクト。たとえば、イメージ ファミリーとして family/debian-10 を指定する場合は、イメージ プロジェクトとして debian-cloud を指定します。
  • IMAGE: 次のいずれかを指定します。
    • 公開イメージの特定のバージョン。たとえば、特定のイメージは、"sourceImage": "projects/debian-cloud/global/images/debian-10-buster-v20200309" です(ここで、debian-cloudIMAGE_PROJECT)。
    • イメージ ファミリー。 これにより、非推奨ではない最新の OS イメージから VM が作成されます。たとえば、"sourceImage": "projects/debian-cloud/global/images/family/debian-10" を指定すると(debian-cloudIMAGE_PROJECT の場合)、Compute Engine は Debian 10 イメージ ファミリーの OS イメージの最新バージョンから VM を作成します。
  • TERMINATION_ACTION: 省略可。Compute Engine が VM をプリエンプトするときに実行するアクション。STOP(デフォルト)または DELETE を指定します。

VM の作成時に指定できるオプションの詳細については、VM インスタンスの作成と起動をご覧ください。

Go


import (
	"context"
	"fmt"
	"io"

	compute "cloud.google.com/go/compute/apiv1"
	"cloud.google.com/go/compute/apiv1/computepb"
	"google.golang.org/protobuf/proto"
)

// createSpotInstance creates a new Spot VM instance with Debian 10 operating system.
func createSpotInstance(w io.Writer, projectID, zone, instanceName string) error {
	// projectID := "your_project_id"
	// zone := "europe-central2-b"
	// instanceName := "your_instance_name"

	ctx := context.Background()
	imagesClient, err := compute.NewImagesRESTClient(ctx)
	if err != nil {
		return fmt.Errorf("NewImagesRESTClient: %w", err)
	}
	defer imagesClient.Close()

	instancesClient, err := compute.NewInstancesRESTClient(ctx)
	if err != nil {
		return fmt.Errorf("NewInstancesRESTClient: %w", err)
	}
	defer instancesClient.Close()

	req := &computepb.GetFromFamilyImageRequest{
		Project: "debian-cloud",
		Family:  "debian-11",
	}

	image, err := imagesClient.GetFromFamily(ctx, req)
	if err != nil {
		return fmt.Errorf("getImageFromFamily: %w", err)
	}

	diskType := fmt.Sprintf("zones/%s/diskTypes/pd-standard", zone)
	disks := []*computepb.AttachedDisk{
		{
			AutoDelete: proto.Bool(true),
			Boot:       proto.Bool(true),
			InitializeParams: &computepb.AttachedDiskInitializeParams{
				DiskSizeGb:  proto.Int64(10),
				DiskType:    proto.String(diskType),
				SourceImage: proto.String(image.GetSelfLink()),
			},
			Type: proto.String(computepb.AttachedDisk_PERSISTENT.String()),
		},
	}

	req2 := &computepb.InsertInstanceRequest{
		Project: projectID,
		Zone:    zone,
		InstanceResource: &computepb.Instance{
			Name:        proto.String(instanceName),
			Disks:       disks,
			MachineType: proto.String(fmt.Sprintf("zones/%s/machineTypes/%s", zone, "n1-standard-1")),
			NetworkInterfaces: []*computepb.NetworkInterface{
				{
					Name: proto.String("global/networks/default"),
				},
			},
			Scheduling: &computepb.Scheduling{
				ProvisioningModel: proto.String(computepb.Scheduling_SPOT.String()),
			},
		},
	}
	op, err := instancesClient.Insert(ctx, req2)
	if err != nil {
		return fmt.Errorf("insert: %w", err)
	}

	if err = op.Wait(ctx); err != nil {
		return fmt.Errorf("unable to wait for the operation: %w", err)
	}

	instance, err := instancesClient.Get(ctx, &computepb.GetInstanceRequest{
		Project:  projectID,
		Zone:     zone,
		Instance: instanceName,
	})

	if err != nil {
		return fmt.Errorf("createInstance: %w", err)
	}

	fmt.Fprintf(w, "Instance created: %v\n", instance)
	return nil
}

Java


import com.google.cloud.compute.v1.AccessConfig;
import com.google.cloud.compute.v1.AccessConfig.Type;
import com.google.cloud.compute.v1.Address.NetworkTier;
import com.google.cloud.compute.v1.AttachedDisk;
import com.google.cloud.compute.v1.AttachedDiskInitializeParams;
import com.google.cloud.compute.v1.ImagesClient;
import com.google.cloud.compute.v1.InsertInstanceRequest;
import com.google.cloud.compute.v1.Instance;
import com.google.cloud.compute.v1.InstancesClient;
import com.google.cloud.compute.v1.NetworkInterface;
import com.google.cloud.compute.v1.Scheduling;
import com.google.cloud.compute.v1.Scheduling.ProvisioningModel;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CreateSpotVm {
  public static void main(String[] args)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // TODO(developer): Replace these variables before running the sample.
    // Project ID or project number of the Google Cloud project you want to use.
    String projectId = "your-project-id";
    // Name of the virtual machine to check.
    String instanceName = "your-instance-name";
    // Name of the zone you want to use. For example: "us-west3-b"
    String zone = "your-zone";

    createSpotInstance(projectId, instanceName, zone);
  }

  // Create a new Spot VM instance with Debian 11 operating system.
  public static Instance createSpotInstance(String projectId, String instanceName, String zone)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    String image;
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (ImagesClient imagesClient = ImagesClient.create()) {
      image = imagesClient.getFromFamily("debian-cloud", "debian-11").getSelfLink();
    }
    AttachedDisk attachedDisk = buildAttachedDisk(image, zone);
    String machineTypes = String.format("zones/%s/machineTypes/%s", zone, "n1-standard-1");

    // Send an instance creation request to the Compute Engine API and wait for it to complete.
    Instance instance =
            createInstance(projectId, zone, instanceName, attachedDisk, true, machineTypes, false);

    System.out.printf("Spot instance '%s' has been created successfully", instance.getName());

    return instance;
  }

  // disks: a list of compute_v1.AttachedDisk objects describing the disks
  //     you want to attach to your new instance.
  // machine_type: machine type of the VM being created. This value uses the
  //     following format: "zones/{zone}/machineTypes/{type_name}".
  //     For example: "zones/europe-west3-c/machineTypes/f1-micro"
  // external_access: boolean flag indicating if the instance should have an external IPv4
  //     address assigned.
  // spot: boolean value indicating if the new instance should be a Spot VM or not.
  private static Instance createInstance(String projectId, String zone, String instanceName,
                                         AttachedDisk disk, boolean isSpot, String machineType,
                                         boolean externalAccess)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (InstancesClient client = InstancesClient.create()) {
      Instance instanceResource =
              buildInstanceResource(instanceName, disk, machineType, externalAccess, isSpot);

      InsertInstanceRequest build = InsertInstanceRequest.newBuilder()
              .setProject(projectId)
              .setRequestId(UUID.randomUUID().toString())
              .setZone(zone)
              .setInstanceResource(instanceResource)
              .build();
      client.insertCallable().futureCall(build).get(60, TimeUnit.SECONDS);

      return client.get(projectId, zone, instanceName);
    }
  }

  private static Instance buildInstanceResource(String instanceName, AttachedDisk disk,
                                                String machineType, boolean externalAccess,
                                                boolean isSpot) {
    NetworkInterface networkInterface =
            networkInterface(externalAccess);
    Instance.Builder builder = Instance.newBuilder()
            .setName(instanceName)
            .addDisks(disk)
            .setMachineType(machineType)
            .addNetworkInterfaces(networkInterface);

    if (isSpot) {
      // Set the Spot VM setting
      Scheduling.Builder scheduling = builder.getScheduling()
              .toBuilder()
              .setProvisioningModel(ProvisioningModel.SPOT.name())
              .setInstanceTerminationAction("STOP");
      builder.setScheduling(scheduling);
    }

    return builder.build();
  }

  private static NetworkInterface networkInterface(boolean externalAccess) {
    NetworkInterface.Builder build = NetworkInterface.newBuilder()
            .setNetwork("global/networks/default");

    if (externalAccess) {
      AccessConfig.Builder accessConfig = AccessConfig.newBuilder()
              .setType(Type.ONE_TO_ONE_NAT.name())
              .setName("External NAT")
              .setNetworkTier(NetworkTier.PREMIUM.name());
      build.addAccessConfigs(accessConfig.build());
    }

    return build.build();
  }

  private static AttachedDisk buildAttachedDisk(String sourceImage, String zone) {
    AttachedDiskInitializeParams initializeParams = AttachedDiskInitializeParams.newBuilder()
            .setSourceImage(sourceImage)
            .setDiskSizeGb(10)
            .setDiskType(String.format("zones/%s/diskTypes/pd-standard", zone))
            .build();
    return AttachedDisk.newBuilder()
            .setInitializeParams(initializeParams)
            // Remember to set auto_delete to True if you want the disk to be deleted
            // when you delete your VM instance.
            .setAutoDelete(true)
            .setBoot(true)
            .build();
  }
}

Python

from __future__ import annotations

import re
import sys
from typing import Any
import warnings

from google.api_core.extended_operation import ExtendedOperation
from google.cloud import compute_v1


def get_image_from_family(project: str, family: str) -> compute_v1.Image:
    """
    Retrieve the newest image that is part of a given family in a project.

    Args:
        project: project ID or project number of the Cloud project you want to get image from.
        family: name of the image family you want to get image from.

    Returns:
        An Image object.
    """
    image_client = compute_v1.ImagesClient()
    # List of public operating system (OS) images: https://cloud.google.com/compute/docs/images/os-details
    newest_image = image_client.get_from_family(project=project, family=family)
    return newest_image


def disk_from_image(
    disk_type: str,
    disk_size_gb: int,
    boot: bool,
    source_image: str,
    auto_delete: bool = True,
) -> compute_v1.AttachedDisk:
    """
    Create an AttachedDisk object to be used in VM instance creation. Uses an image as the
    source for the new disk.

    Args:
         disk_type: the type of disk you want to create. This value uses the following format:
            "zones/{zone}/diskTypes/(pd-standard|pd-ssd|pd-balanced|pd-extreme)".
            For example: "zones/us-west3-b/diskTypes/pd-ssd"
        disk_size_gb: size of the new disk in gigabytes
        boot: boolean flag indicating whether this disk should be used as a boot disk of an instance
        source_image: source image to use when creating this disk. You must have read access to this disk. This can be one
            of the publicly available images or an image from one of your projects.
            This value uses the following format: "projects/{project_name}/global/images/{image_name}"
        auto_delete: boolean flag indicating whether this disk should be deleted with the VM that uses it

    Returns:
        AttachedDisk object configured to be created using the specified image.
    """
    boot_disk = compute_v1.AttachedDisk()
    initialize_params = compute_v1.AttachedDiskInitializeParams()
    initialize_params.source_image = source_image
    initialize_params.disk_size_gb = disk_size_gb
    initialize_params.disk_type = disk_type
    boot_disk.initialize_params = initialize_params
    # Remember to set auto_delete to True if you want the disk to be deleted when you delete
    # your VM instance.
    boot_disk.auto_delete = auto_delete
    boot_disk.boot = boot
    return boot_disk


def wait_for_extended_operation(
    operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300
) -> Any:
    """
    Waits for the extended (long-running) operation to complete.

    If the operation is successful, it will return its result.
    If the operation ends with an error, an exception will be raised.
    If there were any warnings during the execution of the operation
    they will be printed to sys.stderr.

    Args:
        operation: a long-running operation you want to wait on.
        verbose_name: (optional) a more verbose name of the operation,
            used only during error and warning reporting.
        timeout: how long (in seconds) to wait for operation to finish.
            If None, wait indefinitely.

    Returns:
        Whatever the operation.result() returns.

    Raises:
        This method will raise the exception received from `operation.exception()`
        or RuntimeError if there is no exception set, but there is an `error_code`
        set for the `operation`.

        In case of an operation taking longer than `timeout` seconds to complete,
        a `concurrent.futures.TimeoutError` will be raised.
    """
    result = operation.result(timeout=timeout)

    if operation.error_code:
        print(
            f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}",
            file=sys.stderr,
            flush=True,
        )
        print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True)
        raise operation.exception() or RuntimeError(operation.error_message)

    if operation.warnings:
        print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True)
        for warning in operation.warnings:
            print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True)

    return result


def create_instance(
    project_id: str,
    zone: str,
    instance_name: str,
    disks: list[compute_v1.AttachedDisk],
    machine_type: str = "n1-standard-1",
    network_link: str = "global/networks/default",
    subnetwork_link: str = None,
    internal_ip: str = None,
    external_access: bool = False,
    external_ipv4: str = None,
    accelerators: list[compute_v1.AcceleratorConfig] = None,
    preemptible: bool = False,
    spot: bool = False,
    instance_termination_action: str = "STOP",
    custom_hostname: str = None,
    delete_protection: bool = False,
) -> compute_v1.Instance:
    """
    Send an instance creation request to the Compute Engine API and wait for it to complete.

    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone to create the instance in. For example: "us-west3-b"
        instance_name: name of the new virtual machine (VM) instance.
        disks: a list of compute_v1.AttachedDisk objects describing the disks
            you want to attach to your new instance.
        machine_type: machine type of the VM being created. This value uses the
            following format: "zones/{zone}/machineTypes/{type_name}".
            For example: "zones/europe-west3-c/machineTypes/f1-micro"
        network_link: name of the network you want the new instance to use.
            For example: "global/networks/default" represents the network
            named "default", which is created automatically for each project.
        subnetwork_link: name of the subnetwork you want the new instance to use.
            This value uses the following format:
            "regions/{region}/subnetworks/{subnetwork_name}"
        internal_ip: internal IP address you want to assign to the new instance.
            By default, a free address from the pool of available internal IP addresses of
            used subnet will be used.
        external_access: boolean flag indicating if the instance should have an external IPv4
            address assigned.
        external_ipv4: external IPv4 address to be assigned to this instance. If you specify
            an external IP address, it must live in the same region as the zone of the instance.
            This setting requires `external_access` to be set to True to work.
        accelerators: a list of AcceleratorConfig objects describing the accelerators that will
            be attached to the new instance.
        preemptible: boolean value indicating if the new instance should be preemptible
            or not. Preemptible VMs have been deprecated and you should now use Spot VMs.
        spot: boolean value indicating if the new instance should be a Spot VM or not.
        instance_termination_action: What action should be taken once a Spot VM is terminated.
            Possible values: "STOP", "DELETE"
        custom_hostname: Custom hostname of the new VM instance.
            Custom hostnames must conform to RFC 1035 requirements for valid hostnames.
        delete_protection: boolean value indicating if the new virtual machine should be
            protected against deletion or not.
    Returns:
        Instance object.
    """
    instance_client = compute_v1.InstancesClient()

    # Use the network interface provided in the network_link argument.
    network_interface = compute_v1.NetworkInterface()
    network_interface.network = network_link
    if subnetwork_link:
        network_interface.subnetwork = subnetwork_link

    if internal_ip:
        network_interface.network_i_p = internal_ip

    if external_access:
        access = compute_v1.AccessConfig()
        access.type_ = compute_v1.AccessConfig.Type.ONE_TO_ONE_NAT.name
        access.name = "External NAT"
        access.network_tier = access.NetworkTier.PREMIUM.name
        if external_ipv4:
            access.nat_i_p = external_ipv4
        network_interface.access_configs = [access]

    # Collect information into the Instance object.
    instance = compute_v1.Instance()
    instance.network_interfaces = [network_interface]
    instance.name = instance_name
    instance.disks = disks
    if re.match(r"^zones/[a-z\d\-]+/machineTypes/[a-z\d\-]+$", machine_type):
        instance.machine_type = machine_type
    else:
        instance.machine_type = f"zones/{zone}/machineTypes/{machine_type}"

    instance.scheduling = compute_v1.Scheduling()
    if accelerators:
        instance.guest_accelerators = accelerators
        instance.scheduling.on_host_maintenance = (
            compute_v1.Scheduling.OnHostMaintenance.TERMINATE.name
        )

    if preemptible:
        # Set the preemptible setting
        warnings.warn(
            "Preemptible VMs are being replaced by Spot VMs.", DeprecationWarning
        )
        instance.scheduling = compute_v1.Scheduling()
        instance.scheduling.preemptible = True

    if spot:
        # Set the Spot VM setting
        instance.scheduling.provisioning_model = (
            compute_v1.Scheduling.ProvisioningModel.SPOT.name
        )
        instance.scheduling.instance_termination_action = instance_termination_action

    if custom_hostname is not None:
        # Set the custom hostname for the instance
        instance.hostname = custom_hostname

    if delete_protection:
        # Set the delete protection bit
        instance.deletion_protection = True

    # Prepare the request to insert an instance.
    request = compute_v1.InsertInstanceRequest()
    request.zone = zone
    request.project = project_id
    request.instance_resource = instance

    # Wait for the create operation to complete.
    print(f"Creating the {instance_name} instance in {zone}...")

    operation = instance_client.insert(request=request)

    wait_for_extended_operation(operation, "instance creation")

    print(f"Instance {instance_name} created.")
    return instance_client.get(project=project_id, zone=zone, instance=instance_name)


def create_spot_instance(
    project_id: str, zone: str, instance_name: str
) -> compute_v1.Instance:
    """
    Create a new Spot VM instance with Debian 10 operating system.

    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone to create the instance in. For example: "us-west3-b"
        instance_name: name of the new virtual machine (VM) instance.

    Returns:
        Instance object.
    """
    newest_debian = get_image_from_family(project="debian-cloud", family="debian-11")
    disk_type = f"zones/{zone}/diskTypes/pd-standard"
    disks = [disk_from_image(disk_type, 10, True, newest_debian.self_link)]
    instance = create_instance(project_id, zone, instance_name, disks, spot=True)
    return instance

同じプロパティで複数の Spot VM を作成するには、インスタンス テンプレートを作成し、テンプレートを使用してマネージド インスタンス グループ(MIG)を作成します。詳しくは、ベスト プラクティスをご覧ください。

Spot VM を起動する

他の VM と同様に、Spot VM は作成時に起動します。同様に、Spot VM が停止した場合は、VM を再起動して RUNNING 状態を再開できます。プリエンプトされた Spot VM は、容量がある限り、必要に応じて何回でも停止して再起動できます。詳細については、VM インスタンスのライフサイクルをご覧ください。

Compute Engine が、自動スケーリングされるマネージド インスタンス グループ(MIG)または Google Kubernetes Engine(GKE)クラスタ内の 1 つ以上の Spot VM を停止した場合、リソースが再び利用可能になると、グループによって VM が再起動されます。

VM のプロビジョニング モデルと終了アクションを特定する

VM のプロビジョニング モデルを特定して、それが標準の VM、Spot VM、プリエンプティブル VM のいずれであるかを確認します。Spot VM の場合は、終了アクションを特定することもできます。VM のプロビジョニング モデルと終了アクションは、Google Cloud コンソール、gcloud CLI、Compute Engine API のいずれかを使用して特定できます。

コンソール

  1. [VM インスタンス] ページに移動します。

    [VM インスタンス] ページに移動

  2. 変更する VM の [名前] をクリックします。[VM インスタンスの詳細] ページが開きます。

  3. ページの下部にある [管理] セクションに移動します。[可用性ポリシー] サブセクションで、次のオプションを確認します。

    • [VM プロビジョニング モデル] が [スポット] に設定されている場合、VM は Spot VM です。
      • [VM の終了時] には、Compute Engine が VM をプリエンプトしたときに行う操作([停止] または [削除])が示されます。
    • それ以外の場合は、VM プロビジョニング モデルが [標準] または [-] に設定されます。
      • [プリエンプティブル] オプションが [オン] に設定されている場合、VM はプリエンプティブル VM です。
      • それ以外の場合、VM は標準 VM になります。

gcloud

gcloud CLI で VM を確認するには、gcloud compute instances describe コマンドを使用します。

gcloud compute instances describe VM_NAME

VM_NAME は、確認する VM の名前です。

出力で、scheduling フィールドを調べて VM を特定します。

  • 次のように、出力に SPOT に設定された provisioningModel フィールドが含まれている場合、VM は Spot VM です。

    ...
    scheduling:
    ...
    provisioningModel: SPOT
    instanceTerminationAction: TERMINATION_ACTION
    ...
    

    ここで、TERMINATION_ACTION は Compute Engine が VM をプリエンプトするときに実行するアクション(停止(STOP)または削除(DELETE))を示します。instanceTerminationAction フィールドがない場合、デフォルト値は STOP です。

  • それ以外で、出力に standard に設定された provisioningModel フィールドが含まれている場合、または出力で provisioningModel フィールドが省略されている場合は次のようになります。

    • 出力に true に設定された preemptible フィールドが含まれている場合、VM はプリエンプティブル VM です。
    • それ以外の場合、VM は標準 VM になります。

REST

Compute Engine API で VM を確認するには、instances.get メソッドを使用します。

GET https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/VM_NAME

次のように置き換えます。

出力で、scheduling フィールドを調べて VM を特定します。

  • 次のように、出力に SPOT に設定された provisioningModel フィールドが含まれている場合、VM は Spot VM です。

    {
      ...
      "scheduling":
      {
         ...
         "provisioningModel": "SPOT",
         "instanceTerminationAction": "TERMINATION_ACTION"
         ...
      },
      ...
    }
    

    ここで、TERMINATION_ACTION は Compute Engine が VM をプリエンプトするときに実行するアクション(停止(STOP)または削除(DELETE))を示します。instanceTerminationAction フィールドがない場合、デフォルト値は STOP です。

  • それ以外で、出力に standard に設定された provisioningModel フィールドが含まれている場合、または出力で provisioningModel フィールドが省略されている場合は次のようになります。

    • 出力に true に設定された preemptible フィールドが含まれている場合、VM はプリエンプティブル VM です。
    • それ以外の場合、VM は標準 VM になります。

Go


import (
	"context"
	"fmt"
	"io"

	compute "cloud.google.com/go/compute/apiv1"
	"cloud.google.com/go/compute/apiv1/computepb"
)

// isSpotVM checks if a given instance is a Spot VM or not.
func isSpotVM(w io.Writer, projectID, zone, instanceName string) (bool, error) {
	// projectID := "your_project_id"
	// zone := "europe-central2-b"
	// instanceName := "your_instance_name"
	ctx := context.Background()
	client, err := compute.NewInstancesRESTClient(ctx)
	if err != nil {
		return false, fmt.Errorf("NewInstancesRESTClient: %w", err)
	}
	defer client.Close()

	req := &computepb.GetInstanceRequest{
		Project:  projectID,
		Zone:     zone,
		Instance: instanceName,
	}

	instance, err := client.Get(ctx, req)
	if err != nil {
		return false, fmt.Errorf("GetInstance: %w", err)
	}

	isSpot := instance.GetScheduling().GetProvisioningModel() == computepb.Scheduling_SPOT.String()

	var isSpotMessage string
	if !isSpot {
		isSpotMessage = " not"
	}
	fmt.Fprintf(w, "Instance %s is%s spot\n", instanceName, isSpotMessage)

	return instance.GetScheduling().GetProvisioningModel() == computepb.Scheduling_SPOT.String(), nil
}

Java


import com.google.cloud.compute.v1.Instance;
import com.google.cloud.compute.v1.InstancesClient;
import com.google.cloud.compute.v1.Scheduling;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

public class CheckIsSpotVm {
  public static void main(String[] args)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // TODO(developer): Replace these variables before running the sample.
    // Project ID or project number of the Google Cloud project you want to use.
    String projectId = "your-project-id";
    // Name of the virtual machine to check.
    String instanceName = "your-route-name";
    // Name of the zone you want to use. For example: "us-west3-b"
    String zone = "your-zone";

    boolean isSpotVm = isSpotVm(projectId, instanceName, zone);
    System.out.printf("Is %s spot VM instance - %s", instanceName, isSpotVm);
  }

  // Check if a given instance is Spot VM or not.
  public static boolean isSpotVm(String projectId, String instanceName, String zone)
          throws IOException {
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (InstancesClient client = InstancesClient.create()) {
      Instance instance = client.get(projectId, zone, instanceName);

      return instance.getScheduling().getProvisioningModel()
              .equals(Scheduling.ProvisioningModel.SPOT.name());
    }
  }
}

Python

from google.cloud import compute_v1


def is_spot_vm(project_id: str, zone: str, instance_name: str) -> bool:
    """
    Check if a given instance is Spot VM or not.
    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone you want to use. For example: "us-west3-b"
        instance_name: name of the virtual machine to check.
    Returns:
        The Spot VM status of the instance.
    """
    instance_client = compute_v1.InstancesClient()
    instance = instance_client.get(
        project=project_id, zone=zone, instance=instance_name
    )
    return (
        instance.scheduling.provisioning_model
        == compute_v1.Scheduling.ProvisioningModel.SPOT.name
    )

シャットダウン スクリプトを使用してプリエンプションを処理する

Compute Engine が Spot VM をプリエンプトする場合は、シャットダウン スクリプトを使用して、VM がプリエンプトされる前にクリーンアップ アクションの実行を試みることができます。たとえば、実行中のプロセスを適切に停止し、チェックポイント ファイルを Cloud Storage にコピーできます。 特に、プリエンプション通知の場合、シャットダウン期間の最大長は、ユーザーが開始したシャットダウンよりも短くなります。プリエンプション通知のシャットダウン期間の詳細については、Spot VM のコンセプト ドキュメントのプリエンプション プロセスをご覧ください。

シャットダウン スクリプトの例を以下に示します。これは、実行中の Spot VM に追加するか、新しい Spot VM を作成する際に追加できます。このスクリプトは、オペレーティング システムの通常の kill コマンドが残りすべてのプロセスを停止する前に、VM がシャットダウンを開始するときに実行されます。目的のプログラムを正常に停止した後、スクリプトは Cloud Storage バケットにチェックポイント ファイルの並列アップロードを実行します。

#!/bin/bash

MY_PROGRAM="PROGRAM_NAME" # For example, "apache2" or "nginx"
MY_USER="LOCAL_USER"
CHECKPOINT="/home/$MY_USER/checkpoint.out"
BUCKET_NAME="BUCKET_NAME" # For example, "my-checkpoint-files" (without gs://)

echo "Shutting down!  Seeing if ${MY_PROGRAM} is running."

# Find the newest copy of $MY_PROGRAM
PID="$(pgrep -n "$MY_PROGRAM")"

if [[ "$?" -ne 0 ]]; then
  echo "${MY_PROGRAM} not running, shutting down immediately."
  exit 0
fi

echo "Sending SIGINT to $PID"
kill -2 "$PID"

# Portable waitpid equivalent
while kill -0 "$PID"; do
   sleep 1
done

echo "$PID is done, copying ${CHECKPOINT} to gs://${BUCKET_NAME} as ${MY_USER}"

su "${MY_USER}" -c "gcloud storage cp $CHECKPOINT gs://${BUCKET_NAME}/"

echo "Done uploading, shutting down."

このスクリプトの前提条件は次のとおりです。

  • 少なくとも Cloud Storage に対する読み取り / 書き込みアクセス権を付与して VM が作成されていること。適切なスコープを使用して VM を作成する方法については、認証のドキュメントをご覧ください。

  • 既存の Cloud Storage バケットがあり、そのバケットに対する書き込み権限が付与されていること。

このスクリプトを VM に追加するには、VM 上のアプリケーションと連携するようにスクリプトを構成し、そのスクリプトを VM のメタデータに追加します。

  1. シャットダウン スクリプトをコピーまたはダウンロードします。

    • 前述のシャットダウン スクリプトを次のように置き換えてコピーします。

      • PROGRAM_NAME は、シャットダウンするプロセスまたはプログラムの名前です。たとえば、apache2nginx です。
      • LOCAL_USER は、仮想マシンへのログインに使用しているユーザー名です。
      • BUCKET_NAME は、プログラムのチェックポイント ファイルを保存する Cloud Storage バケットの名前です。ここではバケット名が gs:// で始まっていないことにご注意ください。
    • ローカル ワークステーションにシャットダウン スクリプトをダウンロードし、ファイル内の次の変数を置き換えます。

      • [PROGRAM_NAME] は、シャットダウンするプロセスまたはプログラムの名前です。たとえば、apache2nginx です。
      • [LOCAL_USER] は、仮想マシンへのログインに使用しているユーザー名です。
      • [BUCKET_NAME] は、プログラムのチェックポイント ファイルを保存する Cloud Storage バケットの名前です。ここではバケット名が gs:// で始まっていないことにご注意ください。
  2. シャットダウン スクリプトを新しい VM または既存の VM に追加します。

Spot VM のプリエンプションを検出する

Spot VM が Compute Engine によってプリエンプトされたかどうかを確認するには、Google Cloud コンソールgcloud CLI、または Compute Engine API を使用します。

コンソール

VM がプリエンプトされたかどうかを確認するには、システム アクティビティ ログを使用します。

  1. Google Cloud Console のログページに移動します。

    ログに移動

  2. プロジェクトを選択し、[続行] をクリックします。

  3. [ラベルまたはテキスト検索でフィルタ] フィールドに compute.instances.preempted を追加します。

  4. 特定の VM のプリエンプション オペレーションを確認するには、VM 名を入力することもできます。

  5. Enter キーを押して、指定されたフィルタを適用します。Google Cloud コンソールによってログのリストが更新され、VM がプリエンプトされたオペレーションのみが表示されます。

  6. リストでオペレーションを選択し、プリエンプトされた VM に関する詳細を表示します。

gcloud

プロジェクト内のプリエンプション イベントのリストを取得するには、filter パラメータを指定して gcloud compute operations list コマンドを使用します。

gcloud compute operations list \
    --filter="operationType=compute.instances.preempted"

必要に応じて、追加のフィルタ パラメータを使用して、結果をさらに絞り込めます。たとえば、マネージド インスタンス グループ内のインスタンスのプリエンプション イベントのみを表示するには、次のコマンドを使用します。

gcloud compute operations list \
    --filter="operationType=compute.instances.preempted AND targetLink:instances/BASE_INSTANCE_NAME"

ここで、BASE_INSTANCE_NAME は、このマネージド インスタンス グループ内のすべての VM の名前の接頭辞として指定されたベース名です。

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

NAME                  TYPE                         TARGET                                        HTTP_STATUS STATUS TIMESTAMP
systemevent-xxxxxxxx  compute.instances.preempted  us-central1-f/instances/example-instance-xxx  200         DONE   2015-04-02T12:12:10.881-07:00

compute.instances.preempted というオペレーション タイプは、VM インスタンスがプリエンプトされたことを示します。gcloud compute operations describe コマンドを使用すると、特定のプリエンプション オペレーションに関する詳細情報を取得できます。

gcloud compute operations describe SYSTEM_EVENT \
    --zone=ZONE

次のように置き換えます。

  • SYSTEM_EVENT: gcloud compute operations list コマンドの出力のシステム イベント(例: systemevent-xxxxxxxx)。
  • ZONE: システム イベントのゾーン(例: us-central1-f)。

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

...
operationType: compute.instances.preempted
progress: 100
selfLink: https://compute.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f/operations/systemevent-xxxxxxxx
startTime: '2015-04-02T12:12:10.881-07:00'
status: DONE
statusMessage: Instance was preempted.
...

REST

特定のプロジェクトとゾーンの最近のシステム オペレーションのリストを取得するには、zoneOperations.get メソッドを使用します。

GET https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations

次のように置き換えます。

プリエンプション オペレーションのみを表示するようにレスポンスの内容を絞り込むには、API リクエストにフィルタを追加します。

operationType="compute.instances.preempted"

特定の VM のプリエンプション オペレーションを表示するには、targetLink パラメータをフィルタに追加します。

operationType="compute.instances.preempted" AND
targetLink="https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/VM_NAME

次のように置き換えます。 + PROJECT_ID: プロジェクト ID。+ ZONE: ゾーン。+ VM_NAME: このゾーンとプロジェクトの特定の VM の名前。

レスポンスには、最新オペレーションのリストが含まれます。たとえば、プリエンプションは次のようになります。

{
  "kind": "compute#operation",
  "id": "15041793718812375371",
  "name": "systemevent-xxxxxxxx",
  "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f",
  "operationType": "compute.instances.preempted",
  "targetLink": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f/instances/example-instance",
  "targetId": "12820389800990687210",
  "status": "DONE",
  "statusMessage": "Instance was preempted.",
  ...
}

また、VM 自体の内部から VM がプリエンプトされたかどうかを確認することもできます。これは、Compute Engine のプリエンプションのためにシャットダウン スクリプトの通常のシャットダウンとは異なるシャットダウンを処理する場合に便利です。これを行うには、メタデータ サーバーで VM のデフォルトのメタデータに含まれる preempted の値を確認します。

たとえば、VM 内から curl を使用して preempted の値を取得します。

curl "http://metadata.google.internal/computeMetadata/v1/instance/preempted" -H "Metadata-Flavor: Google"
TRUE

この値が TRUE の場合、VM は Compute Engine によってプリエンプトされており、プリエンプトされていない場合は FALSE です。

これをシャットダウン スクリプトの外部で使用する場合は、URL に ?wait_for_change=true を追加します。これにより、HTTP GET リクエストが待機中になり、メタデータが変更されて VM がプリエンプトされたときにのみ結果が返されます。

curl "http://metadata.google.internal/computeMetadata/v1/instance/preempted?wait_for_change=true" -H "Metadata-Flavor: Google"
TRUE

プリエンプション設定をテストする方法

VM でシミュレート メンテナンス イベントを実行して、強制的にインスタンスをプリエンプトできます。この機能を使用して、アプリによる Spot VM の処理方法をテストします。インスタンスでメンテナンス イベントをテストする方法については、ホスト メンテナンス イベントをシミュレートするをご覧ください。

VM インスタンスを停止して、VM プリエンプションをシミュレートすることもできます。メンテナンス イベントをシミュレートする代わりに、この操作を行うことで、割り当ての上限に達するのを回避できます。

ベスト プラクティス

Spot VM を最大限に活用するためのベスト プラクティスは次のとおりです。

  • インスタンス テンプレートを使用する。Spot VM を一度に 1 つずつ作成する代わりに、インスタンス テンプレートを使用して同じプロパティを持つ複数の Spot VM を作成できます。インスタンス テンプレートは、MIG を使用する場合には必須です。別の方法として、インスタンス一括作成 API を使用して複数の Spot VM を作成することもできます。

  • MIG を使用して Spot VM のリージョン内での分散配置と自動再作成を行うMIG を使用して、Spot VM 上のワークロードの柔軟性と復元性を高めます。たとえば、リージョン MIG を使用して VM を複数のゾーンに分散すると、リソース可用性エラーが軽減されます。さらに、自動修復を使用して、プリエンプトされた Spot VM を自動的に再作成します。

  • 小さいマシンタイプを選択する。Spot VM のリソースは、Google Cloud の予備キャパシティの余剰分から取得されます。多くの場合、Spot VM のキャパシティは小さいマシンタイプ、つまり vCPU やメモリなどのリソースが少ないマシンタイプの方が確保しやすくなります。より小さいカスタム マシンタイプを選択すると Spot VM のキャパシティは高くなる可能性がありますが、事前定義されたマシンタイプの中からより小さいマシンタイプを選んだ方が、キャパシティはより一層高くなる可能性があります。たとえば、事前定義されたマシンタイプ n2-standard-32 の容量と比較して、カスタム マシンタイプ n2-custom-24-96 の容量は増加する可能性が高いですが、事前定義されたマシンタイプ n2-standard-16 の容量はさらに増加する可能性があります。

  • オフピーク時に Spot VM の大規模なクラスタを実行する。Google Cloud データセンターの負荷は、ロケーションや時間帯によって異なりますが、一般的には夜間と週末に最も低くなります。そのため、Spot VM の大規模なクラスタを実行する時間としては夜間や週末が最適です。

  • 耐障害性と対プリエンプション性を備えたアプリケーションを設計する。時間が異なればプリエンプションのパターンが変化するという事実に備えることが重要です。たとえば、ゾーンで部分的な停止の問題が発生している場合、復旧作業の一部として移動する必要がある標準 VM 用の余地を作るため、多数の Spot VM がプリエンプトされる可能性があります。この短い時間のプリエンプション レートは他の日とはまったく異なったものとなります。プリエンプションが常に小さなグループの中で行われることを前提としてアプリケーションを設計した場合、このようなイベントには対処できません。

  • プリエンプトされた Spot VM の作成を再試行する。Spot VM がプリエンプトされた場合、標準 VM にフォールバックする前に新しい Spot VM の作成を 1 回または 2 回試行します。実際の要件によっては、クラスタ内で標準 VM と Spot VM を組み合わせて適切なペースで作業が続行されるようにすると有効な場合があります。

  • シャットダウン スクリプトを使用する。ジョブの進行状況を保存できるシャットダウン スクリプトを使用してシャットダウンとプリエンプションの通知を管理すると、最初からやり直す代わりに停止した時点を選択できます。

次のステップ