情シス仕事の備忘録

自身の備忘録を兼ねて、情シス仕事で役に立ちそうな情報を掲載しています

Azure App Service Webアプリ デプロイ(VSCode & GitHub & Node.js+Express使用)

Microsoft社のAzure App Service(Webアプリ)を使い、GitHubに登録したWebアプリを同社のクラウド環境上にデプロイ(ユーザーが使用できるよう運用環境に導入)する方法を紹介します。

・開発環境:
 ・OS:Windows 11
 ・Webブラウザー:Edge
 ・ツール:VSCode(Windows版), Git for windows,Node.js v22.12.0(Windows)
・Azure App Service:Freeプラン(Webアプリ)

 

この記事で使用するWebアプリについて

今回はMicrosoft社のクラウド環境上へのWebアプリのデプロイ方法の紹介ということで、Node.jsとExpressを使用したサーバー側の処理を含む簡単なWebアプリを用意します。
※Node.jsはJavaScriptの実行環境、ExpressはNode.js用のWebアプリのフレームワークです

サーバー側でe-Stat APIを使用し(クライアント側で実行しても動作しません)、取得した統計データを元に、時系列グラフを画面表示します。データベースは使用せず、APIから取得したデータはグローバル変数に格納して保持します。また、WebデザインはCDNのサイトにあるBootstrapのテンプレートを、グラフ描画は同じくCDNのサイトにあるChart.jsのライブラリを参照する形とします。
・Bootstrap:Bootstrap · The most popular HTML, CSS, and JS library in the world.
・Chart.js:Chart.js | Open source HTML5 Charts for your website

 

大まかな処理の流れは以下の通りです。

 

e-Statで使用する統計データは、「労働力調査>全国>年次」の「労働力比率,就業率及び完全失業率(2000年~)」のCSV形式データです。
e-Stat APIの使用方法や取得データの加工については、以前の記事でExcel VBAによる実装例を紹介しています。ご興味あればご覧ください。
※e-Statホーム画面:政府統計の総合窓口

 

Webアプリの開発環境を整備する

Webアプリの開発環境としてVSCodeとGitを使用します。大まかな手順は以下の通りとなります(Node.jsとExpress以外は前の記事と同じです)。
・VSCodeインストール
・VSCode日本語化
・Git for Windowsインストール
・GitHubアカウント作成
・Node.jsインストールおよび設定(この工程で後述)
・アプリ作成(工程3)
・ローカルリポジトリ作成(工程4)
・リモートリポジトリ作成(GitHubと連携)(工程4)

上記手順については紹介記事が多数あり、Node.jsとExpress以外の手順について、よく纏まっていて分かりやすいサイト様のリンクを掲載します。
※紹介記事「GitHubアカウントを作成してVSCodeと連携できるようにする」:【環境構築】GitHubアカウントを作成してVSCodeと連携できるようにする #GitHub - Qiita

また、開発ツールのダウンロード先のリンクも掲載します。
※VSCodeダウンロード:Visual Studio Code - Code Editing. Redefined
※Git for Windowsダウンロード:Git for Windows
※Node.jsダウンロード:Node.js — Download Node.js®

 

Node.jsのインストールおよびVSCode上の設定について紹介します。

Node.jsの公式サイトからモジュールをダウンロードし、これをダブルクリックしてインストールを開始します。

 

画面の指示に従い、インストールを進めます。
※右側の画面の下が見切れていますが、[Next]を選択して先に進みます

 

Node.jsのインストールが完了したら、VSCodeの左メニューの[拡張機能]アイコンを選択し、[node.js Modules]で検索し、[node.js Modules Intellisense]が見つかったら[インストール]を選択します。

 

画面の指示に従い、何かキーを押してインストールを進めます。

 

Expressを設定し、Webアプリを作成する

VSCode上で[estat_api_test]フォルダーを作成し、[ターミナル]でそのフォルダーに移動し(この例ではC:\temp\estat_api_test) 、 以下コマンドを実行します。

npm install express-generator –g
express --view=ejs estat_api_test

[estat_api_test]フォルダーの下に[estat_api_test]フォルダーが生成され、その中にWebアプリの雛形リソースが生成されました。

 

[ターミナル]で先ほど生成されたサブフォルダーに移動し(この例ではC:\temp\estat_api_test\estat_api_test)、以下コマンドを実行します。

npm install

サブフォルダーの中に[node_modules]フォルダーが追加されました。

 

