[Azure] Azure FunctionsからVnet内のRDBを操作する

# 本記事のゴール

Azure FunctionsとAzure Database For MySQLをVnet統合を使って連携してデータの読み書きを行える状態を目指します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/3bb16409-38fa-7e57-8b19-944b1f1a74fd.png)



# キーワード

- Vnet統合

# 準備するもの

- Azure Functions(Function PremiumかApp Service のStandardプラン以上)
- Azure Database For MySQL
- 踏み台仮想マシン
- vnet
- Vnet統合に使うFunction用のsubnet

# Vnet統合とは

仮想ネットワーク内のリソースにWebAppやFunctionAppからアクセスするための機能です

[アプリを Azure 仮想ネットワークと統合する - Azure App Service](https://docs.microsoft.com/ja-jp/azure/app-service/overview-vnet-integration)

Q. なぜDBとの連携でVnet統合が必要なのか

Azure上でDatabaseを作成する際、DBインターネットに公開したくないのでプライベートアクセスに制限することがほとんどかと思います。プライベートアクセスに絞ってDBを作成した場合、DBは指定したVnetの中のSubnetに配置されます。その場合、通常はVnet外にいるWebAppやFunctionからDBにアクセスできなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/061ecc03-949d-92d4-1ff0-b2439f50590e.png)


このようにVnetに隔離されたDBにVnet外のリソースからアクセスするための機能がVnet統合です。Vnet統合では中継用のSubnetを用意してそれをWebAppやFunctionと連携させることでVnet内のリソースにアクセスできるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/5e686875-2ac9-b406-1e98-e1124ea5f03b.png)


# DBを作成する

1. Portal上でAzure Database For MySQLのリソースを作成します
以下のドキュメントに従ってDBをデプロイします。かなり丁寧に書かれているのでドキュメントに従えば、DBだけでなくVMの作成とVMからのSSH接続までできるようになります。
    
    [Azure portal でプライベート アクセスを使用して Azure Database for MySQL フレキシブル サーバーに接続する](https://docs.microsoft.com/ja-jp/azure/mysql/flexible-server/quickstart-create-connect-server-vnet)
    
    参考までに自分の設定も載せておきます。
    
   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/0f66f3c4-7ff8-679b-4db1-a73f747165e3.png)

    

迷いポイント解説

- dbインスタンスの種類(シングルサーバーor フレキシブルサーバー)
フレキシブルサーバーとシングルサーバーを選ぶように言われますが、ドキュメントでは新規でアプリケーションを作る場合はフレキシブルが推奨とのこと。
またシングルサーバーだとMySQLの8系を選択できなかったりします。
- ネットワークの接続方法
これはプライベートアクセスを選択してください
- 仮想ネットワークとSubnet
仮想ネットワークは任意のもの、subnetはdefaultで大丈夫です
- コンピューティングとストレージ
試験目的なので一番安いもので大丈夫です。

 

1. DBインスタンスにアクセスするために、仮想マシンをデプロイする
    
    [DB作成時に参照したドキュメント](https://docs.microsoft.com/ja-jp/azure/mysql/flexible-server/quickstart-create-connect-server-vnet)に従ってデプロイします。
    実は仮想マシンをデプロイする以外にAzureBastionという機能を使う方法もあります。こちらは楽ではありますがかなり高額なので仮想マシンをお勧めします。
    
    ドキュメントにも書いてありますが、仮想マシンは必ず先程作成したDBと同じVnetに所属させてください！DBがプライベートアクセス(同じVnet内からのアクセス)しか受け付けないためです。
    
2. テスト用のデータベース、テーブルを作成
DBにアクセスしてデータベースとテーブルを作成します
    
    ```sql
    CREATE DATABASE test_database;
    
    CREATE TABLE
        test_table (
            id INTEGER AUTO_INCREMENT PRIMARY KEY,
            message VARCHAR(255),
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        );
    ```
    

これでDB側の準備は終わりです！

# Function用のSubnetを作成する

Functionと連携する中継用のsubnetをあらかじめ作っておきます。
先程DBに紐付けたvnetのページにいき、サイドメニューの”サブネット”を選択し、サブネットの追加を行います。名前は適当なもので構いません。サブネットアドレス範囲は自動で入力されるので名前さえ入力できればあとは保存して終了です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/fd95e518-e28d-ea4b-a5de-75ba9ff8741a.png)


# Azure Functionsを作成する

1. AzurePortal上でFunctionAppを作成する
Vnet統合が使えるのは`Functions Premium`か`AppServiceプランのPremium`以上だけなのでそこだけ注意してください。
2. `Azure Functions Core Tools`のfuncコマンドで関数を作成.
    
    コマンド実行後、runtimeと言語を聞かれるので今回はnode,typescriptを選択します
    
    ```bash
    func new --name HttpTrigger --template "HTTP trigger" --authlevel "anonymous"
    ```
    
    ※`Azure Functions Core Tools`のインストール方法は[こちら](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cmacos%2Ccsharp%2Cportal%2Cbash)を参照
    
    以下のようなディテクトリ構造の雛形が作成されるはずです
    
    ```bash
    .
    ├── HttpTrigger
    │   ├── function.json
    │   └── index.ts
    ├── dist
    │   └── HttpTrigger
    │       ├── index.js
    │       └── index.js.map
    ├── host.json
    ├── local.settings.json
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    ```
    
    後々必要になるので一旦最低限必要なパッケージをインストールしておきます。
    
    ```bash
    npm i
    ```
    
