Audyt cloud sprawl - jak zidentyfikować i zredukować niekontrolowany rozrost infrastruktury chmurowej
Identyfikacja i redukcja cloud sprawl: audyt osieroconych zasobów, nieużywanych kont, nieoznakowanej infrastruktury i zduplikowanych usług. Komendy CLI dla AWS, GCP i Azure z szacunkiem odzyskanych kosztów.
Cloud sprawl to niekontrolowany rozrost zasobów, kont i usług chmurowych bez nadzoru. W odróżnieniu od świadomej architektury wielochmurowej, sprawl powstaje organicznie: deweloper zakłada konto „do testów”, zespół wdraża nowe narzędzie bez wyłączania starego, albo przejęcie firmy dodaje infrastrukturę, której nikt nie zinwentaryzował.
Jeśli zastanawiasz się, czy Twoja architektura wielochmurowa to świadomy wybór czy sprawl, sprawdź nasz przewodnik po strategii multi-cloud.
Objawy cloud sprawl
Prawdopodobnie masz cloud sprawl, jeśli trzy lub więcej z poniższych jest prawdą:
- Miesięczny rachunek za chmurę rośnie o 10%+ bez odpowiadającego wzrostu biznesu
- Istnieją zasoby bez właściciela (nikt nie wie, kto je stworzył ani po co)
- Wiele kont/subskrypcji bez konwencji nazewnictwa ani tagowania
- Zduplikowane usługi (np. dwa systemy logowania, trzy narzędzia monitoringu)
- Konta chmurowe „shadow IT” poza centralnym rozliczeniem organizacji
- Porzucone środowiska PoC nadal generujące koszty
- Alokacja kosztów: >30% wydatków nie ma żadnych tagów
Faza 1: Inwentaryzacja - co właściwie posiadasz?
AWS: lista wszystkich kont w organizacji
# Lista wszystkich kont AWS w organizacji
aws organizations list-accounts \
--query 'Accounts[].{Id:Id, Name:Name, Email:Email, Status:Status}' \
--output table
# Znajdź konta bez aktywności (brak zdarzeń CloudTrail w ostatnich 90 dniach)
for account_id in $(aws organizations list-accounts --query 'Accounts[].Id' --output text); do
event_count=$(aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccountId,AttributeValue=$account_id \
--start-time $(date -d '90 days ago' +%Y-%m-%dT%H:%M:%S) \
--max-results 1 --query 'length(Events)' --output text 2>/dev/null)
if [ "$event_count" = "0" ]; then
echo "NIEAKTYWNE: $account_id"
fi
done
AWS: znajdź osierocone zasoby
# Niepodłączone wolumeny EBS (płacisz za storage, nic ich nie używa)
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[].{VolumeId:VolumeId, Size:Size, CreateTime:CreateTime}' \
--output table
# Elastic IP bez przypisania ($3.60/mo każdy od lutego 2024)
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{PublicIp:PublicIp, AllocationId:AllocationId}' \
--output table
# Zatrzymane instancje EC2 (nadal płacisz za EBS)
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=stopped \
--query 'Reservations[].Instances[].{Id:InstanceId, Name:Tags[?Key==`Name`]|[0].Value, StoppedSince:StateTransitionReason}' \
--output table
# Load Balancery bez przypisanych targetów
aws elbv2 describe-target-groups \
--query 'TargetGroups[?length(LoadBalancerArns)==`0`].{Arn:TargetGroupArn, Name:TargetGroupName}' \
--output table
# Snapshoty RDS starsze niż 90 dni (ręczne, zapomniane)
aws rds describe-db-snapshots \
--snapshot-type manual \
--query "DBSnapshots[?SnapshotCreateTime<='$(date -d '90 days ago' +%Y-%m-%d)'].{Id:DBSnapshotIdentifier, Size:AllocatedStorage, Created:SnapshotCreateTime}" \
--output table
GCP: znajdź osierocone zasoby
# Lista wszystkich projektów (szukaj tych, których nikt nie rozpoznaje)
gcloud projects list --format="table(projectId, name, lifecycleState, createTime)"
# Niepodłączone dyski persystentne
gcloud compute disks list --filter="NOT users:*" \
--format="table(name, zone, sizeGb, status, lastAttachTimestamp)"
# Statyczne IP bez użycia
gcloud compute addresses list --filter="status=RESERVED" \
--format="table(name, address, region, status)"
# Klastry GKE bez workload
for cluster in $(gcloud container clusters list --format="value(name,zone)"); do
name=$(echo $cluster | cut -f1)
zone=$(echo $cluster | cut -f2)
pods=$(gcloud container clusters get-credentials $name --zone $zone 2>/dev/null && \
kubectl get pods --all-namespaces --no-headers 2>/dev/null | grep -v kube-system | wc -l)
echo "$name ($zone): $pods podów (poza kube-system)"
done
Azure: znajdź osierocone zasoby
# Lista wszystkich subskrypcji
az account list --query "[].{Name:name, Id:id, State:state}" --output table
# Niepodłączone dyski zarządzane
az disk list --query "[?managedBy==null].{Name:name, Size:diskSizeGb, ResourceGroup:resourceGroup}" \
--output table
# Publiczne IP bez przypisania
az network public-ip list \
--query "[?ipConfiguration==null].{Name:name, IP:ipAddress, ResourceGroup:resourceGroup}" \
--output table
# App Service Plan bez aplikacji
az appservice plan list \
--query "[?numberOfSites==\`0\`].{Name:name, Sku:sku.name, ResourceGroup:resourceGroup}" \
--output table
Faza 2: Analiza kosztów - gdzie ucieka pieniądze?
Znajdź nieoznakowane wydatki
# AWS: procent kosztów bez tagów (ostatnie 30 dni)
aws ce get-cost-and-usage \
--time-period Start=$(date -d '30 days ago' +%Y-%m-%d),End=$(date +%Y-%m-%d) \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=TAG,Key=team \
--query 'ResultsByTime[0].Groups[?Keys[0]==``].Metrics.BlendedCost.Amount' \
--output text
# Jeśli to > 30% całkowitych wydatków, masz problem z tagowaniem
Koszt w podziale na konto/projekt
# AWS: koszty per konto
aws ce get-cost-and-usage \
--time-period Start=$(date -d '30 days ago' +%Y-%m-%d),End=$(date +%Y-%m-%d) \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=DIMENSION,Key=LINKED_ACCOUNT \
--query 'ResultsByTime[0].Groups[].{Account:Keys[0], Cost:Metrics.BlendedCost.Amount}' \
--output table
# GCP: koszt per projekt (ostatnie 30 dni z eksportu billing do BigQuery)
bq query --use_legacy_sql=false '
SELECT project.name, SUM(cost) as total_cost
FROM `billing_dataset.gcp_billing_export`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY project.name
ORDER BY total_cost DESC
'
Typowe odkrycia i oszczędności
| Problem | Częstotliwość występowania | Typowy miesięczny koszt |
|---|---|---|
| Osierocone wolumeny EBS | 60% organizacji | $50-500/mo |
| Zatrzymane instancje z podpiętym EBS | 40% organizacji | $100-1000/mo |
| Zapomniane snapshoty RDS | 50% organizacji | $50-300/mo |
| Nieużywane Elastic IP | 30% organizacji | $10-50/mo |
| Porzucone konta testowe/projektowe | 25% organizacji | $200-2000/mo |
| Zduplikowane narzędzia monitoringu | 40% organizacji | $300-1500/mo |
Faza 3: Governance - zapobieganie nawrotom
Polityka tagowania (minimum viable)
Każdy zasób musi mieć co najmniej:
# Minimalne wymagane tagi - wymuś przez AWS Organizations SCP lub Azure Policy
tags:
team: "platform" # Kto jest właścicielem?
environment: "production" # prod / staging / dev / sandbox
service: "api-gateway" # Jaką usługę biznesową wspiera?
cost-centre: "eng-012" # Kto za to płaci?
Wymuszenie:
// AWS: SCP blokujący tworzenie zasobów bez wymaganych tagów
// Zastosuj do wszystkich kont poza kontem zarządzającym
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUntaggedEC2",
"Effect": "Deny",
"Action": ["ec2:RunInstances"],
"Resource": ["arn:aws:ec2:*:*:instance/*"],
"Condition": {
"Null": {
"aws:RequestTag/team": "true",
"aws:RequestTag/environment": "true"
}
}
}
]
}
Cykl życia konta/projektu
- Utworzenie - wymaga zgody + udokumentowanego celu + właściciela + daty wygaśnięcia
- Przegląd - co kwartał: czy to konto jest jeszcze potrzebne? Czy ma aktywne workload?
- Wygaszenie - brak aktywności przez 90 dni: powiadomienie właściciela → 30 dni na odpowiedź → likwidacja
- Konta sandboxowe - automatyczne czyszczenie po 7 dniach (narzędzia:
aws-nukelubcloud-nuke)
# Instalacja cloud-nuke do automatycznego czyszczenia sandboxów
# https://github.com/gruntwork-io/cloud-nuke
cloud-nuke aws --older-than 168h --config .cloud-nuke.yml --dry-run
Monitorowanie wskaźników rozrostu
Ustaw alerty na wczesne wykrywanie sprawl:
- Nowe konto AWS utworzone bez wymaganych tagów
- Wzrost kosztów >15% miesięcznie bez odpowiadającej aktywności wdrożeniowej
- Zasoby starsze niż 90 dni bez aktywności w CloudTrail
- Nowe subskrypcje/projekty utworzone poza zatwierdzonym procesem
Faza 4: Redukcja - co usunąć, a co skonsolidować
Matryca decyzyjna
| Stan zasobu | Akcja | Ryzyko |
|---|---|---|
| Brak właściciela + brak ruchu od 90 dni | Usunięcie (po wykonaniu snapshot) | Niskie |
| Ma właściciela + brak ruchu od 90 dni | Powiadomienie właściciela, 30 dni na decyzję | Niskie |
| Zduplikowane narzędzie (2 systemy monitoringu) | Konsolidacja do jednego | Średnie |
| Porzucone konto testowe | Pełna likwidacja | Niskie |
| Konto z aktywnym workload produkcyjnym, ale bez governance | Dodaj tagi, włącz do organizacji, przypisz właściciela | Brak |
Bezpieczny proces usuwania
# Przed usunięciem czegokolwiek: stwórz końcowy snapshot/backup
# Wolumen EBS
aws ec2 create-snapshot --volume-id vol-xxx --description "backup przed usunieciem"
aws ec2 delete-volume --volume-id vol-xxx
# Czyszczenie snapshotów RDS (zostaw najnowszy, usuń starsze)
aws rds delete-db-snapshot --db-snapshot-identifier stary-snapshot-nazwa
# Zwolnienie Elastic IP
aws ec2 release-address --allocation-id eipalloc-xxx
Kandydaci do konsolidacji
Jeśli znajdziesz zduplikowane usługi, skonsoliduj do jednej:
| Duplikat | Skonsoliduj do | Powód |
|---|---|---|
| CloudWatch + Datadog + Grafana | Wybierz jedno (Datadog LUB Grafana Cloud) | Trzy narzędzia = potrójny koszt, nikt nie przegląda wszystkich trzech |
| Konta AWS z identycznymi workload | Połącz w jedno konto ze wspólnymi namespace | Zmniejsza narzut zarządzania kontami |
| Wiele narzędzi CI/CD (Jenkins + GitHub Actions + CodePipeline) | GitHub Actions (lub Twoje główne) | Jeden standard pipeline, jeden zestaw poświadczeń |
Walidacja
Po zakończeniu porządków zweryfikuj rezultaty:
# Sprawdzenie: brak niepodłączonych wolumenów EBS
aws ec2 describe-volumes --filters Name=status,Values=available --query 'length(Volumes)'
# Oczekiwane: 0
# Sprawdzenie: brak niepodłączonych Elastic IP
aws ec2 describe-addresses --query 'length(Addresses[?AssociationId==null])'
# Oczekiwane: 0
# Sprawdzenie: zgodność tagowania (powinno być > 90%)
aws ce get-cost-and-usage \
--time-period Start=$(date -d '30 days ago' +%Y-%m-%d),End=$(date +%Y-%m-%d) \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=TAG,Key=team \
--query 'ResultsByTime[0].Groups[?Keys[0]!=``].Metrics.BlendedCost.Amount' --output text
# Oblicz: koszt_otagowany / koszt_calkowity * 100 > 90%
Zaplanuj ten audyt kwartalnie. Pierwsze uruchomienie zazwyczaj pozwala zaoszczędzić 15-30% miesięcznych wydatków chmurowych wyłącznie dzięki usunięciu osieroconych zasobów.
Następne kroki
- Skonfiguruj automatyczne alerty na wskaźniki rozrostu (AWS Budgets, GCP Budget alerts)
- Wdróż dashboard kosztów oparty na tagach (sprawdź nasz przewodnik wdrażania modelu showback)
- Rozważ AWS Organizations z SCP do governance (lub GCP Organisation Policies)
- Zweryfikuj architekturę wielochmurową: czy to świadoma decyzja czy sprawl? (sprawdź nasz przewodnik po strategii multi-cloud)
Jeśli Twoja infrastruktura chmurowa rozrosła się ponad możliwości zarządzania przez zespół, możemy przeprowadzić ten audyt wspólnie i wdrożyć zasady governance. Umów się na bezpłatną konsultację, żebyśmy mogli omówić sytuację.