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. Persistent disks exist independently from VM instances, allowing you to detach, move, or preserve data even after deleting instances. This guide focuses on three capabilities: creating disks from public images, asynchronous cross-zone replication, and guest OS features and licensing.

Disks reference public images, zones, and optionally other disks for replication. The examples are intentionally small. Combine them with your own VM instances, snapshots, and encryption configuration.

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 zone property determines where the disk resides; disks can only attach to instances in the same zone. The physicalBlockSizeBytes property sets the block size to 4096 bytes, which affects performance characteristics for certain workloads.

Set up asynchronous replication between zones

Applications requiring disaster recovery can replicate disks across zones with eventual consistency.

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 creates a secondary disk that mirrors the primary disk’s data. The disk field references the primary disk’s ID. Replication happens asynchronously, meaning the secondary disk may lag slightly behind the primary. Both disks must be the same type (pd-ssd in this case).

Configure guest OS features and licenses

Bootable disks for Windows or specialized workloads require guest OS features and licenses.

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 property enables capabilities like SECURE_BOOT, MULTI_IP_SUBNET, and WINDOWS. These features are only applicable to bootable disks. The licenses property specifies Windows Server Core licensing, which is required for Windows-based VMs. Without the correct license URI, Windows instances won’t activate properly.

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 public images (Debian, Windows) and zones for disk placement. 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)
  • IOPS and throughput provisioning for Hyperdisk (provisionedIops, provisionedThroughput)
  • Resource policies for automatic 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 Resizing & Updates
What happens if I reduce my disk size?
Downsizing triggers disk recreation. Pulumi only supports in-place updates for upsizing. Use lifecycle.prevent_destroy to prevent accidental recreation.
How often can I update IOPS or throughput on Hyperdisk?
Hyperdisk allows provisionedIops and provisionedThroughput updates every 4 hours. For more frequent changes, you’ll need to delete and recreate the disk.
How do I update resource policies attached to a disk?
The resourcePolicies field doesn’t support direct updates (only one policy can be updated at a time). Use gcp.compute.DiskResourcePolicyAttachment instead.
Disk Creation & Initialization
What's the difference between image, snapshot, and sourceDisk?
Use image to initialize from an OS image, snapshot for a point-in-time copy, or sourceDisk to clone an existing disk. All three are immutable after creation.
What are the supported physical block sizes?
4096 and 16384 bytes. This is immutable after creation, so choose carefully based on your workload.
Can I create multiple disks from a Cloud Storage object?
While sourceStorageObject works, it’s not optimized for creating multiple disks. Use gcloud compute images import instead for bulk operations.
Encryption & Security
Can I enable confidential compute on any disk type?
No, enableConfidentialCompute is only supported on Hyperdisk SKUs and requires diskEncryptionKey to be configured.
What guest OS features can I enable on bootable disks?
You can enable SECURE_BOOT, MULTI_IP_SUBNET, and WINDOWS via guestOsFeatures. These are immutable after creation and only apply to bootable disks.
Advanced Features
How do I set up async replication between zones?
Create a primary disk in one zone, then create a secondary disk in another zone with asyncPrimaryDisk.disk pointing to the primary disk’s ID.
What access modes are available for Hyperdisk?
Hyperdisk supports READ_WRITE_SINGLE (default), READ_WRITE_MANY (multi-instance RW), and READ_ONLY_SINGLE (multi-instance RO). Standard disks only support the default mode.
How do I prevent accidental disk deletion?
Set createSnapshotBeforeDestroy to true. This creates a snapshot named {{disk-name}}-YYYYMMDD-HHmm before deletion, preserving your data.
Immutability & Constraints
What properties can't be changed after disk creation?
These are immutable: name, zone, type, physicalBlockSizeBytes, image, snapshot, sourceDisk, guestOsFeatures, diskEncryptionKey, and asyncPrimaryDisk. Plan carefully before creating.
Is the interface property still used?
No, interface is deprecated and will be removed. Disk interfaces (SCSI/NVME) are now automatically determined on attachment, so you can safely remove this from your configuration.

Using a different cloud?

Explore storage guides for other cloud providers: