ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

App Service on Linux で Angular を動かした時にハマったこと(後編)

f:id:horihiro:20200226204413p:plain

はじめに

前編で Azure App Service on Linux 上の Angular 製 Web アプリケーションが動くようになりました。

後編は、Angular アプリに Bootstrap を組み込んでみます。もうしばらくお付き合いいただければ。

といっても、かっこいいアプリはひとっっっつも作りませんし、作れません。志は低く、ボタンに Bootstrap のスタイルが当たるかどうかだけを確認します。

getbootstrap.com

さっさと必要な情報だけ知りたい人はこちら

まずはローカルから

ローカルで Angular アプリに Bootstrap を追加します。

Bootstrap のセットアップ

ググったら、ng-bootstrap というやつを使うそうです。

ng-bootstrap.github.io

流れで、angular/localize というのも追加します。
難しいことはよくわかりませんが、詳しくはココとかココに書いてあります。

$ npm install --save @ng-bootstrap/ng-bootstrap
$ npm run ng add @angular/localize

HTML をいじる

src/app/app.component.html を編集します。

上述の Bootstrap のボタンのサンプルをほぼそのまま使います。

<style>
/* https://www.techiediaries.com/css-centering/ */

.center {
  display: flex;
  justify-content: center;
  height: 100vh;
  margin: 10pt;
}
.center button{
  align-self: center;
  margin: 1pt;
}
</style>

<div class="center">
  <button type="button" class="btn btn-primary">Primary</button>
  <button type="button" class="btn btn-secondary">Secondary</button>
  <button type="button" class="btn btn-success">Success</button>
  <button type="button" class="btn btn-danger">Danger</button>
  <button type="button" class="btn btn-warning">Warning</button>
  <button type="button" class="btn btn-info">Info</button>
  <button type="button" class="btn btn-light">Light</button>
  <button type="button" class="btn btn-dark">Dark</button>
  
  <button type="button" class="btn btn-link">Link</button>
</div>

angular.jsonCSS ファイルを追加します。結構深い場所です。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1, 
  "newProjectRoot": "projects",
  "projects": {
    "sampleapp": {
      "projectType": "application",

      //   :

      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/sampleapp",

            //   :

            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"  // <-- コレ
            ],

npm start で起動してみると。。。

f:id:horihiro:20200226051303p:plain

いい感じですね。Bootstrap っぽいです。

App Service on Linux へデプロイ

この状態で App Service on Linux にデプロイして、ブラウザーで表示してみると。。。

f:id:horihiro:20200226051848p:plain

ゑ?

デフォルトのボタンが並んでいて、Bootstrap らしさが皆無になりました。

何が起こっているんでしょう。

CSS を確認してみる

まずはローカルから。

f:id:horihiro:20200226052659p:plain

Web ブラウザーの開発者ツールで、このページの構成を確認してみると、追加した bootstrap.min.cssブラウザーに読み込まれていますね。

続いて、App Service on Linux の方を。

f:id:horihiro:20200226052227p:plain

node_modules/bootstrap がありません!
なるほど、これでは Bootstrap らしさがでるはずがありません。

なぜ CSS が読み込まれないのでしょう?

デプロイ ファイルを確認してみる

Web SSH で App Service on Linux 上のファイルを確認してみます。

f:id:horihiro:20200226053907p:plain

当然、あります。
# /home/site/wwwroot にデプロイ ファイルが展開され、ここがプロジェクトのルートになります。

もう少し色々見てみると、

f:id:horihiro:20200227040429p:plain

おや?

プロジェクト ルートの node_modules/node_modules (ルートの直下) へのシンボリック リンクになっています。

このシンボリック リンクが何やら怪しそうです。

黒幕発見

まずこのシンボリック リンクが何者なのか、説明しますが、完全に App Service on Linux の内部処理の話です。
興味がない or さっさと解決したい人はこちらへ。

ビルド エンジン Oryx

App Service on Linux には、Oryx というビルド エンジンが組み込まれています。

github.com

これはデプロイ後に、アプリケーションの依存モジュールをインストールしたり、ビルド処理を実行したりします。

また起動時には各種ランタイム向けのスタートアップ スクリプトを生成します。

Oryx による Node.js アプリの起動処理

Node.js ランタイムを設定した App Service on Linux に Node.js アプリケーションをデプロイすると、以下のことが一気に行われます。

  1. npm install
  2. npm build
  3. node_modules ディレクトリを tar.gz 化
    /home/site/wwwroot/node_modules -> /home/site/wwwroot/node_modules.tar.gz

次にアプリを起動した時には、/opt/startup/startup.sh (<- これも Oryx が作る) により下記の処理が行われます。

  1. /home/site/wwwroot/node_modules.tar.gz/node_modules へ展開
  2. /home/site/wwwroot/node_modules があれば、mv/home/site/wwwroot/__del_node_modules へ移動
  3. /node_modules へのシンボリック リンクを /home/site/wwwroot/node_modules に作成
  4. スタートアップ コマンド、もしくは、npm start を実行

アプリ自体は /home/site/wwwroot に展開されるので、アプリが見る node_modules の実態は全て /node_modules にあります。

よりディープなお話 (読み飛ばし可)

なぜこんな(一見面倒そうな)方法を使っているのでしょうか?

App Service on Linux では、/home 配下はネットワーク越しにマウントされた別のファイル サーバーにあります。
これは SSH でログインして mount コマンドからも確認できます。

f:id:horihiro:20200227042222p:plain ということは、/home/site/wwwrootnode_modules とその中のモジュールをそのまま展開すると、モジュールのロードのために、ネットワーク/ファイル I/O が大量に発生します。
さらに、/home 以下は、利用しているプラン サイズに応じた最大容量が決まっています。

一方で、シンボリック リンク方式を使うと、/home 以下にある依存モジュール関連の実ファイルは node_modules.tar.gz のみとなり(これもアプリケーション実行後は使いません)、モジュールのロードは、シンボリック リンクを介して、ローカルディスク上の /node_modules から行われます。

というわけで、シンボリック リンク方式を使うと、下記のメリットがあるのです。

  • /home の容量を節約できる
  • ネットワーク越しの I/O が減る

もちろんこの挙動を変えて、/home/site/wwwroot/node_modules に直接展開することも可能ですが、容量や I/O のことを考えると、あまりお勧めはしません。

Angular の振る舞い

一方、Angular は、既定の振る舞いとして、シンボリック リンクがあれば、それより先は追いかけないようです。

つまり、先ほど追加した Bootstrap の CSS ファイルは /home/site/wwwroot/node_modules にあると思わせて、シンボリック リンクを介して、/node_modules/bootstrap/dist/css/bootstrap.min.css にあるので、(シンボリック リンクを追いかけない) Angular の既定の設定では読み込まれません。

この App Service on Linux と Angular、それぞれの既定の設定の相性で、CSS ファイルが正常に読み込まれていませんでした。

解決編

angular.json に、"preserveSymlinks": true と一行追加してください。
Angular の設定を App Service on Linux に寄せます。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1, 
  "newProjectRoot": "projects",
  "projects": {
    "sampleapp": {
      "projectType": "application",

      //   :

      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "preserveSymlinks": true   // <-- コレ

これで、/home/site/wwwroot/node_modules のシンボリック リンクも Web ブラウザーから読み込むことが可能になります。

App Service on Linux 上のファイルを上のように編集すると、、、

f:id:horihiro:20200227043430p:plain

はい、できました(ふぅ、、、)。

今回、Angular を試した時に、たまたまシンボリック リンクの影響を発見しました。
他のフレームワークでもシンボリック リンクの扱いが実ファイルと異なる動作の場合、App Service on Linux のこの動作の影響を受ける可能性あるので、ご注意いただければ。

ちなみに

2020 年 2 月現在、Oryx が生成する /opt/startup/statup.sh/node_modules 内に循環参照を作る、という予想外の動作をします。 f:id:horihiro:20200227043943p:plain これが発生すると、Angular アプリケーションの画面は真っ白になります。。。

発生条件や回避方法が気になる方は、下記 issue をご覧あれ。

github.com

ではでは。