Web Apps (Node.js) と MongoDB を連携させる

# はじめに
Azure 公式ドキュメントのチュートリアル「[チュートリアル:Azure で Node.js と MongoDB のアプリを構築する](https://docs.microsoft.com/ja-jp/azure/app-service/tutorial-nodejs-mongodb-app?pivots=platform-linux)」を試してみたので、注意点とか、詰まった箇所について補足メモしておきます。

<img width="900" alt="image.png (556.6 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/979d0081-7db6-4aab-9819-7fc0593212ab.png">


# 目次
- 開発環境
- 手順
  1. ローカル Node.js アプリ を作成する
  2. Mongo DB 環境を準備する
  3. node アプリを Azure にデプロイする
  4. Azure Web Apps へのデプロイ
  5. デプロイした Azure アプリを確認する
  6. データモデルを更新してみる
  7. リソースのクリーンアップ
- 終わりに

# 開発環境
- 開発PC ( OS : Windows 10 Pro)
  - Windows Terminal ( Azure Cloud Shell, コマンドプロンプト ) 
  - Node.js
    - Sails.js
    - Angular 12
- Azure
  - App Service
    - OS : Linux
    - サービスプラン  : B1   
   ( Core数 1, RAM 1.75GB, ストレージ 10GB )
  - Cosmos DB ( Mongo DB ) 


# 手順
## 1. ローカル Node.js アプリ を作成する
1.Windows Terminal ( コマンドプロンプト ) を立ち上げ、作業リポジトリへ移動後、
対象リポジトリを Clone します。

```
git clone https://github.com/Azure-Samples/mean-todoapp.git
```

3.Clone したディレクトリ (``mean-todoapp`` ) に移動、依存パッケージをインストールした後、
アプリケーションを起動します。

```
cd mean-todoapp
npm install 
node app.js --alter
```

※ --alter は sails.js の マイグレーションオプション

アプリケーションを正常に立ち上げることができたら、以下ログが表示されます。

<img width="600" alt="image.png (245.2 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/6167f9ff-8955-4808-bb11-0295aa891697.png">

4.Webブラウザで ``http://localhost:1337`` へアクセスします。

<img width="900" alt="image.png (46.0 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/01b61e0f-cc0b-4927-9558-7a521c5c1988.png">

TODO アプリをローカル環境で試すことができます。

<img width="900" alt="image.png (77.8 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/ad6276b8-1117-46be-8271-99c00760b202.png">

Node.js を停止する際は、Windows Terminal ( コマンドプロンプト ) で ``Ctrl + C`` を押します。

続いて、Mongo DB on Cosomos DB を作成していきます。

## 2. Mongo DB 環境を準備する

Mongo DB on Cosmos DB ( リソース ) をデプロイするための、リソースグループを作成します。

1.Windows Terminal ( Azure CLI )  で リソースグループ ( Resource Group ) を作成します。

```
 az group create --name MSDocsTutorial --location "Japan West"
```

```
{
  "id": "/subscriptions/xxxxxxxxxxxx/resourceGroups/MSDocsTutorial",
  "location": "japanwest",
  "managedBy": null,
  "name": "MSDocsTutorial",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": "Microsoft.Resources/resourceGroups"
}
```

2.Cosmos DB アカウントを作成します。

```
az cosmosdb create --name ms-docs-tutorials --resource-group MSDocsTutorial --kind MongoDB
```

``--kind MongoDB`` オプション利用により、MongoDB クライアント接続が有効になります。

```
{
  "analyticalStorageConfiguration": {
    "schemaType": "FullFidelity"
  },
  "apiProperties": {
    "serverVersion": "3.6"
  },
  "backupPolicy": {
    "migrationState": null,
    "periodicModeProperties": {
      "backupIntervalInMinutes": 240,
      "backupRetentionIntervalInHours": 8,
      "backupStorageRedundancy": "Geo"
    },
    "type": "Periodic"
  },
  "capabilities": [
    {
      "name": "EnableMongo"
    }
    ...
 }
```

※ Cosmos DB 名は 英小文字、数字、ハイフン文字のみ使用可能。
　 英大文字は使用不可なので注意。 

3.MongoDB データベース 主キーの取得します。

```
az cosmosdb keys list --name ms-docs-tutorials --resource-group MSDocsTutorial
```

次の手順以降、``primaryMasterKey`` の値を使用するのでコピーしておきます。

```
{
  "primaryMasterKey": "XXXXXXXXXXXXXXXXXXX",
  "primaryReadonlyMasterKey": "XXXXXXXXXXXXXXX",
  "secondaryMasterKey": "XXXXXXXXXXXXXXXX",
  "secondaryReadonlyMasterKey": "XXXXXXXXXXXXXXXX"
}
```

4.Windows Terminal ( コマンドプロンプト ) へ戻り、config/datastores.js でDB接続情報の設定を行います。

```
module.exports.datastores = {
  default: {
    adapter: 'sails-mongo',
    url: process.env.MONGODB_URI,
    ssl: true,
  },
};
```

※ 変更後は、git add & commit を行います。

5.環境変数 ( ``MONGODB_URI`` ) をセットします。

```
SET MONGODB_URI=mongodb://<cosmosdb-name>:<cosmosdb-key>@<cosmosdb-name>.documents.azure.com:10250/todoapp
```

6.再度 node アプリケーションを起動します。

```
node app.js --alter
```

``Mongo DB 接続テスト`` を TODO に追加します。

<img width="900" alt="image.png (62.2 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/0263fc53-a018-4630-949f-4179cac4f127.png">

Azure Portal より Cosmos DB の データエクスプローラーを使用して、データが登録されていることが確認できました。

<img width="900" alt="image.png (281.4 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/2a7bb043-8335-4de6-8881-7735cf9e4a32.png">

## 3. node アプリを Azure にデプロイする
続いて、ローカルで実行していた node アプリケーションを Azure Web Apps にデプロイしていきます。

1.Azure web apps をデプロイするユーザーを設定します。

```
az webapp deployment user set --user-name <username> --password <password>
```

```
{
  "id": null,
  "kind": null,
  "name": "web",
  "publishingPassword": null,
  "publishingPasswordHash": null,
  "publishingPasswordHashSalt": null,
  "publishingUserName": "XXXXXXXXXXXX",
  "scmUri": null,
  "systemData": null,
  "type": "Microsoft.Web/publishingUsers/web"
}
```

※ JSON 出力には、パスワードが  ``null`` として出力されますが、正常に登録されています。
※ 上記のユーザー情報は、Azure サブスクリプションの資格情報とは異なります。
※ この手順で設定したユーザー情報は、ローカル Git リポジトリから リモート ( Azure ) へコードをプッシュする際に使用します。

2.App Service プランを作成する。

```
az appservice plan create --name myAppServicePlan --resource-group myResourceGroup --sku B1 --is-linux
```

--sku オプションで OS を設定します。今回は ``Linux`` を選択しました。

3.App service プランで Web アプリを作成します。

```
az webapp create --resource-group myResourceGroup --plan myAppServicePlan --name <app-name> --runtime 'NODE|14-lts' --deployment-local-git
```

``--runtime`` オプションで 実行環境の設定を行います。
``--deployment-local-git`` オプションを付与することによって、Git デプロイが可能な空の Web アプリが生成されます。

<font color="black">出力時に ``deploymentLocalGitUrl`` プロパティに Git リモート URL が出力されるので、メモしておいてください。</font>

Web アプリをデプロイ後、アクセスするとデフォルトで以下内容が表示されます。

<img width="900" alt="image.png (254.0 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/dc271149-54b7-4c5c-ab67-e3ef7bdc742b.png">

4.Azure Web Apps の環境変数の設定を行います。

```
az webapp config appsettings set --name <app-name> --resource-group myResourceGroup --settings MONGODB_URI='mongodb://<cosmosdb-name>:<cosmosdb-key>@<cosmosdb-name>.documents.azure.com:10250/todoapp' DEPLOYMENT_BRANCH='main'
```

↑ 上記コマンドで、Web アプリ上で  MONGODB_URI の環境変数をセットしています。 

## 4. Azure Web Apps へのデプロイ

1.Azure Web Apps の規定ブランチを ``main`` に変更します。

```
az webapp config appsettings set --name <app-name> --resource-group myResourceGroup --settings DEPLOYMENT_BRANCH='main'
```

2.Windows Terminal ( コマンドプロンプト ) を開き、ローカル Git リポジトリに Azure のリモートブランチを追加します。

```
git remote add azure <deploymentLocalGitUrl>
```

3.ローカル git リポジトリを リモート ( Azure ) へ Push します。

```
git push azure main
```

Push ～ デプロイ完了まで、10分程度時間がかかりました。

## 5. デプロイした Azure アプリを確認する
1.Web ブラウザを使用して、デプロイされた Azure アプリを参照します。

<img width="900" alt="image.png (54.8 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/cc5fbdfd-1c07-4fb8-9e80-de569200fa6b.png">

デフォルト画面とは画面表示が変わりましたが、デプロイしたアプリケーション内でエラーが発生しました。

2.エラーログを確認するために、App Service の 診断ログを有効にします。

```
az webapp log config --resource-group <resource-group-name> --name <app-name> --docker-container-logging filesystem --level Verbose
```

3.診断ログがオンになったら、ログストリームを確認します。

```
az webapp log tail --resource-group <resource-group-name> --name <app-name>
```

<img width="900" alt="image.png (90.3 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/789ca9ef-790e-4352-8e63-12a4d1a12cc5.png">

どうやら sals.js の ソケット設定 ( onlyAllowOrigins || beforeConnect ) がないため、
セキュリティ理由のエラーが発生していたようです。

4.``config\env\production.js`` を開き、``onlyAllowOrigins`` の設定を追加します。

```config\env\production.js
sockets: {
  onlyAllowOrigins: [
  '<Azure Web Apps URL>'
  ]
}
```

修正内容を Commit  後、リモートリポジトリ ( Azure ) へ Push します。
再度、Web Apps へアクセスすると、正常に動作することを確認できました。

<img width="900" alt="image.png (54.1 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/086f92ef-55e9-4ca7-a3fd-74a2e76a2c36.png">

## 6. データモデルを更新してみる
ソースコードに修正を加え、アプリケーションに機能を追加します。追加後は、リモートブランチへコードを Push して、Azure Web Apps へデプロイを行います。

### 6-1. サーバー側コードを変更する

api のデータモデルに完了ステータスを追加します。
デフォルト値は、false (未完了) に設定します。

```api/models/Todo.js
module.exports = {

  attributes: {
    value: {type: 'string'},
    done: {type: 'boolean', defaultsTo: false}
  },

};
```


### 6-2. クライアント側コードを変更する

モデルを更新します。

```client/src/app/todo.ts
export class Todo {
    id!: String;
    value!: String;
    done!: Boolean;
}
```

HTML にチェックボックスを追加します。

```client/src/app/app.component.html
<div class="container">
  <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    <h2 class="h2">Todos List</h2>
  </div>

  <ul class="list-group">
    <li class="list-group-item d-flex list-group-item-action" *ngFor="let todo of Todos; let i = index">
      <input class="form-check-input me-2" type="checkbox" [checked]="todo.done" (click)="toggleDone(todo.id, i)" [disabled]="isProcessing">
      <span class="flex-grow-1"> {{todo.value}}</span>
      <button class="btn btn-sm btn-danger" (click)="delete(todo.id, i)">Delete</button>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center list-group-item-action">
      <input type="text" id="newTodo" class="form-control me-2" placeholder="Type a new todo to add." [formControl]="newTodo" (keyup.enter)="add()" [disabled]="isProcessing" ><button class="btn btn-sm btn-primary" (click)="add()" [disabled]="isProcessing">Add</button>
    </li>
  </ul>
</div>
```

app.component.ts に toggleDone 関数を追加します。

```clinet/src/app/app.component.ts
  toggleDone(id:any, i:any) {
    console.log("Toggled checkbox for " + id);
    this.isProcessing = true;
    this.Todos[i].done = !this.Todos[i].done;
    this.restService.updateTodo(id, this.Todos[i])
    .subscribe((res) => {
        console.log('Data updated successfully!');
        this.isProcessing = false;
      }, (err) => {
        console.log(err);
        this.Todos[i].done = !this.Todos[i].done;
    });
  }
```

アプリケーションを再ビルド、アプリケーションを起動します。

```
npm run build
node app.js --alter
```

ローカルホスト ( ``http://localhost:1337`` ) へアクセスします。

<img width="1200" alt="image.png (92.8 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/01/26/10862/d28b029a-93bd-4eb0-9417-defbec5d00a2.png">

修正が反映され、TODO リストにチェックボックスが追加されました。
修正内容をコミットした後、Azure へ Push すると Azure Web Apps 上でも動作確認できました。
というわけで、チュートリアルが完了となります。

## 7. リソースのクリーンアップ
チュートリアル終了後、構築環境を残しておくと従量課金が発生した状態のままとなります。
不要になったリソースを削除します。

リソースグループを削除することで、リソースグループ内にデプロイされた
リソースすべてを削除することが可能です。

```
az group delete --name <ResourceGroupName>
```

# 終わりに
チュートリアルだと思って、Azure リソース名を適当に命名してしまったせいで、Azure CLI コマンドを実行する際に入力が大変だったので、命名規則の重要性を感じました。また、Web Apps の診断ストリームを有効にして、ログストリームを見てみると、Docker コンテナ起動コマンドが走っていて、Web App って Docker コンテナなんだなと再認識しました。Azure 楽しい！