💾

Amazon EBSのボリュームサイズをElasticにしたい

2025/02/13に公開

2行でまとめると

  • EBSボリュームのサイズを使用中に増やす方法を検討した
  • LVMを使用すればボリュームを継ぎ足してサイズを増やせる

課題

動画のエンコードをAmazon EC2上で行う場合、ファイルの置き場所には通常Amazon EBSが使用されます。
EBSは原則としてボリューム作成時に指定したサイズまでしか使えないのですが、処理によって必要となるボリュームサイズが大きく異なる場合、ボリュームを必要とされる最大サイズで作成すると、主に費用の面で無駄が大きくなってしまう課題があります。

例えばあるエンコード処理では5GB必要で、別のエンコード処理では500GB必要となるような場合、小さいサイズ(5GB+)でボリュームを作成すると500GBの処理では容量が不足し、大きいサイズ(500GB+)で作成すると、5GBの処理では使われなかった分(495GB)の費用が無駄になります。

解決案の検討

金額を気にしない以外の解決案として次の案が考えられます。

Elastic Volumesを使用してボリュームサイズを増やす

Elastic Volumesを使用すると対象のEBSボリュームサイズを拡張することができます。この機能を使用して、ボリュームサイズが不足しそうになったらサイズ拡張を行う方法です。
ドキュメントを読むとサイズ拡張には数分〜数時間掛かり、拡張後は同じボリュームに対して再度サイズ拡張を行うには最低6時間待つように書かれています。

  • After modifying a volume, you must wait at least six hours and ensure that the volume is in the in-use or available state before you can modify the same volume.
  • Modifying an EBS volume can take from a few minutes to a few hours, depending on the configuration changes being applied. ...

Modify an Amazon EBS volume using Elastic Volumes operations

動画エンコード途中でボリュームサイズが不足した時に最大数時間処理を止める前提が必要となると、時間と費用の面で無駄が多くなり、今回の課題を解決するための案としては適切ではありません。

処理開始前に必要となるサイズを計算してEBSボリュームを作成する

動画のエンコードを始める前に、

  • 入力ファイルのサイズ
  • 入力(出力)ファイルの再生時間
  • エンコード出力の合計ビットレート
  • 中間ファイルのサイズ

から必要となるサイズを計算してEBSボリュームを作成する方法です。
この方法は、毎回同じ設定でEC2インスタンスが起動できない点、入力ファイルをダウンロードする前に再生時間を知ることが難しい[1]点や、EC2インスタンスを他の(必要サイズの異なる)動画エンコードには使えないため、都度インスタンスの起動と終了が必要となる点がいまいちな案です。

LVMを使用してEBSボリュームを継ぎ足す

適当なサイズのEBSボリュームをアタッチしたEC2インスタンスを起動し、処理中にボリュームサイズが不足しそうになったら追加のボリュームを作成しLVM (Logical Volume Manager)を使用して既存のボリュームに継ぎ足す方法です。
継ぎ足す際に実行中の動画エンコード等の処理に影響が出なければ今回の課題を解決できそうですので、この案を試してみます。

実装

環境

OSとしてAmazon Linux 2 (x86_64)を使用します。

権限

EBSのボリューム作成とEC2へのアタッチに以下の権限が必要になります。

  • ec2:CreateVolume
  • ec2:AttachVolume
  • ec2:ModifyInstanceAttribute

EC2のIAMロールにこれら権限の許可を追加します。

起動時の処理

ユーザーデータを使用してEC2インスタンスの起動時にボリュームの初期化を行います。

Name Value
Region ap-northeast-1
最初のデバイス名 /dev/sdf
EBS ボリュームサイズ 40GB
EBS ボリュームタイプ gp3
LVM ボリュームグループ名 evg
LVM 論理ボリューム名 elv
ファイルシステム XFS
マウントポイント /data
USER_DATA=`cat << _EOL_ | base64
#!/bin/bash
pvcreate /dev/sdf
vgcreate evg /dev/sdf
lvcreate -n elv -l 100%FREE evg
mkfs -t xfs /dev/evg/elv
mkdir /data
mount /dev/evg/elv /data
chown username:groupname /data
_EOL_
`

aws ec2 run-instances \
    --image-id "ami-XXXXXXXXXXXXXX" \
    --instance-type "c6i.4xlarge" \
    --block-device-mappings "DeviceName=/dev/sdf,Ebs={DeleteOnTermination=True,VolumeSize=40,VolumeType=gp3}" \
    --key-name "XXXXXXXXXXXXXX" \
    --security-group-ids "sg-XXXXXXXXXXXXXX" \
    --subnet-id "subnet-XXXXXXXXXXXXXX" \
    --iam-instance-profile "EC2Role-XXXXXXXXXXXXXX" \
    --instance-initiated-shutdown-behavior terminate \
    --instance-market-options "MarketType=spot" \
    --user-data "${USER_DATA}" \
    --region "ap-northeast-1"

