Claude Media
Claude CodeでEBSのgp2からgp3へ移行する — Terraformで性能不足を先に洗う

Claude CodeでEBSのgp2からgp3へ移行する — Terraformで性能不足を先に洗う

gp2のボリュームをgp3へ変える前に、CloudWatchの実測から必要なIOPSとスループットを割り出し、Terraformのplanで差分を確かめて段階的に適用する手順です。

gp2をgp3に変えるときの落とし穴は、既定のgp3が3,000 IOPS・125 MiB/sからしか始まらない点です。大きなgp2ボリュームは、それより高い性能を容量の大きさだけで得ています。そのままtype = "gp3"に書き換えると、性能が下がる可能性があります。

Claude Codeには、この不足を先に数字で出させます。CloudWatchの実測を読み、gp2の現行性能と突き合わせ、Terraformのplanで差分を確認し、数本ずつ適用する。その間、AWSの設定を直接変える経路は権限で閉じておきます。本記事は、この流れをsettings.jsonとプロンプトまで含めて示します。

gp3の初期値と、gp2から下がる条件

gp3は容量と性能を切り離したボリュームタイプです。gp2はGiB数に比例してIOPSが決まります。両者の数字を並べると、どこで性能が逆転するかが見えます。

数字

gp3の既定値とgp2の上限

  • gp3の基本IOPS

    3,000

    追加で最大80,000まで指定

  • gp3の基本スループット

    125 MiB/s

    追加で最大2,000 MiB/sまで指定

  • gp2の基本IOPS

    100〜16,000

    1GiBあたり3IOPS

  • gp2のスループット

    128〜250 MiB/s

    容量が大きいほど上限に近づく

EBSユーザーガイドの一般用途SSDの項より

gp2の基本IOPSは、33.33 GiB以下で100、それより大きいと1 GiBあたり3 IOPSで、5,334 GiBで上限の16,000に達します。1,000 GiBなら3,000です。つまり1,000 GiBを超えるgp2は、既定のgp3より基本IOPSが高いことになります。

スループットでも同じ逆転が起きます。gp2は334 GiB以上で250 MiB/sです。gp3の基本は125 MiB/sなので、334 GiB以上のボリュームで実際に帯域を使っているなら、throughputを明示しない移行は帯域を半分に落とします。

逆に小さなgp2は有利になります。1 TiB未満のgp2は、I/Oクレジットがある間だけ3,000 IOPSまでバーストします。gp3はバーストの仕組みを使わず、3,000 IOPSを常時出し続けます。100 GiBのgp2は基本300 IOPSですから、gp3にすればクレジット切れの心配がなくなります。

容量別の判定早見表

gp2の容量gp2の基本IOPSgp3へ移すときの注意
33.33 GiB以下gp2の基本IOPS100gp3へ移すときの注意3,000 IOPSに上がる。IOPSの指定は不要
〜1,000 GiB未満gp2の基本IOPS3 × GiBgp3へ移すときの注意基本は上がる。帯域は334 GiB以上で250 MiB/sに注意
1,000 GiB超gp2の基本IOPS3,000超gp3へ移すときの注意iopsを明示しないと下がる
5,334 GiB以上gp2の基本IOPS16,000gp3へ移すときの注意iops = 16000を明示して初めて同等

最終的な判断は、容量ではなく実際の使用量で決めます。早見表は「実測を見るべきボリューム」を絞るための目安です。

Claudeに許す操作を3段に分ける

性能の確認は読み取りだけで済みます。変更はTerraform経由に限り、AWS CLIでの直接変更は塞ぎます。CLIでmodify-volumeを実行するとTerraformの状態とずれ、次のplanで意図しない差分が出るからです。

権限設定のページによると、ルールはdeny、ask、allowの順に評価され、最初に一致したものが結果を決めます。ルールの具体性は評価順を変えません。広いdenyの内側にallowで例外を作ることはできず、同じことがaskとallowの間にも言えます。したがって「止めたいコマンドだけをdenyに、読むものだけをallowに」と、重ならない形で書きます。

