はじめに

本記事では、既存ドメイン配下の特定サブパスだけを別オリジンへ切り替える要件に対して、ECS(Fargate)上のNginxをリバースプロキシとして用いた、パスベース転送の実装についてのポイント解説になります。

リバースプロキシとは?

フォワードプロキシがクライアント側の代理で外へ出るのに対して、リバースプロキシは公開側(サーバー側)の入口でリクエストを受けて、背後のオリジンへ中継します。リバースプロキシを利用することで、次のような利点があります。

  • バックエンドの抽象化
    入口(Nginx)で転送先をまとめるため、背後の追加/切替などが容易にできます。
  • ルーティングの統制
    特定のサブパス配下のみを別オリジン(Kuroco等)に寄せる、といったサブパス切替が可能です。
  • ヘッダー整形/リダイレクト補正
    HostやX-Forwarded-*を明示して、proxy_redirectでLocationを入口のURL体系に合わせることで、リンク崩れやリダイレクトの不整合を回避できます。

今回の実装

今回の構築要件は、既存ドメインはそのままで特定サブパスだけ外部オリジン(Kuroco)へ転送することでした。本構成ではNginxがユーザーからのリクエストを受けて、パスに応じてオリジンへ転送するフローになります。
構成図としては、以下のイメージになります。

構成図

本構成は、既存のCloudFront配下にALBとECS(Nginx)を新たに追加し、特定のパスリクエストのみを外部オリジン(Kuroco)へルーティングする設計としています。
この設計により、稼働中のメインサイト(固定ドメイン)へは影響を与えず、サブディレクトリ配下で別システムを提供することが可能です。

各コンポーネントの役割

  • CloudFront
    TLS終端、コンテンツのキャッシュ、WAFによる境界防御を担当します。
  • ALB
    ECSタスクへの負荷分散に加え、パスベースのルーティングの一次受け口として機能します。
  • ECS(Fargate) と Nginx(リバースプロキシ)
    本構成の核となる部分です。リクエスト転送時の詳細なヘッダー制御、オリジンが返すリダイレクトURLの書き換えを行います。また、オリジン障害時にメンテナンス画面を返すなど、アプリケーションレベルの細かな制御を担当します。
  • Kuroco(オリジン)
    コンテンツの管理と配信を担うバックエンドです。AWS側(Nginx)からは、HTTPSリクエストを受け付ける通常のWebサーバーとして扱われます。

オリジン(Kuroco)の役割と本構成の意図

本構成では、Kuroco(ヘッドレスCMS/バックエンドSaaS)をオリジンとして位置付けるため、インターネットへの直接公開は行いません。全トラフィックをAWS(CloudFront と ALB と ECS)経由にすることで、SaaSの仕様制約に影響されず、セキュリティやパスルーティングといった「入口の統制」をAWS側で一元管理するアーキテクチャを実装する意図となります。

Nginxのルーティング設定(パスベース転送)

今回の要件を実現するための nginx.conf の設定例を示します。以下は、コンテナに配置する nginx.conf の全体像です。特定パス(/external-app/) をKuroco(sample-kuroco-prd.g.kuroco-front.app)へ転送させる設定となっています。

# プロセス数に応じて自動ワーカープロセス数を設定
worker_processes auto; 

# ワーカープロセス毎に対応する接続数の上限設定
events {
    worker_connections 1024; 
}

