Azure App Serviceでリバースプロキシ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/aed22cd9-1703-f546-abae-c4d8f7baf82f.png)

## １．はじめに

会社で生成AIの啓発活動を担当し、社内への浸透を推進していますが、チャットボット以外にも生成AIを活用したサービスが次々と登場してきており、利用者の視点から入口を一本化する必要性を感じています。
さらに、Azure App ServiceでOkta認証を用いるWebサービスを構築する際にはApp Serviceが１リソースあたり１サービスとなるため、Okta認証をサービスごとに設定せざる負えない状況になっています。この方式は構築者の観点からですとシンプルで良いのですが、利用者からみると類似するサービスのアドレスが複数あるような状況になり分かりにくいです。
そこで、生成AIの入口を用途ごとに一本化し、さらにOkta認証もすべてを1つのサービスで実施できないかと検討していたところ、リバースプロキシで実現できる可能性が見えたため、試してみることにしました。

```mermaid
graph TB
  Summary["要約"]
  Trans["翻訳"]
  Speech["文字起こし"]
  Research["調査"]
  Minutes["議事録作成"]

  subgraph blk_MainMenu["メインメニュー"]
    direction LR

    subgraph blk_column[" "]
      subgraph blk_Chatbot["チャットボットサービス"]
		    Chat
      end
      subgraph blk_Chatbot2["チャットボットサービス"]
        ChatbotService
      end
    end

    subgraph blk_Dify["Dify"]
      direction TB
      Research
      Minutes
      Summary
      Trans
      Speech
    end

    subgraph blk_descriptions[" "]
　　　　hoge("社内生成AI<br>業務毎のメニューを選択してください。<br>")
    end

  end
```

## ２．リバースプロキシとは

リバースプロキシは、クライアントからのリクエストを受け取り、適切なバックエンドサーバーに転送する中間サーバーの役割を果たします。これにより、クライアントは直接バックエンドサーバーにアクセスすることなく、必要なリソースやサービスを利用することができます。

リバースプロキシの主な機能は以下の5つに分類されます：

1. セキュリティ面では、Oktaなどによる統合認証の実現、バックエンドサーバーの保護、TLS1.2による暗号化通信の管理、そしてDDoS攻撃からの防御を提供します。
2. 負荷分散機能により、複数のバックエンドサーバーにトラフィックを分散させ、システム全体の可用性と性能を最適化します。
3. キャッシング機能を通じて、よく利用されるコンテンツを一時保存し、応答時間の短縮とバックエンド負荷の軽減を実現します。
4. 管理と可視性の面では、アクセスログ、トラフィック、性能データの一元管理を可能にします。
5. バックエンド統合により、異なる技術やプロトコルを使用するサービスを一つのインターフェースにまとめ、クライアントへのアクセスを簡素化します。

```mermaid
graph TB
    Client["クライアント"]
    RP["リバースプロキシ"]
    Auth["認証(Okta)"]
    Cache["キャッシュ"]
    LB["負荷分散"]
    BE1["バックエンド 1"]
    BE2["バックエンド 2"]
    BE3["バックエンド 3"]

    Client -->|"HTTPS(TLS1.2)"| RP
    RP --> Auth
    RP --> Cache
    RP --> LB
    LB --> BE1
    LB --> BE2
    LB --> BE3

    %% セキュリティ層の表現
    subgraph Security["セキュリティ層"]
        RP
        Auth
    end

    %% パフォーマンス最適化層
    subgraph Performance["パフォーマンス最適化層"]
        Cache
        LB
    end

    %% バックエンド層
    subgraph Backend["バックエンド層"]
        BE1
        BE2
        BE3
    end
```

この図はリバースプロキシのアーキテクチャを3つの層で表現しています：

1. セキュリティ層：クライアントからのHTTPS（TLS1.2）リクエストを受け取り、Oktaによる認証を行います。
2. パフォーマンス最適化層：キャッシュ機能と負荷分散機能を提供し、システムの効率を向上させます。
3. バックエンド層：3つのバックエンドサーバーで構成され、負荷分散機能によってリクエストが適切に振り分けられます。

このアーキテクチャにより、セキュリティの確保とパフォーマンスの最適化を同時に実現しています。

## ３．リバースプロキシの実現方法

Azure App Serviceでリバースプロキシを実現する方法として、主に以下の2つのアプローチがあります。1つ目は、App Serviceの組み込み機能である「URL Rewrite」と「Reverse Proxy」を利用する方法です。2つ目は、Nginxなどのリバースプロキシサーバーをカスタムコンテナとしてデプロイする方法です。

それぞれのアプローチには利点と制約があり、要件に応じて適切な方法を選択する必要があります。本記事では、より柔軟な設定が可能なNginxを使用したアプローチを詳しく見ていきます。

## ４．Nginxとは

Nginx（エンジンエックス）は、ロシアのイーゴリ・シソエフ氏(Igor Sysoev)によって2004年にリリースされた、高性能なWebサーバー、リバースプロキシ、ロードバランサーとして機能するオープンソースソフトウェアです。Sysoev氏は、当時ロシアの大手ポータルサイトRambler.ruで働いていた際、C10K問題（1万以上の同時接続を処理する課題）を解決するために開発を始めました。

Nginxの特徴として、以下が挙げられます：

- 非同期イベント駆動アーキテクチャを採用し、少ないリソースで多数の同時接続を処理できる
- モジュール式の設計により、必要な機能を柔軟に追加・カスタマイズ可能
- 静的コンテンツの配信に優れ、キャッシング機能も充実している
- 設定が直感的で管理がしやすい

2019年には、F5 Networksが総額6億7000万ドルでNginx社を買収し、現在はエンタープライズ向けの商用版「NGINX Plus」も提供しています。2024年現在、世界中のWebサイトの約
30%以上がNginxを使用しており、特に大規模なWebサイトやクラウドサービスで広く採用されています。

https://www.f5.com/

### 4.1. Nginxの機能

