Ishida-IT LLC

< Back

Google Cloud Runで動くNode.jsアプリケーションを開発する(5)Firebase Hosting

Posted: 2020-08-04


  1. ローカル開発環境でNode.jsアプリケーションを作成する
  2. アプリケーションをコンテナ化し、Cloud Runにデプロイする
  3. Gitからの継続的デプロイを設定する
  4. Cloud Translation APIに接続する
  5. Cloud SQL上のMySQLデータベースに接続する
  6. Googleアカウント認証を追加する
  7. フロントエンド部分をFirebase Hostingに移行する



7. フロントエンド部分をFirebase Hostingに移行する

Firebase HostingとCloud Runを併用することのメリット

前回までに作成したサンプルアプリケーションのようにCloud Runのみでアプリケーションを動かした場合、静的コンテンツ(HTML, CSS, JavaScript, 画像など)の配信までCloud Runのコンテナを使っておこなうことになります。

Cloud Runはコンテナがリクエストを処理した時間に応じて課金されますので、静的コンテンツの配信には向いていません。

それに対してFirebase Hostingは静的コンテンツを配信するために設計されたサービスですので、こちらを使う方がコスト的にもパーフォーマンス的にも有利です。

そこで今回は前回作成した「単語帳」アプリケーションのフロントエンド部分をFirebase Hostingに移行し、Cloud RunではバックエンドのAPIのみを実行するように変更しましょう。



Firebaseプロジェクトの作成

Firebase Consoleの画面を開き、「プロジェクトを追加」をクリックします。

すでにGCPプロジェクトが存在している場合はドロップダウンから選択できるようになっているので、該当のプロジェクト名を選択します。

Firebaseコンソールの「Hosting」画面で「始める」をクリックし、Firebase Hostingの設定を開始します。



Firebase CLIツールのインストール

> npm install -g firebase-tools
> firebase login
> firebase init

Firebaseのどの機能を有効にするか聞かれるので、Hostingを選択して続行します。

デプロイ対象となるフォルダとしてpublicを選択します。



Firebase Hostingへのデプロイ

package.jsonのscriptsセクションに下のようにParcelを使ってHTML/CSS/JavaScriptなどをバンドルするコマンドを設定してあるので、これを実行します。

  "scripts": {
    "build:client": "rm -rf public && parcel build -d public src/client/index.html",
  },

これによってpublicフォルダにバンドル結果の静的コンテンツが生成されます。

> npm run build:client


あとは、firebase deployコマンドを実行するだけです。

> firebase deploy

デプロイ完了後、https://{プロジェクトId}.web.app を開くとFirebase HostingにデプロイされたWebアプリケーションにアクセスできます。



Firebase HostingからCloud Runへのルーティング設定

次に、公式ドキュメントにしたがってFirebase HostingからCloud Runへのルーティングを設定します。

firebase,json

{
  "hosting": {
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "/words{,/**}",
        "run": {
          "serviceId": "words-app",
          "region": "us-west1"
        }
      },
      {
        "source": "/translate/**",
        "run": {
          "serviceId": "words-app",
          "region": "us-west1"
        }
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

今回のアプリケーションでは、/wordsと/translateのURLでバックエンドAPIを提供しているので、それらについてCloud Runにリダイレクトされるように設定しています。

/words{,/**} という表現で「/words(末尾のスラッシュなし)」と「/words/**」の両方のケースをカバーできます。



もし正常に動かない場合は、Cloud Runのサービス詳細画面でコンテナの実行ログを確認しましょう。

上の画像のようにMySQLへの接続に失敗している場合は、「Cloud SQL Admin API」を有効にして試してみてください。



また、「APIとサービス」→ 「認証情報」の画面で「OAuth 2.0 クライアント ID」の「承認済みの JavaScript 生成元」としてFirebase Hostingのデプロイ先URLを追加しておくことも忘れないようにしましょう。これをしておかないとGoogleアカウント認証が正しく動きません。



キャッシュの設定

Firebaseの公式ドキュメントには、

Cloud Functions サービスと Cloud Run サービスはコンテンツを動的に生成するため、特定の URL に対応するコンテンツは、ユーザーが入力した情報やユーザー ID などに応じて変わります。これを考慮するため、バックエンド コードによって処理されるリクエストは、デフォルトでは CDN でキャッシュに保存されません。

と書かれているのですが、試したかぎりではGETリクエストについてはキャッシュが効いているようです。(2020年8月4日現在)

更新頻度が低く毎回最新の情報を取得する必要が無いコンテンツについては(適切な有効期限付きであれば)キャッシュが有効になっている方がパフォーマンスが良くなりコンテナの実行コストも抑えられて良いのですが、必ず最新の情報を返して欲しい場合はキャッシュが効いていると誤動作の原因になってしまいます。

今回のアプリケーションでは/wordsへのGETリクエストでユーザーごとの単語一覧を返す仕様になっているのですが、これがFirebase Hosting経由でアクセスした場合に最新の情報を返さない場合がありました。

これに対処するために、src/index.jsを下のように変更します。

function noCache(req, res, next) {
  res.set('Cache-Control', 'no-store');
  next();
}

// 単語一覧
app.get('/words', auth, noCache, async (req, res) => {
  const userId = req.userId;
  const words = await knex
    .select('*')
    .from('words')
    .where({ user_id: userId })
    .orderBy('id');

  res.json({ status: 'ok', data: [...words] });
});

単語一覧のAPIに「noCache」ミドルウェアを適用することで、レスポンスヘッダに「Cache-Control: no-store」を追加してレスポンスがキャッシュされることを防いでいます。




Cloud Runサービスを認証で保護

以上でアプリケーションとしては動作するようになったのですが、もうひとつしておいた方が良い設定があります。

それは「Cloud Runサービスを認証で保護する」ことです。

Cloud Runサービスを認証で保護することのメリットは、未認証のリクエストをコンテナが実行される前の段階でGCP側で弾いてくれることにあります。

「未認証を許可」した場合は認証処理をアプリケーション内でおこなわないといけない分、コンテナの実行時間が増えコスト増加につながります。

認証で保護することによって、たとえば悪意の第三者によって大量の不正なリクエストが送られてきた場合に予想外に課金されるような事態を避けることができます。

現状では下の画像のように「未認証を許可」の設定になっているはずです。

この「未認証を許可」の設定をオフにしないといけないのですが、今のところCloud Runの管理画面からこの設定を変更する方法は無さそうです。

新しいサービスを作成してそちらと入れ替えることは可能ですが、サービスが変わるとデプロイ先のURLも変わるので出来ればすでに動いているサービスの設定を変更したいところです。

管理画面からではなくコマンドラインで glcoud run deploy コマンドを実行することで既存のサービスの認証有無の設定を変更することができます。

> gcloud run deploy $SERVICE_NAME \
>  --image=$IMAGE \
>  --project=$PROJECT_ID \
>  --no-allow-unauthenticated

--no-allow-unauthenticated」パラメータを付けて実行すると認証が必要になります。 また、このパラメータを「--allow-unauthenticated」に変えて実行すると「未認証を許可」に戻ります。

Cloud Runの管理画面で「未認証を許可」がオフになっていることを確認して、サービスのHTTPSエンドポイントを直接ブラウザで開いてみましょう。

例)https://words-app-zknnykyxxz-uw.a.run.app

このようにブラウザから直接index.htmlを開くことが出来なくなっていればOKです。 これでCloud RunサービスへのリクエストはAuthorizationヘッダに認証トークンがセットされていないかぎり必ず403エラーが返されるようになりました。


これに対して、Firbase Hosting経由でindex.htmlにアクセスした場合は問題ないはずです。

この場合のindex.htmlはCloud RunではなくFirebase Hostingから配信されているので当然ですね。また /words などCloud Run側のAPIの呼び出しには、JavaScriptでAuthorizationヘッダを付加しているので問題なく動くというわけです。

例)https://swift-cursor-289999.web.app



以上で「単語帳」アプリケーションのフロントエンド部分をFirebase Hostingに移行する変更が完了しました!






参考URL:

Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング  |  Firebase