AWS에서 돈을 낭비하지 마세요: TypeScript로 좀비 리소스를 찾는 기술 가이드

발행: (2026년 1월 2일 오후 02:02 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역이 필요한 본문 텍스트를 제공해 주시겠어요?
코드 블록, URL 및 마크다운 형식은 그대로 유지하면서 내용만 한국어로 번역해 드리겠습니다.

AWS의 유령 비용

AWS에서 인프라를 관리한다면, 월말에 청구서를 열 때 느끼는 그 소름을 이미 경험했을 가능성이 높습니다.
생산용 EC2 인스턴스의 명백한 비용에 대해 말하는 것이 아니라, **“유령 비용”**에 대해 이야기하고 있습니다:

  • 3개월 전에 삭제된 staging 환경에 남아 있던 NAT Gateway.
  • 인스턴스에서 분리되었지만 여전히 존재하며 매달 US$ 0.08/GB를 청구하는 500 GB EBS 디스크.

한 개의 유휴 NAT Gateway는 월 약 US$ 32.00에 해당합니다 – R$ 2,400.00 정도이며, 한 바이트도 전송되지 않은 리소스에 대한 비용입니다.

AWS는 이러한 낭비를 시각화하기 쉽게 해 주지 않습니다. Cost Explorer는 당신이 얼마나 지출했는지 보여주지만, 예산을 잡아먹는 특정 ID를 가리키는 경우는 드뭅니다.

TypeScript와 AWS SDK v3를 이용한 직접 구현 솔루션

소프트웨어 엔지니어로서 제 첫 반응은: “이것에 비용을 지불하지 않겠다. 코드로 해결하겠다.”
아래에서는 간단한 비용 감사자를 만드는 방법을 보여드립니다. 이 감사자는:

  1. 고아 EBS 볼륨을 감지합니다 (available 상태).
  2. 유휴 NAT Gateway를 감지합니다 (최근 7일 동안 연결이 없는 경우).

의존성

npm install @aws-sdk/client-ec2 @aws-sdk/client-cloudwatch
npm install -D typescript ts-node @types/node

스크립트 (TypeScript)

import {
  EC2Client,
  DescribeVolumesCommand,
  DescribeNatGatewaysCommand,
} from '@aws-sdk/client-ec2';
import {
  CloudWatchClient,
  GetMetricStatisticsCommand,
} from '@aws-sdk/client-cloudwatch';

// -------------------------------------------------
// Configuração
// -------------------------------------------------
const REGION = 'us-east-1';               // região alvo
const ec2 = new EC2Client({ region: REGION });
const cw  = new CloudWatchClient({ region: REGION });

async function findZombies() {
  console.log(`🔍 Iniciando varredura em: ${REGION}...`);

  // -------------------------------------------------
  // 1️⃣ DETECTAR VOLUMES EBS ÓRFÃOS
  // -------------------------------------------------
  const volumesData = await ec2.send(
    new DescribeVolumesCommand({
      Filters: [{ Name: 'status', Values: ['available'] }],
    })
  );

  if (volumesData.Volumes?.length) {
    console.log(
      `\n🚨 ALERTA: ${volumesData.Volumes.length} volume(s) EBS órfão(s) encontrado(s):`
    );
    volumesData.Volumes.forEach(vol => {
      const cost = (vol.Size ?? 0) * 0.08; // estimativa gp3 (US$ 0,08/GB)
      console.log(
        ` - ID: ${vol.VolumeId} | Tamanho: ${vol.Size} GB | Desperdício: ~$${cost.toFixed(2)}/mês`
      );
    });
  } else {
    console.log('\n✅ Nenhum volume EBS órfão.');
  }

  // -------------------------------------------------
  // 2️⃣ DETECTAR NAT GATEWAYS OCIOSOS
  // -------------------------------------------------
  const natData = await ec2.send(
    new DescribeNatGatewaysCommand({
      Filters: [{ Name: 'state', Values: ['available'] }],
    })
  );

  if (natData.NatGateways?.length) {
    console.log(`\n📡 Analisando ${natData.NatGateways.length} NAT Gateway(s)...`);

    for (const nat of natData.NatGateways) {
      // Métricas do CloudWatch: ActiveConnectionCount
      const metrics = await cw.send(
        new GetMetricStatisticsCommand({
          Namespace: 'AWS/NATGateway',
          MetricName: 'ActiveConnectionCount',
          Dimensions: [{ Name: 'NatGatewayId', Value: nat.NatGatewayId! }],
          StartTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // últimos 7 dias
          EndTime:   new Date(),
          Period: 86400, // 1 dia
          Statistics: ['Sum'],
        })
      );

      const totalConnections = metrics.Datapoints?.reduce(
        (acc, dp) => acc + (dp.Sum ?? 0),
        0
      ) ?? 0;

      if (totalConnections === 0) {
        console.log(`⚠️  NAT ZUMBI DETECTADO: ${nat.NatGatewayId}`);
        console.log(`   Custo estimado: ~$32.00/mês | VpcId: ${nat.VpcId}`);
      }
    }
  } else {
    console.log('\n✅ Nenhum NAT Gateway ativo.');
  }
}

// -------------------------------------------------
findZombies().catch(err => {
  console.error('❌ Erro ao executar auditor:', err);
});

기술적 참고 사항

예제를 가독성 있게 유지하기 위해 페이지네이션을 구현하지 않았습니다. 수백 개의 리소스를 보유한 프로덕션 계정에서는 AWS API가 부분적인 결과와 NextToken만 반환합니다. 모든 리소스를 놓치지 않으려면 while (nextToken) 루프를 사용해 호출을 감싸야 합니다.

왜 일상에서 자체 스크립트를 사용하지 말아야 할까?

문제영향
페이지네이션 복잡성수동 루프는 오류에 취약합니다(무한 루프 또는 불완전한 데이터).
멀티‑리전 지옥AWS는 30개가 넘는 리전이 있습니다; 각 리전에 대해 루프를 만들어야 하므로 복잡성이 증가합니다.
로컬 보안관리 권한이 있는 Access Key를 머신에 저장해야 합니다. 키가 유출되면 위험이 전면적입니다.
유지보수API가 바뀌고, 메트릭이 진화합니다. 스크립트를 최신 상태로 유지하는 데 시간이 소요되어 제품 기능에 사용할 수 있는 시간을 빼앗습니다.

엔지니어링은 지루함을 자동화해야 하며, 더 많은 유지보수를 만들지 않아야 합니다.

InfraLens – 코드 없는 자동 감사

이러한 마찰점을 없애기 위해 InfraLens를 개발했습니다. 그것은:

  • 위의 로직을 캡슐화합니다 (자동 페이지네이션 및 다중 리전 스캔 포함).
  • 감사를 위한 무료 웹 도구에 모두 패키징합니다.
  • 아키텍처는 100 % 읽기 전용이며 에이전트가 없습니다 – 계정에 아무 것도 기록되지 않습니다.

설계상의 보안

제어작동 방식
메타데이터만임시 IAM 역할은 Describe*List*에 대한 엄격한 권한만 가집니다.
쓰기 없음Create*, Delete*, Modify* 권한이 없습니다.
데이터 접근 없음데이터베이스 내용이나 S3 객체를 읽을 수 없습니다.

의문: “내 AWS를 제3자 도구에 연결해도 될까요?”
답변: 연결은 CloudFormation이 만든 임시 역할을 통해 이루어집니다. 로컬에 영구 키가 저장되지 않습니다.

요약

  1. 유휴 자원을 식별 빠른 스크립트로 (위 예시).
  2. 유지보수, 보안 및 다중 지역의 복잡성을 고려하세요.
  3. InfraLens와 같은 즉시 사용 가능한 솔루션을 채택하여 지속적이고 안전하며 코드 없는 감사를 수행하세요.

스​크립트를 시험해 보거나 InfraLens에 대해 더 알고 싶다면 아래에 댓글만 달아 주세요! 🚀

수동 스크립트를 계속 실행하고 (주말에 페이지네이션을 디버깅하면서) 할 수도 있고, 5분 안에 해결할 수도 있습니다.

지금 얼마나 많은 돈을 놓치고 있는지 확인하고 싶다면 데모 모드(로그인 없이)와 실제 스캐너에 대한 접근을 열어두었습니다.

👉 InfraLens에서 무료 감사를 진행하세요

재무팀이 문을 두드릴 때까지 낭비를 발견하지 않도록 하세요.

Back to Blog

관련 글

더 보기 »

CloudFront: 손해 보는 곳

!CloudFront: Where You Lose Money 표지 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-...