不要な 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 のリストを取得する際に、--query
でStateTransitionReason
内のタイムスタンプを基にフィルタリングしています。
一点注意点として、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-volumes
とaws 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) がお送りしました。
脚注
- 少なくとも--max-itemsを指定する際には再現できました。他にも何らかの原因で含まれることがあるかもしれません。 ↩