建立 API 請求和處理回應


本文件說明如何透過 Compute Engine API 建立 API 要求及處理 API 回應,所涵蓋的內容如下:

  • 如何建構要求內文。
  • 如何決定要求所需的資源 URI。
  • 如何處理 API 回應。
  • 判斷 API 要求是否成功。

本文件不會說明如何驗證 API。如要瞭解如何驗證 API,請參閱「向 Compute Engine 進行驗證」。

事前準備

建立 API 要求

Compute Engine API 預期 API 要求採用 JSON 格式。如要提出 API 要求,您可以使用 curlhttplib2 等工具,直接提出 HTTP 要求,也可以使用可用的用戶端程式庫

當您提出需要要求主體的 API 要求 (例如 POSTUPDATEPATCH 要求) 時,要求主體會包含您要在該要求中設定的資源屬性。舉例來說,下列 curl 指令會向「Instances」資源 URI 提出 POST 要求。這項要求會使用要求主體中定義的屬性建立例項。要求主體會以 -d 旗標表示:

curl -X POST -H "Authorization: Bearer [OAUTH_TOKEN]" -H "Content-Type: application/json"
https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances -d
'{
  "disks":[
    {
      "boot":"true",
      "initializeParams":{
        "sourceImage":"https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-10-buster-v20210122"
      }
    }
  ],
  "machineType":"https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/machineTypes/e2-standard-2",
  "name":"VM_NAME",
  "networkInterfaces":[
    {
      "accessConfigs":[
        {
          "name":"external-nat",
          "type":"ONE_TO_ONE_NAT"
        }
      ],
      "network":"https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/default"
    }
  ]
}'

映像檔 URI 的專案 ID (debian-cloud) 與您的專案 ID 不同,因為映像檔會根據其類型而屬於不同專案。例如,Compute Engine 提供給大眾使用的所有 Debian 映像檔皆是在 debian-cloud 專案上託管。

參照其他資源時,請使用完整的資源 URI。例如,network 屬性對 default 網路使用的是完整 URI。

API 要求範例

Python

def create_instance(
    compute: object,
    project: str,
    zone: str,
    name: str,
    bucket: str,
) -> str:
    """Creates an instance in the specified zone.

    Args:
      compute: an initialized compute service object.
      project: the Google Cloud project ID.
      zone: the name of the zone in which the instances should be created.
      name: the name of the instance.
      bucket: the name of the bucket in which the image should be written.

    Returns:
      The instance object.
    """
    # Get the latest Debian Jessie image.
    image_response = (
        compute.images()
        .getFromFamily(project="debian-cloud", family="debian-11")
        .execute()
    )
    source_disk_image = image_response["selfLink"]

    # Configure the machine
    machine_type = "zones/%s/machineTypes/n1-standard-1" % zone
    startup_script = open(
        os.path.join(os.path.dirname(__file__), "startup-script.sh")
    ).read()
    image_url = "http://storage.googleapis.com/gce-demo-input/photo.jpg"
    image_caption = "Ready for dessert?"

    config = {
        "name": name,
        "machineType": machine_type,
        # Specify the boot disk and the image to use as a source.
        "disks": [
            {
                "boot": True,
                "autoDelete": True,
                "initializeParams": {
                    "sourceImage": source_disk_image,
                },
            }
        ],
        # Specify a network interface with NAT to access the public
        # internet.
        "networkInterfaces": [
            {
                "network": "global/networks/default",
                "accessConfigs": [{"type": "ONE_TO_ONE_NAT", "name": "External NAT"}],
            }
        ],
        # Allow the instance to access cloud storage and logging.
        "serviceAccounts": [
            {
                "email": "default",
                "scopes": [
                    "https://www.googleapis.com/auth/devstorage.read_write",
                    "https://www.googleapis.com/auth/logging.write",
                ],
            }
        ],
        # Metadata is readable from the instance and allows you to
        # pass configuration from deployment scripts to instances.
        "metadata": {
            "items": [
                {
                    # Startup script is automatically executed by the
                    # instance upon startup.
                    "key": "startup-script",
                    "value": startup_script,
                },
                {"key": "url", "value": image_url},
                {"key": "text", "value": image_caption},
                {"key": "bucket", "value": bucket},
            ]
        },
    }

    return compute.instances().insert(project=project, zone=zone, body=config).execute()

