0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【実機検証】EC2 Application Status ChecksをALBと比較――検知遅延、managed ENIの不可視性、SG削除待ちを観測した

0
Posted at

asc-hero.png

【実機検証】EC2 Application Status ChecksをALBと比較――検知遅延、managed ENIの不可視性、SG削除待ちを観測した

こんにちは!Dirbatoの社内技術横断支援組織Backbeatに所属している柴田です!

2026年8月10日、Amazon EC2 にアプリ層のヘルスチェックが標準機能として入りました(Application Status Checks)。これまで「Webサーバが応答しなくなった」「Dockerデーモンが落ちた」を検知するには ALB を立てるか自作監視を組むしかなかった領域です。しかも Auto Scaling と統合され、異常なインスタンスを自動で置き換えてくれます。

「じゃあ軽い構成なら ALB 要らないのでは?」——そう思って実機で測ったところ、公式ドキュメントの説明だけでは判断しにくい挙動や、サンプルと異なる応答を3点観測しました。 今回の試行では、 ASCの総合判定がALBより遅れて観測されEC2 APIからは課金単位となるmanaged ENIを列挙できず撤去後も少なくとも約1時間、ヘルスチェック元SGを削除できませんでした。

本記事は、その全部を秒単位のログ付きで出します。


免責
本記事は筆者個人の見解であり、所属組織の公式見解・開発体制・業務内容を示すものではありません。記事内の技術的表現は執筆時点(2026年8月)の実機検証に基づくものであり、特定の製品・サービスの性能・仕様を保証するものではありません。アカウントIDはマスク(123456789012)しています。一部は実機観測値であり、公式記載と異なる場合があります。


:bulb: 要点サマリー(結論先出し)

# 検証したこと 結果
1 導入に必要なIAM権限 🚨 公式は「IAM設定は不要」と記載。実際はサービスリンクロールが未作成のアカウントでは、初回実行者に条件付きの iam:CreateServiceLinkedRole が必要
2 describe-application-status の応答形式 ⚠️ 公式サンプルと構造が違う。サンプル通りにパースするコードは動かない
3 managed ENI の可視性 🚨 EC2 APIから課金対象ENIの本数を直接列挙できない。ソースSGを自分で指定しても0本。本数ベースの検算が困難(請求明細での確認可否は未検証)
4 異常検知の速さ(vs ALB・同条件) ⚠️ ALB 81秒 / ASC 146秒。ASCの方が約60〜80秒遅い
5 正常復帰の速さ(vs ALB) ALB 95秒 / ASC 111秒。差は約16秒と小さい
6 Reason は総合ステータスを先行する 双方向で約50〜64秒先行。APIを直接見れば約1分早く気づける
7 ASG による自動置換 ✅ 機能する。ただしエンドツーエンド約8分31秒
8 復旧時間の内訳 🚨 支配的なのは検知(2分)ではなく、検知後のASGのラグ(5.6分)
9 撤去 🚨 ASC削除後もヘルスチェック元SGが削除できない(IaCの冪等性を壊す)

ひとことで: 「ALB なしでアプリ層を見られる」のは本当。ただし速さを期待して置き換えるものではなく、ALB を置けない単体インスタンス群に監視の底を入れる機能として捉えるのが実態に合っています。


Application Status Checks とは

EC2 のステータスチェックは従来2種類でした。

種類 見ているもの
System Status Check AWS側のインフラ(ホスト・ネットワーク・電源)
Instance Status Check インスタンスのOSレベル到達性

ここに Application Status Check が加わりました。指定したポートとパスに HTTP/HTTPS リクエストを投げ、返ってきたステータスコードで判定します。つまり「OSは生きているがアプリが死んでいる」を初めて EC2 標準機能で捕まえられます。

プローブはどこから飛んでくるのか

ここが本記事の主題のひとつです。チェックはインターネットからではなく、あなたのVPC内にAWSが作る「managed ENI」から飛んできます。

下のGIFが実際の経路です。オレンジのきつねが ASC のプローブ、ペンギンが比較用のALBのヘルスチェックです。

asc-probe-path.gif

重要なのは、緑枠(顧客のVPC)の内側にあるピンクの破線のボックスです。AWS が作って管理するのに、置かれる場所はあなたのVPCです。ここに managed ENI があるはずなのですが、顧客のアカウントからは観測できません(後述)。

この構造から2つの帰結が出ます。

  1. 宛先インスタンスのSGに、ヘルスチェック元からのインバウンド許可が必要(許可しないと「アプリは正常なのに impaired」になる)
  2. managed ENI は VPCの「Network interfaces per Region」クォータを消費する(公式記載)

検証環境

項目
リージョン ap-northeast-1(東京)
インスタンス t4g.micro / Amazon Linux 2023 (arm64)
アプリ python3 -m http.server 8080/health を返すだけ)を systemd 常駐
対照 内部 ALB + ターゲットグループ
実費 約¥10

公平比較のための条件統一(ここが肝)

ASC はチェック間隔が60秒固定で変更できません。そこで ALB 側を既定の30秒から60秒に変更し、他も揃えました。

項目 ASC ALB(合わせた値)
プロトコル / ポート / パス HTTP / 8080 / /health 同左
チェック間隔 60秒(固定・変更不可) 60秒(既定30秒から変更)
タイムアウト 6秒 6秒
異常判定しきい値 2回連続 2回連続
正常復帰しきい値 2回連続 2回連続

揃えないと「ALBが速いのは間隔が短いだけ」という当たり前の結論になり、比較の意味が消えます。

失敗注入の方法

アプリを殺す代わりに、セキュリティグループのインバウンドルールを削除して疎通を落としました。この方式には比較実験として3つの利点があります。

  1. 公式が挙げる検知対象(ネットワーク設定の誤り)そのもの
  2. 操作時刻をこちら側が正確に持てる(インスタンス内の時計に依存しない)
  3. ASC と ALB が同一時刻に同時に疎通を失うので、検知時間の差が純粋に方式の差になる

下のGIFは、そのSGを落としたときの様子です。フェーズと字幕が図に焼き込まれているので、動きだけで何が起きたか分かります。

asc-sg-blocked.gif


ハンズオン:作成から関連付けまで

CLI は4ステップで完結します。

# 1) チェック定義を作る(--health-check-paths を省略=AWS managed network paths)
aws ec2 create-application-status-check \
    --protocol http --port 8080 --path "/health" \
    --status-code-matcher "200" \
    --initialization-grace-period-seconds 60

# 2) インスタンスに関連付ける
aws ec2 associate-application-status-check \
    --application-status-check-id asc-xxxxxxxx \
    --instance-ids i-xxxxxxxxxxxxxxxxx

# 3) 結果を見る
aws ec2 describe-application-status --instance-ids i-xxxxxxxxxxxxxxxxx

# 4) 撤去
aws ec2 disassociate-application-status-check --application-status-check-id asc-xxxxxxxx --instance-ids i-xxxxxxxxxxxxxxxxx
aws ec2 delete-application-status-check --application-status-check-id asc-xxxxxxxx

作成レスポンスから既定値が全部読めます。

{
  "ApplicationStatusCheckId": "asc-xxxxxxxx",
  "Aggregation": "included",
  "Protocol": "http", "Port": 8080, "Path": "/health",
  "DeviceIndex": 0, "IpVersion": "ipv4", "IpScope": "private",
  "Interval": 60, "Timeout": 6,
  "FailureThreshold": 2, "SuccessThreshold": 2,
  "StatusCodeMatcher": "200",
  "InitializationGracePeriodSeconds": 60
}

Interval: 60固定値であることに注意してください。ここは変更できません。


🚨 罠①:「IAM設定は不要」でも、初回作成者にはサービスリンクロール作成権限が必要

ユーザーガイドにはこう書かれています。

AWS creates and manages the network interfaces used for application status checks through a service-linked role. No IAM setup is required for the service to create these ENIs.

ところが最小権限で運用しているIAMプリンシパルで叩くと、こうなります。

An error occurred (UnauthorizedOperation) when calling the CreateApplicationStatusCheck operation:
You are not authorized to perform iam:CreateServiceLinkedRole.
Ensure your IAM policy allows iam:CreateServiceLinkedRole for the service
ec2.application-status-checks.amazonaws.com

「IAM設定は不要」はサービス側がENIを管理する話であって、サービスリンクロールを最初に作る権限は呼び出し側に要るのです。しかもエラーは CreateApplicationStatusCheck を叩くまで出ません

サービス名を限定して付ければ最小権限を保てます。

{
  "Effect": "Allow",
  "Action": "iam:CreateServiceLinkedRole",
  "Resource": "*",
  "Condition": {
    "StringEquals": { "iam:AWSServiceName": "ec2.application-status-checks.amazonaws.com" }
  }
}

サービスリンクロールは初回の呼び出し時に自動作成され、このエラーが出るのも未作成のアカウントでの初回だけです。ただし本検証では、ロールができた後に iam:CreateServiceLinkedRole を外しても呼べるかは試していません(検証中はポリシーを付けたままにしていたため)。

⚠️ 罠②:応答形式が公式サンプルと違う

ユーザーガイドのサンプルは配列です。

{ "ApplicationStatuses": [ { "InstanceId": "...", "ApplicationStatus": { ... } } ] }

実際に返ってくるのはネストした構造でした。

{ "ApplicationStatuses": { "Instances": [ { "InstanceId": "...", "ApplicationStatus": { ... } } ] } }

ApplicationStatuses[0] を読むコードは動きません。監視スクリプトを書くときはここで一度躓きます。

なお describe-instance-status にも新フィールドが増えていて、こちらは ImpairedSince を持っています。

"ApplicationStatus": { "Status": "impaired", "ImpairedSince": "2026-08-19T05:18:56.914Z" }

⚠️ 罠③:関連付け直後、応答にインスタンスが出てこない

関連付けてから約2分半、Instances が空配列で返り続けました。その後 initializing が現れ、初回評価の完了までは関連付けから 4分45秒〜9分48秒かかりました(AWS managed モード)。customer-managed モードでは 3分46秒〜11分40秒で、モードと試行によって幅があります。

つまり監視を組む側から見ると、「まだ評価が始まっていない」と「そもそも対象外」が応答から区別できない時間帯があります。デプロイ直後にアラームを張ると誤検知します。


実測①:異常検知は ALB のほうが速い

SGのインバウンドルールを削除した瞬間(t=0)から10秒間隔で追跡した生データです。

経過(秒) ASC 総合ステータス ASC の Reason ALB
0.0〜65.0 ok ResponseCodeMatched / 200 healthy
81.2 ok(まだ正常扱い) ConnectionTimeout / 0 unhealthy / Target.Timeout
97.4〜129.8 ok ConnectionTimeout unhealthy
145.9 impaired ConnectionTimeout unhealthy
             0s            50s     81s   100s         146s     180s
             |-------------|-------v-----|------------v--------|
                                   (1)                (2)

ALB          ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░
             healthy               +-- (1) unhealthy / Target.Timeout

ASC Reason   ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░
             ResponseCodeMatched   +-- (1) ConnectionTimeout

ASC Status   █████████████████████████████████████████░░░░░░░░░
             ok                                       +-- (2) impaired

             █ = 正常と報告   ░ = 異常と報告
             (1) 81秒  : ALB と ASC の Reason が同時に異常を掴む
             (2) 146秒 : ASC の総合ステータスがようやく impaired になる
指標 実測 理論値
ALB が unhealthy を報告 65.0秒 < t ≤ 81.2秒 間隔60秒 × 2回 ≒ 120秒
ASC の Reason が失敗に変化 65.0秒 < t ≤ 81.2秒 間隔60秒 × 1回 ≒ 60秒
ASC の総合が impaired 129.8秒 < t ≤ 145.9秒 猶予後 間隔60秒 × 2回 ≒ 120秒

同一条件で ALB のほうが約60〜80秒早く異常を報告しました。

本試行は1回です。ヘルスチェックの位相(プローブがいつ飛ぶか)により±60秒程度ぶれ得るため、「ALBが速い」と一般化するには追試が必要です。ここでは1試行の実測値として提示します。

