はじめに
OSアップデート作業をサービス無停止で実施するために、「直前AMIからバースト用インスタンスを起動して一時的にALBに参加させ、本体側を更新する」というパターンを採用しました。
事前検証も済ませ、当日もそのまま流すだけ──と思っていたところ、バースト用インスタンスを TG に register した直後にサイトの一部が壊れた という事象に遭遇しました。
原因は、Apache の ProxyPass で背後に置かれていた S3 のバケットポリシーがソースIPで制限されていたこと。Webサーバ自身のIPが許可リストに無いためアクセス拒否されていた、というオチです。
本記事では、この事象に遭遇し、原因特定から対策に至ったまでの流れでをまとめます。
1. 構成の全体像
次のような構成です。
┌───────────────────────┐
User ────► │ ALB (HTTPS, 443) │
└─────────┬─────────────┘
│
▼
┌───────────────────────┐
│ Target Group │
│ - app-ec2 (本体) │
│ - app-ec2-burst (★) │ ← AMIから複製した一時インスタンス
└─────────┬─────────────┘
│
▼
┌───────────────────────┐
│ Apache httpd │
│ /app/ → local │
│ /app/tiles/* → S3 ★ │ ← ProxyPass で S3 静的サイトに転送
└─────────┬─────────────┘
│
▼
http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/
│
▼
┌───────────────────────┐
│ S3 Bucket │
│ Policy: NotIpAddress │ ← 許可IPリスト方式の Deny
└───────────────────────┘
重要なポイント:
/app/tiles/*だけは Apache のProxyPassで S3 静的ホスティングに中継される- S3 バケットは バケットポリシーで「許可IPに含まれないアクセスは Deny」 という制約付き
- バースト用インスタンスは AMI 由来。新しい ENI で起動するため、ソースIPは元インスタンスと別物
2. 前提知識
2-1. Apache の ProxyPass
Apache httpd には「特定のURLへのリクエストを、別のサーバに転送する」機能があります。
ProxyPass /app/tiles http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/tiles
これは「/app/tiles/* へのリクエストを S3 静的サイトに中継する」という意味。
ユーザーから見ると EC2 が応答しているように見えますが、実体は S3 がコンテンツを返しています。
2-2. S3 静的ウェブサイトホスティング
S3 バケットを「Webサーバ」として公開する機能。URL形式は次のとおり。
http://<バケット名>.s3-website-<リージョン>.amazonaws.com/
2-3. バケットポリシーによる IP 制限
バケットポリシーで「このIPからのアクセスのみ許可」という制限がかけられます。
今回踏んだのは、典型的な NotIpAddress 拒否パターン。
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": "arn:aws:s3:::example-tiles/*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["許可IPリスト"]
}
}
}
「許可IPリストに含まれないIPからのアクセスを拒否する」という意味です。
3. 発生した事象
3-1. AccessDenied
バースト用インスタンスを Target Group に register した直後、ブラウザで https://app.example.com/ を開くと、一部リソースで以下が表示されました。
AccessDenied
RequestId: P1XXXXXXXXXXXXX
HostId: oxXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3-2. 最初の推測
上記の形式に見覚えがあったので、S3関係だな。となんとなく予想をしました。
実際にApacheのアクセスログを確認すると、403が出力されていました。
4. 調査ステップ
ステップ 4-1. httpd の ProxyPass 設定を実機で確認
目的: 「全体が S3 プロキシ」なのか、「特定パスだけ S3 プロキシ」なのかを確定する。
SSM Run Command でバースト用インスタンスに対して実行:
grep -nE '^[[:space:]]*ProxyPass' /etc/httpd/conf.d/*.conf
結果(抜粋):
113:ProxyPass /app/docdata http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/docdata
116:ProxyPass /app/tiles http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/tiles
ステップ 4-2. バースト用から S3 に直接 curl
目的: バースト用インスタンスのソースIPから、本当に S3 が拒否するのかを直接検証する。
curl -sv --max-time 10 \
"http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/" 2>&1 | head -30
結果:
> GET / HTTP/1.1
< HTTP/1.1 403 Forbidden
< Server: AmazonS3
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
学び:
- レスポンスヘッダ
Server: AmazonS3で、拒否している主体が確かに S3 だと確定 - HTTP ステータスは
403 Forbidden+ XML エラーコードAccessDenied
ここで初めて「Apache の前段で何かが起きている」のではなく、「S3 自身がブロックしている」と確証を持てました。
ステップ 4-3. バケットポリシーの確認
実際のポリシー(抜粋・IPは架空のものに置換):
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": "arn:aws:s3:::example-tiles/*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"203.0.113.10/32",
"203.0.113.11/32",
"203.0.113.12/32",
"203.0.113.99/32", ← ★ Name タグ "spare" の idle EIP
"(その他 顧客ネットワーク IP)"
]
},
"StringNotEquals": {
"aws:sourceVpc": ["vpc-xxxxxxxx"]
}
}
}
読み解き:
NotIpAddressとStringNotEqualsが AND 結合された Deny- つまり「許可IPに含まれる OR 許可VPCに所属」のいずれかを満たせばアクセス可能
- 注目すべきは
203.0.113.99という、どのインスタンスにも紐づいていない idle EIP がポリシーに登録されていた点
ステップ 4-4. 未関連付けEIP一覧の取得
spare EIP の存在と、アカウント全体の未関連付けEIPを可視化します。
aws ec2 describe-addresses --region ap-northeast-1 \
--query 'Addresses[?!not_null(AssociationId)].[AllocationId,PublicIp,Tags[?Key==`Name`]|[0].Value]' \
--output table
結果(抜粋):
| Public IP | AllocationId | Name タグ |
|---|---|---|
| 203.0.113.99 | eipalloc-0xxxxxxxxxxxxxxxx | spare |
| 198.51.100.50 | eipalloc-0xxxxxxxxxxxxxxxx | (なし) |
| 198.51.100.51 | eipalloc-0xxxxxxxxxxxxxxxx | (なし) |
Name=spare の EIP が「使われていないのに残っている」状態で発見されました。
5. 対処
5-1. 方針
spare EIP をバースト用に関連付ける方針で進めました。
5-2. 実行手順
EC2 コンソールから:
- EC2 → Elastic IP →
spareタグの行にチェック - アクション → Elastic IP アドレスの関連付け
- インスタンス: バースト用、プライベートIP: 当該ENIのもの
- 関連付ける
5-3. 確認: ソースIPが切り替わったか
curl -s --max-time 3 http://checkip.amazonaws.com/
結果: 203.0.113.99 が表示 → OK
5-4. 確認: S3 バケットアクセスが通るようになったか
# 直接S3への疎通(httpd経由せず)
curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 10 \
"http://example-tiles.s3-website-ap-northeast-1.amazonaws.com/tiles/"
# ローカルhttpd経由でのProxyPass動作確認
curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 10 \
"http://localhost/app/tiles/"
結果:
| URL | 修正前 | 修正後 | 判定 |
|---|---|---|---|
/app/tiles/ |
HTTP 403 + AccessDenied | HTTP 404 | ✅ ポリシー通過 |
/app/docdata/ |
HTTP 403 + AccessDenied | HTTP 404 | ✅ |
5-5. 作業完了後の後処理
バースト用 terminate 時には、EIP は 必ず「関連付け解除のみ」で元の idle 状態に戻すこと。「解放」を押してはいけません。
EC2 → Elastic IP → spare EIP → アクション → 関連付け解除
6. おわりに
不自然に残っているEIPがいたら、気を付けたいな。と思えました。
「関連付けなし = 無駄な課金」という一般論は正しいのですが、今回のように他システムから参照されている EIP がある可能性があります。


