Create and Manage GCP Persistent Disks

The gcp:compute/disk:Disk resource, part of the Pulumi GCP provider, provisions persistent disks that provide durable block storage for Compute Engine instances. This guide focuses on three capabilities: creating disks from public images, asynchronous cross-zone replication, and guest OS features and licensing.

Persistent disks exist independently from VM instances and reference public images or snapshots. The examples are intentionally small. Combine them with your own VM instances, encryption keys, and snapshot policies.

Create a disk from a public image

Most deployments start by creating a disk from a public image, specifying the disk type and zone.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const _default = new gcp.compute.Disk("default", {
    name: "test-disk",
    type: "pd-ssd",
    zone: "us-central1-a",
    image: "debian-11-bullseye-v20220719",
    labels: {
        environment: "dev",
    },
    physicalBlockSizeBytes: 4096,
});
import pulumi
import pulumi_gcp as gcp

default = gcp.compute.Disk("default",
    name="test-disk",
    type="pd-ssd",
    zone="us-central1-a",
    image="debian-11-bullseye-v20220719",
    labels={
        "environment": "dev",
    },
    physical_block_size_bytes=4096)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := compute.NewDisk(ctx, "default", &compute.DiskArgs{
			Name:  pulumi.String("test-disk"),
			Type:  pulumi.String("pd-ssd"),
			Zone:  pulumi.String("us-central1-a"),
			Image: pulumi.String("debian-11-bullseye-v20220719"),
			Labels: pulumi.StringMap{
				"environment": pulumi.String("dev"),
			},
			PhysicalBlockSizeBytes: pulumi.Int(4096),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.Compute.Disk("default", new()
    {
        Name = "test-disk",
        Type = "pd-ssd",
        Zone = "us-central1-a",
        Image = "debian-11-bullseye-v20220719",
        Labels = 
        {
            { "environment", "dev" },
        },
        PhysicalBlockSizeBytes = 4096,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Disk;
import com.pulumi.gcp.compute.DiskArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new Disk("default", DiskArgs.builder()
            .name("test-disk")
            .type("pd-ssd")
            .zone("us-central1-a")
            .image("debian-11-bullseye-v20220719")
            .labels(Map.of("environment", "dev"))
            .physicalBlockSizeBytes(4096)
            .build());

    }
}
resources:
  default:
    type: gcp:compute:Disk
    properties:
      name: test-disk
      type: pd-ssd
      zone: us-central1-a
      image: debian-11-bullseye-v20220719
      labels:
        environment: dev
      physicalBlockSizeBytes: 4096

The image property references a public Debian image by name. The type property sets the disk to SSD (pd-ssd) rather than standard HDD. The physicalBlockSizeBytes property controls the block size; 4096 bytes is standard for most workloads. Labels provide metadata for organization and cost tracking.

Set up asynchronous replication between zones

Applications requiring disaster recovery can replicate disks across zones with some replication lag.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const primary = new gcp.compute.Disk("primary", {
    name: "async-test-disk",
    type: "pd-ssd",
    zone: "us-central1-a",
    physicalBlockSizeBytes: 4096,
});
const secondary = new gcp.compute.Disk("secondary", {
    name: "async-secondary-test-disk",
    type: "pd-ssd",
    zone: "us-east1-c",
    asyncPrimaryDisk: {
        disk: primary.id,
    },
    physicalBlockSizeBytes: 4096,
});
import pulumi
import pulumi_gcp as gcp

primary = gcp.compute.Disk("primary",
    name="async-test-disk",
    type="pd-ssd",
    zone="us-central1-a",
    physical_block_size_bytes=4096)
secondary = gcp.compute.Disk("secondary",
    name="async-secondary-test-disk",
    type="pd-ssd",
    zone="us-east1-c",
    async_primary_disk={
        "disk": primary.id,
    },
    physical_block_size_bytes=4096)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		primary, err := compute.NewDisk(ctx, "primary", &compute.DiskArgs{
			Name:                   pulumi.String("async-test-disk"),
			Type:                   pulumi.String("pd-ssd"),
			Zone:                   pulumi.String("us-central1-a"),
			PhysicalBlockSizeBytes: pulumi.Int(4096),
		})
		if err != nil {
			return err
		}
		_, err = compute.NewDisk(ctx, "secondary", &compute.DiskArgs{
			Name: pulumi.String("async-secondary-test-disk"),
			Type: pulumi.String("pd-ssd"),
			Zone: pulumi.String("us-east1-c"),
			AsyncPrimaryDisk: &compute.DiskAsyncPrimaryDiskArgs{
				Disk: primary.ID(),
			},
			PhysicalBlockSizeBytes: pulumi.Int(4096),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var primary = new Gcp.Compute.Disk("primary", new()
    {
        Name = "async-test-disk",
        Type = "pd-ssd",
        Zone = "us-central1-a",
        PhysicalBlockSizeBytes = 4096,
    });

    var secondary = new Gcp.Compute.Disk("secondary", new()
    {
        Name = "async-secondary-test-disk",
        Type = "pd-ssd",
        Zone = "us-east1-c",
        AsyncPrimaryDisk = new Gcp.Compute.Inputs.DiskAsyncPrimaryDiskArgs
        {
            Disk = primary.Id,
        },
        PhysicalBlockSizeBytes = 4096,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Disk;
import com.pulumi.gcp.compute.DiskArgs;
import com.pulumi.gcp.compute.inputs.DiskAsyncPrimaryDiskArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var primary = new Disk("primary", DiskArgs.builder()
            .name("async-test-disk")
            .type("pd-ssd")
            .zone("us-central1-a")
            .physicalBlockSizeBytes(4096)
            .build());

        var secondary = new Disk("secondary", DiskArgs.builder()
            .name("async-secondary-test-disk")
            .type("pd-ssd")
            .zone("us-east1-c")
            .asyncPrimaryDisk(DiskAsyncPrimaryDiskArgs.builder()
                .disk(primary.id())
                .build())
            .physicalBlockSizeBytes(4096)
            .build());

    }
}
resources:
  primary:
    type: gcp:compute:Disk
    properties:
      name: async-test-disk
      type: pd-ssd
      zone: us-central1-a
      physicalBlockSizeBytes: 4096
  secondary:
    type: gcp:compute:Disk
    properties:
      name: async-secondary-test-disk
      type: pd-ssd
      zone: us-east1-c
      asyncPrimaryDisk:
        disk: ${primary.id}
      physicalBlockSizeBytes: 4096

The asyncPrimaryDisk property links the secondary disk to the primary disk’s ID. GCP handles replication automatically; the secondary disk mirrors the primary’s data asynchronously. Both disks must be the same type and size.

Configure guest OS features and licenses

Bootable disks for specialized operating systems need guest OS features and licenses declared explicitly.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const _default = new gcp.compute.Disk("default", {
    name: "test-disk-features",
    type: "pd-ssd",
    zone: "us-central1-a",
    labels: {
        environment: "dev",
    },
    guestOsFeatures: [
        {
            type: "SECURE_BOOT",
        },
        {
            type: "MULTI_IP_SUBNET",
        },
        {
            type: "WINDOWS",
        },
    ],
    licenses: ["https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core"],
    physicalBlockSizeBytes: 4096,
});
import pulumi
import pulumi_gcp as gcp

default = gcp.compute.Disk("default",
    name="test-disk-features",
    type="pd-ssd",
    zone="us-central1-a",
    labels={
        "environment": "dev",
    },
    guest_os_features=[
        {
            "type": "SECURE_BOOT",
        },
        {
            "type": "MULTI_IP_SUBNET",
        },
        {
            "type": "WINDOWS",
        },
    ],
    licenses=["https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core"],
    physical_block_size_bytes=4096)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := compute.NewDisk(ctx, "default", &compute.DiskArgs{
			Name: pulumi.String("test-disk-features"),
			Type: pulumi.String("pd-ssd"),
			Zone: pulumi.String("us-central1-a"),
			Labels: pulumi.StringMap{
				"environment": pulumi.String("dev"),
			},
			GuestOsFeatures: compute.DiskGuestOsFeatureArray{
				&compute.DiskGuestOsFeatureArgs{
					Type: pulumi.String("SECURE_BOOT"),
				},
				&compute.DiskGuestOsFeatureArgs{
					Type: pulumi.String("MULTI_IP_SUBNET"),
				},
				&compute.DiskGuestOsFeatureArgs{
					Type: pulumi.String("WINDOWS"),
				},
			},
			Licenses: pulumi.StringArray{
				pulumi.String("https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core"),
			},
			PhysicalBlockSizeBytes: pulumi.Int(4096),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.Compute.Disk("default", new()
    {
        Name = "test-disk-features",
        Type = "pd-ssd",
        Zone = "us-central1-a",
        Labels = 
        {
            { "environment", "dev" },
        },
        GuestOsFeatures = new[]
        {
            new Gcp.Compute.Inputs.DiskGuestOsFeatureArgs
            {
                Type = "SECURE_BOOT",
            },
            new Gcp.Compute.Inputs.DiskGuestOsFeatureArgs
            {
                Type = "MULTI_IP_SUBNET",
            },
            new Gcp.Compute.Inputs.DiskGuestOsFeatureArgs
            {
                Type = "WINDOWS",
            },
        },
        Licenses = new[]
        {
            "https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core",
        },
        PhysicalBlockSizeBytes = 4096,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Disk;
import com.pulumi.gcp.compute.DiskArgs;
import com.pulumi.gcp.compute.inputs.DiskGuestOsFeatureArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new Disk("default", DiskArgs.builder()
            .name("test-disk-features")
            .type("pd-ssd")
            .zone("us-central1-a")
            .labels(Map.of("environment", "dev"))
            .guestOsFeatures(            
                DiskGuestOsFeatureArgs.builder()
                    .type("SECURE_BOOT")
                    .build(),
                DiskGuestOsFeatureArgs.builder()
                    .type("MULTI_IP_SUBNET")
                    .build(),
                DiskGuestOsFeatureArgs.builder()
                    .type("WINDOWS")
                    .build())
            .licenses("https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core")
            .physicalBlockSizeBytes(4096)
            .build());

    }
}
resources:
  default:
    type: gcp:compute:Disk
    properties:
      name: test-disk-features
      type: pd-ssd
      zone: us-central1-a
      labels:
        environment: dev
      guestOsFeatures:
        - type: SECURE_BOOT
        - type: MULTI_IP_SUBNET
        - type: WINDOWS
      licenses:
        - https://www.googleapis.com/compute/v1/projects/windows-cloud/global/licenses/windows-server-core
      physicalBlockSizeBytes: 4096

The guestOsFeatures array enables capabilities like SECURE_BOOT, MULTI_IP_SUBNET, and WINDOWS. The licenses property references the Windows Server Core license URL. These settings are immutable after disk creation and only apply to bootable disks.

Beyond these examples

These snippets focus on specific disk-level features: image-based disk creation, cross-zone asynchronous replication, and guest OS features and licensing. They’re intentionally minimal rather than full storage solutions.

The examples may reference pre-existing infrastructure such as GCP projects with compute API enabled, and public images (Debian, Windows) in the project or image family. They focus on configuring the disk rather than provisioning everything around it.

To keep things focused, common disk patterns are omitted, including:

  • Disk sizing and resizing (size property)
  • Snapshot-based disk creation (snapshot property)
  • Customer-managed encryption keys (diskEncryptionKey)
  • Performance tuning (provisionedIops, provisionedThroughput)
  • Resource policies for automated snapshots (resourcePolicies)

These omissions are intentional: the goal is to illustrate how each disk feature is wired, not provide drop-in storage modules. See the Compute Disk resource reference for all available configuration options.

Let's create and Manage GCP Persistent Disks

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Disk Sizing & Resizing
What happens if I try to downsize a disk?
Downsizing triggers disk recreation rather than an in-place update. Use lifecycle.prevent_destroy to prevent accidental deletion. Only upsizing is supported as an update operation.
Can I specify a disk size smaller than my source image or snapshot?
No, the size value must not be less than the size of the source image or snapshot.
Hyperdisk-Specific Features
How often can I update Hyperdisk IOPS or throughput?
Hyperdisk allows updates to provisionedIops and provisionedThroughput once every 4 hours. To update more frequently, you’ll need to manually delete and recreate the disk.
Can I use accessMode with any disk type?
No, accessMode (READ_WRITE_MANY, READ_ONLY_SINGLE) is only valid for Hyperdisk disk types. The default READ_WRITE_SINGLE works with all disk types.
What's required to enable confidential compute on a disk?
Set enableConfidentialCompute to true and provide diskEncryptionKey. This feature is only supported on Hyperdisk skus and is immutable after creation.
Immutability & Recreation
What disk properties force recreation if I change them?
Many properties are immutable and trigger recreation: name, zone, type, physicalBlockSizeBytes, image, snapshot, sourceDisk, guestOsFeatures, licenses, diskEncryptionKey, multiWriter, enableConfidentialCompute, resourcePolicies, and several others. Check the schema for the complete list.
Source Options & Data
What are my options for creating a disk from existing data?

You have five options:

  1. Image - Use image to create from a machine image
  2. Snapshot - Use snapshot to restore from a snapshot
  3. Clone disk - Use sourceDisk to clone an existing disk
  4. Instant snapshot - Use sourceInstantSnapshot for instant snapshot restore
  5. Cloud Storage - Use sourceStorageObject with a GCS tarball (.tar.gz) or vmdk file
Configuration & Features
What physical block sizes are supported?
Currently 4096 and 16384 bytes are supported. This property is immutable after disk creation.
Can I attach a disk to multiple instances?
Yes, set multiWriter to true (immutable). For Hyperdisk, you can also use accessMode with READ_WRITE_MANY or READ_ONLY_SINGLE for multi-instance attachment.
How do I create a snapshot automatically when deleting a disk?
Set createSnapshotBeforeDestroy to true. The snapshot will be named {{disk-name}}-YYYYMMDD-HHmm by default, or use createSnapshotBeforeDestroyPrefix for a custom prefix.
How do I update resource policies on a disk?
Don’t use the resourcePolicies property for updates, as it doesn’t support updating more than one policy at a time. Instead, use the gcp.compute.DiskResourcePolicyAttachment resource.
Why don't I see all labels on my disk in Pulumi state?
The labels field is non-authoritative and only manages labels present in your configuration. Use effectiveLabels to see all labels on the resource, including those set by other clients or services.

Using a different cloud?

Explore storage guides for other cloud providers: