EC2 스팟 인스턴스 설정 실전 가이드 — 비용 70% 절감과 중단 핸들링

목차

$ aws ec2 describe-spot-price-history \
    --instance-types m5.xlarge \
    --product-descriptions "Linux/UNIX" \
    --region ap-northeast-2 \
    --max-results 3 \
    --query 'SpotPriceHistory[*].[AvailabilityZone,SpotPrice,Timestamp]' \
    --output table

----------------------------------------------------------------
|                  DescribeSpotPriceHistory                    |
+------------------+------------+--------------------------------+
|  ap-northeast-2a |  0.069600  |  2026-06-26T08:14:32+00:00    |
|  ap-northeast-2b |  0.071200  |  2026-06-26T08:14:32+00:00    |
|  ap-northeast-2c |  0.068800  |  2026-06-26T08:14:32+00:00    |
+------------------+------------+--------------------------------+

게다가, m5.xlarge 온디맨드 가격이 시간당 0.236달러다. 같은 인스턴스가 스팟에서는 0.07달러 근처에 머문다. 약 70% 할인. EC2 스팟 인스턴스 설정이 매력적인 이유는 이 한 줄짜리 가격표에서 시작된다. 다만 이 가격은 공짜가 아니라, "AWS가 필요할 때 2분 안에 회수할 수 있다"는 조건이 붙은 가격이다.

프론트엔드에서 백엔드로 넘어온 지 2년쯤 됐는데, 인프라 비용 절감 과제를 받았을 때 가장 먼저 검토한 게 스팟이었다. 후보는 세 개였다. 온디맨드를 유지하되 인스턴스 타입을 다운사이징, Savings Plans 3년 약정, 그리고 스팟 + Auto Scaling. 결과적으로 스팟을 골랐고, 그 과정에서 정리한 비교 기준과 설정값을 공유한다.

스팟이 싸지만 항상 답은 아닌 이유

또한, 스팟 인스턴스는 AWS의 여유 컴퓨팅 자원을 경매식으로 빌려주는 모델이다. 가격은 수요·공급에 따라 결정되고, AWS가 자원을 회수해야 하면 2분 전에 알림을 보낸 뒤 인스턴스를 중단(또는 종료)한다. 2017년 이후로 호가 입찰 방식은 없어졌고, 사용자는 "최대 지불 의사 가격"만 지정하면 된다. (출처: AWS Spot Instances 공식 문서, 2026년 6월 기준)

물론, 회수율은 인스턴스 패밀리와 리전에 따라 다르다. AWS Spot Instance Advisor 기준으로 m5, c5 패밀리는 회수율이 5% 미만인 경우가 많고, p3, g4 같은 GPU 패밀리는 15%를 넘기도 한다. 한국 리전(ap-northeast-2)은 대체로 안정적인 편이다.

가격은 시간당이 아니라 초당으로 청구된다

스팟은 최소 60초 사용 후 초 단위로 청구된다. 짧은 배치 작업에서는 이 점이 의외로 중요하다. 1분 35초 동안 돈 작업은 정확히 95초 분량의 요금만 나온다. 온디맨드도 동일하지만, 스팟에서 이게 의미 있는 건 작업이 중단됐을 때 "쓴 만큼만" 나간다는 점이다.

그래도 안 맞는 워크로드

또한, 스팟이 안 맞는 케이스는 명확하다. 상태를 디스크에 유지해야 하는 단일 노드 데이터베이스, 세션을 메모리에 들고 있는 WebSocket 서버, 종료 시점이 곧 SLA에 영향을 주는 결제 API. 이런 워크로드에 스팟을 쓰면 회수 한 번에 장애가 난다.

비교 기준: 온디맨드 vs Savings Plans vs 스팟

즉, 세 옵션을 비교할 때 단순히 단가만 보면 잘못된 결정을 한다. 약정 기간, 워크로드 변동성, 운영 오버헤드를 함께 봐야 한다.

구분 온디맨드 Compute Savings Plans (3년, 전액선납) 스팟
할인율 0% (기준) 최대 약 66% 최대 약 90% (평균 60~70%)
약정 기간 없음 1년 또는 3년 없음
중단 위험 없음 없음 2분 알림 후 회수 가능
인스턴스 변경 자유도 자유 패밀리/리전 변경 가능 자유 (다양한 타입 혼합 권장)
운영 오버헤드 낮음 낮음 중간~높음 (중단 핸들링 필요)
추천 워크로드 단기 실험, 상태 보유 서비스 베이스라인 부하 무상태 워커, 배치, CI 러너

그래서, (할인율과 정책은 2026년 6월 기준이며, 시점에 따라 변동한다. 출처: AWS Savings Plans, Spot Instance Pricing)

여기서 자주 놓치는 부분이 있다. 세 옵션은 양자택일이 아니다. 베이스라인은 Savings Plans, 트래픽 스파이크는 스팟, 결제처럼 끊기면 안 되는 경로는 온디맨드로 분리하는 식의 혼합이 실제로는 가장 흔하다. 프론트엔드에서 CDN/엣지 캐시 계층을 나누던 감각과 비슷하더라. 어떤 요청을 어디서 받느냐를 비용 단위로 다시 자르는 거다.

스팟 인스턴스 설정 — CLI로 끝내는 최소 구성

그런데, 콘솔에서 클릭으로 띄우는 건 한 번만 해보면 충분하다. 실제 운영은 CLI나 IaC로 한다. 가장 단순한 형태부터 보자.

단일 스팟 요청 (persistent vs one-time)

# 일회성 스팟 요청 — 중단되면 끝
aws ec2 request-spot-instances \
    --instance-count 1 \
    --type "one-time" \
    --launch-specification file://spec.json

spec.json은 다음과 같다.

{
  "ImageId": "ami-0c9c942bd7bf113a2",
  "InstanceType": "m5.xlarge",
  "KeyName": "my-keypair",
  "SecurityGroupIds": ["sg-0abc123"],
  "SubnetId": "subnet-0def456",
  "IamInstanceProfile": {
    "Name": "ec2-spot-worker-role"
  }
}

--typepersistent로 바꾸면 회수된 뒤 가격이 다시 맞을 때 자동으로 재기동을 시도한다. 배치 워커에는 persistent가 편하지만, 회수가 잦은 시간대에 재시도 폭주가 일어나기도 한다. 일회성으로 두고 상위 스케줄러에서 재시도를 통제하는 방식을 선호하는 편이다.

Launch Template — 진짜 운영용

운영 환경에서는 단일 요청 대신 Launch Template을 만들고 Auto Scaling Group이 그걸 참조하게 한다. 인스턴스 타입을 다양화하지 않은 스팟은 운명을 한 패밀리에 거는 거나 마찬가지다.

aws ec2 create-launch-template \
    --launch-template-name spot-worker-template \
    --version-description "v1" \
    --launch-template-data '{
      "ImageId": "ami-0c9c942bd7bf113a2",
      "IamInstanceProfile": {"Name": "ec2-spot-worker-role"},
      "SecurityGroupIds": ["sg-0abc123"],
      "UserData": "'$(base64 -w0 user-data.sh)'",
      "TagSpecifications": [{
        "ResourceType": "instance",
        "Tags": [{"Key": "Role", "Value": "spot-worker"}]
      }]
    }'

예를 들어, 여기서 InstanceType을 지정하지 않는 게 포인트다. ASG의 Mixed Instances Policy에서 후보 타입을 여러 개 넘기기 때문이다.

Auto Scaling Mixed Instances Policy 연동

스팟 풀은 인스턴스 타입 × 가용영역 조합 단위로 나뉜다. 한 풀이 마르면 다른 풀에서 받아야 한다. ASG의 Mixed Instances Policy가 이 분산을 자동으로 해준다.

{
  "AutoScalingGroupName": "spot-workers-asg",
  "MinSize": 2,
  "MaxSize": 20,
  "DesiredCapacity": 4,
  "VPCZoneIdentifier": "subnet-0aaa,subnet-0bbb,subnet-0ccc",
  "MixedInstancesPolicy": {
    "LaunchTemplate": {
      "LaunchTemplateSpecification": {
        "LaunchTemplateName": "spot-worker-template",
        "Version": "$Latest"
      },
      "Overrides": [
        {"InstanceType": "m5.xlarge"},
        {"InstanceType": "m5a.xlarge"},
        {"InstanceType": "m5n.xlarge"},
        {"InstanceType": "m4.xlarge"},
        {"InstanceType": "m6i.xlarge"}
      ]
    },
    "InstancesDistribution": {
      "OnDemandBaseCapacity": 1,
      "OnDemandPercentageAboveBaseCapacity": 25,
      "SpotAllocationStrategy": "capacity-optimized"
    }
  }
}

그래서, 핵심 옵션을 짚어두면:

  • OnDemandBaseCapacity: 1 — 최소 1대는 무조건 온디맨드. 전체가 회수되는 시나리오를 막는 보험이다.
  • OnDemandPercentageAboveBaseCapacity: 25 — 베이스 이상은 25%만 온디맨드, 나머지 75%는 스팟. 비용 가중치는 스팟에 두면서도 회수 충격을 완화한다.
  • SpotAllocationStrategy: capacity-optimized — 회수율이 가장 낮은 풀을 우선 선택. 과거 lowest-price가 기본이었지만 capacity-optimized로 바꾼 뒤 회수 빈도가 체감상 절반 이하로 줄었다. (출처: Auto Scaling Spot allocation strategies)

그래서, 5개 타입 × 3개 AZ면 풀이 15개로 분산된다. 한 풀이 회수돼도 다른 풀에서 채우면서 평균 가동률이 유지된다.

중단 핸들링 — 2분 안에 끝내야 하는 일

스팟의 가장 큰 함정이 여기다. 회수 알림은 인스턴스 메타데이터 v2(IMDSv2)로 들어온다. 2분 안에 진행 중인 작업을 끝내거나, 다른 노드로 인계해야 한다.

메타데이터 폴링 — 가장 단순한 방식

#!/bin/bash
# /usr/local/bin/spot-watcher.sh
# 5초마다 회수 알림 확인
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 60")