{
  "permissions": {
    "allow": [
      "Bash(aws ec2 describe-volumes *)",
      "Bash(aws ec2 describe-volumes-modifications *)",
      "Bash(aws cloudwatch get-metric-statistics *)",
      "Bash(terraform plan *)",
      "Bash(terraform show *)"
    ],
    "ask": [
      "Bash(terraform apply *)"
    ],
    "deny": [
      "Bash(aws ec2 modify-volume *)",
      "Bash(aws ec2 delete-volume *)",
      "Bash(terraform destroy *)"
    ]
  }
}

末尾の*の前の半角スペースは、ルールの一部です。Bash(ls *)がls -laに一致してlsofに一致しないのと同じで、スペースを抜くと別のサブコマンドまで巻き込みます。複合コマンドは区切りごとに個別に判定されるため、aws ... | python3 ...のようにパイプでつなぐと、後ろの実行にも許可が要ります。整形はAWS CLIの--queryで済ませ、集計は後述のスクリプトファイルに分けます。ただし手順2のforループやpython3でのスクリプト実行は上のallowに一致しないので、初回は確認プロンプトが出ます。必要ならBash(python3 *)の許可を足すか、その都度承認します。

denyは、絶対パスでの呼び出しやsh -cの中身までは止めません。削除や変更を確実に防ぐ層は、Claudeに渡す認証情報のIAMポリシーを読み取り専用にすることです。権限ルールはClaudeの振る舞いを整える層、IAMは最終的な防壁と分けます。Terraformのapplyは人が自分の認証情報で実行する運用にすれば、どちらの層も崩れません。

同じ権限設計でAWSの棚卸しを行う例はAWSの未使用リソースを洗い出す記事にあります。

手順1: 対象ボリュームを一覧にする

まず、gp2のボリュームとその現行性能を出します。describe-volumesは、タイプで絞るフィルターを持ちます。

aws ec2 describe-volumes --profile audit \
  --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[].{Id:VolumeId,GiB:Size,AZ:AvailabilityZone,
    State:State,Attached:Attachments[0].InstanceId}' \
  --output table

出力された容量から、gp2側の基本IOPSをmax(100, 3 × GiB)、上限16,000で計算します。この列を足した表を作らせると、先ほどの早見表のどこに当たるかが一目で分かります。Stateがavailableのボリュームは、移行ではなく未使用リソースの整理の対象です。

手順2: CloudWatchの実測から必要IOPSを割り出す

EBSのボリューム単位の指標は、1分間隔で課金なしに取得できます。VolumeReadOpsとVolumeWriteOpsは、期間内の完了した操作の合計数です。EBSユーザーガイドの説明では、1秒あたりの値を出すには、合計を期間の秒数で割ります。

読みと書きの両方を足してから割る点が肝です。片方だけだと、必要性能を過小に見積もります。

for m in VolumeReadOps VolumeWriteOps; do
  aws cloudwatch get-metric-statistics --profile audit \
    --namespace AWS/EBS --metric-name "$m" \
    --dimensions Name=VolumeId,Value=vol-0123456789abcdef0 \
    --start-time 2026-10-23T00:00:00Z --end-time 2026-10-28T00:00:00Z \
    --period 300 --statistics Sum \
    --query 'sort_by(Datapoints,&Timestamp)[].[Timestamp,Sum]' \
    --output json > "/tmp/$m.json"
done

期間の指定には制約があります。1回の呼び出しで返るデータ点は最大1,440個です。period 300なら5日分が上限で、それより長く見たいなら日付を分けて呼びます。また、1分データが残るのは15日間、5分データは63日間です。15日より前の期間では、periodを300の倍数にしないとデータが返りません。

periodを長くすると、平均化されて短い山が見えなくなります。バッチ処理で数分だけ跳ねるボリュームは、最も忙しい日を1日だけperiod 60で測り直すと、5分値では隠れた山が現れます。

二つのJSONを足して秒あたりに直す作業は、Claudeにスクリプトとして書かせると繰り返せます。次のような形です。