| **項番** | **機能** | 機能(英語表記) | **詳細** |
| --- | --- | --- | --- |
| 1 | Webサーバー機能 | [HTTPS Servers](https://nginx.org/en/docs/http/configuring_https_servers.html) | 静的コンテンツの高速配信、動的コンテンツの処理、バーチャルホスティング |
| 2 | リバースプロキシ | Reverse Proxy | バックエンドサーバーへのリクエスト転送、URLリライト、ヘッダー制御 |
| 3 | ロードバランシング | Load Balancing | 複数サーバーへの負荷分散、ヘルスチェック、セッション永続性 |
| 4 | キャッシング | Caching | 静的・動的コンテンツのキャッシュ、キャッシュ制御、パフォーマンス最適化 |
| 5 | セキュリティ | Security | SSL/TLS終端、アクセス制御、DDoS保護、WAF機能 |
| 6 | ストリーミング | Streaming | メディアストリーミング、HTTP Live Streaming (HLS)対応 |
| 7 | モニタリング | Monitoring | アクセスログ、エラーログ、メトリクス収集、状態監視 |

### 4.2. Nginxの定義ファイル

Nginxの設定は、主にnginx.confファイルとdefault.confファイルを通じて行われます。

### nginx.conf

nginx.confは、Nginxの全体的な動作を制御するメインの設定ファイルです。このファイルは階層的な構造を持ち、http、server、locationなどの主要な設定ブロックで構成されています。通常は/etc/nginx/nginx.confに配置されます。

```
# メインの設定ブロック
http {
    # グローバル設定
    include       mime.types;
    default_type  application/octet-stream;
    
    # サーバーブロック
    server {
        listen 80;
        server_name example.com;

        # ロケーションブロック
        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
        }
    }
}
```

### default.conf

default.confは、特定のバーチャルホストの設定を定義するファイルです。このファイルはnginx.confからincludeされ、個別のサイトやアプリケーション固有の設定を記述します。通常は/etc/nginx/conf.d/default.confに配置されます。

```
# デフォルトのサーバー設定
server {
    listen       80;
    server_name  localhost;

    # ルートディレクトリの設定
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # エラーページの設定
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

これらの設定ファイルを適切に組み合わせることで、複雑なルーティングやプロキシルール、セキュリティ設定などを柔軟に実現できます。

## ５．リバースプロキシで実現したいこと

リバースプロキシを使用して、以下の機能を実現したいと考えています：

- **単一のエントリーポイント：** すべてのAIサービスに対して、一つのドメインからアクセス可能にする
- **集中認証管理：** Okta認証を一箇所で実装し、すべてのサービスに適用する
- **柔軟なルーティング：** URLパスに基づいて、適切なバックエンドサービスにリクエストを振り分ける
- **セキュリティの強化：** TLS 1.2以上の暗号化通信を強制し、セキュアな接続を確保する
- **統一されたユーザー体験：** メインメニューから各サービスへのシームレスな遷移を実現する

以下の図は、実現したい構成を示しています。クライアントからの全てのリクエストは、まずOkta認証を通過し、その後メインメニューを経由して各サービスにアクセスする流れとなっています。

```mermaid
graph TB
    Client["クライアント"]
    Auth["Okta認証/認可"]
    AppService["AzureAppService"]
    Main["メインメニュー"]
    Chat["チャットボット"]
    Agent["AIエージェント"]
    Chatbot["チャットボット"]
    Helpbot["社内問い合わせボット"]
    Summary["要約"]
    Trans["翻訳"]
    Speech["文字起こし"]
    Research["調査"]
    Minutes["議事録作成"]
    ChatbotService["チャットボットサービス"]

    Client -->TLS12--> AppService
    AppService --> Auth
    Auth -->Main  
    Main --> Chat
    Main --> Summary
    Main --> Trans
    Main --> Speech
    Main --> Research
    Main --> Minutes
    Main --> ChatbotService
    Chat --> Agent
    Agent --> Chatbot
    Agent --> Helpbot

    subgraph blk_Security["セキュリティ層"]
        AppService 
        Auth
    end
    
    subgraph blk_Chatbot["チャットボットサービス"]
		    Chat
    end

    subgraph blk_Chatbot2["チャットボットサービス"]
        ChatbotService
    end

    subgraph blk_Dify1["Dify"]
		    Agent
		    Chatbot
		    Helpbot
    end
    
    subgraph blk_Dify2["Dify"]
		    Summary
			  Trans
			  Speech
			  Research
			  Minutes
    end

    style Main fill:#f9f,stroke:#333,stroke-width:2px
    style Auth fill:#faa,stroke:#333,stroke-width:2px
    style Chat fill:#bbf,stroke:#333,stroke-width:1px
    style Summary fill:#bbf,stroke:#333,stroke-width:1px
    style Trans fill:#bbf,stroke:#333,stroke-width:1px
    style Speech fill:#bbf,stroke:#333,stroke-width:1px
    style Research fill:#bbf,stroke:#333,stroke-width:1px
    style Minutes fill:#bbf,stroke:#333,stroke-width:1px
```

## ６．フィジビリティ

リバースプロキシ実装のフィジビリティを確認するため、以下の機能について検証が必要です

| No | **検証項目** | **必要な機能** | **実現可能性** | **備考** |
| --- | --- | --- | --- | --- |
| 1 | 単一エントリーポイント | リバースプロキシ、URLルーティング | ◎ | Nginxの基本機能で実現可能 |
| 2 | Okta認証の集中管理 | 認証ヘッダー転送、セッション管理 | ○ | proxy_set_headerディレクティブで実現可能 |
| 3 | TLS 1.2強制 | SSL/TLS設定、暗号スイート制御 | ◎ | ssl_protocolsディレクティブで制御可能 |
| 4 | バックエンドサービス振り分け | location設定、プロキシパス | ◎ | location blockとproxy_passで実現可能 |
| 5 | HTTPSリダイレクト | リダイレクト設定 | ○ | return 301ディレクティブで実現可能 |
| 6 | ヘルスチェック | アップストリームヘルスモニタリング | △ | 追加モジュールが必要な可能性あり |

◎：標準機能で容易に実現可能

○：設定の調整で実現可能

△：追加の設定や機能が必要

総合的な判断として、要件の大部分はNginxの標準機能で実現可能であり、フィジビリティは高いと考えられます。特に重要な認証統合とルーティングについては、十分な機能が提供されています。

## ７．nginx.conf

リバースプロキシで実現したいことを入力にして生成AIでnginx.confを生成してみました。
このまま動作するかは分かりませんが、HTTPSのTLS1.2通信の設定やOkta認証などのセキュリティ設定、メインメニューを表示するための静的コンテンツ、Difyや複数のチャットボットへのバックエンドサービスへのプロキシルーティングなどを実現できていそうです。

```
user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    # SSL設定
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

    # プロキシバッファ設定
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;

    # アップストリームサーバーの定義
    upstream chatbot_service1 {
        server chatbot-service1:8080;
    }

    upstream chatbot_service2 {
        server chatbot-service2:8080;
    }

    upstream dify_service {
        server dify-service:8080;
    }

    server {
        listen 443 ssl;
        server_name ai-services.example.com;

        # SSL証明書
        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;

        # Okta認証設定
        location = /auth {
            internal;
            proxy_pass https://your-okta-domain/oauth2/default/v1/authorize;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # メインメニュー（静的コンテンツ）
        location / {
            auth_request /auth;
            root /usr/share/nginx/html;
            index index.html;
        }

        # チャットボットサービス1
        location /chatbot1 {
            auth_request /auth;
            proxy_pass http://chatbot_service1;
            proxy_set_header Host $host;
            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;
        }

        # チャットボットサービス2
        location /chatbot2 {
            auth_request /auth;
            proxy_pass http://chatbot_service2;
            proxy_set_header Host $host;
            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;
        }

        # Difyサービス（要約、翻訳、文字起こし等）
        location /dify {
            auth_request /auth;
            proxy_pass http://dify_service;
            proxy_set_header Host $host;
            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;
            
            # WebSocket対応
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        # エラーページ
        error_page 401 = @error401;
        location @error401 {
            return 302 /login?redirect_uri=$request_uri;
        }
    }

    # HTTPをHTTPSにリダイレクト
    server {
        listen 80;
        server_name ai-services.example.com;
        return 301 https://$server_name$request_uri;
    }
}
```

この設定では以下の機能を実現しています：

- TLS 1.2以上の強制使用とセキュアな暗号スイートの指定
- Okta認証の統合と認証要求の転送
- 各サービスへのプロキシルーティング設定
- WebSocket接続のサポート（Difyサービス用）
- HTTPからHTTPSへの自動リダイレクト

実際の計画ではNginxの上位にApp Serviceがあり、App ServiceにはNginxが導入されているので、TLS1.2とOkta認証/認可の設定を外してシンプルな設定にしてLinuxマシンで実験します。

```
user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log
notice;
pid       /var/run/nginx.pdf;

event {
    worker_connection 1024;
}

# メインの設定ブロック
http {
    # グローバル設定
    include       mime.types;
    default_type  application/octet-stream;
    
    # サーバーブロック
    server {
        listen 80;
        server_name example.com;
        
        # 静的コンテンツ（メニュー）
				root /webapp/public;
				
        # ロケーションブロック
        location /dify {
            proxy_pass http://backend;
            proxy_redirect off;
            proxy_set_header Host $host;
        }
        location /chatbot {
            proxy_pass http://backend;
            proxy_redirect off;
            proxy_set_header Host $host;
        }
        location /chatbot2 {
            proxy_pass http://backend;
            proxy_redirect off;
            proxy_set_header Host $host;
        }
    }
}
```

## ８．検証構成

今回は単一コンテナなので、docker runでも実行できますが、パラメタなどを定義しておきたいので、あえてdocker-compose.yamlで構成を作りました。

イメージ的にはdocker compose upコマンドを実行するとdocker-compose.yamlを読み込んでコンテナを作成し、ホストコンピュータのconfigフォルダ内nginx.confとdefault.confを読み込みnginx:latestコンテナを起動するという流れです。
ホストコンピュータにはwwwrootフォルダを用意し、メインメニュー用のindex.htmlを管理しています。
configフォルダ内のファイルとwwwrootフォルダ、logフォルダはコンテナにvolume マウントします。

これによりコンテナ内のnginx.confをホストコンピュータを介して編集できます。

```mermaid
graph LR
    A["開始"] -- docker compose up -d --> B["docker compose"]
    B -- コンテナの実行 -->E["nginx:latestコンテナ"]
    E --> G["終了"]
    C["docker-compose.yaml"] --読み込み--> B
    D["nginx.conf"] --読み込み--> E

    

```

上記の構成を実現するためのフォルダ構造はこのような感じです。nginxフォルダ内でdocker compose upコマンドを実行すると動きます。

```
nginx/
|-- config
|   |-- nginx.conf
|-- docker-compose.yaml
|-- log
|   |-- access.log
|   |-- error.log
|-- wwwroot
    |-- index.html
```

nginx.conf

今回はYahoo newsをバックエンドに見立てて構成を組んでます。

```
error_log /var/log/nginx/error.log notice;
pid       /var/run/nginx.pid;

events {
        worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name localhost;
    location / {
      root /home/wwwroot;
      index index.html;
    }
    location /yahoo {
      proxy_pass https://news.yahoo.co.jp/;
      proxy_redirect off;
    }
  }
}
```

default.conf

```
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
```

index.html

※長くなるのでangular.jsのスタイルシートは省きます。

```
<!DOCTYPE html>
<html lang="ja" ng-app="menuApp">
<head>
    <meta charset="UTF-8">
    <title>メニュー画面</title>
</head>
<body>
    <div class="menu" ng-controller="MenuController">
        <div class="menu-header">社内生成AI業務毎のメニューを選択してください</div>
        <div class="menu-content">
            <div class="menu-list">
                <div class="button" ng-click="selectMenu('要約')">要約</div>
                <div class="button" ng-click="selectMenu('翻訳')">翻訳</div>
                <div class="button" ng-click="selectMenu('文字起こし')">文字起こし</div>
                <div class="button" ng-click="selectMenu('調査')">調査</div>
                <div class="button" ng-click="selectMenu('議事録作成')">議事録作成</div>
            </div>
            <div class="submenu">
                <div ng-if="selectedMenu === '議事録作成'">
                    <div class="button" ng-click="navigateToYahoo()">Yahoo!</div>
                    <div class="button" ng-click="navigateToYahoo()">会議記録</div>
                    <div class="button" ng-click="selectSubmenu('議題追加')">議題追加</div>
                    <div class="button" ng-click="selectSubmenu('議事録送信')">議事録送信</div>
                </div>
                <div ng-if="selectedMenu && selectedMenu !== '議事録作成'">
                    <p>{{ selectedMenu }}に関連するサブメニューはありません。</p>
                </div>
            </div>
        </div>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
    <script>
        angular.module('menuApp', [])
            .controller('MenuController', ['$scope', '$window', function($scope, $window) {
                $scope.selectedMenu = '';
                $scope.selectedSubmenu = '';

                $scope.selectMenu = function(menu) {
                    $scope.selectedMenu = menu;
                    $scope.selectedSubmenu = '';
                    <!-- alert(menu + 'を選択しました！'); -->
                };

                $scope.selectSubmenu = function(submenu) {
                    $scope.selectedSubmenu = su![メインメニュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/f03198e7-b8b4-db15-3205-2a482dfa72c6.png)
bmenu;
                    <!-- alert(submenu + 'を選択しました！'); -->
                };

                $scope.navigateToYahoo = function() {
                    $window.location.href = '/yahoo';
                };
            }]);
    </script>
</body>
</html>

```

docker-compose.yaml

```
services:
  rp:
    image: nginx:latest
    volumes:
      - ./config/nginx.conf:/etc/nginx/nginx.conf
      - ./config/nginx.conf.template:/etc/nginx/nginx.conf.template
      - ./log:/var/log/nginx
    port:
      - "80:80"
```

![メインメニュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/34d2eb82-88d3-5248-6cf5-0d6ca329e682.png)

![yahoo_news_with_issue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/39f62130-c5e0-aa58-f8b5-5552198f09bf.png)

## ９．課題

実際に動かして見ると課題が見えてきます。大きな課題はサブディレクトリに割り当てたバックエンドサーバのアドレス解決です。

| No | 課題 | 具体的なケース | 対策 |
| --- | --- | --- | --- |
| 1 | 相対アドレスリンクが解決できない | リバースプロキシしたバックエンドサーバのコンテンツに相対アドレスリンクがあると、クリック時に404 Not Foundエラーとなる | nginx.confのsub_filterディレクティブでリンクアドレスを書き換える |
| 2 | 絶対アドレスリンクが解決できない | リバースプロキシしたバックエンドサーバのコンテンツに絶対アドレスリンクで同じサーバのコンテンツへのリンクがあるとクリック時にリバースプロキシを外れてしまう。 | 同上 |
| 3 | text/html以外のコンテンツがsub_filterディレクティブで書き変わらない | cgiやPythonで記述されたページは書き変わりません。 | nginx.confのsub_filter_typesを*として全てのコンテンツを書き換え対象にする |
| 4 | gzipで圧縮されたコンテンツが書き変わらない | gzipで圧縮されて転送されるコンテンツページは書き変わりません。 | nginx.confのproxy_set_header Accept-Encoding でgzip圧縮しないように設定する |
| 5 | envsubstの書き換え問題 | docker-compose.yamlのcommandディレクティブでenvsubstを使い変数を書き換えると$hostなどが空に書き変わってしまう | envsubstは使わないt好かう場合は置き換える変数を指定する。 |

## １０．対策

課題を解決するためのnginx.confファイルです。
まだ、不足があるかもしれませんが、これでバックエンドサーバのコンテンツをおおむね解決できます。

nginx.conf(アドレス書き換え成功)

```
error_log /var/log/nginx/error.log notice;
pid       /var/run/nginx.pid;

events {
        worker_connections 1024;
}

http {
  # Support websocket
  map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
  }

  server {
          listen 80;
          server_name localhost;

          location / {
            root /home/wwwroot;
            index index.html;
          }

          ######################################################################
          # reffer to https://qiita.com/k1tajima/items/732ec6694ecb3e928533
          location /yahoo/ {
            # definitions
            set $context "/yahoo/";
            set $backend_url "https://news.yahoo.co.jp";

            # Basic rewrite Rule
            rewrite ^/yahoo/(.*) /$1 break;

            # reffer to http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
            # Could not set $backend_url with "proxy_redirect default" setting by potofo
            proxy_pass https://news.yahoo.co.jp;

            # reffer to http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect
            # proxy_redirect    $scheme://$host/    $context;       # Webアプリのレスポンスに応じて追加
            proxy_redirect / $context;
            proxy_redirect default;

            # If you do not enable this setting, gzip compressed pages cannot be rewritten with sub_filter.
            proxy_set_header Accept-Encoding "";
            # If this setting is not enabled, pages other than text/html, such as cgi, will not be rewritten by the sub_filter.
            # reffer to https://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter_types
            #sub_filter_types text/html;  # This is defalut setting
            sub_filter_types *;

            # reffer to https://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter_once
            sub_filter_once off;

            # Relative Address Rewrite Rules
            # See http://nginx.org/en/docs/http/ngx_http_sub_module.html
            sub_filter  'href="/' 'href="$context';
            sub_filter  'src="/' 'src="$context';
            sub_filter  'url("/' 'url("$context';
            sub_filter  'action="/' 'action="$context';
            sub_filter  'href=\'/' 'href=\'$context';
            sub_filter  'src=\'/' 'src=\'$context';
            sub_filter  'url(\'/' 'url(\'$context';
            sub_filter  'action=\'/' 'action=\'$context';
#            sub_filter  'http://$proxy_host/' '$scheme://$host$context';
#            sub_filter  'http:\/\/$proxy_host\/' '$scheme:\/\/$host\/some-service\/';
#            sub_filter  '//$host/' '//$host$context';             # Webアプリのレスポンスに応じて選択
#            sub_filter  '\/\/$host\/' '\/\/$host\/some-service\/';

            # Absolute Address Rewrite Rules
            sub_filter 'href="$backend_url' 'href="/yahoo/';

            # Set Proxy headers
            # Support websocket
            # reffer to https://nginx.org/en/docs/http/websocket.html
            proxy_set_header    Upgrade             $http_upgrade;
            proxy_set_header    Connection          $connection_upgrade;

            # For reverse proxy, set Host to $proxy_host
            #proxy_set_header    Host                $host;
            #proxy_set_header    Host                $http_host;
            #proxy_set_header    Host                $proxy_host;

#            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_set_header    X-Forwarded-Host    $http_host;
            proxy_set_header    X-Forwarded-Port    $server_port;
          }
  }
}
```

![yahoo_news.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/47b35cac-24d0-cfb2-f4e7-7f7e5255d311.png)

キャプチャでは分かりにくいですが、きちんと解決できています。

![yahoo_news_article.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/c81680c6-9384-21d6-413a-7ec0f672cfc1.png)

## １１．App Serviceでの実装
Nginx Dockerイメージのサイズ

```
[root@localmotion ~]# docker images
REPOSITORY                  TAG         IMAGE ID       CREATED         SIZE
nginx                       latest      4f67c83422ec   6 months ago    188MB
[root@localmotion ~]#

```

nginxのDockerイメージのベースイメージの確認

```
root@723295a29390:/etc# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```

nginxのDockerイメージの構成ファイルの配置は以下のようになっています。

```
/
|-- etc
|   |-- nginx
|   |   |-- conf.d
|   |   |   |-- default.d
|   |   |-- nginx.conf
```

ユーザの確認

ユーザはnginxにした方がよさそうですね。

```
root@723295a29390:/# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
nginx:x:101:101:nginx user:/nonexistent:/bin/false
root@723295a29390:/#
```

構成から/etc/nginxにファイル共有をボリュームマウントすれば/etc/nginx/nginx.confをAzure App Serviceのコンテナから参照できそうです。

### 11.2. ファイル共有の作成

Azureポータルからストレージアカウントを作成して、ファイル共有を作成します。

**①ストレージアカウントの作成**

ここではptfaccstorageをストレージアカウントとして作成します。

![Azure_create_storageaccount.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/b0e80a4f-45da-e140-3c08-851aade03b5f.png)

**②ファイル共有の作成**

ここではファイル共有としてnginxという共有を作成します。
![Azure_create_sharedfile_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/5718cb4b-d74d-5822-695f-114e9d609e95.png)

![Azure_create_sharedfile_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/46063a03-87e0-17d2-5ab3-cf8c708e60b0.png)

③nginx.confファイルのアップロード
![Azure_upload_config_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/55e13a9f-0cbd-e2b3-5534-b3e709b609b5.png)

![Azure_upload_config_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/79586403-c2e4-a23c-f9eb-0cb7926fe79b.png)

/etc/nginx配下のフォルダを作成します。作成するのはnginx.confとwwwroot/index.htmlだけで良いです。

ファイルはAppendixにあります。
![Azure_upload_config_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/6b012b36-f46c-95a0-0eda-b4965addda9d.png)

### 11.3. App Serviceの作成

**①Wevアプリの作成**
![Azure_create_webapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/c149d69a-4f99-5154-cba6-ebb7fc249f1f.png)

②Nginxコンテナのデプロイ
![Azure_create_nginx_container_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/ca6770c6-1c8c-4d34-d5cc-55ca45ed4f82.png)
![Azure_create_nginx_container_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/223ad135-947f-0298-09c9-66629f3ce4c6.png)
![Azure_create_nginx_container_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/d5937c2a-32fc-0bf5-b820-6f19151622e4.png)

③ストレージアカウントのファイル共有のパスのマッピング
![Azure_mapping_fileshare_path.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/227a929d-e1a4-40fd-d83e-3cc4efcb6943.png)

④パスのマッピング後のNginxの画面
![Azure_メインメニュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/352f0d6c-a98e-edca-489c-11193e704645.png)
![Azure_yahoo_news.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/c0282a55-71da-d339-a98a-e4df28b9f47c.png)
![Azure_yahoo_news_detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764962/74c157ac-1969-a054-6325-0f0a4da5264a.png)

B1(Basic B1)プランとしていますが、NginxのDockerイメージは188MB程度なので、App ServiceのスケールダウンでF1(Free F1)にしてみましたが、F1プランでも動きました。

踏み台にされるのはいやなので、サービスは停止しておきました。

## １２.参考URL

https://nginx.org


https://nginx.org/en/docs/


## １３．Appendix

nginx.conf

```
**user nginx;
worker_processes auto;**

error_log /var/log/nginx/error.log notice;
pid       /var/run/nginx.pid;

events {
        worker_connections 1024;
}

http {
  # Support websocket
  map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
  }

  server {
          listen 80;
          server_name localhost;

          location / {
            **root /etc/nginx/wwwroot;**
            index index.html;
          }

          ######################################################################
          # reffer to https://qiita.com/k1tajima/items/732ec6694ecb3e928533
          location /yahoo/ {
            # definitions
            set $context "/yahoo/";
            set $backend_url "https://news.yahoo.co.jp";

            # Basic rewrite Rule
            rewrite ^/yahoo/(.*) /$1 break;

            # reffer to http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
            # Could not set $backend_url with "proxy_redirect default" setting by potofo
            proxy_pass https://news.yahoo.co.jp;

            # reffer to http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect
            # proxy_redirect    $scheme://$host/    $context;       # Webアプリのレスポンスに応じて追加
            proxy_redirect / $context;
            proxy_redirect default;

            # If you do not enable this setting, gzip compressed pages cannot be rewritten with sub_filter.
            proxy_set_header Accept-Encoding "";
            # If this setting is not enabled, pages other than text/html, such as cgi, will not be rewritten by the sub_filter.
            # reffer to https://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter_types
            #sub_filter_types text/html;  # This is defalut setting
            sub_filter_types *;

            # reffer to https://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter_once
            sub_filter_once off;

            # Relative Address Rewrite Rules
            # See http://nginx.org/en/docs/http/ngx_http_sub_module.html
            sub_filter  'href="/' 'href="$context';
            sub_filter  'src="/' 'src="$context';
            sub_filter  'url("/' 'url("$context';
            sub_filter  'action="/' 'action="$context';
            sub_filter  'href=\'/' 'href=\'$context';
            sub_filter  'src=\'/' 'src=\'$context';
            sub_filter  'url(\'/' 'url(\'$context';
            sub_filter  'action=\'/' 'action=\'$context';

            # Absolute Address Rewrite Rules
            sub_filter 'href="$backend_url' 'href="/yahoo/';

            # Set Proxy headers
            # Support websocket
            # reffer to https://nginx.org/en/docs/http/websocket.html
            proxy_set_header    Upgrade             $http_upgrade;
            proxy_set_header    Connection          $connection_upgrade;

            # For reverse proxy, set Host to $proxy_host
            #proxy_set_header    Host                $host;
            #proxy_set_header    Host                $http_host;
            #proxy_set_header    Host                $proxy_host;

            proxy_set_header    X-Forwarded-Proto   $scheme;
            proxy_set_header    X-Forwarded-Host    $http_host;
            proxy_set_header    X-Forwarded-Port    $server_port;
          }
  }
}
```