ボリュームサイズ拡張処理

ボリュームサイズの拡張処理は以下の順に行います。処理はボリュームの空きが不足したEC2上で行います。

  1. EBSのボリューム作成
  2. 作成されたボリュームをEC2にアタッチ
  3. アタッチされたボリュームのDeleteOnTerminationを有効に設定
  4. 論理ボリュームに追加
  5. ファイルシステムの拡張

EBSのボリューム作成

EBSのボリュームを作成します。成功するとVolumeIdを含む結果が返ってきます。

aws ec2 create-volume \
    --region "ap-northeast-1" \
    --availability-zone "ap-northeast-1a" \
    --volume-type gp3 \
    --size 40

作成されたボリュームをEC2にアタッチ

作成されたEBSボリュームをEC2インスタンスにアタッチします。デバイス名には/dev/sd[f-p]推奨されていますので、使っていないの名前を使用します。(/dev/sdf/dev/sdg/dev/sdh)

aws ec2 attach-volume \
    --region "ap-northeast-1" \
    --device "/dev/sdg" \
    --instance-id "i-XXXXXXXXXXXXXX" \
    --volume-id "vol-XXXXXXXXXXXXXX"

アタッチされたボリュームのDeleteOnTerminationを有効に設定

アタッチされたEBSボリュームがEC2の終了時に削除されるように設定します。この設定を行わないとEC2終了後もボリュームが残り(費用が発生し)続けます。

MAPPINGS=`cat << _EOL_
{
  "DeviceName":"/dev/sdg",
  "Ebs":{
    "DeleteOnTermination":true
  }
}
_EOL_
`

aws ec2 modify-instance-attribute \
    --region "ap-northeast-1" \
    --instance-id "i-XXXXXXXXXXXXXX" \
    --block-device-mappings "${MAPPINGS}"

論理ボリュームに追加

アタッチされたボリュームの全領域を、起動時に作成したLVMの論理ボリュームに追加します。

/usr/sbin/pvcreate /dev/sdg
/usr/sbin/vgextend evg /dev/sdg
/usr/sbin/lvextend -l +100%FREE "/dev/evg/elv"

ファイルシステムの拡張

最後にファイルシステムの拡張を行います。

/usr/sbin/xfs_growfs /data

全体

ボリュームサイズ拡張処理全体のスクリプトはこちらになります。

ボリュームサイズ拡張スクリプト
extend-volume.sh
#!/bin/bash

# jq コマンドが必要です

# EC2インスタンスメタデータからRegion, AvailabilityZone, InstanceIdを取得します
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
AZ=`curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone`
REGION=${AZ:0:-1}
INSTANCE_ID=`curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id`

# 設定値
SIZE="40"
VG_NAME="evg"
LV_NAME="elv"
MOUNT_POINT="/data"

