停止中の EC2 インスタンスにアタッチされた EBS ボリュームの一覧を取得したい

EC2 インスタンスが停止されている場合、インスタンスの稼働時間に応じたオンデマンド料金はかかりませんが、アタッチされた EBS ボリュームの料金は通常通りかかります。環境にある停止中のインスタンスにアタッチされている EBS ボリュームでどのくらいコストがかかっているのかを確認したいあれこれがありました。

不要な EBS ボリュームを整理したい

コンバンハ、千葉(幸)です。

数百台規模の EC2 インスタンスを利用している環境を支援したことがありました。それだけの規模になれば当然 EBS ボリュームも大量にあり、EBS でかかる料金も無視できない金額になっています。

不要な EBS ボリュームのコストを削減したいとなった場合、以下を洗い出したくなります。

  • インスタンスにアタッチされていない EBS ボリューム一覧
  • 停止状態にあるインスタンスにアタッチされた EBS ボリューム一覧
    • 「一定期間以上停止」まで取れるとなお嬉しい

前者は簡単にイメージがつきましたが、後者をどのように実現すればいいか少し試行錯誤することになりました。いい感じにコマンドがまとまりましたので、その内容をご紹介します。

ボリュームタイプごとの EBS の料金一覧

先に EBS ボリュームの料金の考え方についておさらいしておきます。ボリュームタイプごとの料金を後段の表にまとめました。


  • 2023/06/20現在の東京リージョンでの料金
  • 料金の単位はいずれも USD
  • 各列の内訳は以下の月あたりの料金
    • [ストレージ] GB あたり
    • [IOPS] IOPS あたり
    • [スループット] MB/秒 あたり
区分 ボリュームタイプ ストレージ IOPS スループット
汎用 SSD gp3 0.096 0.006[1] 0.048[2]
汎用 SSD gp2 0.12 - -
プロビジョンド IOPS SSD io2 0.142 0.074[3] -
プロビジョンド IOPS SSD io1 0.142 0.074 -
スループット最適化 HDD st1 0.054 - -
Cold HDD sc1 0.018 - -

[1] 無料分の3,000IOPSを超えた分から課金対象です
[2] 無料分の125MB/秒を超えた分から課金対象です
[3] 段階的に安くなります


スループットが料金に跳ねるのは gp3 だけ、とひとまず押さえておきましょう。

最新の情報・詳細な情報は以下から確認してください。

不要な EBS ボリュームを洗い出すコマンド例

macOS(zsh)、AWS CloudShell(bash)で動作確認しています。手元の検証環境で実行したので実行例は出力が少ないです。

今回はいずれの例もテーブル形式で出力していますが、--output textと変えていただくことでテキスト形式でも出力できます。スプレッドシートに貼り付けてコスト感の試算などにもどうぞ。

インスタンスにアタッチされていない EBS ボリュームを一覧で取得する

aws ec2 describe-volumes\
  --filters Name=status,Values=available\
  --query 'Volumes[].{
    VolumeId: VolumeId,
    VolumeType: VolumeType,
    Size: Size,
    Iops: Iops,
    Throughput: Throughput,
    Name: Tags[?Key==`Name`]|[0].Value}'\
  --output table

実行結果の例は以下です。

---------------------------------------------------------------------------------------
|                                   DescribeVolumes                                   |
+------+---------------+-------+-------------+-------------------------+--------------+
| Iops |     Name      | Size  | Throughput  |        VolumeId         | VolumeType   |
+------+---------------+-------+-------------+-------------------------+--------------+
|  100 |  TestTest     |  8    |  None       |  vol-0d393242e5033bf86  |  gp2         |
|  100 |  Test         |  10   |  None       |  vol-03abce5e383164b8b  |  gp2         |
|  100 |  TestTestTest |  8    |  None       |  vol-0e3784fe72562be07  |  gp2         |
+------+---------------+-------+-------------+-------------------------+--------------+

--filters Name=status,Values=availableで「どのインスタンスにもアタッチされていない」という条件をフィルタリングしています。

出力する項目は好みで選んでみました。他にも「作成時刻」や「暗号化の有無」なども出力できますので、お好みに応じてカスタマイズしてください。

リファレンスの例より引用

