【イチから学ぶ! 初めての Azure と生成 AI】Azure App Service / Container Apps を用いた生成AIアプリケーション開発 - サイドカーコンテナ連携編

# はじめに

本記事は、【イチから学ぶ! 初めての Azure と生成 AI】セミナーのセッション『AI アプリのクラウド運用入門 ～App Service と Container Apps の選び方と管理のポイント』の解説記事です。

Azure を活用した生成 AI アプリケーション開発の際に役立つポイントをご紹介します。実装時に直面しがちなハマりどころやその解決策を、全 5 回に分けてお届けします。

本記事では、SLM を題材として、Azure で稼働するアプリケーションをサイドカーコンテナとどのように連携するかについて、一例をご紹介します。

本記事でご紹介するサンプルアプリケーションのソースコードは、以下のリポジトリで公開しています。

https://github.com/kohei3110/azure-101-day

# SLM をサイドカーコンテナで稼働させる方法

ファインチューニングしたモデルをサイドカーコンテナで実行し、アプリケーションと連携する手法は、生成 AI アプリケーションの開発で注目されるユースケースの一つです。
これにより、以下の利点が得られます。

- コスト削減
- モデル精度の最適化
- 独立したコンテナとしてのスケーリング

また、監視・ログ等のコンテナアプリケーションをサイドカーに配置し、メインのコンテナアプリケーションと疎結合に構成することもユースケースの一つです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/e2925181-0847-4ea6-c7c8-b3a24d851dbc.png)

セミナーでは、Azure App Service や Container Apps を利用し、SLM（Specialized Language Model）をや OpenTelemetry コレクターをサイドカーコンテナとして稼働させる具体的な方法について解説しました。こうした実装により、モデルを既存のアプリケーションにシームレスに統合できます。

# サイドカーコンテナとは

サイドカーコンテナとは、メインのアプリケーションコンテナと同じ実行環境（例えば、同一の Pod やホスト）内で動作する補助的なコンテナです。

その主な役割は、以下の通りです。

- **補助機能の提供:** ログの収集、監視、トレース、セキュリティチェックなど、アプリケーション本体の機能には直接関与しないが、運用上不可欠な機能を提供します。


- **独立性と柔軟性:** メインコンテナのコードを変更せずに、新たな機能やサポートツールを追加できるため、システム全体のメンテナンス性や拡張性が向上します。

- **スケーラビリティ:** 補助的な処理（例えば、AI モデルの推論や監視ログの収集）を独立したコンテナとして分離することで、必要に応じて個別にスケールアウトが可能となり、リソースの効率的な利用が実現します。

本記事のサンプルアプリケーションでは、SLM や OpenTelemetry コレクターをサイドカーコンテナとして実行することで、アプリケーション本体と密結合せずに、AI モデルの運用や監視機能を提供しています。

# メインアプリケーションとサイドカーコンテナとの通信

サイドカーコンテナは、メインのアプリケーションコンテナと同一ホストまたは同一 Pod 内で稼働し、補助的な機能を提供する役割を担います。

これらのコンテナ間の通信は、システム全体の機能性と監視、ロギング、セキュリティといった補助機能の実装において有益です。

## 基本的な通信パターン

- localhost 経由の通信

サイドカーコンテナは、同じホスト上で実行されるため、メインコンテナは通常 localhost を介してサイドカーのサービス（例: モデル推論、ロギング、監視エージェント）にアクセスします。

例えば、メインコンテナ内のアプリケーションが、`http://localhost:5001` のエンドポイントにリクエストを送ることで、サイドカーで稼働している AI モデルの推論機能にアクセスすることが可能、といった具合です。

- コンテナ内ポートの公開

サイドカーコンテナは、必要なポートを公開することで、メインコンテナが指定したポート経由でサービスを呼び出すことができます。

- 実装例

以下は、Python ベースの簡単な例です。メインアプリケーションがサイドカーコンテナに配置された推論サービスへ HTTP リクエストを送信する場合のコード例です。

```python
import requests

def get_inference(input_data):
    # サイドカーコンテナは同一ホスト上のポート5001で稼働している前提
    sidecar_url = "http://localhost:5001/api/inference"
    response = requests.post(sidecar_url, json={"data": input_data})
    return response.json()

# アプリケーション内で推論結果を取得する例
result = get_inference({"text": "生成 AI による自然言語処理の例"})
print("Inference Result:", result)
```

# サンプルアプリケーション

今回は、サイドカーコンテナに SLM (phi-3) 、OpenTelemetry Collector をそれぞれ配置し、メインのアプリケーションからリクエストを投げる構成としました。アプリケーションは、Web App for Containers、Container Apps それぞれで稼働します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/a5d52951-44b0-4bee-8b58-1c3485dd0ec0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/34e98927-fd8c-4405-9e94-f85c4d322a35.png)

## コード例

具体的なコードは以下の通りです。

```python:sidecar_service.py
class SidecarService:
    def __init__(self):
        self.url = os.getenv("SIDECAR_SLM_URL", "http://localhost:11434/api/generate")

    def post_slm(self, prompt: str) -> dict:
        response = requests.post(
            self.url,
            json={
                "model": "phi3",
                "prompt": prompt,
                "stream": False
            },
            headers={"Content-Type": "application/json"}
        )
        return response.json()
```

```python:controller.py
@router.post("/slm")
@inject
def post_slm(
    request_data: PromptRequest,
    sidecar_service: SidecarService = Depends(
        Provide[Container.sidecar_service]
    )
):
    try:
        with tracer.start_as_current_span("post_slm") as parent:
            parent.set_attributes(
                {
                    "span_type": "GenAI",
                    "gen_ai.operation.name": "chat",
                    "gen_ai.system": "_OTHER",
                    "gen_ai.request.model": "phi3",
                }
            )
            result = sidecar_service.post_slm(request_data.prompt)
            return result
    except Exception as e:
        logging.error(e)
        raise HTTPException(status_code=500, detail="Failed to generate text")
```

なお、OpenTelemetry Collector は、サイドカーコンテナ経由で Application Insights にログ・メトリック・トレースを送信する構成としています。

OpenTelemetry Collector サイドカーコンテナのコミット履歴を git で管理したいと考え、commit 値から定義ファイルを参照する構成としたかったため、定義ファイルは GitHub Actions で動的に生成する形としています。

```yaml:common_container_image_build_push.yml
      - id: generate-otelcollector-config
        name: Generate otel-collector-config.yaml with Application Insights Connection String
        run: |
          cat <<EOF > app/backend/opentelemetry/otel-collector-config.yaml
          receivers:
            otlp:
              protocols:
                grpc:
                http:
          
          processors:
            batch:
              send_batch_max_size: 100
              send_batch_size: 10
              timeout: 10s            
          
          exporters:
            debug:
              verbosity: detailed
            azuremonitor:
              connection_string: ${{secrets.APPLICATIONINSIGHTS_CONNECTION_STRING}}
              spaneventsenabled: true
          
          service:
            pipelines:
              traces:
                receivers: [otlp]
                exporters: [azuremonitor, debug]
                processors: [batch]
              metrics:
                receivers: [otlp]
                exporters: [azuremonitor, debug]
                processors: [batch]
              logs:
                receivers: [otlp]
                exporters: [azuremonitor, debug]
                processors: [batch]
          EOF
```


https://github.com/kohei3110/azure-101-day/blob/main/.github/workflows/common_container_image_build_push.yml#L73

本サンプルアプリケーションでは、GitHub Actions 内で動的に生成された上記の定義ファイルをもとに、OpenTelemetry Collector イメージをビルドする形としています。

```Dockerfile:Dockerfile
FROM otel/opentelemetry-collector-contrib:0.117.0

COPY otel-collector-config.yaml /etc/otelcol-contrib/config.yaml

ENTRYPOINT ["/otelcol-contrib"]

CMD ["--config", "/etc/otelcol-contrib/config.yaml"]
EXPOSE 4317 55679 4318
```
https://github.com/kohei3110/azure-101-day/blob/main/app/backend/opentelemetry/Dockerfile

実際にサイドカーで稼働する phi-3 に「こんにちは」と話しかけた際に仕様した REST API、クライアントからのリクエスト・サーバーからのレスポンスをそのまま添付します。

```python:controller.py
@router.post("/slm")
@inject
def post_slm(
    request_data: PromptRequest,
    sidecar_service: SidecarService = Depends(
        Provide[Container.sidecar_service]
    )
):
    try:
        with tracer.start_as_current_span("post_slm") as parent:
            parent.set_attributes(
                {
                    "span_type": "GenAI",
                    "gen_ai.operation.name": "chat",
                    "gen_ai.system": "_OTHER",
                    "gen_ai.request.model": "phi4",
                }
            )
            result = sidecar_service.post_slm(request_data.prompt)
            return result
    except Exception as e:
        logging.error(e)
        raise HTTPException(status_code=500, detail="Failed to generate text")
```

https://github.com/kohei3110/azure-101-day/blob/main/app/backend/api/src/controller.py

```http:client
POST https://xxxxxxxxxx.xxxxxxxxxx.canadaeast.azurecontainerapps.io/slm
Content-Type: application/json

{
  "prompt": "こんにちは。"
}
```

```json:phi-3からのレスポンス
{
  "model": "phi3",
  "created_at": "2025-02-25T16:06:21.191642547Z",
  "response": "この指示は日本語で、非常に基本的な挨拶「こんにちは」という文を用いています。これは日常的なコミュニケーションにおける最初のステップであり、指示に直接対して答える必要があります。\n\n\nこんにちは。",
  "done": true,
  "done_reason": "stop",
  "context": [
    32010,
    29871,
    13,
    30589,
    30389,
    30353,
    30644,
    30449,
    30267,
    32007,
    29871,
    13,
    32001,
    29871,
    13,
    30589,
    30199,
    31084,
    30858,
    30449,
    30325,
    30346,
    30968,
    30499,
    30330,
    31838,
    31190,
    30353,
    31359,
    30346,
    30210,
    30371,
    233,
    143,
    171,
    233,
    142,
    185,
    30481,
    30589,
    30389,
    30353,
    30644,
    30449,
    30482,
    30364,
    30298,
    30465,
    30333,
    30396,
    30406,
    30298,
    30466,
    30298,
    30441,
    30427,
    30267,
    30589,
    30553,
    30449,
    30325,
    31190,
    30210,
    30371,
    30459,
    30627,
    30645,
    30635,
    30978,
    30185,
    30373,
    30907,
    30203,
    30353,
    30697,
    30807,
    30332,
    30878,
    31120,
    30199,
    30255,
    30572,
    30317,
    30605,
    30499,
    30641,
    30453,
    30330,
    31084,
    30858,
    30353,
    31157,
    31092,
    232,
    178,
    193,
    30326,
    30466,
    234,
    176,
    151,
    30914,
    30332,
    31641,
    30698,
    30458,
    30641,
    30453,
    30441,
    30427,
    30267,
    13,
    13,
    13,
    30589,
    30389,
    30353,
    30644,
    30449,
    30267
  ],
  "total_duration": 20959054035,
  "load_duration": 6043453243,
  "prompt_eval_count": 15,
  "prompt_eval_duration": 1133000000,
  "eval_count": 106,
  "eval_duration": 13781000000
}
```

精度は置いておいて、サイドカーに配置した phi-3 と会話できていることがわかります。

# おわりに

本記事では、Azure App Service や Container Apps 上で SLM（Specialized Language Model）や OpenTelemetry Collector をサイドカーコンテナとして稼働させ、メインアプリケーションと連携する手法について解説しました。  

このアプローチにより、以下のメリットが得られます。

- **コスト削減:**  
  サイドカーコンテナとして AI モデルや監視ツールを独立して実行することで、必要なリソースを効率的に利用でき、メインアプリケーションへの負荷を分散できます。

- **柔軟な運用:**  
  メインアプリケーションのコードを変更せずに、新たな機能（推論、ログ収集、監視など）を追加できるため、システム全体のメンテナンス性と拡張性が向上します。

- **高い拡張性と信頼性:**  
  同一ホスト内の localhost 通信により、コンポーネント間の疎結合な連携が実現され、サービス間の通信が堅牢になります。

また、本サンプルでは、GitHub Actions による動的な OpenTelemetry Collector の定義ファイル生成と、Dockerfile を用いたイメージビルドの自動化など、最新の DevOps 手法を取り入れることで、継続的な運用管理が可能なシステム設計を実現しています。

今回ご紹介した実装例は、生成 AI アプリケーションの一例に過ぎませんが、Azure 環境での柔軟なシステム構築と運用を目指す上での一手法として、皆様のプロジェクトにお役立ていただけると幸いです。  
今後も、新たな技術や運用手法を取り入れながら、生成 AI の可能性を広げるための情報を発信していきます。  

ぜひ、皆様も試行錯誤の中で、最適なシステム運用方法を見つけていただければと思います。