Há alguns dias perguntei no r/devsarg como cada um encontra discos não anexados e IPs órfãos antes de aparecerem na fatura. Administro contas de OCI, Azure e AWS, e de vez em quando aparece um disco de 200 GB que sobrou de uma VM apagada meses atrás. Foram 48 comentários e quase todos contavam a mesma história com outro nome: o disco que alguém descobre um ano depois, o recurso de dez anos que existe desde antes de qualquer política de tags, os volumes de Kubernetes que o driver CSI desanexou e ninguém olhou de novo. No r/googlecloud, na mesma semana, alguém tinha feito a limpeza das VMs de dev e a fatura quase não se mexeu: o que crescia mês a mês era storage, incluindo snapshots de discos que já não existiam.
Este post é a resposta longa às duas perguntas. Quais recursos nascem com a VM e não morrem com ela em cada nuvem, uma listagem somente leitura por provedor para vê-los, e o que essa listagem não consegue dizer.
Por que acontece: o console lista VMs, a fatura lista recursos
A página de instâncias mostra instâncias. O disco, o IP público e o snapshot são recursos à parte, com ID próprio e linha própria na fatura. Quando você apaga a VM, cada um deles segue a regra que tinha configurada, e essa regra quase nunca foi escolhida por alguém de propósito.
- AWS. Cada volume EBS tem um atributo DeleteOnTermination. Segundo o guia do EC2, o volume raiz criado no lançamento da instância é apagado com ela; um volume de dados adicionado no lançamento pelo console é preservado (pela CLI é apagado), e qualquer volume anexado depois do lançamento é preservado. Volumes preservados continuam sendo cobrados. O Elastic IP é um recurso independente: desde fevereiro de 2024 a AWS cobra USD 0,005 por hora por cada IPv4 público, e a tarifa é a mesma para um IP em uso e para um ocioso. Snapshots nunca dependem do volume: sobrevivem ao volume e à instância.
- GCP. Cada disco anexado tem autoDelete. Para o disco de boot criado com a instância o padrão é sim; para um disco existente que você anexa com --disk o padrão é não, e a referência do gcloud avisa que, se o disco for desanexado depois, a opção deixa de valer. Um endereço externo estático reservado fica no status RESERVED quando não está atribuído a nada, e a página de preços de rede diz que um IP estático reservado sem uso é cobrado a uma tarifa maior do que um em uso. Snapshots guardam o nome do disco de origem em sourceDisk, mas não são apagados com ele.
- Azure. O IP público, a NIC e o disco do sistema operacional são recursos separados da VM. Na criação, cada um tem um deleteOption que pode ser Delete ou Detach; o portal marca por padrão "Delete with VM" para o disco do SO, mas o padrão quando você cria a VM pela CLI ou pelo PowerShell é Detach, e o IP público tem a própria caixa de seleção. Um disco que ficou solto aparece com diskState em Unattached.
- OCI. Segundo a documentação do Compute, ao terminar uma instância o boot volume é preservado por padrão; apagá-lo junto com a instância é uma opção que precisa ser marcada. Os block volumes são desanexados e ficam em lifecycleState AVAILABLE, que é o mesmo estado que têm quando estão anexados, então o estado sozinho não basta. Um IP público reservado que não está atribuído a nada tem assignedEntityId em null.
As quatro listagens, somente leitura
Nenhum desses comandos apaga nada. Todos saem da referência oficial de cada CLI. Deixo os campos que depois ajudam a decidir: tamanho, data de criação e tags.
# AWS: volumes sem instância, Elastic IPs sem associação e snapshots próprios com o volume de origem aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[].{id:VolumeId,gb:Size,created:CreateTime,tags:Tags}" aws ec2 describe-addresses --query "Addresses[?AssociationId==null].[PublicIp,AllocationId]" aws ec2 describe-snapshots --owner-ids self --query "Snapshots[].[SnapshotId,VolumeId,VolumeSize,StartTime]" # cruzar o VolumeId de cada snapshot com describe-volumes: se não estiver lá, o disco já não existe # GCP: discos sem usuários, endereços estáticos reservados sem atribuição e snapshots com o disco de origem gcloud compute disks list --filter="-users:*" --format="table(name,zone.basename(),sizeGb,creationTimestamp)" gcloud compute addresses list --filter="status=RESERVED" gcloud compute snapshots list --format="table(name,sourceDisk.basename(),diskSizeGb,creationTimestamp)" # Azure: discos Unattached, IPs públicos sem ipConfiguration e snapshots com o recurso de origem az disk list --query "[?diskState=='Unattached'].{name:name,rg:resourceGroup,gb:diskSizeGb,created:timeCreated}" -o table az network public-ip list --query "[?ipConfiguration==null].{name:name,rg:resourceGroup,ip:ipAddress}" -o table az snapshot list --query "[].{name:name,source:creationData.sourceResourceId,created:timeCreated}" -o table # OCI: AVAILABLE não significa solto; o cruzamento é com os attachments oci bv volume list --compartment-id "$C" --lifecycle-state AVAILABLE --all --query 'data[].{id:id,name:"display-name",gb:"size-in-gbs",created:"time-created"}' oci compute volume-attachment list --compartment-id "$C" --all --query 'data[?"lifecycle-state"==`ATTACHED`]."volume-id"' # boot volumes que sobreviveram a um terminate: mesmo cruzamento, por availability domain oci bv boot-volume list --compartment-id "$C" --availability-domain "$AD" --all oci compute boot-volume-attachment list --compartment-id "$C" --availability-domain "$AD" --all # IPs públicos reservados sem atribuição oci network public-ip list --compartment-id "$C" --scope REGION --lifetime RESERVED --all --query 'data[?"assigned-entity-id"==null].{ip:"ip-address",state:"lifecycle-state"}' Dois detalhes das listagens. Na OCI o estado do volume não distingue anexado de solto, por isso são dois comandos e uma subtração. E nas quatro nuvens os IPs merecem uma passada própria, porque são o único desses recursos que custa o mesmo em uso e sem uso; o detalhe por provedor está em IPs públicos que cobram.
O que a listagem não diz
A listagem diz qual disco está solto. Não diz se é lixo. Foi a parte mais discutida do thread, e acho que é a parte difícil de verdade:
- Desde quando está solto. describe-volumes devolve CreateTime, não a data do detach. Nenhum provedor guarda o momento em que o disco ficou sem VM; se você quer um período de carência de 14 ou 30 dias, o relógio é seu: uma tag com a data da primeira vez que o viu, ou uma tabela que leve o registro entre execuções.
- Quem criou. Isso sai do audit log do provedor, não do recurso. E o recurso de dez anos, anterior à política de tags, não tem owner nem centro de custo: esse nenhum filtro resolve, resolve alguém que o reconhece.
- Os volumes de Kubernetes. Um PersistentVolume com persistentVolumeReclaimPolicy: Retain sobrevive à exclusão do PVC: o PV fica em Released e o disco da nuvem continua existindo, solto, com o PV ainda apontando para ele. Para a listagem é mais um disco não anexado; para o time que deixou em Retain, é um backup.
- O disco com os mdf e ldf de um SQL Server de 2018 que alguém deixou de propósito. Um comentário do thread descrevia exatamente isso, e é o motivo pelo qual quase ninguém tem exclusão automática sem revisão humana em produção.
Um script lista; uma pessoa decide. O script que saiu do thread está no GitHub como cloud-orphans: lista discos não anexados e IPs sem associação nas quatro nuvens, com a idade de cada um, e não apaga nada. Hoje a saída contra a OCI é real e o resto roda com dados de teste, e ele ainda não guarda a primeira vez que viu cada recurso, que é justamente o que o período de carência precisa.
Ver isso sem rodar as quatro listagens
O Control Cloud lê a conta pela API do provedor com uma credencial somente leitura e desenha o mapa do que existe: a VM, a subnet, os discos, os IPs. Um disco ou um IP que ficou sem VM aparece no mapa sem nada anexado, então você o vê sem rodar as quatro listagens nem cruzar saídas na mão. O que ele não faz é decidir o que apagar: isso continua sendo seu. Os guias por provedor explicam como conectar cada conta de AWS, GCP, Azure ou OCI com permissões de leitura, e se quiser testar com a sua, peça acesso.