while true; do
  HTTP_CODE=$(curl -s -o /tmp/spot-action -w "%{http_code}" \
    -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/spot/instance-action)

  if [ "$HTTP_CODE" = "200" ]; then
    logger "Spot interruption detected: $(cat /tmp/spot-action)"
    # 작업 인계 시작
    /usr/local/bin/drain-worker.sh
    break
  fi
  sleep 5
done

/spot/instance-action 엔드포인트는 평소엔 404를 반환하다가, 회수가 결정되면 200과 함께 {"action": "terminate", "time": "..."} 같은 JSON을 돌려준다. 토큰은 60초마다 갱신해줘야 한다(IMDSv2 요구사항).

EventBridge로 더 깔끔하게

폴링이 싫으면 EventBridge가 답이다. EC2 Spot Instance Interruption Warning 이벤트를 SQS나 Lambda로 받아서 처리한다.

{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Spot Instance Interruption Warning"]
}

거기에, ASG를 쓰고 있다면 한 단계 더 편하다. Capacity Rebalancing을 켜면 회수 신호가 오기 전에 ASG가 미리 대체 인스턴스를 띄우기 시작한다. ELB Target Group에서는 새 인스턴스가 healthy가 된 뒤 기존 인스턴스를 deregister 하므로 트래픽 손실이 거의 없다.

aws autoscaling update-auto-scaling-group \
    --auto-scaling-group-name spot-workers-asg \
    --capacity-rebalance

즉, 이 설정 한 줄이 운영 난이도를 크게 떨어뜨린다. 백엔드 전환 초기에 이걸 모르고 cron에서 폴링 스크립트를 박아두던 시절이 있었는데, 지금 보면 시행착오였다.

실제 비용 절감 사례 — 무상태 워커 클러스터

내부 데이터 처리 워커를 m5.xlarge 8대로 24시간 돌리던 환경을 예로 들면, 월 비용 차이는 대략 다음과 같다.

구성 시간당 단가 월 비용 (8대 × 730h) 비고
온디맨드 전량 $0.236 약 $1,378 단순, 변동 없음
Savings Plans 3년 (전액선납) $0.080 (실효) 약 $467 약정 리스크
스팟 75% + 온디맨드 25% $0.117 (가중평균) 약 $683 중단 핸들링 필요
스팟 전량 (이론치) $0.070 약 $409 베이스라인 보장 없음

따라서, (단가는 ap-northeast-2 기준 추정치이며 실제 청구는 시점·할인에 따라 다르다.)

그래서, Savings Plans 3년 전액선납이 숫자상 가장 싸 보이지만, 워크로드가 1년 안에 사라지거나 인스턴스 패밀리를 ARM(Graviton)으로 갈아탈 가능성이 있다면 약정이 손해다. 우리 팀의 경우 워크로드 변화 가능성이 컸고 인스턴스 다양화로 스팟 회수 충격을 줄일 수 있어서 "스팟 75% + 온디맨드 25%" 구성을 골랐다. 결과적으로 월 비용은 50% 수준으로 떨어졌고, 중단으로 인한 작업 재시도 비율은 0.6% 이하였다.

판단 기준 — 언제 쓰고 언제 안 쓰는가

스팟을 도입할지 말지의 결정은 단순화하면 세 질문으로 끝난다.

스팟이 맞는 상황이라면 스팟이 답이다. 워크로드가 무상태고, 작업 단위가 작거나 재시도 가능하고, 노드 수가 10대 이상이라 일부 회수돼도 전체 처리량에 큰 충격이 없는 경우다. 배치 ETL, CI 러너, 비동기 큐 워커, 렌더링 파이프라인, ML 학습(체크포인트 저장 전제) 등이 여기 들어간다.

예를 들어, Savings Plans가 맞는 상황이라면 SP가 답이다. 향후 1년 이상 베이스라인 부하가 확실하고, 인스턴스 패밀리 변경 계획이 없으며, 회수 위험을 운영에서 받아내기 어려운 경우. 24시간 떠 있어야 하는 API 백엔드의 기본 capacity가 대표적이다.

온디맨드를 유지해야 하는 상황이라면 그냥 온디맨드를 쓴다. 단기 실험, 트래픽 패턴 파악 단계, 그리고 끊기면 즉시 사용자 영향이 나는 단일 노드 컴포넌트. 비용 최적화는 부하 패턴이 안정된 뒤에 해도 늦지 않다.

실제로, 지금 당장 할 수 있는 액션 세 가지로 정리하자면, 첫째는 aws ec2 describe-spot-price-history로 운영 중인 인스턴스 타입의 최근 90일 스팟 가격을 조회해보는 것이다. 둘째는 Spot Instance Advisor에서 회수율이 5% 미만인 인스턴스 타입을 후보 3~5개 추리는 것이다. 셋째는 기존 ASG에 capacity-rebalance를 켜고 Mixed Instances Policy로 OnDemandBaseCapacity만 설정해 보는 것. 이 세 단계만으로도 한 달치 청구서가 눈에 띄게 바뀐다.

관련 글