Java

public static Operation startInstance(Compute compute, String instanceName) throws IOException {
  System.out.println("================== Starting New Instance ==================");

  // Create VM Instance object with the required properties.
  Instance instance = new Instance();
  instance.setName(instanceName);
  instance.setMachineType(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/machineTypes/e2-standard-1",
          PROJECT_ID, ZONE_NAME));
  // Add Network Interface to be used by VM Instance.
  NetworkInterface ifc = new NetworkInterface();
  ifc.setNetwork(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/global/networks/default",
          PROJECT_ID));
  List<AccessConfig> configs = new ArrayList<>();
  AccessConfig config = new AccessConfig();
  config.setType(NETWORK_INTERFACE_CONFIG);
  config.setName(NETWORK_ACCESS_CONFIG);
  configs.add(config);
  ifc.setAccessConfigs(configs);
  instance.setNetworkInterfaces(Collections.singletonList(ifc));

  // Add attached Persistent Disk to be used by VM Instance.
  AttachedDisk disk = new AttachedDisk();
  disk.setBoot(true);
  disk.setAutoDelete(true);
  disk.setType("PERSISTENT");
  AttachedDiskInitializeParams params = new AttachedDiskInitializeParams();
  // Assign the Persistent Disk the same name as the VM Instance.
  params.setDiskName(instanceName);
  // Specify the source operating system machine image to be used by the VM Instance.
  params.setSourceImage(SOURCE_IMAGE_PREFIX + SOURCE_IMAGE_PATH);
  // Specify the disk type as Standard Persistent Disk
  params.setDiskType(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/diskTypes/pd-standard",
          PROJECT_ID, ZONE_NAME));
  disk.setInitializeParams(params);
  instance.setDisks(Collections.singletonList(disk));

  // Initialize the service account to be used by the VM Instance and set the API access scopes.
  ServiceAccount account = new ServiceAccount();
  account.setEmail("default");
  List<String> scopes = new ArrayList<>();
  scopes.add("https://www.googleapis.com/auth/devstorage.full_control");
  scopes.add("https://www.googleapis.com/auth/compute");
  account.setScopes(scopes);
  instance.setServiceAccounts(Collections.singletonList(account));

  // Optional - Add a startup script to be used by the VM Instance.
  Metadata meta = new Metadata();
  Metadata.Items item = new Metadata.Items();
  item.setKey("startup-script-url");
  // If you put a script called "vm-startup.sh" in this Google Cloud Storage
  // bucket, it will execute on VM startup.  This assumes you've created a
  // bucket named the same as your PROJECT_ID.
  // For info on creating buckets see:
  // https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets
  item.setValue(String.format("gs://%s/vm-startup.sh", PROJECT_ID));
  meta.setItems(Collections.singletonList(item));
  instance.setMetadata(meta);

  System.out.println(instance.toPrettyString());
  Compute.Instances.Insert insert = compute.instances().insert(PROJECT_ID, ZONE_NAME, instance);
  return insert.execute();
}

建立資源 URI

在 Compute Engine API 中,另一個 Google Cloud 資源的參照會以完整的 URI 形式提供:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/RESOURCE_TYPE/SPECIFIC_RESOURCE

無論您指定的是映像檔、機器類型、網路或任何其他資源,使用 API 時都必須提供資源的 URI。Google Cloud CLI 和 Google Cloud 控制台等用戶端工具會隱藏這項複雜性,並為您處理這些資源 URI 的建立作業,但如果要直接與 API 互動,您必須自行建立這些資源 URI。

不同類型的資源有略有不同的資源 URI。舉例來說,區域資源的 URI 中含有 zone 規格:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/machineTypes/e2-standard-2

區域資源會將 zone 規格替換為 region 規格:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/regions/REGION/addresses/ADDRESS_NAME

同樣地,全域資源也有 global 規格:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/global/images/VM_NAME

Compute Engine API 也接受部分 URI,因為這項服務可以推斷專案 ID 等資訊。因此,下列早期 URI 的部分版本也適用:

zones/ZONE/machineTypes/e2-standard-2
regions/REGION/addresses/ADDRESS_NAME
project/PROJECT_ID/global/images/VM_NAME

在部分 URI 中,區域和區域 URI 都省略了專案 ID,但圖片 URI 並未省略。這是因為 Compute Engine 提供的公開映像檔會託管在其他專案中,例如 debian-cloud 是用於所有 Debian 映像檔,ubuntu-os-cloud 則是用於所有 Ubuntu 映像檔。您必須先提供適當的專案 ID,才能使用這些圖片。如果您省略映像檔的專案 ID,Compute Engine 會嘗試在專案中尋找該映像檔,但由於該映像檔不存在,因此要求會失敗。

不過,如果您使用屬於專案的自訂映像檔 (也就是您建立此資源的專案),則在提供映像檔 URI 時,可以省略專案規格。

判斷必要屬性

v1beta API 的 Compute Engine API 參考資料文件中說明您可以為特定資源設定的所有屬性。參考說明文件會區分可變動屬性和不可變動屬性 (在屬性說明中以 [Output Only] 標示),但如要判斷資源的必要屬性,您必須查看該任務的特定說明文件。

舉例來說,如果您要建立執行個體,請參閱「從映像檔建立執行個體」說明文件,瞭解要求所需的 API 屬性。如果您想在 API 中建立靜態外部 IP 位址,請參閱「靜態外部 IP 位址」說明文件。

驗證 API 要求

驗證 API 要求的做法如下:

  1. Compute Engine API 參考資料中,找出程式碼呼叫的方法。例如:v1/compute.instances.insert
  2. 在內容選單中,按一下「試試看」。系統會開啟「試用這個 API」視窗。

    內容選單中的「試試看」按鈕。

  3. 在「Request parameters」下方,您不需要提供專案區域,因為驗證程序不需要提交要求。

  4. 在「要求主體」下方貼上要求。

    「Try this API」視窗,顯示「Request body」欄位,指出要將要求貼到哪裡進行驗證。

要求中格式不正確的元素會以藍色底線標示。按一下每個底線標示的部分,即可進一步瞭解要解決的問題。

處理 API 回應

如果您提出會變異 (變更) 資料的要求,Compute Engine 會傳回 Operation 物件,您可以透過輪詢取得要求的作業狀態。Operation 資源如下所示:

{
 "kind": "compute#operation",
 "id": "7127550864823803662",
 "name": "operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b",
 "zone": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE",
 "operationType": "insert",
 "targetLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM",
 "targetId": "4132355614508179214",
 "status": "PENDING",
 "user": "user@example.com",
 "progress": 0,
 "insertTime": "2016-03-24T14:53:37.788-07:00",
 "selfLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations/operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b"
}

如果原始要求是修改 (變更) 區域資源,例如建立磁碟快照或停止執行個體,Compute Engine 會傳回 zoneOperations 物件。同樣地,區域和全域資源會分別傳回 regionOperationsglobalOperations 物件。如要取得作業的狀態,您可以針對特定 Operation 資源執行使用 getwait 方法的要求,並提供作業的 name

Operation 資源的狀態必須為 DONE,您的要求才算完成。視要求的性質而定,這項作業可能需要一段時間才能完成。接著,當 Operation 資源的狀態傳回為 DONE 時,您可以檢查作業是否成功,以及是否發生任何錯誤。

舉例來說,下列回應會指出先前的作業現在已完成,並指定 DONE 狀態:

endTime: '2016-03-24T14:54:07.119-07:00'
id: '7127550864823803662'
insertTime: '2016-03-24T14:53:37.788-07:00'
kind: compute#operation
name: operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b
operationType: insert
progress: 100
selfLink: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations/operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b
startTime: '2016-03-24T14:53:38.397-07:00'
status: DONE
targetId: '4132355614508179214'
targetLink: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM
user: user@example.com
zone: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE

如要確認,請對資源提出 get 要求,檢查資源是否存在且正在執行。例如:

GET /compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM

{
  "cpuPlatform": "Intel Haswell",
  "creationTimestamp": "2016-03-24T14:53:37.170-07:00",
  "disks": [
    ..[snip]..
  "selfLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM",
  "status": "RUNNING",
  "tags": {
    "fingerprint": "42WmSpB8rSM="
  },
  "zone": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE"
}

輪詢作業

您可以編寫程式碼,以便在作業狀態為 DONE 時,透過 getwait 要求定期輪詢作業。

使用 get 要求時,無論作業狀態為何,系統都會立即傳回作業。您需要定期輪詢作業,才能瞭解作業何時完成。

如果您提出 wait 要求,系統會在作業 DONE 或要求即將達到 2 分鐘的期限時傳回要求。您可以選擇使用 waitget 來輪詢作業,但 wait 方法相較於 get 方法,具有以下特定優點:

  • 您可以設定用戶端,讓其較少輪詢作業狀態,進而減少 Compute Engine API 的 QPS 用量。
  • 作業完成後,到用戶端收到作業完成通知之間的平均延遲時間會大幅縮短,因為伺服器會在作業完成後立即回應。
  • 這個方法會提供受限的等待時間。該方法最多會等待預設的 HTTP 逾時時間 (2 分鐘),然後傳回作業目前的狀態,該狀態可能是 DONE 或仍在進行中。

wait 方法是盡力 API。如果伺服器超載,要求可能會在達到預設期限前,或在等待零秒後就傳回。而且,方法也不保證只會在作業為 DONE 時傳回。舉例來說,如果要求接近 2 分鐘的期限,即使作業尚未完成,方法也會傳回。如要檢查作業,建議您在重試迴圈中使用 waitget 方法,並在兩者之間加入休眠時間,以便定期輪詢作業狀態。重試間隔上限不得超過最短作業保留期限

輪詢範例

以下範例使用 get 方法。您可以將 get 方法替換為 wait 方法:

Python

def wait_for_operation(
    compute: object,
    project: str,
    zone: str,
    operation: str,
) -> dict:
    """Waits for the given operation to complete.

    Args:
      compute: an initialized compute service object.
      project: the Google Cloud project ID.
      zone: the name of the zone in which the operation should be executed.
      operation: the operation ID.

    Returns:
      The result of the operation.
    """
    print("Waiting for operation to finish...")
    while True:
        result = (
            compute.zoneOperations()
            .get(project=project, zone=zone, operation=operation)
            .execute()
        )

        if result["status"] == "DONE":
            print("done.")
            if "error" in result:
                raise Exception(result["error"])
            return result

        time.sleep(1)

Java

/**
 * Wait until {@code operation} is completed.
 *
 * @param compute the {@code Compute} object
 * @param operation the operation returned by the original request
 * @param timeout the timeout, in millis
 * @return the error, if any, else {@code null} if there was no error
 * @throws InterruptedException if we timed out waiting for the operation to complete
 * @throws IOException if we had trouble connecting
 */
public static Operation.Error blockUntilComplete(
    Compute compute, Operation operation, long timeout) throws Exception {
  long start = System.currentTimeMillis();
  final long pollInterval = 5 * 1000;
  String zone = getLastWordFromUrl(operation.getZone()); // null for global/regional operations
  String region = getLastWordFromUrl(operation.getRegion());
  String status = operation.getStatus();
  String opId = operation.getName();
  while (operation != null && !status.equals("DONE")) {
    Thread.sleep(pollInterval);
    long elapsed = System.currentTimeMillis() - start;
    if (elapsed >= timeout) {
      throw new InterruptedException("Timed out waiting for operation to complete");
    }
    System.out.println("waiting...");
    if (zone != null) {
      Compute.ZoneOperations.Get get = compute.zoneOperations().get(PROJECT_ID, zone, opId);
      operation = get.execute();
    } else if (region != null) {
      Compute.RegionOperations.Get get = compute.regionOperations().get(PROJECT_ID, region, opId);
      operation = get.execute();
    } else {
      Compute.GlobalOperations.Get get = compute.globalOperations().get(PROJECT_ID, opId);
      operation = get.execute();
    }
    if (operation != null) {
      status = operation.getStatus();
    }
  }
  return operation == null ? null : operation.getError();
}