![[アップデート] EC2 の新メトリクス ImdsV1Enabled を使って、IMDSv1 を許可しているインスタンスを数えてアラーム化してみた](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/f_auto,q_auto/v1790870021/eyecatch/eugkoepwmyirwgiv7nzi.png)
[アップデート] EC2 の新メトリクス ImdsV1Enabled を使って、IMDSv1 を許可しているインスタンスを数えてアラーム化してみた
こんにちは
実家の住所は169.254.169.254
運用イノベーション部のかわいです。
2026/11/5、Amazon EC2 が CloudWatch メトリクスの ImdsV1Enabled を発行するようになりました。
これを使うことで、インスタンスが IMDSv1 を許可しているかどうかを0か1で判定できます。
ドキュメント上、「このメトリクスの Sum が、IMDSv1 を許可しているインスタンスの台数になる」と書かれています。
この記述に従えば、Metrics Insights からクエリを実行するだけで、IMDSv2 に移行できていないインスタンス件数を調べられそうです。本記事では、実際に EC2 インスタンスを3台立てて検証した結果を紹介します。
To see how many instances in an account and Region still allow IMDSv1, use the ImdsV1Enabled CloudWatch metric. This metric is published for each instance based on its configuration, so the Sum of the metric is the number of instances that still allow IMDSv1, even when no IMDSv1 calls are being made.
IMDSv1とは?
※知ってる人は読み飛ばしてね
IMDS(インスタンスメタデータサービス。EC2内から169.254.169.254にアクセスして、インスタンスIDやIAMロールの一時認証情報などを取れる仕組み)には、トークンなしで読めるIMDSv1 と、トークンが必要なIMDSv2があります。IMDSv1 は SSRFで認証情報を抜かれる等のリスクがあるので、IMDSv2だけを受け付ける設定にするのが推奨されています。特に、2024年以前に作成されているインスタンスでは注意が必要です。
また、以前からも IMDSv1 関連のメトリクスは存在していました。
| メトリクス | 内容 |
|---|---|
MetadataNoToken |
IMDSv1でのアクセスが成功した回数 |
MetadataNoTokenRejected |
IMDSv1を無効時に、IMDSv1でアクセスしようとして拒否された回数 |
しかし、上記のメトリクスはアクセスがあった時にしか出ないので、"IMDSv1を許可しているのに、IMDSv1が使用されていない"インスタンスは、従来のメトリクスからは実質見つけられませんでした。
今回追加されたImdsV1Enabled は設定値を元にに発行されるので、アクセスの有無に関わらず確認が可能です。
検証環境
EC2インスタンスを3種類用意しました。
| 名前 | 初期設定 | IMDSv1 でのアクセス | 途中でやったこと |
|---|---|---|---|
| A(imds-v1-allowed) | optional | 1 分ごと | required に変更 |
| B(imds-v2-only) | required | 1 分ごと(拒否される) | optional に変更後に停止、その後起動 |
| C(imds-v1-noaccess) | optional | なし | required に変更 |
特にCは「IMDSv1 を許可しているのに、誰も使っていない」インスタンスです。
今回のメトリクスで一番見つけたいのはこのタイプなので、今回差分を取るためにパターン別の3種類を作成します。
事前準備
操作はすべてCloudShellから実施します。時間はかかりますが、ポチポチでもokです。
今回IMDSv1 でアクセスし続ける用として、トークンなしでcurlを1分置きに打つユーザデータを用意します。
export AWS_REGION=ap-northeast-1
cat > poll.sh <<'EOF'
#!/bin/bash
# トークンなしで1分置きにインスタンスIDを読みにいく
systemd-run --unit=imds-v1-poll /bin/bash -c 'while true; do curl -s -o /dev/null http://169.254.169.254/latest/meta-data/instance-id; sleep 60; done'
EOF
インスタンス3台を起動します。
上述の通り、AとBにはユーザデータを渡し、Cには渡しません。
AMI=$(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 --query Parameter.Value --output text)
# A: IMDSv1 を許可、IMDSv1 でアクセス
A=$(aws ec2 run-instances --image-id "$AMI" --instance-type t4g.nano \
--metadata-options HttpTokens=optional,HttpEndpoint=enabled \
--user-data file://poll.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=imds-v1-allowed},{Key=Purpose,Value=imds-metric-test}]' \
--query 'Instances[0].InstanceId' --output text)
# B: IMDSv2 のみ、IMDSv1 でアクセス(拒否される)
B=$(aws ec2 run-instances --image-id "$AMI" --instance-type t4g.nano \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--user-data file://poll.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=imds-v2-only},{Key=Purpose,Value=imds-metric-test}]' \
--query 'Instances[0].InstanceId' --output text)
# C: IMDSv1 を許可、アクセスなし
C=$(aws ec2 run-instances --image-id "$AMI" --instance-type t4g.nano \
--metadata-options HttpTokens=optional,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=imds-v1-noaccess},{Key=Purpose,Value=imds-metric-test}]' \
--query 'Instances[0].InstanceId' --output text)
※Amazon Linux 2023 の AMI はImdsSupport=v2.0なので、何も指定しないとrequiredで起動します。
今回は--metadata-optionsで明示的にoptionalを指定しています。
値を見るための関数も2件作成しておきます。
# 1台分のメトリクス(期間60秒)
m() {
aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name "$2" \
--dimensions Name=InstanceId,Value="$1" \
--start-time "$(date -u -d "-${3:-30} min" +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--period 60 --statistics Maximum Sum SampleCount \
--query 'sort_by(Datapoints,&Timestamp)[].[Timestamp,Maximum,Sum,SampleCount]' --output text
}
# Metrics Insights で全インスタンスをSUM
q() {
aws cloudwatch get-metric-data \
--metric-data-queries "[{\"Id\":\"q1\",\"Expression\":\"SELECT SUM(ImdsV1Enabled) FROM SCHEMA(\\\"AWS/EC2\\\", InstanceId)\",\"Period\":${1:-300}}]" \
--start-time "$(date -u -d "-${2:-60} min" +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--query 'MetricDataResults[0].[Timestamps,Values]' --output text
}
検証
※以降の記載時刻はすべてUTCとなるので、日本時間で読む場合は+9時間してください。
1. メトリクスの形を見る
Metrics Insights で集計する前に、以下の2点を確かめておきます。
- どんなディメンションで出ているか?(クエリの
SCHEMA()に何を書けばよいかを確認) - 1台分の値がどう入っているか?(後ほどSUMの結果を読み解く際に使用)
まずAとBを起動後 約10分後にImdsV1Enabledを取得して、どんなディメンションで記録されているかを確認しました。
$ aws cloudwatch list-metrics --namespace AWS/EC2 --metric-name ImdsV1Enabled --output json
# 出力
{
"Metrics": [
{
"Namespace": "AWS/EC2",
"MetricName": "ImdsV1Enabled",
"Dimensions": [
{
"Name": "InstanceId",
"Value": "i-02632f4736eb6fdd6"
}
]
},
{
"Namespace": "AWS/EC2",
"MetricName": "ImdsV1Enabled",
"Dimensions": [
{
"Name": "InstanceId",
"Value": "i-04b9e013b2f2a6667"
}
]
}
]
}
ディメンションはInstanceIdのみが返りました。
全台を集計するクエリはSCHEMA("AWS/EC2", InstanceId)で書けそうです。
続いて、事前準備で作ったm関数を使って1台ずつの値を見てみます。
期間は60秒で。列は左から「時刻 / Maximum / Sum / SampleCount」です。
▼ A(optional)
2026-11-10T01:55:00+00:00 1.0 2.0 2.0
2026-11-10T02:00:00+00:00 1.0 5.0 5.0
2026-11-10T02:05:00+00:00 1.0 3.0 3.0
▼ B(required)
2026-11-10T01:55:00+00:00 0.0 0.0 2.0
2026-11-10T02:00:00+00:00 0.0 0.0 5.0
2026-11-10T02:05:00+00:00 0.0 0.0 3.0
Maximumを見ると、Aは 1、Bは0で設定通りの値を返していることが分かります。
気になるのは SampleCount で、60秒で要求していても、5分ごとのポイントで返っており、1つのポイントに値が5個(1分に 1件)入っていました。
これは、次項で確認する SUM の結果に影響してきます。
次に、IMDSv1で一度もアクセスしていないCでも、起動後 約4分で値が出始めました。
--- C ImdsV1Enabled
2026-11-10T02:15:00+00:00 1.0 4.0 4.0
アクセスがない場合ても1で返っているため、公式ドキュメントに記載通りの挙動を取っていることが分かりました。
2. Metrics Insights の SUM は台数にならない
次に、Metrics Insights で全インスタンスの SUM を取ってみます。
この時点で IMDSv1 を許可しているのはAの1台だけです。
$ q 300
2026-11-10T02:03:00+00:00 2026-11-10T01:58:00+00:00 2026-11-10T01:53:00+00:00
4.0 5.0 2.0
$ q 60
2026-11-10T02:05:00+00:00 2026-11-10T02:00:00+00:00 2026-11-10T01:55:00+00:00
4.0 5.0 2.0
1台だけなのに、SUMは5で返りました。
これはどういうことかというと、
値が1のインスタンス1台につき、5分間のポイントに値が5個入っているので、SUMの出力は「台数×5」で出ています。
詳細モニタリング(1分ごと)を有効にして期間 60 秒で取れば台数どおりになるのかは試せていないですが、デフォルト集計だと1分ごとに1の値が入ります。