wwwroor/index.html

```
<!DOCTYPE html>
<html lang="ja" ng-app="menuApp">
<head>
    <meta charset="UTF-8">
    <title>メニュー画面</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            height: 100vh;
            width: 100vw;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #f4f4f4;
        }
        .menu {
            width: 1920px;
            height: 1080px;
            display: flex;
            flex-direction: column;
            background: #fff;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }
        .menu-header {
            flex: 0 0 auto;
            text-align: center;
            padding: 20px;
            font-size: 24px;
            background-color: #0078d7;
            color: white;
        }
        .menu-content {
            flex: 1 1 auto;
            display: flex;
            flex-direction: row;
            padding: 20px;
        }
        .menu-list {
            flex: 1 0 20%;
            border-right: 1px solid #ddd;
            padding: 10px;
            display: flex;
            flex-direction: column;
        }
        .menu-list .button {
            margin: 10px 0;
            padding: 15px;
            background-color: #f0f0f0;
            border: 1px solid #ccc;
            border-radius: 5px;
            text-align: center;
            font-size: 18px;
            cursor: pointer;
            transition: background-color 0.3s, transform 0.2s;
        }
        .menu-list .button:hover {
            background-color: #e0e0e0;
            transform: scale(1.02);
        }
        .submenu {
            flex: 1 0 80%;
            padding: 20px;
            display: flex;
            flex-wrap: wrap;
            justify-content: flex-start;
            align-items: flex-start;
            background-color: #fafafa;
        }
        .submenu .button {
            margin: 10px;
            padding: 20px;
            background-color: #f9f9f9;
            border: 1px solid #ccc;
            border-radius: 5px;
            text-align: center;
            font-size: 16px;
            cursor: pointer;
            width: calc(100% - 40px);
            box-sizing: border-box;
            transition: background-color 0.3s, transform 0.2s;
        }
        .submenu .button:hover {
            background-color: #eaeaea;
            transform: scale(1.02);
        }
    </style>
</head>
<body>
    <div class="menu" ng-controller="MenuController">
        <div class="menu-header">社内生成AI業務毎のメニューを選択してください</div>
        <div class="menu-content">
            <div class="menu-list">
                <div class="button" ng-click="selectMenu('要約')">要約</div>
                <div class="button" ng-click="selectMenu('翻訳')">翻訳</div>
                <div class="button" ng-click="selectMenu('文字起こし')">文字起こし</div>
                <div class="button" ng-click="selectMenu('調査')">調査</div>
                <div class="button" ng-click="selectMenu('議事録作成')">議事録作成</div>
            </div>
            <div class="submenu">
                <div ng-if="selectedMenu === '議事録作成'">
                    <div class="button" ng-click="navigateToYahoo()">Yahoo!</div>
                    <div class="button" ng-click="selectSubmenu('会議記録')">会議記録</div>
                    <div class="button" ng-click="selectSubmenu('議題追加')">議題追加</div>
                    <div class="button" ng-click="selectSubmenu('議事録送信')">議事録送信</div>
                </div>
                <div ng-if="selectedMenu && selectedMenu !== '議事録作成'">
                    <p>{{ selectedMenu }}に関連するサブメニューはありません。</p>
                </div>
            </div>
        </div>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
    <script>
        angular.module('menuApp', [])
            .controller('MenuController', ['$scope', '$window', function($scope, $window) {
                $scope.selectedMenu = '';
                $scope.selectedSubmenu = '';

                $scope.selectMenu = function(menu) {
                    $scope.selectedMenu = menu;
                    $scope.selectedSubmenu = '';
                    <!-- alert(menu + 'を選択しました！'); -->
                };

                $scope.selectSubmenu = function(submenu) {
                    $scope.selectedSubmenu = submenu;
                    <!-- alert(submenu + 'を選択しました！'); -->
                };

                $scope.navigateToYahoo = function() {
                    $window.location.href = '/yahoo';
                };
            }]);
    </script>
</body>
</html>

```

Linuxでdocker-compose.yamlで環境変数を書き換える方法
```
  rp:
    image: nginx:latest
    volumes:
      - ./config/nginx.conf:/etc/nginx/nginx.conf
      - ./config/nginx.conf.template:/etc/nginx/nginx.conf.template
      - ./config/default.conf:/etc/nginx/cond.d/default.conf
      - ./log:/var/log/nginx
      - ./wwwroot:/home/wwwroot
    ports:
      - "80:80"
    environment:
      - NGINX_HOST=localmotion
      - NGINX_YAHOO=https://news.yahoo.co.jp/
    command: /bin/bash -c "envsubst '${NGINX_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"
    #command: /bin/bash -c "envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"
```