http {
    # MIMEタイプ設定の読み込みとデフォルト値
    include /etc/nginx/mime.types;

    # 拡張子に応じたContent-Typeを適切に返す
    default_type application/octet-stream;

    # (※1)AWS VPC DNSリゾルバの設定(upstreamの名前解決用)
    resolver 169.254.169.253 valid=300s;
    resolver_timeout 5s;

    # Nginxバージョン情報の隠蔽
    server_tokens off; 

    # (※2)CloudWatch Logsへの出力設定
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" "$http_user_agent"';
    access_log /dev/stdout main; 
    error_log /dev/stderr warn; 

    # (※3)クライアントIP取得(set_real_ip_fromにはALBのサブネットCIDRを指定)
    # set_real_ip_fromにはALBのサブネットCIDRを指定
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
    set_real_ip_from 10.0.5.0/24;
    set_real_ip_from 10.0.6.0/24;

    # XFFヘッダからクライアントIPを抽出するマップ定義
    map $http_x_forwarded_for $client_ip {
        "~^([^, ]+)" $1;
        default $remote_addr;
    }

    server {
        listen 80;      # IPv4での待受ポート
        listen [::]:80; # IPv6での待受ポート
        server_name _;  # 全ホスト名を受け付ける
        proxy_intercept_errors on; # エラーページ差し替え用

        # 静的アセット(assets)の転送
        # パス階層のズレを防ぐため、assetsディレクトリを明示的に定義
        location /external-app/assets/ {
            proxy_pass https://sample-kuroco-prd.g.kuroco-front.app/assets/;
            proxy_set_header Host sample-kuroco-prd.g.kuroco-front.app;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_ssl_server_name on;
            proxy_ssl_name sample-kuroco-prd.g.kuroco-front.app;
        }

        # ページ本体の転送
        # /external-app(/external-app/)へのリクエストを転送
        location ~ ^/external-app(?:/|$) {

            # 末尾スラッシュの正規化
            rewrite ^/external-app$ /external-app/ break;

            proxy_pass https://sample-kuroco-prd.g.kuroco-front.app;
            proxy_set_header Host sample-kuroco-prd.g.kuroco-front.app;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_ssl_server_name on;
            proxy_ssl_name sample-kuroco-prd.g.kuroco-front.app;
            proxy_redirect https://sample-kuroco-prd.g.kuroco-front.app/ /external-app/;
        }

        # (※4)ヘルスチェック用エンドポイント(ALB用)
        location /healthcheck {
            access_log off;
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }

        # 未マッチはメインパスへリダイレクト
        location / {
            return 301 /external-app/;
        }

        # (※5)5xxエラーページの配置
        error_page 500 502 503 504 505 507 508 /5xx.html;
        location = /5xx.html {
            root /usr/share/nginx/html;
            internal;
        }

        # エラーページ用アセットのフォールバック
        location ^~ /assets/ {
            root /usr/share/nginx/html;
            try_files $uri @assets_upstream;
        }

        location @assets_upstream {
            proxy_pass https://sample-kuroco-prd.g.kuroco-front.app;
            proxy_set_header Host sample-kuroco-prd.g.kuroco-front.app;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_ssl_server_name on;
            proxy_ssl_name sample-kuroco-prd.g.kuroco-front.app;
        }
    }
}

上記の構成ファイルには、単なる転送設定だけでなく、AWS環境での考慮事項や、セキュリティ対策など、実運用で必要な設計ポイントが含まれています。

⚪︎ (※1)AWS VPC DNSリゾルバの設定

Nginxの proxy_pass にドメイン名を指定する場合、通常は起動時のみ名前解決が行われます。しかしAWS環境ではIPが動的に変わる可能性があるため、resolver 169.254.169.253 を指定し、必要に応じて変数を介してアクセスすることで、TTL期限切れ時の再解決(IP変更への追従)を可能にする設計としています。

⚪︎ (※2)CloudWatch Logsへの出力設定

Fargate環境ではローカルファイルへのログ出力は扱いづらいため、標準出力・標準エラー出力(/dev/stdout, /dev/stderr)にログを流す設定にしています。これにより、awslogs ログドライバーを通じて自動的にCloudWatch Logsへログが転送・蓄積され、追加のエージェント無しでログ管理が可能になります。

⚪︎ (※3)クライアントIP取得

CloudFrontやALBを経由すると、送信元IPがプロキシのIPに置き換わります。アクセスログやレート制限のために実IPを正しく取得するには、set_real_ip_from で信頼するCIDR(今回はVPC内のALB)を定義し、X-Forwarded-For ヘッダの右端から遡って信頼できるIPアドレスを特定する設定が必要です。

⚪︎ (※4)ヘルスチェック用エンドポイント