3. DBに接続するためにmysqlクライアントパッケージを追加
今回は、型定義もあり、async/awaitも使えるのでmysql2をインストールします。
    
    ```bash
    npm i mysql2
    ```
    
4. 雛形を修正
まず、変更後にprocess.envを利用するので警告が出ないように以下をインストールしておきます
    
    ```bash
    npm i --save-dev @types/node
    ```
    
    次に既存の雛形を以下のように修正します。
    
    ```tsx
    import { AzureFunction, Context, HttpRequest } from "@azure/functions"
    import {createConnection, QueryError, RowDataPacket} from 'mysql2/promise';
    
    const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // ①接続情報オブジェクトを作成し、コネクションを確立する
        const config =
            {
                host: process.env['DB_HOST'],
                user: process.env['DB_USER_NAME'],
                password: process.env['DB_PASSWORD'],
                database: 'test_database',
                port: 3306,
                ssl : {
                    rejectUnauthorized: false
                 }
            };
    
        const conn = await createConnection(config);
    
    //②DBへinsertとselectを行い、selectの結果をresponseとして返す
        try {
            const [rows,fields] = 
              await conn.query("insert into test_table (message) values ('Hello,world!')")
                .catch(e=>{throw Error(e)});
            context.log(fields)
            const [results,] = 
              await conn.query("select * from test_table")
                .catch(e=>{throw Error(e)});
                
            context.res = {
                body: {
                  ok:true,
                  data:results
                }
            };
          } catch(e) {
            context.res = {
              body: {
                ok:false,
                data:'failed to insert'
              }
            };
          } finally {
            conn.end();
          }
    
    };
    
    export default httpTrigger;
    ```
    
    順番に説明していきます。
    
    ①接続情報オブジェクトを作成し、コネクションを確立する
    ここでは環境変数を参照しつつDBへの接続情報をオブジェクトとしてconfigに格納し、mysql2のcreateConnection関数に渡しDBと接続します。
    各種環境変数はAzurePortal上で後ほど設定します。AzureKeyVaultに入れたDBパスワードを参照したい場合は[こちらの記事](https://qiita.com/iron-samurai/items/0ca65cf7262111bc62a0)を参考にしてください

    
    ```tsx
    
    const config =
            {
                host: process.env['DB_HOST'],
                user: process.env['DB_USER_NAME'],
                password: process.env['DB_PASSWORD'],
                database: 'test_database',
                port: 3306,
                ssl : {
                    rejectUnauthorized: false
                 }
            };
    
    const conn = await createConnection(config);
    ```
    
    ②DBへinsertとselectを行い、selectの結果をresponseとして返す
    
    mysql2ではquery関数に生のSQLを渡すことができるので以下それぞれをquery関数に渡して実行します(代入時はセミコロンは不要です)
    
    - insert
        
        ```sql
        insert into test_table (message) values ('Hello,world!');
        ```
        
    - select
        
        ```sql
        select * from test_table;
        ```
        
    
    そして、context.resで成功すればselectの結果を,どこかでエラーが出ればエラーメッセージをレスポンスとして返します。
    context.resに渡すオブジェクトのbody部分がhttpレスポンスのbodyに相当します。
    
    ```tsx
    context.res = {
                body: {
                  ok:true,
                  data:results
                }
            };
    ```
    
5. デプロイ
ここまででコードは完成したのでデプロイします。
デプロイ前にビルドするのを忘れずに
    
    ```bash
    npm run build
    
    func azure functionapp publish ${FunctionsAppの名前}
    ```
    
    以下のような表示が出ればデプロイ完了です！
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/3e49f05e-8c1c-c1b0-726e-5c4d8528eae5.png)

    
6. AzurePortalで環境変数を設定する
作成した関数アプリのサイドメニューから「構成」を選びます。
そこで”新しいアプリケーション設定”をクリックして,DB_HOST,DB_USER_NAME,DB_PASSWORDを作成します。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/d0f65062-8375-ae34-6537-ca30d5e8337c.png)

    

# 実行してみる

デプロイ成功時に表示されたエンドポイントに対してcurlを投げてみます

```bash
curl https://${FunctionAppの名前}.azurewebsites.net/api/${関数名}
```

下記のようにレスポンスのdataにデータの配列が入っていれば成功です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/1eea7665-11da-010b-1487-46ca855cc010.png)


# おわりに

これでAzure FunctionsからDBに対して操作を行えるようになりました！

今回紹介したVnet統合はFunctionだけでなくWebAppでも使えるので同様のやり方でWebAppにデプロイしたAPIサーバーからDBにアクセスするといったこともできます。こちらも後日記事にできればと考えています。お楽しみに！