AMIから複製したインスタンスをALB配下に入れたら S3 が AccessDenied で詰まった話

はじめに

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"]
    }
  }
}

読み解き:

  • NotIpAddressStringNotEqualsAND 結合された 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 コンソールから:

  1. EC2 → Elastic IPspare タグの行にチェック
  2. アクションElastic IP アドレスの関連付け
  3. インスタンス: バースト用、プライベートIP: 当該ENIのもの
  4. 関連付ける

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 がある可能性があります。

Last modified: 2026-05-07

Author