{
    "Volumes": [
        {
            "AvailabilityZone": "us-east-1a",
            "Attachments": [
                {
                    "AttachTime": "2013-12-18T22:35:00.000Z",
                    "InstanceId": "i-1234567890abcdef0",
                    "VolumeId": "vol-049df61146c4d7901",
                    "State": "attached",
                    "DeleteOnTermination": true,
                    "Device": "/dev/sda1"
                }
            ],
            "Encrypted": true,
            "KmsKeyId": "arn:aws:kms:us-east-2a:123456789012:key/8c5b2c63-b9bc-45a3-a87a-5513eEXAMPLE,
            "VolumeType": "gp2",
            "VolumeId": "vol-049df61146c4d7901",
            "State": "in-use",
            "Iops": 100,
            "SnapshotId": "snap-1234567890abcdef0",
            "CreateTime": "2019-12-18T22:35:00.084Z",
            "Size": 8
        },
......

停止状態にあるインスタンスにアタッチされた EBS ボリュームを一覧で取得する

aws ec2 describe-volumes\
  --volume-ids $(aws ec2 describe-instances\
    --filters Name=instance-state-name,Values=stopped\
    --query 'Reservations[].Instances[].BlockDeviceMappings[].Ebs.VolumeId'\
    --output text)\
  --query 'Volumes[].{
    VolumeId: VolumeId,
    VolumeType: VolumeType,
    Size: Size,
    Iops: Iops,
    Throughput: Throughput,
    Name: Tags[?Key==`Name`]|[0].Value,
    Instance: Attachments[].InstanceId|[0]}'\
  --output table

実行結果の例は以下です。ひとつのインスタンスに複数のボリュームがアタッチされているケースにも対応しています。

--------------------------------------------------------------------------------------------------------------
|                                               DescribeVolumes                                              |
+---------------------+-------+---------------+-------+-------------+-------------------------+--------------+
|      Instance       | Iops  |     Name      | Size  | Throughput  |        VolumeId         | VolumeType   |
+---------------------+-------+---------------+-------+-------------+-------------------------+--------------+
|  i-05433e098a050e6ee|  3000 |  test-2-vol-1 |  8    |  125        |  vol-081935b2b813a69b1  |  gp3         |
|  i-0cd663c74bfb59d19|  3000 |  test-3-vol   |  8    |  125        |  vol-09466e3f2397da7aa  |  gp3         |
|  i-046010bb5a29e2204|  3000 |  test-1-vol   |  8    |  125        |  vol-081419a673a91a419  |  gp3         |
|  i-05433e098a050e6ee|  None |  test-2-vol-2 |  125  |  None       |  vol-05a268eeef05a6fa4  |  st1         |
+---------------------+-------+---------------+-------+-------------+-------------------------+--------------+

aws ec2 describe-volumesでは、--volume-idsとしてボリューム ID のリストを渡すことで出力結果をフィルタリングできます。

渡すボリューム ID のリストをaws ec2 describe-instancesで取得しています。

ここでは--filters Name=instance-state-name,Values=stoppedとして停止状態にあるインスタンスをフィルタリングし、そのインスタンスにアタッチされたボリューム ID を取得しています。

末尾に None が含まれる場合

aws ec2 describe-instancesでボリューム ID のリストを取得する場合、以下のように末尾にNoneが含まれることがありました。 *1

$ aws ec2 describe-instances\
  --query 'Reservations[].Instances[].BlockDeviceMappings[].Ebs.VolumeId'\
  --output text\
  --max-items 1
vol-xxxxxxxx    vol-yyyyyyyy    vol-zzzzzzzz
None

この状態でaws ec2 describe-volumes--volume-idsの値として渡そうとすると、以下のようにエラーになります。

An error occurred (InvalidParameterValue) when calling the DescribeVolumes operation: Value (vol-xxxxxxxx	vol-yyyyyyyy	vol-zzzzzzzz
None) for parameter volumes is invalid. Expected: 'vol-...'.

この場合、| sed 's/None$//'で末尾を削ることで対応できました。

先ほどのコマンドにそのまま加えてもいいですし、以下のように一度変数に格納してから実行するのも分かりやすくていいかもしれません。

volume_ids=$(aws ec2 describe-instances\
  --filters Name=instance-state-name,Values=stopped\
  --query 'Reservations[].Instances[].BlockDeviceMappings[].Ebs.VolumeId'\
  --output text\
  | sed 's/None$//')

一定期間停止状態にあるインスタンスにアタッチされた EBS ボリュームを一覧で取得する

単に「停止状態であるインスタンスにアタッチされたボリューム」だと、たまたま停止のタイミングに被った可能性もあるため、不要かどうかは判別しづらいです。

例えば「30日以上停止状態」といった条件を加える場合のコマンド例です。

# 事前に遡る日数を環境変数に格納:今回は30日前を指定
## GNU dateの場合
TIME=$(date -u -d '30 days ago' +"%Y-%m-%d %H:%M:%S %Z")
## BSD dateの場合
TIME=$(date -ju -v-30d +"%Y-%m-%d %H:%M:%S %Z")

# コマンドを実行
aws ec2 describe-volumes\
  --volume-ids $(aws ec2 describe-instances\
    --filters Name=instance-state-name,Values=stopped\
    --query "Reservations[].Instances[?StateTransitionReason < 'User initiated ($TIME)']|[].BlockDeviceMappings[].Ebs.VolumeId"\
    --output text)\
  --query 'Volumes[].{
    VolumeId: VolumeId,
    VolumeType: VolumeType,
    Size: Size,
    Iops: Iops,
    Throughput: Throughput,
    Name: Tags[?Key==`Name`]|[0].Value,
    Instance: Attachments[].InstanceId|[0]}'\
  --output table