# "次"のデバイス名を生成します
# /dev/sdf がある場合は /dev/sdg (f→g)
LAST_DEVICE=`find /dev -name "sd*" | sort | tail -n 1`
LAST_DEVICE_SUFFIX=${LAST_DEVICE: -1}
NEXT_DEVICE_SUFFIX=$(printf "\x$((`printf "%x" "'$LAST_DEVICE_SUFFIX"`+1))")
NEXT_DEVICE_NAME="/dev/sd${NEXT_DEVICE_SUFFIX}"

# ボリュームの作成
RESULT_CV=`aws ec2 create-volume --region "${REGION}" --availability-zone "${AZ}" --volume-type gp3 --size ${SIZE}`
VOLUME_ID=`echo $RESULT_CV | jq -r .VolumeId`

# ボリューム作成後すぐにアタッチを行うと失敗する場合があるため3秒待機します("3秒"に深い意味はありません)
sleep 3

# ボリュームをEC2にアタッチ
aws ec2 attach-volume \
    --region "${REGION}" \
    --device "${NEXT_DEVICE_NAME}" \
    --instance-id "${INSTANCE_ID}" \
    --volume-id "${VOLUME_ID}"

# DeleteOnTerminationを有効に設定
MAPPINGS=`cat << _EOL_
{
  "DeviceName":"${NEXT_DEVICE_NAME}",
  "Ebs":{
    "DeleteOnTermination":true
  }
}
_EOL_
`
aws ec2 modify-instance-attribute \
    --region "${REGION}" \
    --instance-id "${INSTANCE_ID}" \
    --block-device-mappings "${MAPPINGS}"

# 論理ボリュームの拡張
/usr/sbin/pvcreate "${NEXT_DEVICE_NAME}"
/usr/sbin/vgextend "${VG_NAME}" "${NEXT_DEVICE_NAME}"
/usr/sbin/lvextend -l +100%FREE "/dev/${VG_NAME}/${LV_NAME}"

# ファイルシステムの拡張
/usr/sbin/xfs_growfs "${MOUNT_POINT}"

空き容量の監視

ボリュームの空き容量監視はcronを使用して1分ごとに行います。

watch-volume-space.sh
#!/bin/bash

set -o pipefail

THRESHOLD_IN_MB=20000
TARGET="/data"

declare -i AVAIL
if AVAIL=$(df --block-size=1M --output=avail $TARGET | tail -n 1); then
  if [ $AVAIL -lt $THRESHOLD_IN_MB ]; then
    echo "Extend Volume"
    # ボリューム拡張処理呼び出し
    /path/to/extend-volume.sh
  fi
else
  echo "Failed to retrieve available volume space."
  exit 1
fi

このスクリプトをcronから1分ごとに呼び出します。

cronへの登録処理はユーザーデータに追加してEC2起動時に行います。

echo "* * * * * root /path/to/watch-volume-space.sh" > /etc/cron.d/volume-space
chmod 644 /etc/cron.d/volume-space

watch-volume-space.shスクリプトは、ロックファイルを作るなどして重複実行されないようにした方が安全です。

空き容量が足りないと判断する閾値は上記サンプルではTHRESHOLD_IN_MB=20000と約20GBにしています。cronの実行間隔が60秒で、拡張処理extend-volume.shは通常10秒以内で完了しますので、最大スループットの70秒でも消費し切れないサイズを指定します。

Type Size Throughput
gp2 ≦ 170GiB 128MiB
gp2 ≧ 334GiB 250MiB
gp3[2] - 125MiB

計算すると、最も低いスループットのgp3の125MiBで約8.5GiB、最も高いgp2の250MiBでは約17GiBが70秒で消費し得る最大サイズになります。

テスト

ボリュームサイズの拡張処理中に著しいパフォーマンスの低下やエラーが起きないかのテストを行いました。

fioでの通常時と拡張処理時の簡易的なパフォーマンステストでは、読み込みの遅延が若干大きい傾向があります[3]が、全体として大きなパフォーマンスの低下はありませんでした。(t3.medium[4], gp2 128MiB, blocksize=32M)

通常時 拡張時
Throughput (Read) 96182KB/s 92231KB/s
Throughput (Write) 122526KB/s 124964KB/s
Latency (avg Read) 336.58msec 351.04msec
Latency (max Read) 447msec 830msec
Latency (avg Write) 263.59msec 258.25msec
Latency (max Write) 847msec 744msec

実際の使用方法に近いファイルダウンロード→エンコードのテストでは、EBSボリューム単独使用と比較してわずかに処理時間が増加しましたが1%未満[5]の差でした。

まとめ

稼働中のEC2インスタンスでEBSのボリュームサイズ不足が起きた場合の対策として、LVMを使用してボリュームを継ぎ足す方法を検討しました。
インスタンス起動時や稼働中に定期的な処理を行う必要がありますが、一度環境を作ってしまえば通常運用時はボリュームサイズの空きをほとんど意識する必要がなくなります。

EC2インスタンスから孤立したEBSボリュームが残り続けてしまう可能性があるため、何らかの対策が必要となります。
gp2を使用する場合、ボリュームサイズの設定によってはスループットパフォーマンス上昇の恩恵が受けられなくなります。
動画エンコードのような短期間でEBSを使い捨てする用途以外では、例えばEBSスナップショットが使えないなどでこの拡張方法が向いていないケースもあります。

脚注
  1. 入力ファイルのサイズ+の入力ファイル用ボリュームを作成してダウンロードし、再生時間を調べて追加で出力ファイル用ボリュームを作成する方法で回避はできるが動作が複雑になる ↩︎

  2. ベースライン ↩︎

  3. 標準偏差 通常時=103.86 拡張時=135.52 ↩︎

  4. テスト時Cタイプインスタンスがキャパシティ不足でテスト完走できなかったため ↩︎

  5. 36:48→37:00 ファイルダウンロード中に2回拡張処理 ↩︎

Discussion