published on Monday, Oct 5, 2026 by Pulumi
Migrating from GCP 9.x to 10.x
published on Monday, Oct 5, 2026 by Pulumi
Pulumi GCP Provider Version Upgrade Guide
Version 10.0.0 of the GCP provider for Pulumi is a major release and includes changes that you need to consider when upgrading. This guide will help with that process and focuses only on changes from version 9.x to version 10.0.0. See the Version 9 Upgrade Guide for information on upgrading from 8.x to version 9.0.0. Version 10.0.0 tracks the upstream terraform-provider-google-beta v8.5.0 release, so the google-beta v8 upgrade guide is the companion document; each breaking change below links to the upstream section it comes from, where there is one.
How to upgrade
Follow the procedure for a provider major version:
pulumi up --refresh # before the bump, with 9.x still installed
# bump @pulumi/gcp (or the equivalent for your language) to 10.0.0
pulumi up --refresh --run-program # after the bump
--refresh keeps pre-existing drift out of the diff, and --run-program is what lets the new provider compute a clean one: a refresh without it does not execute your program and works from the provider configuration and inputs already recorded in state. See Removing spurious diffs after a provider upgrade.
The bump itself, per language:
npm install @pulumi/gcp@^10.0.0
pip install --upgrade 'pulumi-gcp>=10.0.0,<11.0.0'
go get github.com/pulumi/pulumi-gcp/sdk/v10@v10.0.0
Then change every import from github.com/pulumi/pulumi-gcp/sdk/v9/... to github.com/pulumi/pulumi-gcp/sdk/v10/... and run go mod tidy to drop the v9 module.
dotnet add package Pulumi.Gcp --version 10.*
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>gcp</artifactId>
<version>10.0.0</version>
</dependency>
pulumi package add gcp@10.0.0
Run the detection commands in this guide before the bump, while 9.x is still installed. They read your stack’s state and change nothing.
Breaking changes
GCP provider v10.0 includes several breaking changes. These are the ones we consider the most impactful and noteworthy, ordered by what each one does to a live stack. Every other change in the release, including the rest of the removed resources and functions, is listed under Other changes.
gcp.compute.Instance: aguestAcceleratorcount of0now replaces the instancegcp.iap.Brandandgcp.iap.Client: removedgcp.notebooks: the module has been removedgcp.compute.BackendServiceandgcp.compute.GlobalForwardingRule:loadBalancingSchemenow defaults toEXTERNAL_MANAGEDgcp.bigquery.Dataset: an undeclareddefaultCollationis now clearedgcp.secretmanager.SecretVersion:secretDataWoVersionis required alongsidesecretDataWo, and is a stringgcp.monitoring.UptimeCheckConfig: the two password fields are now mutually exclusivegcp.container.Clusterandgcp.container.NodePool:namePrefixmay now be up to 31 charactersgcp.workflows.Workflow:sourceContentsis now requiredgcp.cloudrunv2.WorkerPool: probe header fields changedgcp.compute.ServiceAttachment:natSubnetsandconsumerRejectListsare now setsgcp.container.Cluster: theenableComponentsfields are now setsgcp.compute.Reservation: top-levelreservationBlockCountremoved
gcp.compute.Instance: a guestAccelerator count of 0 now replaces the instance
gcp.compute.Instance now acts on a guestAccelerator block whose count is 0, where v9 ignored it. Detaching an accelerator cannot be done in place, so the instance is replaced.
This affects instances that have an accelerator card attached, and instances that declare two or more zero-count blocks with nothing attached.
Omitting the block is the safe form: it leaves an attached accelerator in place. An empty guestAccelerators list is not the same as omitting it. With an accelerator attached it replaces the instance, just as a count: 0 block does, and it did so on v9 too.
Upstream: google_compute_instance in the google-beta v8 upgrade guide.
Impact/Risk
If you declare a count: 0 block and the instance has an accelerator card attached, pulumi preview shows a replacement of the instance and pulumi up carries it out. Replacement, not an update. No type or signature changed, so the program still compiles and pulumi preview still exits cleanly; the replacement shows up only in what pulumi preview prints.
A replacement destroys the boot disk and any local SSDs, and changes the external IP, the internal IP and the instance id. Local SSDs cannot be snapshotted, so what was on them is unrecoverable. Disks and addresses declared as their own resources survive and are reattached.
Nothing is snapshotted on the way through, so take a snapshot first. An instance with an explicit name is deleted before its replacement is created, so there is an interval with no instance and nothing to roll back to if the create fails.
Neither pulumi up --refresh before the bump nor pulumi up --refresh --run-program after it avoids the replacement.
Am I affected?
This reads your stack’s state and changes nothing. Run it before you upgrade.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:compute/instance:Instance")
| (.inputs.guestAccelerators // []) as $cfg
| select(($cfg | map(select(.count == 0)) | length) > 0)
| (.outputs.guestAccelerators // []) as $live
| "\(.urn)\n configured = \($cfg | map({count, type}) | tojson)\n attached = \($live | map({count, type: (.type | split("/") | last)}) | tojson)"
'
Output from a stack with one instance of each kind:
urn:pulumi:dev::my-stack::gcp:compute/instance:Instance::no-accelerator-instance
configured = [{"count":0,"type":"nvidia-tesla-t4"}]
attached = []
urn:pulumi:dev::my-stack::gcp:compute/instance:Instance::gpu-instance
configured = [{"count":0,"type":"nvidia-tesla-t4"}]
attached = [{"count":1,"type":"nvidia-tesla-t4"}]
Reading it:
attachedis not empty — this instance will be replaced. See the Remediation section below before upgrading.attachedis empty andconfiguredholds two or more blocks — this instance will be replaced. See the Remediation section below before upgrading.attachedis empty andconfiguredholds exactly one block — nothing changes on upgrade, but the block stays live: attaching an accelerator or adding a second zero-count block later replaces the instance, on v10 only. See the Remediation section below anyway.- no output at all — no instance in the stack declares a zero-count accelerator, so you are not affected.
Remediation
When migrating from v9, build the guestAccelerators list so that it is omitted when you want no accelerator, rather than present with count: 0. Omitting it leaves an attached accelerator in place, so nothing is replaced.
const gpu = new gcp.compute.Instance("gpu-instance", {
name: "gpu-instance",
zone: zone,
machineType: "n1-standard-1",
bootDisk: {
initializeParams: { image: image, size: 10 },
},
scratchDisks: [{ interface: "NVME" }],
attachedDisks: [{ source: dataDisk.selfLink, deviceName: "data-disk" }],
networkInterfaces: [{
network: "default",
accessConfigs: [{}],
}],
metadataStartupScript: startupScript,
// v9 (will cause replacement)
// guestAccelerators: [{ count: enableGpu ? 1 : 0, type: "nvidia-tesla-t4" }],
// v10 (fixed)
guestAccelerators: enableGpu ? [{ count: 1, type: "nvidia-tesla-t4" }] : undefined,
scheduling: { onHostMaintenance: "TERMINATE", automaticRestart: true },
});
gpu = gcp.compute.Instance("gpu-instance",
name="gpu-instance",
zone=zone,
machine_type="n1-standard-1",
boot_disk={
"initialize_params": {
"image": image,
"size": 10,
},
},
scratch_disks=[{
"interface": "NVME",
}],
attached_disks=[{
"source": data_disk.self_link,
"device_name": "data-disk",
}],
network_interfaces=[{
"network": "default",
"access_configs": [{}],
}],
metadata_startup_script=startup_script,
# v9 (will cause replacement)
# guest_accelerators=[{"count": 1 if enable_gpu else 0, "type": "nvidia-tesla-t4"}],
# v10 (fixed)
guest_accelerators=[{
"count": 1,
"type": "nvidia-tesla-t4",
}] if enable_gpu else None,
scheduling={
"on_host_maintenance": "TERMINATE",
"automatic_restart": True,
})
// v9 (will cause replacement)
// GuestAccelerators: compute.InstanceGuestAcceleratorArray{&compute.InstanceGuestAcceleratorArgs{
// Count: pulumi.Int(gpuCount), Type: pulumi.String("nvidia-tesla-t4"),
// }},
// v10 (fixed)
var tmp0 compute.InstanceGuestAcceleratorArray
if enableGpu {
tmp0 = compute.InstanceGuestAcceleratorArray{
&compute.InstanceGuestAcceleratorArgs{
Count: pulumi.Int(1),
Type: pulumi.String("nvidia-tesla-t4"),
},
}
} else {
tmp0 = nil
}
_, err := compute.NewInstance(ctx, "gpu-instance", &compute.InstanceArgs{
Name: pulumi.String("gpu-instance"),
Zone: pulumi.String(zone),
MachineType: pulumi.String("n1-standard-1"),
BootDisk: &compute.InstanceBootDiskArgs{
InitializeParams: &compute.InstanceBootDiskInitializeParamsArgs{
Image: pulumi.String(image),
Size: pulumi.Int(10),
},
},
ScratchDisks: compute.InstanceScratchDiskArray{
&compute.InstanceScratchDiskArgs{
Interface: pulumi.String("NVME"),
},
},
AttachedDisks: compute.InstanceAttachedDiskArray{
&compute.InstanceAttachedDiskArgs{
Source: dataDisk.SelfLink,
DeviceName: pulumi.String("data-disk"),
},
},
NetworkInterfaces: compute.InstanceNetworkInterfaceArray{
&compute.InstanceNetworkInterfaceArgs{
Network: pulumi.String("default"),
AccessConfigs: compute.InstanceNetworkInterfaceAccessConfigArray{
&compute.InstanceNetworkInterfaceAccessConfigArgs{},
},
},
},
MetadataStartupScript: pulumi.String(startupScript),
GuestAccelerators: tmp0,
Scheduling: &compute.InstanceSchedulingArgs{
OnHostMaintenance: pulumi.String("TERMINATE"),
AutomaticRestart: pulumi.Bool(true),
},
})
if err != nil {
return err
}
var args = new Gcp.Compute.InstanceArgs
{
Name = "gpu-instance",
Zone = zone,
MachineType = "n1-standard-1",
BootDisk = new Gcp.Compute.Inputs.InstanceBootDiskArgs
{
InitializeParams = new Gcp.Compute.Inputs.InstanceBootDiskInitializeParamsArgs
{
Image = image,
Size = 10,
},
},
ScratchDisks = new[]
{
new Gcp.Compute.Inputs.InstanceScratchDiskArgs
{
Interface = "NVME",
},
},
AttachedDisks = new[]
{
new Gcp.Compute.Inputs.InstanceAttachedDiskArgs
{
Source = dataDisk.SelfLink,
DeviceName = "data-disk",
},
},
NetworkInterfaces = new[]
{
new Gcp.Compute.Inputs.InstanceNetworkInterfaceArgs
{
Network = "default",
AccessConfigs = new[]
{
new Gcp.Compute.Inputs.InstanceNetworkInterfaceAccessConfigArgs(),
},
},
},
MetadataStartupScript = startupScript,
Scheduling = new Gcp.Compute.Inputs.InstanceSchedulingArgs
{
OnHostMaintenance = "TERMINATE",
AutomaticRestart = true,
},
};
// v9 (will cause replacement)
// GuestAccelerators = new[] { new Gcp.Compute.Inputs.InstanceGuestAcceleratorArgs
// { Count = enableGpu ? 1 : 0, Type = "nvidia-tesla-t4" } },
// v10 (fixed): set the property only when an accelerator is wanted.
if (enableGpu)
{
args.GuestAccelerators = new[]
{
new Gcp.Compute.Inputs.InstanceGuestAcceleratorArgs
{
Count = 1,
Type = "nvidia-tesla-t4",
},
};
}
var gpu = new Gcp.Compute.Instance("gpu-instance", args);
var builder = InstanceArgs.builder()
.name("gpu-instance")
.zone(zone)
.machineType("n1-standard-1")
.bootDisk(InstanceBootDiskArgs.builder()
.initializeParams(InstanceBootDiskInitializeParamsArgs.builder()
.image(image)
.size(10)
.build())
.build())
.scratchDisks(InstanceScratchDiskArgs.builder()
.interface_("NVME")
.build())
.attachedDisks(InstanceAttachedDiskArgs.builder()
.source(dataDisk.selfLink())
.deviceName("data-disk")
.build())
.networkInterfaces(InstanceNetworkInterfaceArgs.builder()
.network("default")
.accessConfigs(InstanceNetworkInterfaceAccessConfigArgs.builder()
.build())
.build())
.metadataStartupScript(startupScript)
.scheduling(InstanceSchedulingArgs.builder()
.onHostMaintenance("TERMINATE")
.automaticRestart(true)
.build());
// v9 (will cause replacement)
// .guestAccelerators(InstanceGuestAcceleratorArgs.builder()
// .count(enableGpu ? 1 : 0).type("nvidia-tesla-t4").build())
// v10 (fixed): call the setter only when an accelerator is wanted. The varargs
// overload has no null you can pass to mean "omitted"; List.of throws on it.
if (enableGpu) {
builder.guestAccelerators(InstanceGuestAcceleratorArgs.builder()
.count(1)
.type("nvidia-tesla-t4")
.build());
}
var gpu = new Instance("gpu-instance", builder.build());
resources:
gpu:
type: gcp:compute:Instance
name: gpu-instance
properties:
name: gpu-instance
zone: ${zone}
machineType: n1-standard-1
bootDisk:
initializeParams:
image: ${image}
size: 10
scratchDisks:
- interface: NVME
attachedDisks:
- source: ${dataDisk.selfLink}
deviceName: data-disk
networkInterfaces:
- network: default
accessConfigs:
- {}
metadataStartupScript: ${startupScript}
# v9 (will cause replacement): count: 0 meant "no accelerator"
# v10 (fixed): omit the whole guestAccelerators key instead. Pulumi YAML
# has no conditional, so this arm shows the accelerator attached.
guestAccelerators:
- count: 1
type: nvidia-tesla-t4
scheduling:
onHostMaintenance: TERMINATE
automaticRestart: true
If you do want the accelerators detached, a count: 0 block will detach and replace your instance.
gcp.iap.Brand and gcp.iap.Client: removed
Google shut down the IAP OAuth Admin APIs and there is no replacement resource.
So gcp.iap.Brand, gcp.iap.Client and the gcp.iap.getClient data source are removed in v10. OAuth clients keep existing in Google Cloud; manage them in the Google Cloud Console.
Upstream: google_iap_brand and google_iap_client in the google-beta v8 upgrade guide.
Impact/Risk
The rest of the IAP module, including gcp.iap.Settings, gcp.iap.TunnelDestGroup and the IAP IAM resources, is unchanged.
If your program contains these resources, you have to remove them from both your program and your state before running it on v10. If you only remove them from your program, the next pulumi up on v10 deletes them: each state entry records the provider version that created it, so the engine downloads that v9 provider and asks it to do the delete, and the run succeeds and prints no warning. The client ID and secret are gone, every application signing in through them stops working, and no v10 resource can recreate them.
If you deleted the resources by mistake, reissuing one is a manual step in Google Cloud, see Google’s guidance, and it produces a different client ID and secret, so everything referencing the old one has to be updated.
Am I affected?
Run the following command against each stack to determine whether you are affected. It is read-only.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:iap/brand:Brand" or .type == "gcp:iap/client:Client")
| "\(.type)\n \(.urn)"
'
Output from a stack holding both:
gcp:iap/brand:Brand
urn:pulumi:dev::my-stack::gcp:iap/brand:Brand::iap-brand
gcp:iap/client:Client
urn:pulumi:dev::my-stack::gcp:iap/client:Client::iap-client
Reading it:
- a
gcp:iap/client:Clientline — remove it from state before your nextpulumi up, or that OAuth client is deleted in Google Cloud. See the Remediation section below and follow it in order. - a
gcp:iap/brand:Brandline — remove it from state too, see the Remediation section below. Nothing in Google Cloud is lost either way. - no output at all — no IAP brand or client is under management in this stack, so you are not affected.
The command cannot see gcp.iap.getClient, because a data source is never written to state. Search your program for it separately.
Remediation
First take the resources out of state. Neither command calls Google Cloud, so the brand and the OAuth client stay exactly as they are. Delete the client before the brand, because the brand cannot go while something still references it.
pulumi state delete 'urn:pulumi:dev::my-stack::gcp:iap/client:Client::iap-client'
pulumi state delete 'urn:pulumi:dev::my-stack::gcp:iap/brand:Brand::iap-brand'
Note: Taking the brand first fails, with the fix in the message:
error: urn:pulumi:dev::...::gcp:iap/brand:Brand::iap-brand can't be safely deleted because the following resources depend on it:
* "iap-client" (urn:pulumi:dev::...::gcp:iap/client:Client::iap-client)
Delete those resources first or pass --target-dependents.
Then delete every gcp.iap.Brand and gcp.iap.Client from your program. Nothing takes their place; Pulumi just stops managing resources that go on existing in Google Cloud.
If you read an existing client with the gcp.iap.getClient data source, you will need to read the client from the IAP Console and keep it in config; there is no replacement data source. Google documents the credentials themselves in Use custom OAuth clients with IAP:
const cfg = new pulumi.Config();
// v9
// const existing = gcp.iap.getClientOutput({ brand: brand.name, clientId: client.clientId });
// export const iapClientSecret = existing.secret;
// v10 (fixed), after:
// pulumi config set iapClientId <id>
// pulumi config set --secret iapClientSecret <secret>
export const iapClientId = cfg.require("iapClientId");
export const iapClientSecret = cfg.requireSecret("iapClientSecret");
gcp.notebooks: the module has been removed
The whole gcp.notebooks namespace has been removed: Instance, Runtime, Environment, their IAM resources (InstanceIamPolicy, InstanceIamBinding, InstanceIamMember, RuntimeIamPolicy, RuntimeIamBinding, RuntimeIamMember) and the getInstanceIamPolicy and getRuntimeIamPolicy functions. The products behind them, Vertex AI Workbench User-Managed and Google-Managed Notebooks, have reached end of life, and Google already refuses to create new instances of either. Use gcp.workbench.Instance and gcp.workbench.InstanceIamMember instead.
Upstream: google_notebooks_instance in the google-beta v8 upgrade guide.
Impact/Risk
Workbench resources are untouched by this change.
If your program contains these resources, you have to remove them from both your program and your state before running it on v10. Otherwise the upgrade breaks in two stages.
Your program stops working. Every reference to gcp.notebooks fails: a build error in compiled languages, and in interpreted ones the program throws while loading, so pulumi preview exits non-zero before it reaches your resources.
Then removing the references deletes your notebook. If you only remove them from your program, pulumi preview succeeds and shows a delete of the entries left in state. It is a delete rather than a replacement, and the engine does not need v10 to know the type: it routes the delete to the 9.x provider recorded alongside the resource, which calls the legacy API. For a notebook that is still a notebook, that destroys the VM, its boot disk and, unless the instance was created with noRemoveDataDisk: true, its data disk and everything in the home directory. Running the documented pulumi up --refresh --run-program produces the same plan.
The machine may no longer be a notebook. Google’s published schedule converted user-managed notebooks that were never migrated into plain Compute Engine VMs on 2026-03-30, and a converted instance is not visible to Vertex AI Workbench. What a legacy delete does to a converted machine is untested, because Google no longer lets anyone create a legacy notebook to try it on. Both readings lead to the same move: take the resources out of state rather than letting pulumi up delete them.
Am I affected?
Run the following command against each stack to list the resources that the v10 provider no longer has a type for:
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type | startswith("gcp:notebooks/"))
| "\(.type)\n \(.urn)"
'
Output from a stack with a legacy instance and one of its IAM bindings:
gcp:notebooks/instance:Instance
urn:pulumi:dev::my-stack::gcp:notebooks/instance:Instance::legacy-notebook
gcp:notebooks/instanceIamMember:InstanceIamMember
urn:pulumi:dev::my-stack::gcp:notebooks/instanceIamMember:InstanceIamMember::legacy-notebook-viewer
No output means the stack holds nothing from the removed namespace and you are not affected. Any output at all means the stack is affected: follow the Remediation section below before you upgrade, because the wrong order deletes the machine.
Remediation
If you want to keep the existing machine, do not let pulumi up carry out that delete. Nothing is destroyed on this path.
Migrate the instance at the GCP level first. A legacy notebook is served by
notebooks.googleapis.com/v1and a Workbench instance by/v2, so until it is migratedgcp.workbench.Instancecannot see it.curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \ "https://notebooks.googleapis.com/v1/projects/PROJECT/locations/ZONE/instances/NAME:migrate"Drop the legacy entries from Pulumi’s state. This edits state only and leaves the machine running. Delete the IAM resources before the instance they point at.
pulumi state delete 'urn:pulumi:dev::my-stack::gcp:notebooks/instanceIamMember:InstanceIamMember::legacy-notebook-viewer' pulumi state delete 'urn:pulumi:dev::my-stack::gcp:notebooks/instance:Instance::legacy-notebook'Rewrite the code against
gcp.workbench(below), then adopt the migrated machine rather than creating a second one.pulumi import gcp:workbench/instance:Instance legacy-notebook \ projects/PROJECT/locations/ZONE/instances/NAME
If you are content to recreate, change the types and run pulumi up. The legacy instance is destroyed and a new Workbench instance is created; disk contents do not carry over. The machine settings move under gceSetup, the disk sizes become strings, and the IAM resource’s instanceName becomes name.
v9:
const instance = new gcp.notebooks.Instance("legacy-notebook", {
name: "legacy-notebook",
location: "europe-west2-a",
machineType: "e2-medium",
vmImage: {
project: "cloud-notebooks-managed",
imageFamily: "workbench-instances",
},
dataDiskSizeGb: 100,
dataDiskType: "PD_BALANCED",
});
const viewer = new gcp.notebooks.InstanceIamMember("legacy-notebook-viewer", {
project: instance.project,
location: instance.location,
instanceName: instance.name,
role: "roles/notebooks.viewer",
member: sa.email.apply(e => `serviceAccount:${e}`),
});
instance = gcp.notebooks.Instance("legacy-notebook",
name="legacy-notebook",
location="europe-west2-a",
machine_type="e2-medium",
vm_image={
"project": "cloud-notebooks-managed",
"image_family": "workbench-instances",
},
data_disk_size_gb=100,
data_disk_type="PD_BALANCED")
viewer = gcp.notebooks.InstanceIamMember("legacy-notebook-viewer",
project=instance.project,
location=instance.location,
instance_name=instance.name,
role="roles/notebooks.viewer",
member=sa.email.apply(lambda email: f"serviceAccount:{email}"))
instance, err := notebooks.NewInstance(ctx, "legacy-notebook", ¬ebooks.InstanceArgs{
Name: pulumi.String("legacy-notebook"),
Location: pulumi.String("europe-west2-a"),
MachineType: pulumi.String("e2-medium"),
VmImage: ¬ebooks.InstanceVmImageArgs{
Project: pulumi.String("cloud-notebooks-managed"),
ImageFamily: pulumi.String("workbench-instances"),
},
DataDiskSizeGb: pulumi.Int(100),
DataDiskType: pulumi.String("PD_BALANCED"),
})
if err != nil {
return err
}
_, err = notebooks.NewInstanceIamMember(ctx, "legacy-notebook-viewer", ¬ebooks.InstanceIamMemberArgs{
Project: instance.Project,
Location: instance.Location,
InstanceName: instance.Name,
Role: pulumi.String("roles/notebooks.viewer"),
Member: sa.Email.ApplyT(func(email string) (string, error) {
return fmt.Sprintf("serviceAccount:%v", email), nil
}).(pulumi.StringOutput),
})
if err != nil {
return err
}
Go needs fmt imported for the interpolation.
var instance = new Gcp.Notebooks.Instance("legacy-notebook", new()
{
Name = "legacy-notebook",
Location = "europe-west2-a",
MachineType = "e2-medium",
VmImage = new Gcp.Notebooks.Inputs.InstanceVmImageArgs
{
Project = "cloud-notebooks-managed",
ImageFamily = "workbench-instances",
},
DataDiskSizeGb = 100,
DataDiskType = "PD_BALANCED",
});
var viewer = new Gcp.Notebooks.InstanceIamMember("legacy-notebook-viewer", new()
{
Project = instance.Project,
Location = instance.Location,
InstanceName = instance.Name,
Role = "roles/notebooks.viewer",
Member = sa.Email.Apply(email => $"serviceAccount:{email}"),
});
var instance = new Instance("legacy-notebook", InstanceArgs.builder()
.name("legacy-notebook")
.location("europe-west2-a")
.machineType("e2-medium")
.vmImage(InstanceVmImageArgs.builder()
.project("cloud-notebooks-managed")
.imageFamily("workbench-instances")
.build())
.dataDiskSizeGb(100)
.dataDiskType("PD_BALANCED")
.build());
var viewer = new InstanceIamMember("legacy-notebook-viewer", InstanceIamMemberArgs.builder()
.project(instance.project())
.location(instance.location())
.instanceName(instance.name())
.role("roles/notebooks.viewer")
.member(sa.email().applyValue(_email -> String.format("serviceAccount:%s", _email)))
.build());
resources:
instance:
type: gcp:notebooks:Instance
name: legacy-notebook
properties:
name: legacy-notebook
location: europe-west2-a
machineType: e2-medium
vmImage:
project: cloud-notebooks-managed
imageFamily: workbench-instances
dataDiskSizeGb: 100
dataDiskType: PD_BALANCED
viewer:
type: gcp:notebooks:InstanceIamMember
name: legacy-notebook-viewer
properties:
project: ${instance.project}
location: ${instance.location}
instanceName: ${instance.name}
role: roles/notebooks.viewer
member: serviceAccount:${sa.email}
v10:
const instance = new gcp.workbench.Instance("workbench-instance", {
name: "workbench-instance",
location: "europe-west2-a",
gceSetup: {
machineType: "e2-medium",
dataDisks: {
diskSizeGb: "100",
diskType: "PD_BALANCED",
},
},
});
const viewer = new gcp.workbench.InstanceIamMember("workbench-viewer", {
project: instance.project,
location: instance.location,
name: instance.name,
role: "roles/notebooks.viewer",
member: sa.email.apply(e => `serviceAccount:${e}`),
});
instance = gcp.workbench.Instance("workbench-instance",
name="workbench-instance",
location="europe-west2-a",
gce_setup={
"machine_type": "e2-medium",
"data_disks": {
"disk_size_gb": "100",
"disk_type": "PD_BALANCED",
},
})
viewer = gcp.workbench.InstanceIamMember("workbench-viewer",
project=instance.project,
location=instance.location,
name=instance.name,
role="roles/notebooks.viewer",
member=sa.email.apply(lambda email: f"serviceAccount:{email}"))
instance, err := workbench.NewInstance(ctx, "workbench-instance", &workbench.InstanceArgs{
Name: pulumi.String("workbench-instance"),
Location: pulumi.String("europe-west2-a"),
GceSetup: &workbench.InstanceGceSetupArgs{
MachineType: pulumi.String("e2-medium"),
DataDisks: &workbench.InstanceGceSetupDataDisksArgs{
DiskSizeGb: pulumi.String("100"),
DiskType: pulumi.String("PD_BALANCED"),
},
},
})
if err != nil {
return err
}
_, err = workbench.NewInstanceIamMember(ctx, "workbench-viewer", &workbench.InstanceIamMemberArgs{
Project: instance.Project,
Location: instance.Location,
Name: instance.Name,
Role: pulumi.String("roles/notebooks.viewer"),
Member: sa.Email.ApplyT(func(email string) (string, error) {
return fmt.Sprintf("serviceAccount:%v", email), nil
}).(pulumi.StringOutput),
})
if err != nil {
return err
}
Go needs fmt imported for the interpolation.
var instance = new Gcp.Workbench.Instance("workbench-instance", new()
{
Name = "workbench-instance",
Location = "europe-west2-a",
GceSetup = new Gcp.Workbench.Inputs.InstanceGceSetupArgs
{
MachineType = "e2-medium",
DataDisks = new Gcp.Workbench.Inputs.InstanceGceSetupDataDisksArgs
{
DiskSizeGb = "100",
DiskType = "PD_BALANCED",
},
},
});
var viewer = new Gcp.Workbench.InstanceIamMember("workbench-viewer", new()
{
Project = instance.Project,
Location = instance.Location,
Name = instance.Name,
Role = "roles/notebooks.viewer",
Member = sa.Email.Apply(email => $"serviceAccount:{email}"),
});
var instance = new Instance("workbench-instance", InstanceArgs.builder()
.name("workbench-instance")
.location("europe-west2-a")
.gceSetup(InstanceGceSetupArgs.builder()
.machineType("e2-medium")
.dataDisks(InstanceGceSetupDataDisksArgs.builder()
.diskSizeGb("100")
.diskType("PD_BALANCED")
.build())
.build())
.build());
var viewer = new InstanceIamMember("workbench-viewer", InstanceIamMemberArgs.builder()
.project(instance.project())
.location(instance.location())
.name(instance.name())
.role("roles/notebooks.viewer")
.member(sa.email().applyValue(_email -> String.format("serviceAccount:%s", _email)))
.build());
resources:
instance:
type: gcp:workbench:Instance
name: workbench-instance
properties:
name: workbench-instance
location: europe-west2-a
gceSetup:
machineType: e2-medium
dataDisks:
diskSizeGb: '100'
diskType: PD_BALANCED
viewer:
type: gcp:workbench:InstanceIamMember
name: workbench-viewer
properties:
project: ${instance.project}
location: ${instance.location}
name: ${instance.name}
role: roles/notebooks.viewer
member: serviceAccount:${sa.email}
gcp.notebooks.Runtime and gcp.notebooks.Environment are removed the same way and have the same replacement: a Runtime becomes a gcp.workbench.Instance, and an Environment’s settings (VM or container image, post-startup script) are set directly on gcp.workbench.Instance.
gcp.compute.BackendService and gcp.compute.GlobalForwardingRule: loadBalancingScheme now defaults to EXTERNAL_MANAGED
loadBalancingScheme defaulted to EXTERNAL, a Classic Application Load Balancer, and now defaults to EXTERNAL_MANAGED, a global external Application Load Balancer. Set loadBalancingScheme: "EXTERNAL" explicitly to keep what you have. See the Application Load Balancer overview for the difference between the two.
Only these two resources changed. The regional gcp.compute.ForwardingRule and gcp.compute.RegionBackendService keep their v9 defaults, EXTERNAL and INTERNAL.
Upstream: google_compute_backend_service and google_compute_global_forwarding_rule in the google-beta v8 upgrade guide.
Impact/Risk
Resources already in state keep EXTERNAL. The field is not replacing on either resource, so upgrading on its own neither recreates nor reconfigures a live load balancer.
The new default arrives whenever one of these resources is created, which includes a replacement and includes running the same program against a new stack. Expanding a service into another region builds a global external Application Load Balancer where the original is classic, and nothing in the diff says so.
A new load balancer that mixes the two schemes fails part way through the deployment. GCP checks that the schemes match when the forwarding rule is created, so a forwarding rule pinned to EXTERNAL against a backend service on the new default fails with Error 400: ... Load balancing scheme EXTERNAL does not match the backend service load balancing scheme EXTERNAL_MANAGED, after most of the other resources already exist.
An existing classic load balancer accepts a mismatched backend service silently. GCP does not run that check when a classic load balancer’s URL map points at an EXTERNAL_MANAGED backend service. The configuration is accepted and serves traffic normally, leaving the load balancer mixed with nothing to show it.
Standard Tier cannot use the new default. A global external Application Load Balancer requires Premium Tier; see the Network Service Tiers overview. Setting loadBalancingScheme: "EXTERNAL" explicitly keeps these load balancers working.
Going back is time limited and is a staged migration, not an edit. Pinning EXTERNAL on a resource that already exists as EXTERNAL_MANAGED is rejected with Downgrading the load balancing scheme to EXTERNAL is only supported for EXTERNAL_MANAGED backend services that were migrated from EXTERNAL in the last 90 days, and needs pulumi up --replace. Migrating forwards deliberately requires externalManagedMigrationState to reach TEST_ALL_TRAFFIC, via PREPARE and optionally TEST_BY_PERCENTAGE with externalManagedMigrationTestingPercentage, before loadBalancingScheme may become EXTERNAL_MANAGED, and the reverse order to roll back. The provider does not automate either direction.
Am I affected?
Run the following command against each stack to find the resources whose scheme would change if they were recreated. It is a preview and changes nothing. --replace is what surfaces them: a plain preview shows no scheme change at all, because the provider replays the default it recorded when the resource was created.
pulumi preview --replace '**' --json \
| jq -r '.steps[]
| select(.oldState.inputs.loadBalancingScheme != .newState.inputs.loadBalancingScheme)
| "\(.op)\t\(.oldState.inputs.loadBalancingScheme // "-") => \(.newState.inputs.loadBalancingScheme // "-")\t\(.urn)"'
Output from a stack holding one classic Application Load Balancer whose gcp.compute.BackendService and gcp.compute.GlobalForwardingRule do not name a scheme:
replace EXTERNAL => EXTERNAL_MANAGED urn:pulumi:dev::my-stack::gcp:compute/backendService:BackendService::backend
replace EXTERNAL => EXTERNAL_MANAGED urn:pulumi:dev::my-stack::gcp:compute/globalForwardingRule:GlobalForwardingRule::frontend
Anything it prints is a classic load balancer that would come back as a global external one. No output means every gcp.compute.BackendService and gcp.compute.GlobalForwardingRule in the stack already names its scheme, or the stack has none.
Remediation
Name the scheme on every Classic Application Load Balancer resource that does not already. This changes nothing live; it stops the default from moving under you.
const backend = new gcp.compute.BackendService("backend", {
protocol: "HTTP",
healthChecks: healthCheck.id,
// v9: the default was EXTERNAL, a Classic Application Load Balancer.
// v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
loadBalancingScheme: "EXTERNAL",
});
const rule = new gcp.compute.GlobalForwardingRule("rule", {
target: proxy.id,
portRange: "80",
loadBalancingScheme: "EXTERNAL",
});
backend = gcp.compute.BackendService("backend",
protocol="HTTP",
health_checks=health_check.id,
# v9: the default was EXTERNAL, a Classic Application Load Balancer.
# v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
load_balancing_scheme="EXTERNAL")
rule = gcp.compute.GlobalForwardingRule("rule",
target=proxy.id,
port_range="80",
load_balancing_scheme="EXTERNAL")
backend, err := compute.NewBackendService(ctx, "backend", &compute.BackendServiceArgs{
Protocol: pulumi.String("HTTP"),
HealthChecks: healthCheck.ID(),
// v9: the default was EXTERNAL, a Classic Application Load Balancer.
// v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
LoadBalancingScheme: pulumi.String("EXTERNAL"),
})
if err != nil {
return err
}
_, err = compute.NewGlobalForwardingRule(ctx, "rule", &compute.GlobalForwardingRuleArgs{
Target: proxy.ID(),
PortRange: pulumi.String("80"),
LoadBalancingScheme: pulumi.String("EXTERNAL"),
})
if err != nil {
return err
}
var backend = new Gcp.Compute.BackendService("backend", new()
{
Protocol = "HTTP",
HealthChecks = healthCheck.Id,
// v9: the default was EXTERNAL, a Classic Application Load Balancer.
// v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
LoadBalancingScheme = "EXTERNAL",
});
var rule = new Gcp.Compute.GlobalForwardingRule("rule", new()
{
Target = proxy.Id,
PortRange = "80",
LoadBalancingScheme = "EXTERNAL",
});
var backend = new BackendService("backend", BackendServiceArgs.builder()
.protocol("HTTP")
.healthChecks(healthCheck.id())
// v9: the default was EXTERNAL, a Classic Application Load Balancer.
// v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
.loadBalancingScheme("EXTERNAL")
.build());
var rule = new GlobalForwardingRule("rule", GlobalForwardingRuleArgs.builder()
.target(proxy.id())
.portRange("80")
.loadBalancingScheme("EXTERNAL")
.build());
resources:
backend:
type: gcp:compute:BackendService
properties:
protocol: HTTP
healthChecks: ${healthCheck.id}
# v9: the default was EXTERNAL, a Classic Application Load Balancer.
# v10 (fixed): name it, or a new or replaced resource becomes EXTERNAL_MANAGED.
loadBalancingScheme: EXTERNAL
rule:
type: gcp:compute:GlobalForwardingRule
properties:
target: ${proxy.id}
portRange: "80"
loadBalancingScheme: EXTERNAL
gcp.bigquery.Dataset: an undeclared defaultCollation is now cleared
defaultCollation is the collation that a new table inherits when it does not specify its own. If your dataset has one in BigQuery and your program does not declare it, the first pulumi up on v10 clears the dataset’s default, not any table’s collation.
v9 read the collation back from the API and kept it, so leaving the field out of your program preserved whatever BigQuery already had. v10 does not: an undeclared defaultCollation sends the empty string, which BigQuery treats as case sensitive.
Writing defaultCollation: "" explicitly already cleared the collation on v9.21.0 and later. Omitting the field and writing the empty string now produce the same request, so only the omitted case is new.
defaultCollation is also now an optional output, so code that reads dataset.defaultCollation has to handle an absent value.
Upstream: google_bigquery_dataset in the google-beta v8 upgrade guide.
Impact/Risk
No table is touched and no data changes. Existing tables keep the collation they were created with. Only tables created after the clear inherit none, so their string fields are case sensitive. The dataset update is in place and nothing is replaced.
The change affects a dataset only if BigQuery has a collation for it and your program declares none. That happens when the collation was set outside Pulumi, or when a defaultCollation line was deleted from the program and v9 went on reading the old value back.
Code that reads the defaultCollation output now has to handle an absent value. Where types are checked, an unguarded read fails to build. Elsewhere the value is simply missing at run time.
Am I affected?
Run the following command against each stack. It reads state and changes nothing.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:bigquery/dataset:Dataset")
| (if (.inputs | has("defaultCollation")) then .inputs.defaultCollation else null end) as $cfg
| (.outputs.defaultCollation // "") as $live
| (if ($live != "") and (($cfg // "") == "") then " AFFECTED" else " ok" end) as $flag
| "\(.urn)\n program: \($cfg | tojson) dataset: \($live | tojson) \($flag)"
'
Output from a stack holding one dataset of each kind:
urn:pulumi:dev::my-stack::gcp:bigquery/dataset:Dataset::ci-dataset
program: null dataset: "und:ci" AFFECTED
urn:pulumi:dev::my-stack::gcp:bigquery/dataset:Dataset::empty-collation-dataset
program: "" dataset: "" ok
urn:pulumi:dev::my-stack::gcp:bigquery/dataset:Dataset::no-collation-dataset
program: null dataset: "" ok
Reading it:
AFFECTED— the dataset has a collation your program does not set, and upgrading clears it. The literal you need is the one printed afterdataset:. See the Remediation section below.okwith a collation afterprogram:— your program already names it, nothing changes.okwith""afterdataset:— the dataset has no collation, nothing changes.- no output at all — the stack has no
gcp.bigquery.Dataset, so you are not affected.
Remediation
If your program does not set defaultCollation and the dataset has one, name it. This is an in-place update; the dataset is not replaced.
const ci = new gcp.bigquery.Dataset("ci-dataset", {
datasetId: "ci_dataset",
location: "europe-west2",
// v9: the provider read the collation back from the API, so it survived being unset.
// v10 (fixed): name it, or it is cleared.
defaultCollation: "und:ci",
deleteContentsOnDestroy: true,
});
ci = gcp.bigquery.Dataset("ci-dataset",
dataset_id="ci_dataset",
location="europe-west2",
# v9: the provider read the collation back from the API, so it survived being unset.
# v10 (fixed): name it, or it is cleared.
default_collation="und:ci",
delete_contents_on_destroy=True)
_, err := bigquery.NewDataset(ctx, "ci-dataset", &bigquery.DatasetArgs{
DatasetId: pulumi.String("ci_dataset"),
Location: pulumi.String("europe-west2"),
// v9: the provider read the collation back from the API, so it survived being unset.
// v10 (fixed): name it, or it is cleared.
DefaultCollation: pulumi.String("und:ci"),
DeleteContentsOnDestroy: pulumi.Bool(true),
})
if err != nil {
return err
}
var ci = new Gcp.BigQuery.Dataset("ci-dataset", new()
{
DatasetId = "ci_dataset",
Location = "europe-west2",
// v9: the provider read the collation back from the API, so it survived being unset.
// v10 (fixed): name it, or it is cleared.
DefaultCollation = "und:ci",
DeleteContentsOnDestroy = true,
});
var ci = new Dataset("ci-dataset", DatasetArgs.builder()
.datasetId("ci_dataset")
.location("europe-west2")
// v9: the provider read the collation back from the API, so it survived being unset.
// v10 (fixed): name it, or it is cleared.
.defaultCollation("und:ci")
.deleteContentsOnDestroy(true)
.build());
resources:
ci:
type: gcp:bigquery:Dataset
name: ci-dataset
properties:
datasetId: ci_dataset
location: europe-west2
# v9: the provider read the collation back from the API, so it survived being unset.
# v10 (fixed): name it, or it is cleared.
defaultCollation: und:ci
deleteContentsOnDestroy: true
If your code reads the defaultCollation output, give it a fallback. Because the provider always filled it in, the output was typed string on v9 and is string | undefined on v10, so an unguarded read stops compiling.
// v9
// export const ciCollationUpper = ci.defaultCollation.apply(c => c.toUpperCase());
// v10 (fixed)
export const ciCollationUpper = ci.defaultCollation.apply(c => (c ?? "").toUpperCase());
gcp.secretmanager.SecretVersion: secretDataWoVersion is required alongside secretDataWo, and is a string
gcp.secretmanager.SecretVersion changed in two ways.
secretDataWoandsecretDataWoVersionmust now be set together. On v9secretDataWoVersiondefaulted to0and could be omitted; on v10 it is required.secretDataWoVersionchanged type from integer to string.
Upstream: google_secret_manager_secret_version in the google-beta v8 upgrade guide.
Impact/Risk
If you set secretDataWoVersion to a number, quoting it is the whole fix and replaces nothing.
If you set secretDataWo but not secretDataWoVersion, preview fails on v10. Set it to "" and nothing is replaced. Set it to any other value, including "0", and the live secret version is replaced.
Am I affected?
Run the following command against each stack. It reads state and changes nothing.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:secretmanager/secretVersion:SecretVersion")
| select(.inputs.secretDataWo != null)
| .inputs.secretDataWoVersion as $v
| "\(.urn)\n secretDataWoVersion = \($v | tojson) (\($v | type))"
'
Output from a v9 stack with one of each: a resource that set secretDataWoVersion, and one that omitted it.
urn:pulumi:dev::my-stack::gcp:secretmanager/secretVersion:SecretVersion::pinned-payload
secretDataWoVersion = 1 (number)
urn:pulumi:dev::my-stack::gcp:secretmanager/secretVersion:SecretVersion::unpinned-payload
secretDataWoVersion = null (null)
Reading it:
(null)—secretDataWoVersionis not set. See the Remediation section below.(number)— quote the literal in your program. See the Remediation section below.(string)— already done, nothing to do.- no output at all — the stack has no
SecretVersionusing the write-only pair, so you are not affected.
Remediation
If secretDataWoVersion was set to a number, quote it.
const pinned = new gcp.secretmanager.SecretVersion("pinned-payload", {
secret: secret.id,
secretDataWo: "payload-pinned-v1",
// v9
// secretDataWoVersion: 1,
// v10 (fixed)
secretDataWoVersion: "1",
deletionPolicy: "DELETE",
});
If secretDataWoVersion was omitted, it is now required alongside secretDataWo. Use "", which is what the upgrade leaves in state, and nothing is replaced.
v9:
const unpinned = new gcp.secretmanager.SecretVersion("unpinned-payload", {
secret: secret.id,
secretDataWo: "payload-unpinned",
});
v10:
const unpinned = new gcp.secretmanager.SecretVersion("unpinned-payload", {
secret: secret.id,
secretDataWo: "payload-unpinned",
// Use "" and nothing is replaced.
secretDataWoVersion: "",
});
gcp.monitoring.UptimeCheckConfig: the two password fields are now mutually exclusive
gcp.monitoring.UptimeCheckConfig’s httpCheck.authInfo block must now set exactly one of password and passwordWo. On v9 both fields were optional.
Upstream: google_monitoring_uptime_check_config in the google-beta v8 upgrade guide.
Impact/Risk
Nothing is replaced. Every fix below is an in-place update, so no uptime check is recreated and no check id changes.
If authInfo sets both fields, pulumi preview fails outright, so nothing is deployed. Delete one of the two. The choice matters: on v9 the value that reached the check was password, because passwordWo was not applied. Keep password and the check’s credential is unchanged. Keep passwordWo and the next update changes the password the check sends, without preview showing the new value.
If authInfo sets neither field, pulumi preview fails as well. This was legal on v9 and means the check authenticates with the username alone.
Note: pulumi preview is what catches this. The program compiles, because no type changed, and the provider rejects the inputs when preview validates them.
Am I affected?
This reads your existing state and changes nothing:
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:monitoring/uptimeCheckConfig:UptimeCheckConfig")
| (((.inputs // {}).httpCheck // {}).authInfo) as $a
| select($a != null)
| "\(.urn)\n password = \($a | has("password")) passwordWo = \($a | has("passwordWo"))"
'
Output from a v9 stack with one check of each kind:
urn:pulumi:dev::my-stack::gcp:monitoring/uptimeCheckConfig:UptimeCheckConfig::both-passwords-check
password = true passwordWo = true
urn:pulumi:dev::my-stack::gcp:monitoring/uptimeCheckConfig:UptimeCheckConfig::username-only-check
password = false passwordWo = false
urn:pulumi:dev::my-stack::gcp:monitoring/uptimeCheckConfig:UptimeCheckConfig::password-only-check
password = true passwordWo = false
urn:pulumi:dev::my-stack::gcp:monitoring/uptimeCheckConfig:UptimeCheckConfig::write-only-check
password = false passwordWo = true
Reading it:
- both
true— delete one of the two. See the Remediation section below. - both
false— addpassword: "". See the Remediation section below. - exactly one
true— nothing to do. - no output at all — no uptime check in the stack sets
httpCheck.authInfo, so you are not affected.
Remediation
If both fields are set, delete passwordWo and passwordWoVersion. This is an update, not a replacement, and it keeps the password the check is authenticating with today.
v9:
authInfo: {
username: "check-user",
password: "example-password",
passwordWo: "example-write-only-password",
passwordWoVersion: "1",
},
v10:
authInfo: {
username: "check-user",
// v10 (fixed): keep password, drop passwordWo and passwordWoVersion.
password: "example-password",
},
Deleting password instead, and keeping passwordWo with passwordWoVersion, is also an update rather than a replacement. It changes the password the check sends.
If neither password field is set, add password: "". This is not a password for the endpoint: the constraint tests whether the field is present, not what it holds, and an empty string is never sent to Google. The check goes on authenticating with the username alone, exactly as it did on v9, and preview shows no diff. If the endpoint does need a password, set a real one in password or passwordWo instead.
v9:
authInfo: {
username: "check-user",
},
v10:
authInfo: {
username: "check-user",
// v10 (fixed): one of the two is now required. "" is not sent to the API.
password: "",
},
gcp.container.Cluster and gcp.container.NodePool: namePrefix may now be up to 31 characters
gcp.container.NodePool, and the inline node pools of gcp.container.Cluster under nodePools[], can have their GKE name generated from namePrefix. The provider appends a generated suffix to the prefix, and GKE limits the finished name to 40 characters.
The suffix used to be 26 characters long, which capped namePrefix at 14. It now depends on the length of the prefix:
- 14 characters or fewer: a 26-character suffix, unchanged.
- 15 to 31 characters: a 9-character suffix, a 6-digit date plus a 3-digit counter.
- longer than 31 characters: rejected.
Node pool names already recorded in state are not regenerated.
Upstream: google_container_node_pool and google_container_cluster in the google-beta v8 upgrade guide.
Impact/Risk
Beware: changing namePrefix on a live node pool replaces it, deleting the nodes and everything running on them.
Every existing stack is unaffected: only a prefix of 14 characters or fewer could be deployed on v9, and those generate exactly the same names on v10. A namePrefix of 15 to 31 characters is now possible, but use it with caution: the suffix is a date plus a counter that restarts at 1 on every deployment, so two deployments of one prefix into the same cluster on the same UTC day generate the same name. The second fails with already exists from GKE, and preview cannot warn about it, because the name is generated while the resource is being created.
Am I affected?
No. This change only widens what is allowed; read the warning above before using the new range.
Remediation
N/A
gcp.workflows.Workflow: sourceContents is now required
gcp.workflows.Workflow now requires sourceContents, the workflow code the resource deploys. On v9 the argument was optional.
Google has always rejected a workflow with no source code, so the change turns an error you got from the cloud on pulumi up into one you get locally at preview.
Upstream: google_workflows_workflow in the google-beta v8 upgrade guide.
Impact/Risk
None. Google already rejected a workflow without source code, so a program that deploys today already sets sourceContents.
Am I affected?
No, unless a program that has never deployed omits sourceContents.
Remediation
No action needed.
gcp.cloudrunv2.WorkerPool: probe header fields changed
gcp.cloudrunv2.WorkerPool changed in three ways.
httpGet.httpHeaders, on both the startup probe and the liveness probe of a container, changed from a single header to a list of headers.httpGet.httpHeaders.nameis now required, andhttpGet.httpHeaders.portwas removed.- The top-level
customAudienceswas removed, from the resource and from thegcp.cloudrunv2.getWorkerPooldata source.
Upstream: google_cloud_run_v2_worker_pool in the google-beta v8 upgrade guide.
Impact/Risk
If a probe sets httpGet.httpHeaders, the value must become a list. Where there is a compile step, the program stops building until you change it; where there is not, the old shape still reaches the provider, which warns that an array was expected and deploys the header correctly anyway. Wrapping the header in a list is the whole fix: nothing is replaced and nothing is updated.
If you set customAudiences, delete the line. Cloud Run worker pools never accepted the field: on v9 the API returned an empty list and the worker pool ran exactly as if it had not been set. Deleting it replaces nothing. A value read back from customAudiences, on the resource or on the gcp.cloudrunv2.getWorkerPool data source, is gone as well.
If you set httpGet.httpHeaders.port, or left httpGet.httpHeaders.name unset, that worker pool does not exist: GCP rejects both with a 400 at create time. Delete the port and give the header a name.
Am I affected?
Run the following command against each stack. It reads state and changes nothing.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:cloudrunv2/workerPool:WorkerPool")
| [ (if .inputs.customAudiences then " customAudiences is set" else empty end),
( (.inputs.template.containers // []) | to_entries[] as $c
| ("startupProbe", "livenessProbe") as $p
| ($c.value[$p].httpGet.httpHeaders // empty)
| " containers[\($c.key)].\($p).httpGet.httpHeaders is \(type)" ) ] as $found
| select($found | length > 0)
| "\(.urn)\n\($found | join("\n"))"
'
Output from a stack with one worker pool of each kind:
urn:pulumi:dev::my-stack::gcp:cloudrunv2/workerPool:WorkerPool::worker-pool
customAudiences is set
urn:pulumi:dev::my-stack::gcp:cloudrunv2/workerPool:WorkerPool::probe-worker-pool
containers[0].startupProbe.httpGet.httpHeaders is object
containers[0].livenessProbe.httpGet.httpHeaders is object
Reading it:
is object— wrap that header in a list, see the Remediation section below.customAudiences is set— delete that line from your program, see the Remediation section below.is array— already done, nothing to do.- no output at all — the stack has no affected worker pool, so you are not affected.
Remediation
If a probe sets httpGet.httpHeaders, put the header in a list. The liveness probe takes the same edit. Nothing is replaced.
startupProbe: {
initialDelaySeconds: 0,
periodSeconds: 10,
timeoutSeconds: 5,
failureThreshold: 10,
httpGet: {
path: "/",
port: 8080,
// v9
// httpHeaders: { name: "X-Startup-Probe", value: "named" },
// v10 (fixed)
httpHeaders: [{ name: "X-Startup-Probe", value: "named" }],
},
},
If you set customAudiences, delete the line. Nothing is replaced.
const audiences = new gcp.cloudrunv2.WorkerPool("worker-pool", {
name: "worker-pool",
location: location,
deletionProtection: false,
scaling: { manualInstanceCount: 1 },
// v9, removed in v10
// customAudiences: ["https://worker-pool.example.com"],
template: {
containers: [{ image: image }],
},
});
audiences = gcp.cloudrunv2.WorkerPool("worker-pool",
name="worker-pool",
location=location,
deletion_protection=False,
scaling={
"manual_instance_count": 1,
},
# v9, removed in v10
# custom_audiences=["https://worker-pool.example.com"],
template={
"containers": [{
"image": image,
}],
})
_, err := cloudrunv2.NewWorkerPool(ctx, "worker-pool", &cloudrunv2.WorkerPoolArgs{
Name: pulumi.String("worker-pool"),
Location: pulumi.String(location),
DeletionProtection: pulumi.Bool(false),
Scaling: &cloudrunv2.WorkerPoolScalingArgs{
ManualInstanceCount: pulumi.Int(1),
},
// v9, removed in v10
// CustomAudiences: pulumi.StringArray{pulumi.String("https://worker-pool.example.com")},
Template: &cloudrunv2.WorkerPoolTemplateArgs{
Containers: cloudrunv2.WorkerPoolTemplateContainerArray{
&cloudrunv2.WorkerPoolTemplateContainerArgs{
Image: pulumi.String(image),
},
},
},
})
if err != nil {
return err
}
var audiences = new Gcp.CloudRunV2.WorkerPool("worker-pool", new()
{
Name = "worker-pool",
Location = location,
DeletionProtection = false,
Scaling = new Gcp.CloudRunV2.Inputs.WorkerPoolScalingArgs
{
ManualInstanceCount = 1,
},
// v9, removed in v10
// CustomAudiences = new[] { "https://worker-pool.example.com" },
Template = new Gcp.CloudRunV2.Inputs.WorkerPoolTemplateArgs
{
Containers = new[]
{
new Gcp.CloudRunV2.Inputs.WorkerPoolTemplateContainerArgs
{
Image = image,
},
},
},
});
var audiences = new WorkerPool("worker-pool", WorkerPoolArgs.builder()
.name("worker-pool")
.location(location)
.deletionProtection(false)
.scaling(WorkerPoolScalingArgs.builder()
.manualInstanceCount(1)
.build())
// v9, removed in v10
// .customAudiences("https://worker-pool.example.com")
.template(WorkerPoolTemplateArgs.builder()
.containers(WorkerPoolTemplateContainerArgs.builder()
.image(image)
.build())
.build())
.build());
resources:
audiences:
type: gcp:cloudrunv2:WorkerPool
name: worker-pool
properties:
name: worker-pool
location: ${location}
deletionProtection: false
scaling:
manualInstanceCount: 1
# v9, removed in v10
# customAudiences:
# - https://worker-pool.example.com
template:
containers:
- image: ${image}
gcp.compute.ServiceAttachment: natSubnets and consumerRejectLists are now sets
On gcp.compute.ServiceAttachment, natSubnets and consumerRejectLists are now sets rather than lists. The order of their entries is no longer significant: reordering them in your program produces no diff, and the order the provider stores and hands back is its own, not the order you wrote.
This removes a recurring diff. On v9 a service attachment with more than one NAT subnet could show an update on every pulumi up and never settle, because GCP returns the subnets in its own order and in a different URL form than the one you supplied. That no longer happens.
Upstream: google_compute_service_attachment in the google-beta v8 upgrade guide.
Impact/Risk
No program change is required in any case, and nothing is replaced.
If you read natSubnets or consumerRejectLists back by index, the value at a given index can change. The first pulumi up --refresh after the upgrade rewrites both collections in state into the provider’s order, which is neither the order you wrote nor the order GCP returns. A stack output built from the first entry changes value silently, with the attachment itself reported as unchanged. If the entry feeds another resource, that resource gets whatever diff its own rules imply.
If you had a recurring diff on natSubnets, it is gone. An attachment that showed an update on every run now previews clean with no edit to your program.
Am I affected?
Run this against each stack. It is read-only.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:compute/serviceAttachment:ServiceAttachment")
| "\(.urn)",
" natSubnets : \((.outputs.natSubnets // []) | map(split("/") | last))",
" consumerRejectLists : \((.outputs.consumerRejectLists // []) | map(split("/") | last))"
'
Output from a stack with one service attachment, before the upgrade:
urn:pulumi:dev::my-stack::gcp:compute/serviceAttachment:ServiceAttachment::service-attachment
natSubnets : ["nat-subnet-b","nat-subnet-a"]
consumerRejectLists : ["pulumi-k8s-provider","pulumi-k8s-operator"]
Reading it:
- two or more entries in either collection — the order can change on the first refresh after the upgrade. Check whether anything indexes that collection, and if so see the Remediation section below.
- one entry or none in both — nothing to do.
- no output at all — the stack has no
gcp.compute.ServiceAttachment, so you are not affected.
The same command after the upgrade and a refresh shows the new order, so you can see exactly which positions moved:
natSubnets : ["nat-subnet-a","nat-subnet-b"]
consumerRejectLists : ["pulumi-k8s-operator","pulumi-k8s-provider"]
Remediation
If nothing reads the two collections back, there is nothing to change. The program below is valid on both versions, and on v10 it previews clean where on v9 it showed an update on every run.
const attachment = new gcp.compute.ServiceAttachment("service-attachment", {
name: "service-attachment",
region: region,
description: "example service attachment",
enableProxyProtocol: false,
connectionPreference: "ACCEPT_MANUAL",
targetService: targetService.id,
natSubnets: [natA.id, natB.id],
consumerRejectLists: ["pulumi-k8s-provider", "pulumi-k8s-operator"],
consumerAcceptLists: [{
projectIdOrNum: "pulumi-ci-gcp-provider",
connectionLimit: 1,
}],
});
attachment = gcp.compute.ServiceAttachment("service-attachment",
name="service-attachment",
region=region,
description="example service attachment",
enable_proxy_protocol=False,
connection_preference="ACCEPT_MANUAL",
target_service=target_service.id,
nat_subnets=[
nat_a.id,
nat_b.id,
],
consumer_reject_lists=[
"pulumi-k8s-provider",
"pulumi-k8s-operator",
],
consumer_accept_lists=[{
"project_id_or_num": "pulumi-ci-gcp-provider",
"connection_limit": 1,
}])
_, err := compute.NewServiceAttachment(ctx, "service-attachment", &compute.ServiceAttachmentArgs{
Name: pulumi.String("service-attachment"),
Region: pulumi.String(region),
Description: pulumi.String("example service attachment"),
EnableProxyProtocol: pulumi.Bool(false),
ConnectionPreference: pulumi.String("ACCEPT_MANUAL"),
TargetService: targetService.ID().ToIDOutput().ToStringOutput(),
NatSubnets: pulumi.StringArray{
natA.ID().ToIDOutput().ToStringOutput(),
natB.ID().ToIDOutput().ToStringOutput(),
},
ConsumerRejectLists: pulumi.StringArray{
pulumi.String("pulumi-k8s-provider"),
pulumi.String("pulumi-k8s-operator"),
},
ConsumerAcceptLists: compute.ServiceAttachmentConsumerAcceptListArray{
&compute.ServiceAttachmentConsumerAcceptListArgs{
ProjectIdOrNum: pulumi.String("pulumi-ci-gcp-provider"),
ConnectionLimit: pulumi.Int(1),
},
},
})
if err != nil {
return err
}
var attachment = new Gcp.Compute.ServiceAttachment("service-attachment", new()
{
Name = "service-attachment",
Region = region,
Description = "example service attachment",
EnableProxyProtocol = false,
ConnectionPreference = "ACCEPT_MANUAL",
TargetService = targetService.Id,
NatSubnets = new[]
{
natA.Id,
natB.Id,
},
ConsumerRejectLists = new[]
{
"pulumi-k8s-provider",
"pulumi-k8s-operator",
},
ConsumerAcceptLists = new[]
{
new Gcp.Compute.Inputs.ServiceAttachmentConsumerAcceptListArgs
{
ProjectIdOrNum = "pulumi-ci-gcp-provider",
ConnectionLimit = 1,
},
},
});
var attachment = new ServiceAttachment("service-attachment", ServiceAttachmentArgs.builder()
.name("service-attachment")
.region(region)
.description("example service attachment")
.enableProxyProtocol(false)
.connectionPreference("ACCEPT_MANUAL")
.targetService(targetService.id())
.natSubnets(
natA.id(),
natB.id())
.consumerRejectLists(
"pulumi-k8s-provider",
"pulumi-k8s-operator")
.consumerAcceptLists(ServiceAttachmentConsumerAcceptListArgs.builder()
.projectIdOrNum("pulumi-ci-gcp-provider")
.connectionLimit(1)
.build())
.build());
resources:
attachment:
type: gcp:compute:ServiceAttachment
name: service-attachment
properties:
name: service-attachment
region: ${region}
description: example service attachment
enableProxyProtocol: false
connectionPreference: ACCEPT_MANUAL
targetService: ${targetService.id}
natSubnets:
- ${natA.id}
- ${natB.id}
consumerRejectLists:
- pulumi-k8s-provider
- pulumi-k8s-operator
consumerAcceptLists:
- projectIdOrNum: pulumi-ci-gcp-provider
connectionLimit: 1
If you index into either collection, stop indexing the value the provider returns and index the value you supplied. Nothing is replaced by this edit.
v9:
export const firstNatSubnet = attachment.natSubnets.apply(s => s[0]);
export const firstRejectedProject = attachment.consumerRejectLists.apply(s => (s ?? [])[0]);
v10:
export const firstNatSubnet = natA.id;
export const firstRejectedProject = "pulumi-k8s-provider";
Make this edit before the refresh. The reorder lands on the first pulumi up --refresh after the upgrade, and until then state keeps the old order, so a stack that has been bumped but not refreshed will not have shown you the change yet.
gcp.container.Cluster: the enableComponents fields are now sets
gcp.container.Cluster now treats loggingConfig.enableComponents and monitoringConfig.enableComponents as sets rather than lists.
The declared type is unchanged. Both fields are still arrays of strings and no resource argument needs editing. What changes is order: the values you read back from the cluster now come back sorted alphabetically rather than in the order GKE returned them, and the order you write them in no longer has any effect. That reaches your program only if it reads a position out of one of those arrays.
Upstream: google_container_cluster in the google-beta v8 upgrade guide.
Impact/Risk
Nothing is replaced, before or after the upgrade.
If your program reads a position out of either array, such as enableComponents[0], that value changes once, at your first refresh after the upgrade: a cluster declaring ["APISERVER", "SYSTEM_COMPONENTS"] used to hand back SYSTEM_COMPONENTS at position 0 and now hands back APISERVER. A recurring enableComponents update that never settled also stops, because the order no longer counts as a difference.
Am I affected?
Run the following command against each stack to see whether the recorded order of enableComponents will move. It reads state and changes nothing.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:container/cluster:Cluster")
| . as $r
| ("loggingConfig", "monitoringConfig")
| . as $block
| ($r.outputs[$block].enableComponents // empty)
| select(length > 1)
| "\($r.urn) \($block)\n now = \(tojson)\n after = \(sort | tojson)"
'
Output from a stack with one cluster that declares two logging components and two monitoring components:
urn:pulumi:dev::my-stack::gcp:container/cluster:Cluster::cluster loggingConfig
now = ["WORKLOADS","SYSTEM_COMPONENTS"]
after = ["SYSTEM_COMPONENTS","WORKLOADS"]
urn:pulumi:dev::my-stack::gcp:container/cluster:Cluster::cluster monitoringConfig
now = ["SYSTEM_COMPONENTS","APISERVER"]
after = ["APISERVER","SYSTEM_COMPONENTS"]
Reading it:
- no output at all — no cluster in the stack sets more than one component in either field, so no order can move and you are not affected.
nowandafteridentical — the components are already in sorted order, so nothing moves.nowandafterdifferent — the order changes at your first refresh after the upgrade. That only matters if something in your program reads a position out of the array; if something does, see the Remediation section below. If nothing does, you are not affected either.
Remediation
If you read a position out of loggingConfig.enableComponents or monitoringConfig.enableComponents, that position now holds a different component. Replace the index with the question you were really asking. Nothing is replaced either way.
v9:
export const firstLoggingComponent = cluster.loggingConfig.apply(c => c.enableComponents[0]);
export const firstMonitoringComponent = cluster.monitoringConfig.apply(c => c.enableComponents[0]);
v10, when the question was membership:
export const loggingHasWorkloads = cluster.loggingConfig.apply(c => c.enableComponents.includes("WORKLOADS"));
export const monitoringHasApiserver = cluster.monitoringConfig.apply(c => c.enableComponents.includes("APISERVER"));
v10, when you need a stable ordering of your own:
export const loggingComponentsSorted = cluster.loggingConfig.apply(c => [...c.enableComponents].sort());
export const monitoringComponentsSorted = cluster.monitoringConfig.apply(c => [...c.enableComponents].sort());
gcp.compute.Reservation: top-level reservationBlockCount removed
The top-level reservationBlockCount attribute has been removed from gcp.compute.Reservation and from the gcp.compute.getReservation function. Read resourceStatuses[0].reservationBlockCount instead, which is available in v9 as well.
reservationBlockCount was never settable, so no configuration changes. Only code that reads the value has to move.
Upstream: google_compute_reservation in the google-beta v8 upgrade guide.
Impact/Risk
The reservation’s own configuration is untouched by this change and no reservation is replaced, on upgrade or on any later update. If your program reads reservationBlockCount, in a stack output, a StackReference or any derived value, what happens depends on whether your language checks types before the deployment runs:
- Compiled languages fail at build, before anything is deployed.
- Interpreted languages have no build step. The read produces no value, the deployment succeeds, and any stack output built from it silently disappears. Anything downstream that consumed that output gets no value back rather than an error.
resourceStatuses is empty for a reservation that has no blocks, where the removed attribute reported 0. Substituting the new path on its own gives you no value; supply 0 as the fallback to keep what v9 reported.
Am I affected?
The change is in the values your program reads, so it is your source that decides whether you are affected. Search it for reservationBlockCount; every occurrence that is not already under resourceStatuses has to move.
To find which stacks hold reservations, and what value their state carries today, run the following command against each stack. It reads state and changes nothing.
pulumi stack export | jq -r '
.deployment.resources[]
| select(.type == "gcp:compute/reservation:Reservation")
| "\(.urn)\n reservationBlockCount = \(.outputs.reservationBlockCount // "absent")\n resourceStatuses[0].reservationBlockCount = \(.outputs.resourceStatuses[0].reservationBlockCount // "absent")"
'
Output from a stack still on v9:
urn:pulumi:dev::my-stack::gcp:compute/reservation:Reservation::reservation
reservationBlockCount = 0
resourceStatuses[0].reservationBlockCount = absent
Reading it:
- no output at all — the stack has no reservations, so you are not affected.
- a number next to
reservationBlockCountandabsentnext to the nested path — the reservation has no blocks, and this is the value to preserve with a fallback. See the Remediation section below. - a number next to both — the reservation has blocks, and the nested path already reports the same value.
Remediation
If you read the attribute on the resource, move the read under resourceStatuses. Nothing is replaced.
v9:
export const blockCount = reservation.reservationBlockCount;
v10:
export const blockCount = reservation.resourceStatuses.apply(
s => s[0]?.reservationBlockCount ?? 0);
If you read the attribute on a reservation you looked up rather than created, the same move applies to the gcp.compute.getReservation result.
The ?? 0 is the part to keep. resourceStatuses is empty for a reservation with no blocks, and without the fallback the value is absent rather than the 0 v9 reported.
Deprecations
Nothing in this section breaks on v10. These are warnings you may start seeing after you upgrade.
gcp.compute.Instance: metadata carrying a container declaration
An instance whose metadata carries the container declaration that the VM startup agent consumes:
instance = gcp.compute.Instance(
"poc",
machine_type="f1-micro",
metadata={"gce-container-declaration": container_declaration},
)
now previews with:
warning: verification warning: property "metadata" is deprecated: The option to deploy a container
during VM creation using the container startup agent is deprecated. Use alternative services to run
containers on your VMs.
The instance still deploys and nothing is replaced. There is no property to rename, because Google is retiring the container-on-VM startup agent itself. Its four documented paths off it are startup scripts or cloud-init on the VM, Cloud Run, Batch, and GKE. See Migrate containers that were deployed on VMs during VM creation.
Other changes
Everything in the release that is not covered above, listed for completeness.
Removed resources
Resource
gcp.beyondcorp.AppConnectionremoved:- Use
gcp.beyondcorp.SecurityGatewayandgcp.beyondcorp.SecurityGatewayApplicationfor modern BeyondCorp Zero Trust application deployments.
- Use
Resource
gcp.beyondcorp.AppConnectorremoved.Resource
gcp.beyondcorp.AppGatewayremoved:- Use
gcp.beyondcorp.SecurityGatewayinstead.
- Use
Resource
gcp.ml.EngineModelremoved:- The underlying Cloud ML Engine (AI Platform Prediction) API has been deprecated. Migrate to Vertex AI resources such as
gcp.vertex.AiEndpoint. - This was the only resource in the
gcp.mlmodule, so the module no longer exists.
- The underlying Cloud ML Engine (AI Platform Prediction) API has been deprecated. Migrate to Vertex AI resources such as
Resource
gcp.vertex.AiScheduleremoved:- Use
gcp.colab.Scheduleinstead.
- Use
Removed functions
gcp.beyondcorp.getAppConnectiongcp.beyondcorp.getAppConnectorgcp.beyondcorp.getAppGatewaygcp.iap.getClient, covered in detail above
Changed resources
Resource
gcp.applicationintegration.Client:- Field
runAsServiceAccountis removed.
- Field
Resource
gcp.bigquery.DataTransferConfig:- Exactly one of
sensitiveParams.secretAccessKeyandsensitiveParams.secretAccessKeyWomust now be set. - Field
sensitiveParams.secretAccessKeyWoVersionhas changed type from integer to string.
- Exactly one of
Resource
gcp.cloudsecuritycompliance.Framework:- Field
cloudControlDetailsis now a set. Ordering is no longer significant and duplicate entries are rejected.
- Field
Resource
gcp.compute.ServiceAttachment:- Fields
consumerAcceptLists[].projectIdOrNum,consumerAcceptLists[].networkUrlandconsumerAcceptLists[].endpointUrlnow default to an empty string rather than to null, so that the API omitting an unpopulated attribute no longer produces a perpetual diff. A program that reads one of those values back gets""where v9 gave no value; the change lands on the first refresh after the upgrade. The set conversions on the same resource are covered in detail above.
- Fields
Resource
gcp.compute.InterconnectAttachmentGroup:- Field
logicalStructures[].regions[].metros[].facilities[].zones[].attachmentis removed. Useattachmentsinstead.
- Field
Resource
gcp.dataloss.PreventionJobTrigger:- Field
actions.publishFindingsToCloudDataCatalogis removed.
- Field
Resource
gcp.iam.WorkforcePoolProviderScimTenant:- Field
claimMappingis now required on create.
- Field
Resource
gcp.netapp.StoragePool:- Field
scaleTieris removed.
- Field
Changed functions
Function
gcp.backupdisasterrecovery.getBackupPlanAssociations:- Field
resourceTypeis removed.
- Field
Function
gcp.backupdisasterrecovery.getDataSourceReferences:- Field
resourceTypeis removed.
- Field
published on Monday, Oct 5, 2026 by Pulumi