実測②:正常復帰は差が小さい

SGルールを戻したときの計測です。

指標 実測
ASC の Reason が成功に変化 46.1秒 < t ≤ 62.3秒
ALB が healthy に復帰 78.5秒 < t ≤ 94.6秒
ASC の総合が ok に復帰 94.6秒 < t ≤ 110.8秒

復帰側の差は約16秒。検知側(60〜80秒差)よりずっと小さく、ここは実用上ほぼ同等です。


★ 実測③:Reason は総合ステータスを約1インターバル先行する

ここが本記事でいちばん実用的な発見です。異常検知でも正常復帰でも、個別チェックの Reason が総合ステータスより約1周期(50〜64秒)早く変わりました。

方向 Reason の変化 総合の変化
異常検知 ≤81.2秒 ≤145.9秒 約64秒
正常復帰 ≤62.3秒 ≤110.8秒 約49秒

FailureThreshold=2 / SuccessThreshold=2 / 間隔60秒という仕様から導かれる挙動ですが、1回目の失敗がAPI上は即座に公開されているという点は公式ドキュメントに書かれていません。

監視実装への提言

実装 異常検知 正常復帰
CloudWatch メトリクス StatusCheckFailed_Application でアラーム 約146秒 約111秒
describe-application-statusDetails[].Reason.Code を直接ポーリング 約81秒 約62秒

既定の監視経路(メトリクス)を使うと、サービス側が既に検知している失敗が約1分間見えません。 一刻を争う用途では Reason を直接見る実装を検討する価値があります。


🚨 実測④:課金対象の managed ENI が観測できない

ユーザーガイドは以下を明記しています。

  • 「AWS はあなたのVPCにマネージド ENI を作成する」
  • 「マネージド ENI はNetwork interfaces per Region クォータを消費する
  • 料金: 「マネージド ENI 1本あたり $0.01/時・AZごと

ところが、どの状態でも ENI が観測できませんでした

時点 ASCの状態 VPC内のENI ASC由来のENI
ASC作成前 4 0
関連付け直後 応答なし 4 0
疎通断 impaired 4 0
正常疎通 ok 4 0

リージョン全体21本を走査しても該当なし。プローブ自体は動作しています(ConnectionTimeout200 が返る)。

「モードのせい」ではなかった

「AWSがソースを選ぶモードだから見えないのでは?」と考え、ソースのサブネットとSGを自分で指定する customer-managed network paths で再検証しました。

aws ec2 create-application-status-check \
    --protocol http --port 8080 --path "/health" --status-code-matcher "200" \
    --health-check-paths '[{
      "Source":       {"SubnetId":"subnet-aaaa","SecurityGroupId":"sg-source"},
      "Destinations": [{"SubnetId":"subnet-aaaa","SecurityGroupId":"sg-dest"}]
    }]'

宛先SGのインバウンドは、CIDRではなくソースSG参照だけで許可しました。結果はこうです。

確認項目 結果
ASC のステータス ok / ResponseCodeMatched / HTTP 200 = プローブは確実に動作
指定したソースSGを使うENIの直接照会 0本(8サンプル連続)
aws ec2 describe-network-interfaces --filters Name=group-id,Values=sg-source
# → NetworkInterfaces: []

顧客が「このSGを使え」と指定し、そのSG参照でのインバウンド許可だけでプローブが通っているのに、そのSGに紐づくENIは EC2 API から1本も列挙されません。

ここで言えるのは「列挙できない」までです。ENIが存在しないことまでは示せません(通常の EC2 API に公開されない管理リソースである可能性があります)。一方で、不可視性がモード固有ではないことは、ソースを自分で指定した customer-managed でも同じ結果になった点から言えます。

両モードの比較

観点 AWS managed customer-managed
ソースのサブネット/SG AWSが選択・顧客に開示されない 顧客が指定
定義への反映 HealthCheckPaths が応答に現れない 指定どおり保持される
宛先SGのインバウンド許可 許可元が分からず広い範囲で開けがち ソースSG参照で最小権限に書ける
managed ENI の可視性 ❌ EC2 APIで0本 ❌ EC2 APIで0本

