Raspberry PiとAzure VMを使ってAPIを外部公開

RaspberryPi 5 上のAPIを、AzureVMを踏み台にして外部公開するまでの内容です。勉強と備忘録も兼ねてまとめてみました。

RaspberryPiOSのインストールやSSH接続といった初期設定は済んでいる前提で話を進めます。初期設定のやり方は他に沢山の記事でわかりやすく説明されているので、そちらを参照ください。

※今回、API常時稼働まではできませんでした。度々Raspberry Pi上でAPIサーバーを起動する必要があります。

## アーキテクチャ図

ざっくりとした全体像です。

![APIサーバーアーキテクチャ図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629116/71d43e4c-4401-4ec5-b058-c9c20583ecba.png)


RaspberryPi 5 上のFastAPIで開発したAPIを、AzureVMのパブリックIPアドレスを使って公開します。インターネット上でAPIが叩かれると、Azure VM上のNginxからRaspberry Piへリクエストを転送するイメージです。

## 1. Azure VMの構築

### Azure VMの設計

- Azure Portal で **「仮想マシンの作成」**
- **Ubuntu 20.04 LTS or 22.04 LTS** を選択
- **パブリックIPを有効化**（固定IP推奨）
- **ポート 80, 443 を開放**
- **SSH を有効化（22番ポート）**
    - RSA SSH 形式を選択し、SSHキーをPCに保存

## 2. Raspberry PiのFastAPIを起動

以下、FastAPIプロジェクトの場合の起動方法

プロジェクトディレクトリに移動

```bash
cd src/hot-bubbles-api/
```

venv仮想環境を起動（未作成であれば作成する）

※下記fishでの実行コマンド

```bash
source venv/bin/activate.fish
```

サーバー起動

```bash
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

## 3. SSH接続：Raspberry Pi　→　Azure VM

参考Doc

[Linux VM に接続する - Azure Virtual Machines](https://learn.microsoft.com/ja-jp/azure/virtual-machines/linux-vm-connect?tabs=Linux)

### sshキーの転送

ここでいうSSHキーとは、VM構築時に払い出された`.pem`ファイルのことです。
私は自分のMacでVMを構築したので、Raspberry Piに転送します。
はじめからRaspberry PiでSSHキーを保存した方はこの手順は不要です。
使うのは、scpコマンドです。ssh接続された端末間でのファイルの送受信が可能です。

PCの`~/.ssh/hotbubbles-api-dev_key.pem`ファイルを`raspberry@pi`というRaspberry Piの`/.ssh`ディレクトリ配下に転送します。

```bash
scp ~/.ssh/hotbubbles-api-dev_key.pem raspberry@pi.local:~/.ssh
```

### Raspberry Pi上での操作

読み取り権限の確認

```bash
chmod 400 ~/.ssh/myKey.pem
```

下記sshコマンドを実行し、Azrue VMに接続確認

```bash
ssh -i ~/.ssh/myKey.pem ${VMのuser名}@${VMのパブリックIP}
```

VMのuser名を忘れてしまった場合、Azure PortalのJSONビューから確認できます。

## 4. Raspberry Pi から Azure VM に SSH リバーストンネルを設定

```mermaid
flowchart LR
    subgraph Raspberry Pi
        A(FastAPI localhost:8000)
        B[SSH Client -R 9000:localhost:8000]
    end

    subgraph Azure VM
        C[SSH Daemon<br>ポート9000で待ち受け]
        D[localhost:9000<br>アクセス]
    end

    B -- リバーストンネルを確立<br>(ポート22) --> C
    D -- リクエスト --> C
    C -- リクエスト転送<br>(リバーストンネル) --> A
```

Raspberry Piで以下コマンドを実行

```bash
ssh -i ~/.ssh/myKey.pem -R 9000:localhost:8000 ${VMのuser名}@${VMのパブリックIP}
```

これで Azure VM の `localhost:9000` にアクセスすると、Raspberry Pi の `localhost:8000` に接続可能となります。

Azure VM環境で下記コマンドを実行してみて、FastAPIで実装したAPI値が返って来れば成功です。

```bash
curl http://localhost:9000
```

### アクセス失敗時に考えられる原因

```bash
ssh: connect to host ${IPアドレス} port 22: No route to host
```

私の場合はRaspberry Piがインターネットにアクセスできていませんでした。

その他にも以下のような原因が考えられます。

- Azure VMの起動
- Azure VMがポート22を開放していない
- VMのSSHサーバーを起動
- VMのファイアウォールを設定

## 5. Azure VM に Nginx をインストールしリバースプロキシを設定

インストール

```bash
sudo apt update && sudo apt install nginx -y
```

設定を変更

```bash
sudo nano /etc/nginx/sites-available/api
```

```bash
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://localhost:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

設定を有効化する。

Nginxは`/etc/nginx/sites-enabled/` にある設定しか読み込まないので、シンボリックリンクを貼る。
※参考→　[Nginxのconf.dとsites-availableとsites-enabledの違い - Qiita](https://qiita.com/tomokon/items/e782636c1e5ec6b5dfdc)

```bash
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
```

Nginxを再起動

```bash
sudo systemctl restart nginx
```

## **6. インターネットから API にアクセス**

### ドメイン名を設定

今の状態でもVMのパブリックIPにアクセスすれば、インターネット上からAPIが叩けるのですが、せっかくならドメイン名で叩きたいですよね。

Azure Portal上で下記操作をして独自のドメイン名を作りましょう。

今回使っているVMの画面のパブリックIPアドレスをクリックすると、構成画面に遷移すると思います。そこの`DNS名ラベル`という設定値（はじめは空欄のはず）に任意のドメイン名を設定しましょう。

`${設定したドメイン名}.japaneast.cloudapp.azure.com`にアクセスすると、インターネット上からAPIにアクセスできるはずです！

### 失敗した場合

パブリックIPアドレスにアクセスしてもNginxのデフォルト画面が表示される。

`/etc/nginx/sites-enabled/default` が残っているのが原因かも。

削除

```bash
sudo rm /etc/nginx/sites-enabled/default
```

nginxを再起動する

### APIの結果の日本語が文字化け

Azure VMのパブリックIPアドレスにアクセスして、jsonファイルが返ってくることを確認したのですが、日本語部分が文字化けした状態で表示されます。

FastAPIの文字コードの設定をいじると治りました。

詳しいことはまだわかりませんが、ensure_ascii=Falseでユニコードエスケープを抑制するようです。