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


本記事のゴール

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

キーワード

  • Vnet統合

準備するもの

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

Vnet統合とは

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

アプリを Azure 仮想ネットワークと統合する - Azure App Service

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

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

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

DBを作成する

  1. Portal上でAzure Database For MySQLのリソースを作成します
    以下のドキュメントに従ってDBをデプロイします。かなり丁寧に書かれているのでドキュメントに従えば、DBだけでなくVMの作成とVMからのSSH接続までできるようになります。

    Azure portal でプライベート アクセスを使用して Azure Database for MySQL フレキシブル サーバーに接続する

    参考までに自分の設定も載せておきます。

迷いポイント解説

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

    DB作成時に参照したドキュメントに従ってデプロイします。
    実は仮想マシンをデプロイする以外にAzureBastionという機能を使う方法もあります。こちらは楽ではありますがかなり高額なので仮想マシンをお勧めします。

    ドキュメントにも書いてありますが、仮想マシンは必ず先程作成したDBと同じVnetに所属させてください!DBがプライベートアクセス(同じVnet内からのアクセス)しか受け付けないためです。

  2. テスト用のデータベース、テーブルを作成
    DBにアクセスしてデータベースとテーブルを作成します

    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のページにいき、サイドメニューの”サブネット”を選択し、サブネットの追加を行います。名前は適当なもので構いません。サブネットアドレス範囲は自動で入力されるので名前さえ入力できればあとは保存して終了です!

Azure Functionsを作成する

  1. AzurePortal上でFunctionAppを作成する
    Vnet統合が使えるのはFunctions PremiumAppServiceプランのPremium以上だけなのでそこだけ注意してください。

  2. Azure Functions Core Toolsのfuncコマンドで関数を作成.

    コマンド実行後、runtimeと言語を聞かれるので今回はnode,typescriptを選択します

    func new --name HttpTrigger --template "HTTP trigger" --authlevel "anonymous"
    

    Azure Functions Core Toolsのインストール方法はこちらを参照

    以下のようなディテクトリ構造の雛形が作成されるはずです

    .
    ├── HttpTrigger
    │   ├── function.json
    │   └── index.ts
    ├── dist
    │   └── HttpTrigger
    │       ├── index.js
    │       └── index.js.map
    ├── host.json
    ├── local.settings.json
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    

    後々必要になるので一旦最低限必要なパッケージをインストールしておきます。

    npm i
    
  3. DBに接続するためにmysqlクライアントパッケージを追加
    今回は、型定義もあり、async/awaitも使えるのでmysql2をインストールします。

    npm i mysql2
    
  4. 雛形を修正
    まず、変更後にprocess.envを利用するので警告が出ないように以下をインストールしておきます

    npm i --save-dev @types/node
    

    次に既存の雛形を以下のように修正します。

    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パスワードを参照したい場合はこちらの記事を参考にしてください

    
    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

      insert into test_table (message) values ('Hello,world!');
      
    • select

      select * from test_table;
      

    そして、context.resで成功すればselectの結果を,どこかでエラーが出ればエラーメッセージをレスポンスとして返します。
    context.resに渡すオブジェクトのbody部分がhttpレスポンスのbodyに相当します。

    context.res = {
                body: {
                  ok:true,
                  data:results
                }
            };
    
  5. デプロイ
    ここまででコードは完成したのでデプロイします。
    デプロイ前にビルドするのを忘れずに

    npm run build
    
    func azure functionapp publish ${FunctionsAppの名前}
    

    以下のような表示が出ればデプロイ完了です!

  6. AzurePortalで環境変数を設定する
    作成した関数アプリのサイドメニューから「構成」を選びます。
    そこで”新しいアプリケーション設定”をクリックして,DB_HOST,DB_USER_NAME,DB_PASSWORDを作成します。

実行してみる

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

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

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

おわりに

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

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