customer-managed を選ぶ意味は「ENIが見えること」ではなく「許可の書き方」にあります。 AWS managed だとソースSGが開示されないため、宛先SGを VPC CIDR などで広く開けることになりがちです。セキュリティ設計を重視するなら customer-managed を選ぶ理由があります。

含意: 料金表に「ENI 1本あたり $0.01/時」と書かれているのに、どちらのモードでも EC2 API から本数を数えられません。そのため「ENI本数 × 単価」というリソースベースの事前試算・照合が困難でした。Cost Explorer や CUR の請求明細で確認できるかどうかは、本記事では未検証です。 トラブルシューティングの「ENIクォータ枯渇でチェックが動かない」という項目も、ENIが見えなければ自力で切り分けられません。


実測⑤:ASG統合は動く。ただし遅い

Auto Scaling との統合はドキュメントどおり、追加設定なしで動きました

タグ経由の関連付けを使うと、置換後の新インスタンスにも自動で適用されます。

aws ec2 associate-application-status-check \
    --application-status-check-id asc-xxxxxxxx \
    --target-tag-associations Key=aws:autoscaling:groupName,Value=my-asg
# → {"AssociationType": "EC2TAG", "AssociationValue": "aws:autoscaling:groupName=my-asg"}

HealthCheckTypeEC2 のままで構いません(UpdateAutoScalingGroup に無効値を渡して確認したところ、有効値は [EC2, ELB, EBS, VPC_LATTICE] のみで、アプリケーションステータス専用の型は存在しませんでした)。

エンドツーエンドの復旧時間

イベント t=0 からの経過
ASC の Reason → ConnectionTimeout 約86秒
ASC 総合 → impaired 約137秒
ASG がインスタンスを切り離し 473秒(7分53秒)
新インスタンスが InService ≤511秒(8分31秒)

ASG の Scaling Activity には原因が明示されます。

Terminating EC2 instance: i-xxxxxxxxxxxxxxxxx
Cause: At 2026-08-19T05:22:43Z an instance was taken out of service
       in response to an EC2 Application Status check failure.

支配的なのは検知ではなくラグ

ASC が impaired を報告してから ASG が実際に動くまで約5分36秒(137秒 → 473秒)。

つまり検知の速さ(約2分)よりも、検知後のラグ(約5.6分)のほうが復旧時間を支配しています。「ASCとALBのどちらが60秒速いか」を議論しても、ASG任せの自動復旧では体感がほぼ変わりません。素早い復旧が要件なら、ASG任せにせず Reason を直接見て自前で置換を起動する設計が必要です。

観測上の注意: 検証の途中、約7分経過時点で「置換されない」と判断しかけましたが、その直後に置換が発生しました。ASGの反応は遅く、観測窓を10分以上取らないと誤った結論に至ります。


🚨 実測⑥:撤去してもSGが削除できない

これは実運用でいちばん困る話かもしれません。

ASC削除・全インスタンス終了・ALB削除をすべて終えたあとも、ヘルスチェックに使ったSGが DependencyViolation で削除できませんでした

2つのSGを同じ条件で並べたら、結果が分かれました。

SG 用途 インスタンス紐付け 30分後 約1時間後
宛先SG(EC2に付与) 通常のSG あり(すべて終了済み) 削除成功
ヘルスチェック元SG ASCのソース指定のみ 一度もなし 削除できず

後者が決定的です。このSGは一度もインスタンスに付与しておらず、ALBにも紐付けておらず、SG参照のインバウンドルールも削除済みで、describe-network-interfaces --filters group-id=... は一貫して0本です。

それでも削除できません。観測できる範囲では参照元を特定できず、ASC由来の非公開な管理リソース、または AWS 内部の別の参照機構が残存している可能性があります。

宛先SGのほうは「終了直後のインスタンスがSG参照を保持する既知の遅延」で説明でき、実際に時間経過で解放されました。一方こちらはインスタンスが一度も紐付いていないため、その説明が成立しません。

AWS内部の別の参照機構である可能性は排除できないため断定はしません。また本観測は1アカウント・1リージョン・1回の試行に基づきます。

IaC への含意

SG自体は課金対象ではないので金銭的な実害はありません。ただし、

CloudFormation / Terraform で ASC を含むスタックを削除すると、SG削除が完了せずスタック削除がタイムアウト・ロールバックする可能性があります。

ASC を IaC で管理するなら、ヘルスチェック元SGをスタック外で管理するか、削除リトライを織り込む設計が要ります。金銭ではなく冪等性を壊すタイプの問題です。

(なお CFn / Terraform での実挙動は本検証では未確認です。手動撤去のみで確かめました。)


主要な仕様(公式)

実務で効く数字をまとめておきます。

設定 既定 備考
チェック間隔 60秒 固定・変更不可
FailureThreshold / SuccessThreshold 2 / 2 変更可
Timeout 6秒 1〜30秒。強制タイムアウト
StatusCodeMatcher 200 リダイレクト(301/302)は追跡しない
InitializationGracePeriodSeconds 300秒 1〜600秒。短すぎるとASGが起動途中で置換
プロトコル HTTP / HTTPS HTTP/2 で送信。HTTPSはサーバ証明書を検証しない
Aggregation included excluded で本番影響なく試験導入できる

クォータ: チェック/アカウント 50、関連付け/チェック 50、関連付け/アカウント 200、ターゲット/アカウント 5,000(超過分は静かに未監視になる)

Reason codes: ResponseCodeMatched / ResponseCodeMismatch / ConnectionTimeout / ResponseTimeout / ConnectionRefused / ConnectionReset

導入手順として現実的なのは

  1. --aggregation excluded で作る(総合ステータスに影響させない)
  2. 宛先SGのインバウンド許可を入れる(ここを忘れると「アプリは正常なのに impaired」になる
  3. 初回評価が出るまで待ってから describe-application-status で期待どおりか確認する(本検証では関連付けから 3分46秒〜11分40秒Instances が空配列で返っている間はまだ評価前)
  4. --aggregation included に切り替える

ASG配下で included のまま SG許可を忘れると、正常なインスタンスが延々と置換され続けます。ドキュメントが excluded での試験導入を勧めているのは、この事故を避けるためです。


まとめ

EC2 Application Status Checks は「ALB の代わりに速く検知する仕組み」ではありません。 ALB を置けない・置きたくない単体インスタンス群に、アプリ層の監視の底を標準機能で入れるためのものだと捉えるのが実測に合っています。

実測から言える使いどころと注意点を並べます。

こういう時は向く こういう時は注意
ALBの背後にいない単体インスタンスを監視したい 素早い自動復旧が要件(ASGのラグが5.6分ある)
自作の死活監視Lambdaを畳みたい 検知の速さでALBを置き換えたい(ALBの方が速い)
ASGの置換条件にアプリ層を足したい ENIの課金を本数から検算したい(EC2 APIで数えられない)
監視の底を安く入れたい IaCでスタック削除まで自動化したい(SGが残る)

そしていちばん実用的な学びは、Details[].Reason.Code を直接見れば CloudWatch メトリクス経由より約1分早く異常を掴めるという点でした。GA直後の機能なので、ここは仕様が変わる可能性もあります。ご自身の環境で測ってみてください。

最後まで読んでいただきありがとうございました!


出典

記事内のGIFについて

構成図上をキャラクターが走るアニメーションは、OSS の diagram-runner(MIT License / Copyright (c) 2026 nekorobiaws-maker)を使って .drawio から生成しました。ブラウザ内で完結し、GIF書き出しまでローカルで処理されます。図の理解を助ける用途に便利です。

本記事のGIFはオリジナル版ではなく、筆者が手元で改変したビルドで生成しています(レーンごとに走行線を色分け/凡例をcanvasに描画)。したがって配色や凡例の見た目はオリジナル版とは異なります。走っているキャラクター(きつね・ペンギン)は Kenney「Cube Pets」(CC0)の素材です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?