ここではaws ec2 describe-instancesでボリューム ID のリストを取得する際に、--queryStateTransitionReason内のタイムスタンプを基にフィルタリングしています。

一点注意点として、StateTransitionReasonに停止時刻が記載されるのは AWS API によって停止された場合のみ、という条件があります。言い換えると、EC2 インスタンスの OS 上の操作でシャットダウンした場合には記載されません。その場合はこの手法を用いるのを……諦めてください。

(詳細は以下をご参考ください。)

終わりに

不要な EBS ボリュームを整理するため、未アタッチの EBS ボリュームだけでなく「停止中の EC2 インスタンスにアタッチされた EBS ボリューム」も一覧表示したい、という話でした。

aws ec2 describe-volumes--volume-idsを利用することでうまく出力対象を絞る、というのを思いついたので捗りました。--volume-idsは文字列のリストで渡す必要があるので、aws ec2 describe-instancesの結果を流用しやすくて助かります。

今回はaws ec2 describe-instancesで「一定期間停止状態にあるインスタンス」というフィルタリングをしましたが、他にも対応しているフィルタ条件は多々ありますのでお好みのものを活用してください。

一点注意点として、aws ec2 describe-volumes--volume-idsに何も値を渡さないと、すべてのボリュームが対象になります。何も出力されないわけではないので、うまくフィルタリングが効いているか判別しづらいこともあるでしょう。

今回は横着してaws ec2 describe-volumesaws ec2 describe-instancesを一度に実行しましたが、きちんと意図したリストが渡せているかの確認のため、以下のように分けて実行した方が確実かもしれません。

# ボリュームIDのリストを取得
volume_ids=$(aws ec2 describe-instances\
  --filters Name=instance-state-name,Values=stopped\
  --query "Reservations[].Instances[?StateTransitionReason < 'User initiated ($TIME)']|[].BlockDeviceMappings[].Ebs.VolumeId"\
  --output text)
  
# 適切な値が入っているか確認
echo $volume_ids

# コマンドを実行
aws ec2 describe-volumes\
  --volume-ids $volume_ids\
  --query 'Volumes[].{
    VolumeId: VolumeId,
    VolumeType: VolumeType,
    Size: Size,
    Iops: Iops,
    Throughput: Throughput,
    Name: Tags[?Key==`Name`]|[0].Value,
    Instance: Attachments[].InstanceId|[0]}'\
  --output table

お好みの方式でどうぞ。何らか役立てば幸いです。

以上、 チバユキ (@batchicchi) がお送りしました。

脚注

  1. 少なくとも--max-itemsを指定する際には再現できました。他にも何らかの原因で含まれることがあるかもしれません。