この例では生成されたリソースのうち、以下3ファイルを編集します。
・app.js
・routers\index.js
・views\index.ejs
また、index.jsから呼び出すサブ関数のコードがやや長くなるため、サブフォルダー直下に[utils.js]ファイルを用意し、ここに実装する形とします(サブフォルダーで右クリックして[新しいファイル]を選択し、ファイル名を[utils.js]とします)。


以下は今回編集したファイルの内容です。

index.ejsの内容
  • 画面初期表示時は、サーバー側のe-Stat APIで取得したデータを元に、就業状態の選択肢を表示している
  • [グラフを表示]ボタン押下時は、選択した就業状態に関するグラフ描画データをサーバー側から取得し、グラフを表示している
  • Webデザイン用のCSSとJavaScriptはCDNのサイトにあるBootstrapのファイルを参照して使用している
  • グラフ描画用のJavaScriptはCDNのサイトにあるChart.jsのファイルを参照して使用している
  • index.ejsのScriptタグ内にコーディングしているJavaScriptにおいて、グラフ描画データをサーバー側から受け取り、クライアント側のグラフ描画領域(id=“line_chart”)にこれを設定している
    • const res_chart = <%- res_chart %>; により、サーバー側から受け取ったグラフ描画データをJavaScriptオブジェクトとして使用している
    • document.addEventListener(‘DOMContentLoaded’, function() { ・・・});により、画面が読み込まれてからグラフを描画するようにしている
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script>
      /*
      サーバー側からグラフ描画データを受け取り、グラフを生成する
      ※<%- res_chart %>はJSON文字列を直接埋め込み、
       クライアント側でJavaScriptオブジェクトとして扱う
      */
      document.addEventListener('DOMContentLoaded', function() {
        const res_chart = <%- res_chart %>;
        let line_chart = new Chart(document.getElementById("line_chart"), res_chart);
      });
    </script>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
  </head>
  <body>
    <h1 class="p-3 mb-2 bg-success text-white"><%= title %></h1>
    <BR>
    <h6>リストを選択してボタンを押すと、e-Stat APIのグラフを表示します。</h6>
    <form action="/" method="post">
      <label>就業状態(割合) </label>
      <select name="list">
        <% for (let cnt_row = 0; cnt_row < options.length; cnt_row++) { %>
          <option value="<%= options[cnt_row] %>" <%= option_selected[cnt_row] %>><%= options[cnt_row] %></option>
        <% } %>
      </select> 
      <input type="submit" value="グラフを表示" class="btn btn-success" data-bs-toggle="collapse">
    </form>
    <BR>
    <canvas id="line_chart" width="400" height="200"></canvas>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
  </body>
</html>

 

index.jsの内容
  • 就業状態リストの選択肢(options)、選択肢の選択状態(option_selected)、統計データの二次元配列(res_data)、グラフ描画データ(res_chart)をグローバル変数として扱い、画面が遷移しても値を保持するようにしている
  • 画面初期表示はGET処理として、APIから統計データを取得してタイトル行とデータ行のみ二次元配列に格納するとともに、就業状態の選択肢も格納している(utils.jsのsetStatData()で処理)
  • [グラフを表示]ボタン押下時はPOST処理として、統計データから選択した就業状態のデータに絞り込み、グラフ描画データを格納している(utils.jsのgetChartConfig()で処理)
  • [グラフを表示]ボタン押下後の画面更新時に就業状態の選択状態が維持されるよう、画面表示用のSELECTタグのSELECTEDオプションの値を保持している(utils.jsのgetOptionSelected()で処理)
const express = require('express');
const router = express.Router();

//サブ関数のインポート
const { 
  setStatData, 
  getChartConfig, 
  getOptionSelected
} = require('../utils');

let options = [];//選択肢データの配列
let option_selected = [];//選択肢が選択状態かどうかの配列
let res_data = [];//統計データ(タイトル行・データ行)の二次元配列
let res_chart = new Object();//グラフ描画データのJSONオブジェクト

/* 画面の初期表示 */
router.get('/', async function(req, res, next) {

  //e-Statの統計データを取得・加工し、引数に値をセットする
  await setStatData(res_data, options);

  //option_selectedを取得する
  option_selected = getOptionSelected("", options);

  //index.ejs画面に値を渡して遷移する(res_chartは文字列として渡す)
  res.render('index', { 
    title: 'e-Stat API Chart',
    res_data: res_data,
    res_chart: JSON.stringify(res_chart),
    options: options,
    option_selected: option_selected,
   });
});