import json
 
def load(path):
    return {t: s for t, s in json.load(open(path))}
 
reads = load("/tmp/VolumeReadOps.json")
writes = load("/tmp/VolumeWriteOps.json")
period = 300  # 取得時のperiodと同じ秒数
iops = [(reads.get(t, 0) + writes.get(t, 0)) / period
        for t in sorted(set(reads) | set(writes))]
print(f"peak={max(iops):.0f} IOPS  points={len(iops)}")

スループットはVolumeReadBytesとVolumeWriteBytesで同じ計算をします。バイト数の合計を期間の秒数と1,048,576で割ると、MiB/sになります。

実測を目標値に変える

ピーク値をそのまま設定値にはしません。次の式で目標値を作り、Terraformのiopsとthroughputに書きます。

  1. 目標IOPSは、次の3つのうち最も大きい値にします。実測ピークに余裕を足した値、3,000、gp2の現行基本IOPSです。
  2. 目標スループットは、実測の帯域ピークに余裕を足した値と、125 MiB/sのうち大きい方です。gp2が250 MiB/sを出せる容量なら、それを下限にすると退行を避けられます。
  3. gp3の制約も確認します。IOPSは1 GiBあたり500まで、スループットは1 IOPSあたり0.25 MiB/sまでで、たとえば帯域250 MiB/sには1,000 IOPS以上が要ります。

余裕の割合は運用で決める値で、公式に決まった数字はありません。ピークが月末などに偏る場合は、その時期を含めて測ってください。

手順3: Terraformで差分を作り、planで確かめる

Terraformのaws_ebs_volumeでは、typeの既定値がgp2です。iopsはgp3・io1・io2でのみ、throughputはgp3でのみ有効です。EC2インスタンスのルートボリュームを書くaws_instanceのroot_block_deviceは、volume_typeを省くとAMIの値を使います。移行対象には、この「書いていないから現状のまま」のボリュームが含まれます。

段階適用のために、gp3にするボリュームを変数で選びます。

variable "gp3_volume_keys" {
  type    = set(string)
  default = []   # 段階ごとにキーを足していく
}
 
locals {
  volumes = {
    app-data = { az = "ap-northeast-1a", size = 2000, iops = 6000, throughput = 250 }
    logs     = { az = "ap-northeast-1a", size = 200,  iops = 3000, throughput = 125 }
  }
}
 
resource "aws_ebs_volume" "this" {
  for_each          = local.volumes
  availability_zone = each.value.az
  size              = each.value.size
 
  type       = contains(var.gp3_volume_keys, each.key) ? "gp3" : "gp2"
  iops       = contains(var.gp3_volume_keys, each.key) ? each.value.iops : null
  throughput = contains(var.gp3_volume_keys, each.key) ? each.value.throughput : null
}

default = []のままplanを走らせると、差分が出ないことを確認できます。それから-var 'gp3_volume_keys=["logs"]'のようにキーを足して、差分を見ます。見る点は2つです。

  • 対象ボリュームだけが変わるか。他のボリュームに差分が出るなら、書き換えの範囲が広すぎます
  • 更新が「その場の更新」か「作り直し」か。作り直しを示す表示が出たら、その時点で止めます。データを持つボリュームの作り直しは取り返せません

この確認はClaudeがterraform planの出力を読んで行えます。許可ルールでterraform plan *を通してあるため、プロンプトなしで繰り返せます。

手順4: 数本ずつ適用し、完了を見てから次へ

AWS側のボリューム変更は、コンソールでもCLIでも、取り付けたまま実行できます。Elastic Volumesという機能で、サイズの拡大、性能の増減、タイプの変更を、デタッチせずに行えます。ただし対象のインスタンスが現行世代であることなど、対応条件があります。

手順