3. AVG を GROUP BY して足してみる
1台ずつであれば、5分の平均は0か1になります。
それを全台ぶん足せば台数になるはずなので、Metrics Insights のクエリ結果は表示せず(ReturnData: false)、式 SUM(g) で足した結果だけを返してみます。
# AVG を GROUP BY して合計した値(台数)
qa() {
aws cloudwatch get-metric-data \
--metric-data-queries '[{"Id":"g","Expression":"SELECT AVG(ImdsV1Enabled) FROM SCHEMA(\"AWS/EC2\", InstanceId) GROUP BY InstanceId","Period":300,"ReturnData":false},{"Id":"e1","Expression":"SUM(g)","Period":300,"ReturnData":true}]' \
--start-time "$(date -u -d "-${1:-30} min" +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--query 'MetricDataResults[0].[Timestamps,Values]' --output text
}
▼ Aだけが IMDSv1 を許可している時間帯で実行してみた結果です。
2026-11-10T02:11:00+00:00 2026-11-10T02:06:00+00:00 2026-11-10T02:01:00+00:00 2026-11-10T01:56:00+00:00 2026-11-10T01:51:00+00:00
1.0 1.0 1.0 1.0 1.0
期待通り、1が返りました。
台数が変わった後の結果も見ておきます。
途中でCのインスタンスを重複して起動させてしまったので、一時的に4台の状態となりました。
2026-11-10T02:34:00+00:00 2026-11-10T02:29:00+00:00 2026-11-10T02:24:00+00:00 2026-11-10T02:19:00+00:00 2026-11-10T02:14:00+00:00 2026-11-10T02:09:00+00:00
1.0 1.0 1.4 2.0 3.0 1.0
| 開始 | 値 | IMDSv1 を許可していたインスタンス |
|---|---|---|
| 02:09 | 1 | A |
| 02:14 | 3 | AとC、C-2 |
| 02:19 | 2 | AとC |
| 02:24 | 1.4 | Cと、途中で required に変更したA |
| 02:29 | 1 | C |
※「1.4」はAを途中で required に変えた影響です。
Aの5件の値のうち2件が1、3件が0なので、平均 0.4。
台数として減ったことが読み取れます。
これをそのままアラーム化してみます。
aws cloudwatch put-metric-alarm --alarm-name imds-v1-allowed-count-avg \
--alarm-description "IMDSv1 を許可している EC2 の台数(AVG を GROUP BY して合計)" \
--metrics '[{"Id":"g","Expression":"SELECT AVG(ImdsV1Enabled) FROM SCHEMA(\"AWS/EC2\", InstanceId) GROUP BY InstanceId","Period":300,"ReturnData":false},{"Id":"e1","Expression":"SUM(g)","Label":"IMDSv1 allowed instances","ReturnData":true}]' \
--comparison-operator GreaterThanThreshold --threshold 0 \
--evaluation-periods 1 --treat-missing-data notBreaching

※グラフの時刻は日本時間
グラフ縦軸が台数を表しています。
4. 設定変更後、値とアラームが変化するまでの時間
SUM のアラーム(imds-v1-allowed-count)と、AVG のアラーム(imds-v1-allowed-count-avg)の 2 つを並べて、設定変更時の動きを確認しました。どちらも値が0より大きい場合にALARM状態と判定します。
| 操作時刻(UTC) | 操作後の状態 | SUM のアラーム | AVG のアラーム |
|---|---|---|---|
| 02:26:33 A を required に変更 | Cが残っているため1台 | ALARMのまま | ALARMのまま |
| 02:39:17 C を required に変更 | 0台 | 02:41:10 にOK | 02:41:48 にOK |
| 02:45:35 B を optional に変更 | 1台 | 02:46:10 にALARM | 02:46:48 にALARM |
→ 設定を変えてから 1〜2分でアラーム状態に遷移しました。
SUM / AVG のアラームはそれぞれ同じタイミングで発報し、同じタイミングでOK状態に戻りました。
5. 停止中のインスタンスは集計されない
次に、停止中インスタンスの扱いを見てみます。
Bを optional に変えてアラームが ALARM状態になった後、そのままBを止めてみました。
$ aws ec2 describe-instances --instance-ids "$B" --query 'Reservations[0].Instances[0].[State.Name,MetadataOptions.HttpTokens]' --output text
stopped optional
$ m "$B" ImdsV1Enabled 90
# ※出力なし
$ qa 90
2026-11-10T04:26:00+00:00 ... 2026-11-10T03:01:00+00:00
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
$ aws cloudwatch describe-alarms --alarm-name-prefix imds-v1 \
--query 'MetricAlarms[].[AlarmName,StateValue,StateUpdatedTimestamp,StateReason]' --output text
imds-v1-allowed-count OK 2026-11-10T02:51:10.210000+00:00 Threshold Crossed: 1 datapoint [0.0 (10/11/26 02:46:00)] was not greater than the threshold (0.0).
imds-v1-allowed-count-avg OK 2026-11-10T02:51:48.892000+00:00 Threshold Crossed: 1 datapoint [0.0 (10/11/26 02:46:00)] was not greater than the threshold (0.0).
Bは optional のまま停止状態だとメトリクスに値は出力されず、台数も0、アラームはOK状態であることが確認できました。
その後Bを起動し直すと、程なくして値が返るようになりました。
--- B ImdsV1Enabled
2026-11-10T04:35:00+00:00 1.0 1.0 1.0
このことから、ImdsV1Enabled は動いているインスタンスの設定値を取得するメトリクスであり、止まっているインスタンスは対象外になることが分かりました。
停止中インスタンスも含めて洗い出したいときは、従来通り describe-instances で確認するのが確実そうです。
aws ec2 describe-instances \
--filters Name=metadata-options.http-tokens,Values=optional \
--query 'Reservations[].Instances[].[InstanceId,State.Name]' --output text
結果の振り返り
| 確かめたこと | 結果 |
|---|---|
| IMDSv1のアクセスが発生していなくても値が出るか? | 出る(Cで確認) |
| ディメンション | InstanceId のみ |
| 値の間隔 | 5分ごとのポイントに値が5件入る |
| 台数を検出する方法 | AVG を GROUP BY InstanceId して SUM() |
| 設定変更から値が変わるまで | 約1分 |
| 設定変更からアラームが変わるまで | 1〜2分 |
| 停止中のインスタンス | 数に入らない。起動すると戻る |
検証環境お片付け
アラーム2件とインスタンス3台を削除します。
aws cloudwatch delete-alarms --alarm-names imds-v1-allowed-count imds-v1-allowed-count-avg
aws ec2 terminate-instances --instance-ids $(aws ec2 describe-instances \
--filters Name=tag:Purpose,Values=imds-metric-test Name=instance-state-name,Values=pending,running,stopping,stopped \
--query 'Reservations[].Instances[].InstanceId' --output text) \
--query 'TerminatingInstances[].[InstanceId,CurrentState.Name]' --output text
rm -f poll.sh
リソースが残っていないことを確認します。
aws ec2 describe-instances --filters Name=tag:Purpose,Values=imds-metric-test \
--query 'Reservations[].Instances[].[InstanceId,State.Name]' --output text
aws cloudwatch describe-alarms --alarm-name-prefix imds-v1 --query 'MetricAlarms[].AlarmName' --output text
まとめ的な
本記事では、EC2の新しいメトリクスImdsV1Enabledで、IMDSv1 を許可しているインスタンスの台数を数えてみました。
IMDSv1で誰もアクセスしていないインスタンスまで見つけられるのは、以前までのMetadataNoTokenにはなかった強みです。停止中インスタンスは集計できない点は注意が必要です。
参考






