Recursos fuera de las regiones permitidas
Inventario de recursos por región que señala lo que está fuera de las regiones que indiques. Por defecto toma la región Mexico Central (mexicocentral).
Ejemplo en México
Clínica privada en Mérida, 120 personas
Un proveedor montó servicios de pruebas en Estados Unidos y el comité de privacidad pide saber dónde están los recursos antes de actualizar su aviso de privacidad.
.\get-recursos-fuera-de-mexico.ps1 -RegionesPermitidas mexicocentral -RutaCsv .\regiones.csvObtiene el inventario por región con lo que está en eastus y westus marcado, y lo usa para decidir qué migrar y qué documentar.
Cómo implementarlo
- Instala el módulo con Install-Module Az.Accounts -Scope CurrentUser si no lo tienes.
- Corre el script con tu lista de regiones en -RegionesPermitidas; usa el nombre programático, por ejemplo mexicocentral.
- Abre el CSV y filtra la columna Estado por "Fuera de regiones permitidas".
- Decide con cada dueño si el recurso se queda, se mueve o se documenta la excepción.
Ver el código completo de get-recursos-fuera-de-mexico.ps1
<#
.SYNOPSIS
¿En qué regiones de Azure están mis recursos y cuáles quedan fuera de las permitidas?
.DESCRIPTION
Lista todos los recursos con su región y marca los que no están en -RegionesPermitidas. Sirve para saber dónde están tus recursos; no determina en qué país debes alojarlos. Solo lectura: no cambia nada.
.PARAMETER RegionesPermitidas
Nombres programáticos de las regiones permitidas. Por defecto, mexicocentral.
.PARAMETER Suscripciones
IDs o nombres de suscripción. Si se omite, usa todas a las que tienes acceso.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\get-recursos-fuera-de-mexico.ps1 -RegionesPermitidas mexicocentral -RutaCsv .\regiones.csv
.NOTES
Permisos: Lector (Reader) en cada suscripción
Rol mínimo: Lector (Reader)
Licencia: Basta una suscripción de Azure; no requiere plan adicional.
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en una suscripción de pruebas.
#>
[CmdletBinding()]
param(
[string[]]$RegionesPermitidas = @('mexicocentral'),
[ValidateNotNullOrEmpty()][string[]]$Suscripciones,
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
if (-not (Get-Command Connect-AzAccount -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module Az.Accounts -Scope CurrentUser'
}
if (-not (Get-AzContext)) { $null = Connect-AzAccount }
function InvocarAzure($Metodo, $Ruta, $Cuerpo) {
$parametros = @{ Method = $Metodo }
if ($Ruta -like 'http*') { $parametros.Uri = $Ruta } else { $parametros.Path = $Ruta }
if ($Cuerpo) { $parametros.Payload = $Cuerpo | ConvertTo-Json -Depth 10 }
# Invoke-AzRestMethod no lanza error con 4xx o 5xx. 429 es una limitación temporal: se reintenta.
for ($intento = 1; ; $intento++) {
$respuesta = Invoke-AzRestMethod @parametros
if ($respuesta.StatusCode -ne 429 -or $intento -ge 4) { break }
Start-Sleep -Seconds 30
}
if ($respuesta.StatusCode -ge 400) { throw "Azure respondió $($respuesta.StatusCode) en ${Ruta}: $($respuesta.Content)" }
if ($respuesta.Content) { $respuesta.Content | ConvertFrom-Json -AsHashtable }
}
function ListarPaginado($Ruta) {
while ($Ruta) {
$pagina = InvocarAzure GET $Ruta
$pagina.value
$Ruta = $pagina.nextLink
}
}
$activas = @(ListarPaginado '/subscriptions?api-version=2022-12-01' | Where-Object {
$_.state -notin 'Disabled', 'Deleted' -and
(-not $Suscripciones -or $_.subscriptionId -in $Suscripciones -or $_.displayName -in $Suscripciones)
})
if (-not $activas) { throw 'No hay suscripciones activas a las que tengas acceso con ese filtro.' }
$noEncontradas = @($Suscripciones | Where-Object { $_ -and $_ -notin $activas.subscriptionId -and $_ -notin $activas.displayName })
if ($noEncontradas) { throw "No se encontró o no está activa: $($noEncontradas -join ', '). Revisa -Suscripciones." }
$nombres = @{}
foreach ($s in $activas) { $nombres[$s.subscriptionId] = $s.displayName }
function ConsultarGrafo($Consulta) {
$ids = @($activas.subscriptionId)
# Resource Graph admite hasta 1,000 suscripciones por consulta.
for ($i = 0; $i -lt $ids.Count; $i += 1000) {
$lote = @($ids[$i..([Math]::Min($i + 999, $ids.Count - 1))])
$cuerpo = @{ query = $Consulta; subscriptions = $lote; options = @{ '$top' = 1000 } }
do {
$pagina = InvocarAzure POST '/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01' $cuerpo
$pagina.data
$cuerpo.options['$skipToken'] = $pagina.'$skipToken'
} while ($pagina.'$skipToken')
}
}
$permitidas = @($RegionesPermitidas | ForEach-Object { ($_ -replace '\s', '').ToLower() })
$recursos = ConsultarGrafo 'resources | project id, name, type, location, resourceGroup, subscriptionId'
$filas = @(foreach ($recurso in $recursos) {
$region = "$($recurso.location)".ToLower()
[pscustomobject]@{
Suscripcion = $nombres[$recurso.subscriptionId]
GrupoRecursos = $recurso.resourceGroup
Recurso = $recurso.name
Tipo = $recurso.type
Region = $recurso.location
Estado = if ($region -in $permitidas) { 'Región permitida' }
elseif ($region -in '', 'global') { 'Sin región (recurso global)' }
else { 'Fuera de regiones permitidas' }
Id = $recurso.id
}
})
Write-Host "Recursos revisados: $($filas.Count)"
$filas | Group-Object Region | Sort-Object Count -Descending | ForEach-Object { Write-Host " $($_.Name): $($_.Count)" }
$fuera = @($filas | Where-Object Estado -EQ 'Fuera de regiones permitidas')
Write-Host "Fuera de regiones permitidas: $($fuera.Count)"
if ($filas.Count -eq 0) {
Write-Host 'Sin resultados.'
# Sin esto, el CSV de una corrida anterior parecería el resultado de esta.
if ($RutaCsv) { Set-Content -Path $RutaCsv -Value '' -Encoding utf8BOM }
} elseif ($RutaCsv) {
# UTF-8 con BOM para que Excel en español muestre bien los acentos.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
Write-Host "CSV guardado en $RutaCsv"
} else {
$fuera | Format-Table -AutoSize
}