適用の進め方

  1. 1

    スナップショットを取る

    AWSの手順にも、価値のあるデータを持つボリュームの変更前にスナップショットを作るのがよい、と書かれています。

  2. 2

    影響の小さいボリュームから1本だけ

    変数にキーを1つだけ足し、planを人が確かめてterraform applyを実行します。applyはaskなので、確認プロンプトが出ます。

  3. 3

    変更の状態を見る

    describe-volumes-modificationsのModificationStateが、modifying、optimizing、completedのどれかを見ます。failedなら次へ進みません。

  4. 4

    適用後の実測を取り直す

    手順2のスクリプトで、変更後の同じ曜日・時間帯を測り、遅延や待ち行列が悪化していないかを見ます。

  5. 5

    次の組を足す

    問題がなければ、次のキーを足します。重要なボリュームは最後に回します。

状態の確認は読み取りコマンドで足ります。

aws ec2 describe-volumes-modifications --profile audit \
  --volume-ids vol-0123456789abcdef0 \
  --query 'VolumesModifications[].{Id:VolumeId,State:ModificationState,
    From:OriginalVolumeType,To:TargetVolumeType,Progress:Progress}'

ModificationStateにはmodifying、optimizing、completed、failedの4値があります。completedになってから次の組へ進みます。これは進め方の好みではなく、ModifyVolumeのAPI仕様です。同じボリュームについて、前の変更がcompletedに達するまで次の変更は開始できません。

Claudeに渡す指示の例

プロジェクトのCLAUDE.mdに、手順をそのまま書いておくと毎回の指示が短くなります。

## EBSのgp2からgp3への移行
- 変更はTerraformのみ。`aws ec2 modify-volume`は使わない
- 移行前に、対象ごとに次の4列の表を作る:
  gp2基本IOPS(max(100, 3×GiB)、上限16000)/ 実測ピークIOPS /
  実測ピーク帯域(MiB/s)/ 目標iops・throughput
- 目標iopsは3000以上かつgp2基本IOPS以上。帯域250 MiB/sには1000 IOPS以上が必要
- planで対象外のリソースに差分が出たら適用せず報告する
- 「作り直し」を示す差分が出たら止めて報告する

これで、ClaudeはCloudWatchの値を読んで表を作り、.tfの変更案とplan結果をひとまとめに出せます。適用は人の操作が残ります。

よくあるつまずき

落とし穴

1,000 GiBを超えるgp2を、型だけ書き換えた

gp2の基本IOPSは容量に比例するため、2,000 GiBなら6,000 IOPSです。type = "gp3"だけを書くと、gp3の既定である3,000 IOPSになります。iopsとthroughputを明示してからplanを読みます。

次の点も、切り分けでよく当たります。

  • 実測の期間が短すぎる。月末や週次バッチを含まない5日分で決めると、ピークを逃します
  • 移行後に性能が足りない。gp3のiopsとthroughputはmodify-volumeの対象項目なので、Terraformの値を直して再度applyする道があります。ただし同じボリュームの再変更には条件があります。直前の変更がcompletedになるまで次の変更は始められず、ボリュームの状態がin-useかavailableであることも要ります。変更は24時間のうちに4回までで、超えると次に変更できる時刻がエラーで返ります。1回で当てるつもりで目標値を決めます

まとめ

gp2からgp3への移行で確認する対象は、容量の小さいボリュームではなく、1,000 GiBを超えるものと、334 GiB以上で帯域を使っているものです。この2つは、typeだけを変えると性能が下がることがあります。CloudWatchの読みと書きを足した実測を目標値に直し、planで範囲を確かめ、数本ずつ適用すれば、不足は適用前に見つかります。

AWS全体の移行計画を立てる場面では、migration-to-awsプラグインの記事が別の入口になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html
  3. docs.aws.amazon.com/ebs/latest/userguide/requesting-ebs-volume-modifications.h…
  4. docs.aws.amazon.com/ebs/latest/userguide/using_cloudwatch_ebs.html
  5. docs.aws.amazon.com/cli/latest/reference/cloudwatch/get-metric-statistics.html
  6. docs.aws.amazon.com/cli/latest/reference/ec2/describe-volumes-modifications.ht…
  7. docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyVolume.html
この記事を共有:XはてブLinkedIn