Summary
vultr_instance's Read unconditionally fetches VPC 2.0 membership and treats any error as fatal. Since the VPC 2.0 API endpoint now returns 404, every read of a vultr_instance fails — which means every apply, refresh and destroy fails — even for configurations that never mention VPC at all.
Error: error getting list of attached VPCs 2.0: {"error":"Not found.","status":404}
with module.edge["…"].vultr_instance.edge,
on main.tf line …, in resource "vultr_instance" "edge":
Environment
- provider
vultr/vultr v2.32.0 (newest 2.x; installed via OpenTofu 1.12.4)
- account has plain VPC only — no VPC 2.0
Endpoint behaviour with a valid, root-ACL API key:
| request |
result |
GET /v2/account |
200 |
GET /v2/instances |
200 |
GET /v2/vpcs |
200 {"vpcs":[]} |
GET /v2/vpc2 |
404 {"error":"Not found.","status":404} |
Why this is severe rather than cosmetic
Terraform reads back after create and before destroy, so the create path leaves real state behind:
apply creates the instance successfully
- the follow-up read fails on the VPC 2.0 call
- the apply errors, so the instance is never recorded in state
- Vultr is billing a server Terraform doesn't know exists — and the next
apply creates another
I hit exactly that: two identical live instances billing, neither in state. destroy is affected too, since it reads first — so a plain vultr_instance can be neither reliably created nor reliably removed.
The value is discarded anyway
This is what makes it look like a clear bug rather than a config problem. In resource_vultr_instance.go the fetch is unconditional:
vpcs, err := getVPCs(client, d.Id())
if err != nil {
return diag.Errorf("%s", err.Error())
}
vpc2s, err := getVPC2s(client, d.Id()) // always runs
if err != nil {
return diag.Errorf("%s", err.Error()) // always fatal
}
…but the result is only used if the practitioner configured vpc2_ids:
if _, vpc2Update := d.GetOk("vpc2_ids"); vpc2Update {
if err := d.Set("vpc2_ids", vpc2s); err != nil { … }
}
So for any config without vpc2_ids — mine sets no VPC fields whatsoever — the provider fails the entire read over a value it is about to throw away. There is no configuration that avoids it, because the call is in Read, not in a vultr_vpc2 resource.
Suggested fix
Treat "the VPC 2.0 endpoint is gone" as no VPC 2.0 attachments, which seems the truthful answer for a retired product, and matches the 404 idiom already used in resource_vultr_firewall_group.go:60:
// vultr/instances.go
func vpc2Gone(err error) bool {
return strings.Contains(err.Error(), `"status":404`)
}
applied in both getVPC2s (vultr/instances.go:35) and getBareMetalServerVPC2s (vultr/instances.go:60):
if err != nil {
if vpc2Gone(err) {
return nil, nil
}
return nil, fmt.Errorf("error getting list of attached VPCs 2.0: %v", err)
}
I'm running exactly this as a local build and it resolves the problem: apply creates an instance and destroy removes it, both clean. Happy to open a PR if the approach looks right — a typed status check would be nicer than string matching if govultr exposes one.
Related: VPC2 elements were marked deprecated in v2.25.0, but only the schema fields were deprecated; the read path still calls the endpoint. master is unchanged as of today.
One caveat, in case it helps triage
I can't fully explain the timing, and it may point at a partial rollout rather than a clean cutover: earlier the same day, with byte-identical configuration and the same provider version, one apply and its destroy completed successfully. Every attempt afterwards failed as above. I also can't find other reports of this, which is surprising for something that would break every vultr_instance read globally — so it may only affect accounts where VPC 2.0 has already been withdrawn.
Reproduction
Any minimal instance is enough — no VPC configuration involved:
terraform {
required_providers {
vultr = { source = "vultr/vultr", version = "~> 2.0" }
}
}
resource "vultr_instance" "example" {
region = "fra"
plan = "vhp-2c-4gb-amd"
os_id = 2284
}
tofu apply → the instance is created, then the apply fails on the VPC 2.0 read.
Summary
vultr_instance'sReadunconditionally fetches VPC 2.0 membership and treats any error as fatal. Since the VPC 2.0 API endpoint now returns404, every read of avultr_instancefails — which means everyapply,refreshanddestroyfails — even for configurations that never mention VPC at all.Environment
vultr/vultrv2.32.0 (newest 2.x; installed via OpenTofu 1.12.4)Endpoint behaviour with a valid,
root-ACL API key:GET /v2/account200GET /v2/instances200GET /v2/vpcs200{"vpcs":[]}GET /v2/vpc2404{"error":"Not found.","status":404}Why this is severe rather than cosmetic
Terraform reads back after create and before destroy, so the create path leaves real state behind:
applycreates the instance successfullyapplycreates anotherI hit exactly that: two identical live instances billing, neither in state.
destroyis affected too, since it reads first — so a plainvultr_instancecan be neither reliably created nor reliably removed.The value is discarded anyway
This is what makes it look like a clear bug rather than a config problem. In
resource_vultr_instance.gothe fetch is unconditional:…but the result is only used if the practitioner configured
vpc2_ids:So for any config without
vpc2_ids— mine sets no VPC fields whatsoever — the provider fails the entire read over a value it is about to throw away. There is no configuration that avoids it, because the call is inRead, not in avultr_vpc2resource.Suggested fix
Treat "the VPC 2.0 endpoint is gone" as no VPC 2.0 attachments, which seems the truthful answer for a retired product, and matches the 404 idiom already used in
resource_vultr_firewall_group.go:60:applied in both
getVPC2s(vultr/instances.go:35) andgetBareMetalServerVPC2s(vultr/instances.go:60):I'm running exactly this as a local build and it resolves the problem:
applycreates an instance anddestroyremoves it, both clean. Happy to open a PR if the approach looks right — a typed status check would be nicer than string matching ifgovultrexposes one.Related: VPC2 elements were marked deprecated in v2.25.0, but only the schema fields were deprecated; the read path still calls the endpoint.
masteris unchanged as of today.One caveat, in case it helps triage
I can't fully explain the timing, and it may point at a partial rollout rather than a clean cutover: earlier the same day, with byte-identical configuration and the same provider version, one
applyand itsdestroycompleted successfully. Every attempt afterwards failed as above. I also can't find other reports of this, which is surprising for something that would break everyvultr_instanceread globally — so it may only affect accounts where VPC 2.0 has already been withdrawn.Reproduction
Any minimal instance is enough — no VPC configuration involved:
tofu apply→ the instance is created, then the apply fails on the VPC 2.0 read.