ALBからのヘルスチェックリクエストは、オリジン(Kuroco)へ転送せず、Nginx自身が即座に 200 OK を返すようにします。これにより、Kuroco側がメンテナンス中であっても、リバースプロキシ自体は正常稼働しているとALBに判定させ、不要な切り離しを防ぎます。

⚪︎ (※5)5xxエラーページの配置

オリジンがダウンした場合やタイムアウト時に備え、Nginxコンテナ内に静的な 5xx.html を配置することで、オリジンへのアクセスを行わずに自己完結してエラー画面を表示できる設計となります。

実装に必要なファイル構成

前述のnginx.confの他に、本実装において、コンテナビルドに必要なファイル構成は以下の通りです。設定ファイルだけでなく、カスタムエラーページ(5xx.html)もコンテナ内に含める形です。

.
├── Dockerfile       # コンテナ定義
├── nginx.conf       # Nginx設定
└── access-error/
    ├── 5xx.html     # カスタムエラーページ
    └── assets/      # エラーページ用画像など

コンテナイメージのビルド(Dockerfile)

上記のファイルを元にコンテナイメージを作成するための Dockerfile 例です。
nginx:latest をベースに、設定ファイルとエラーページ資材(CSSや画像を含む)をコンテナ内のパスに配置します。

FROM nginx:latest

# Nginx設定ファイルの配置
COPY nginx.conf /etc/nginx/nginx.conf

# カスタムエラーページの配置
COPY access-error/5xx.html /usr/share/nginx/html/5xx.html

# エラーページ用アセット(CSS, 画像など)の配置
# 必要なディレクトリを作成してコピー
RUN mkdir -p /usr/share/nginx/html/assets/css \
    /usr/share/nginx/html/assets/img/common

# CSS・画像・JS等の配置
COPY access-error/assets/css/style.css /usr/share/nginx/html/assets/css/style.css
COPY access-error/assets/common/ /usr/share/nginx/html/assets/img/common/
COPY access-error/js/bundle.js /usr/share/nginx/html/assets/js/bundle.js

実装時に直面した課題と解決策(Tips)

本構成の構築中に実際に遭遇した問題点と、それをどのように解消したかを紹介します。

1. オリジン配下のアセット(画像等)が404エラーになる

  • 事象: トップページは表示されるものの、画像やCSSが読み込まれず画面が崩れました。
  • 原因: オリジンのHTMLがルート相対パス(例: /assets/img.png)で記述されており、リバースプロキシ経由時にパス(/external-app/)が欠落してしまっていました。
  • 解消: proxy_redirect によるLocationヘッダの書き換えと合わせて、オリジン側でも相対パス記述を徹底してもらうことで対処しました。

2. HTTPSリダイレクトループの発生

  • 事象: CloudFront経由でアクセスすると、ブラウザで「リダイレクトが多すぎます」というエラーが発生しました。
  • 原因: CloudFrontから奥はHTTPで通信していたため、オリジン側が「非SSLアクセス」と判断し、HTTPSへのリダイレクトを返し続けていました(無限ループ)。
  • 解消: Nginx設定で proxy_set_header X-Forwarded-Proto $scheme; を付与し、オリジンに対して大元のリクエストはHTTPSであることを伝達させることで解決しました。

3. アクセスログの送信元IPが全てALBのIPになる

  • 事象: CloudWatch Logsに出力されるアクセスログのIPアドレスが、全てALBのプライベートIPになりました。
  • 原因: Nginxのデフォルト挙動では、直前の接続元(ALB)のIPを記録するためでした。
  • 解消: real_ip モジュールを活用し、set_real_ip_from にALBのサブネットCIDRを指定、信頼できるプロキシからの X-Forwarded-For ヘッダを解析して、実IPを抽出する設定を追加しました。

おわりに

本記事では、Kurocoの手前にECS(Nginx)を配置し、AWS側で入口の統制を行うアーキテクチャを紹介しました。この構成により、SaaS側の制約に縛られず、パス統合や詳細なトラフィック制御といった要件を柔軟に実現できました。
この「SaaSの利便性」と「AWSによる統制」を両立するパターンは、様々なSaaS連携やマイクロサービス移行に応用できそうです。本記事が設計の一助となれば幸いです。