/* ボタン押下時の処理 */
router.post('/', function(req, res, next) {

  //選択されている就業状態の値を格納
  const option = req.body.list;

  //グラフ描画用データを引数res_chartにセットする
  res_chart = getChartConfig(option, res_data);

  //option_selectedを取得する
  option_selected = getOptionSelected(option, options);

  //index.ejs画面に値を渡して遷移する(res_chartは文字列として渡す)
  res.render('index', { 
    title: 'e-Stat API Chart',
    res_data: res_data,
    res_chart: JSON.stringify(res_chart),
    options: options,
    option_selected: option_selected,
   });
});

module.exports = router;

 

app.jsの内容
  • varは非推奨のためconstに変更した
  • router関連で使用しないコードはコメントアウトし(該当行に//削除を記載)、今回のアプリ用に一行追加した(該当行に//追加を記載)
const createError = require('http-errors');   //varをconstに変更
const express = require('express');//varをconstに変更
const path = require('path');//varをconstに変更
const cookieParser = require('cookie-parser');//varをconstに変更
const logger = require('morgan');//varをconstに変更

//var indexRouter = require('./routes/index');//削除
//var usersRouter = require('./routes/users');//削除

const app = express();//varをconstに変更

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', require('./routes'));//追記
//app.use('/', indexRouter);//削除
//app.use('/users', usersRouter);//削除

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

utils.jsの内容

概要説明は割愛します。ソース内のコメントをご覧ください。
なお、e-Stat APIのappidの取得方法は以前の記事で紹介しています。

/* 
  e-Statの統計データを取得・加工し、引数に値をセットする
  res_data(二次元配列):統計データのタイトル行・データ行を格納(グラフ元データになる)
  options(配列):統計データの就業状態を格納(リスト選択肢になる)
*/
async function setStatData(res_data, options) {

    const appId = "xxxxxx";//e-Stat APIのappId
    let apiMethod = "GET";
    let apiUrl = `http://api.e-stat.go.jp/rest/3.0/app/getSimpleStatsData?appId=${appId}&lang=J&statsDataId=0003008332&metaGetFlg=Y&cntGetFlg=N&explanationGetFlg=Y&annotationGetFlg=Y&sectionHeaderFlg=1&replaceSpChars=0`;
    let apiHeaders = {
        "Content-Type": "application/json",
    };
  
    try {
  
        //e-Stat APIを使って統計データを取得する
        let response = await fetch(apiUrl, {
            method: apiMethod,
            headers: apiHeaders,
        });
  
        //取得した統計データをテキスト型変数に格納する
        let res_str = await response.text();
  
        //データ取得でエラーが発生した場合、コンソールにエラー番号と内容を出力して終了する
        if (!response.ok) { 
          console.log("データ取得でエラーが発生しました。:" + response.status + ":" + response.statusText);
          return;
        }
    
        //テキスト型変数を改行コード区切り、配列res_arrayに格納する
        let res_array = res_str.split(/\n/);
    
        //配列の要素数が4より小さい場合はエラーと判断し、コンソールにエラー番号と内容を出力して終了する
        if (res_array.length < 4) {
            console.log("データ取得でエラーが発生しました。:" + res_array[1] + ":" + res_array[2]);
            return;
        }
  
        let is_exist_label = 0;//タイトル行の判定フラグ(0=タイトル行以前、1=タイトル行以降)
        let cnt_data = 0;//引数res_dataにデータを格納する際の行数カウンタ
        let tmp_options = [];//引数optionsの一時配列(重複除去前の配列)
        for (let cnt_row = 0; cnt_row < res_array.length; cnt_row++) {//配列res_arrayの各要素の処理
  
            //タイトル行("tab_code"で始まる行)を特定し、判定フラグを立てる
            if (res_array[cnt_row] && res_array[cnt_row].startsWith('"' + 'tab_code' + '"')) {
                is_exist_label = 1;
            }
  
            //タイトル行以降について、引数に必要なデータを加工・格納する
            if (is_exist_label === 1) {
                if (res_array[cnt_row]) {
                    //"を除去してカンマ区切りにした値を、一時配列tmp_arrayに格納する
                    let tmp_array = (res_array[cnt_row].replace(/"/g,'')).split(',');
                    //二次元配列res_arrayに対象行がない場合、行を追加する
                    if (!res_data[cnt_data]) {
                      res_data[cnt_data] = [];
                    }
                    //一時配列tmp_arrayの各値を、二次元配列res_arrayの対象行の各列に格納する
                    for (let cnt_col = 0; cnt_col < tmp_array.length; cnt_col++) {
                      res_data[cnt_data][cnt_col] = tmp_array[cnt_col];
                      //一時配列tmp_arrayの就業状態の値を(タイトル行を除く)、一時配列tmp_optionsに格納する
                      if (cnt_data > 0 && cnt_col === 5) {
                        tmp_options[cnt_data] = tmp_array[cnt_col];
                      }
                    }
                    cnt_data++;
                }
            }
        }
  
        //一時配列tmp_optionsの重複値を除去し、引数optionsに格納する
        options.length = 0;
        tmp_options.forEach((element, index) => {
          if (tmp_options.indexOf(element) === index) {
             options.push(element); // options 配列に値を追加
          }
        });
    
    } catch (error) {
        //例外発生時はコンソールにエラー出力して終了する
        console.error('Error:', error);
    }
  }
  
  /* 
    選択した就業状態のグラフ描画データを生成して返す
    option:画面で選択した就業状態
    res_data(二次元配列):グラフ元データとなる統計データ
    chart_config(Json):グラフ描画用のデータ(戻り値)
  */
  function getChartConfig(option, res_data) {
  
    let data_all = [];//総数データ
    let data_male = [];//男性データ
    let data_female = [];//女性データ
    let labels = [];//時系列ラベル
  
    //datasets用変数に必要なデータ(2011年以前は除外)を格納する
    data_all = res_data.filter(
      tmp_all => (tmp_all[5] === option && tmp_all[7] === "総数" && tmp_all[10] > 2011000000)
    ).map(tmp_all_value => tmp_all_value[13]);
    data_male = res_data.filter(
      tmp_male => (tmp_male[5] === option && tmp_male[7] === "男" && tmp_male[10] > 2011000000)
    ).map(tmp_male_value => tmp_male_value[13]);
    data_female = res_data.filter(
      tmp_female => (tmp_female[5] === option && tmp_female[7] === "女" && tmp_female[10] > 2011000000)
    ).map(tmp_female_value => tmp_female_value[13]);
     
    //label用変数に必要なデータ(2011年以前は除外)を格納する
    labels = res_data.filter(
      tmp_labels => (tmp_labels[5] === option && tmp_labels[7] === "総数" && tmp_labels[10] > 2011000000)
    ).map(tmp_labels_value => tmp_labels_value[11]);
  
    //グラフデータの設定
    let chart_config  = {
      type: 'line',
      data: {
        labels: labels,
        datasets: [{
          label: '総数',
          data: data_all,
          borderColor: '#f88'
        }, {
          label: '男性',
          data: data_male,
          borderColor: '#484'
        }, {
          label: '女性',
          data: data_female,
          borderColor: '#48f'
        }],
      },
      options: {
        scales: {
            y: {
              beginAtZero: true
            }
        }
      }
    };
  
    return chart_config;
  }
  
  /* 
    選択肢の選択状態を返す(・・のselectedの有無)
    option:画面で選択した就業状態
    options(配列):統計データの就業状態を格納(リスト選択肢)
    chart_config(Json):グラフ描画用のデータ(戻り値)
  */
  function getOptionSelected(option, options) {
  
    let tmp_selected = [];
  
    for (let cnt_row = 0; cnt_row < options.length; cnt_row++){
      if (options[cnt_row] === option) {
        tmp_selected[cnt_row] = "selected"
      } else {
        tmp_selected[cnt_row] = ""
      }
    }
  
    return tmp_selected;
  };

  //上記サブ関数をエクスポート
  module.exports = {
    setStatData,
    getChartConfig,
    getOptionSelected,
  }

 

Webアプリのリポジトリを用意する

VSCodeの[ターミナル]で以下コマンドを実行します。

git config –-global user.name {GitHubのユーザー名}
git config -–global user.email {GitHubのEmailアドレス}
git init

※コマンドは作成したWebアプリのpackage.jsonのあるフォルダーで実行します(この例ではC:\temp\estat_api_test\estat_api_test)
※git initコマンドにより、ローカルリポジトリが作成されます

左メニューの[ソース管理]アイコンを選択し、[node_modules]フォルダーを選択して右クリックし、[.gitignoreに追加]を選択し、[コミット]を選択します
※ [node_modules]フォルダー以下のような開発環境に依存するリソースは[.gitignoreに追加]を選択することで、ソース管理から除外できます

 

[COMMIT_EDITMSG]ファイルが表示されたら、ソース管理対象のファイルの先頭の#を削除し、[Ctrl]+[s]キーを押して保存し、閉じます。
※この例では、ブランチはmasterという名前で作成されています

 

[Branchの発行]を選択します。
GitHubへのサインインを促されたら、[許可]を選択してサインインします。
リポジトリが正常に発行された旨のメッセージが表示されれば問題ありません。[GitHub上で開く]を選択します。

 

対象リポジトリ(この例では[estat_api_test])内にソース管理対象ファイルが表示されれば問題ありません。
また、先ほどソース管理から除外した[node_modules]フォルダー以下のリソースがないことも確認できます。

 

Azure App ServiceにWebアプリをデプロイする

Microsoft Azureのポータル画面にアクセスし、[その他のサービス]を選択します。
※Microsoft Azureポータル画面:Microsoft Azure

検索欄に[app]と入力し、[App Service]が表示されたらこれを選択します。
[作成>Webアプリ]を選択します。
なお、Azureにまだサインアップしていない場合は、こちらの記事を参考にサインアップしてください。

 

[基本]画面で以下の通り設定し、[次:デプロイ>]を選択します。
・サブスクリプション:(既存のサブスクリプションを選択)
・リソースグループ:(App Service用のリソースグループを選択し、ない場合は新規作成する)
・名前:(アプリ名を適宜入力)
・公開:コード
・ランタイムチェック:Node 20 LTS
・オペレーティングシステム:Linux
・リージョン:Canada Central(Japan Eastを選ぶとエラーが発生し、別のSKUにするよう指示されたため、[Canada Central]にしました)
・Linuxプラン:ASP-grpwebapps-xxxx(F1)
・価格プラン:Free F1(共有インフラストラクチャ)

 

[デプロイ]画面でGitHubの設定ができない場合は、後ほど設定します。
[ネットワーク]画面ではWebアプリを公開するかどうか選択できます。この例では公開する([オン]の状態)ことにし、先に進みます。

 

[監視とセキュリティ保護]画面では、今回はお試しなので各設定は有効化せず、[確認および作成]を選択します。
[確認および作成]画面で設定内容を確認し問題なければ[作成]を選択します。

 

[デプロイが完了しました]と表示されたら、[アプリのデプロイを管理します]を選択します。
なお、先ほどのアプリ作成画面でGitHubの設定を済ませた場合は、[リソースに移動]を選択し工程6の動作確認に進みます。

 

デプロイセンターの画面が表示されたら、[設定]タブを選択して以下の通り設定し、[保存]を選択します。
・ソース:GitHub
・次のユーザーとしてサインイン:(工程4で使用したGitHubアカウントでサインインする)
・組織:(工程4のGitHubユーザーを選択)
・リポジトリ:(工程4のリポジトリ名を選択)
・ブランチ:(工程4のブランチ名を選択)
・認証タイプ:ユーザー割り当てID
・サブスクリプション:(既存のサブスクリプションを選択)
・ID:新規作成(ただし既存のWebアプリをデプロイする場合はそのアプリのIDを選択)

 

[ログ]タブを選択し、デプロイ結果が表示されるのを待ちます(おそらく5分以上かかると思います)。[状態]が[成功]になれば問題ありません。
左メニューの[概要]を選択し、[既存のドメイン]欄のリンクを選択し、工程6の動作確認に進みます。

 

VSCodeの左メニューに[Azure]アイコンが表示されていない場合、拡張機能の[Azure App Service]を導入しておきます。
左メニューの[拡張機能]アイコンを選択し、[azure app]で検索し、[Azure App Service]が表示されたら[インストール]を選択します。
左メニューに[Azure]アイコンが表示されますのでこれを選択し、[Sign in to Azure]を選択します。

 

[許可]を選択します。
先ほどMicrosoft Azureポータル画面でアプリを設定したアカウントでサインインします。
Microsoft AzureのリソースがVSCode上で確認できるようになり、[App Services]の下に先ほどのアプリが表示されます。

 

なお、GitHub上のリポジトリを確認すると、デプロイ時に生成された[.github/workflows]が追加されています。

 

デプロイしたWebアプリの動作確認を行う

Microsoft Azureの画面に戻り、改めてURLのリンクからWebアプリにアクセスしてみます。
※他のユーザーにはこのURLを教えてアクセスしてもらう形になります

 

[グラフを表示]ボタンを選択すると、e-Stat APIから取得したデータのうち選択されている[就業状態]のグラフが表示されます。

 

[就業状態]リストの他の選択肢のグラフも表示してみました。
デプロイしたアプリが正常に動作することを確認できました。

 

おわりに

作成したWebアプリをMicrosoft社のクラウド環境上にデプロイする手順を紹介しました。
前の記事の静的Webアプリのデプロイよりも設定は若干増えますが、Azure App Service上の実行環境としてNode.jsが選択できるようになっており、簡単に実施できるものだなと感じました。
ちなみに、Azure App ServiceのWebアプリの設定では、実行環境(ランタイムスタック)として、Node.jsだけでなく.NET,Java,PHP,Pythonも選択でき、様々なWebアプリで活用できそうです。