Rso's Jotter

日々の開発の知見のメモやその他雑記

Nuxt.js(SPA), Firebase Hosting, Cloud Function でOGP対応 その2

以前、 タイトルの構成でOGP対応で試行錯誤した内容を書いていたのですが、少しだけ進化したので、その内容をメモしておきます。

rso.hateblo.jp

以前の方法

QuizHubというWebサービスを作っていたのですが、 SPAなので、 /quizzes/:quiz-id のようなクイズ個別ページのURLにアクセスしたときにOGP情報を含んだmetaタグが出力できないので、Cloud Functionを動かしてOGP情報を含むmetaタグを出力し、 /quizzes/show?id=xxx のようなURLにリダイレクトするというやり方をしていました。このやり方では以下のような問題がありました。

  • リダイレクトされたURLをそのまま共有すると、そのURLはOGPがでない。
  • Cloud Functionがそこそこ遅い。

この内の1つ目は、SPAフォールバックの仕組みを理解していなかったので、このへんをうまくやると解消できました。

SPAフォールバックとは

/quizzes/:quiz-id のようなURLはSPAだと実際にこのURLにファイルが存在するわけでないので、vue-routerなどから遷移はできても、リロードしたりURL直指定でアクセスするとページが404になってしまいます。このような場合、 このURL直指定での404を SPAのルートのindex.htmlを返してやることにより、ページを表示させてやることができます。

Nuxtだと以下のように fallback: true を入れてやることにより、SPAフォールバックの設定ができます。

ja.nuxtjs.org

export default {
  generate: {
    fallback: true
  }
}

(catch-allフォールバックとも言うんですかね、正式な定義が分かっておりません..)

Cloud Functionを用いたSPAフォールバックでOGP対応

今回はこのSPAフォールバックを用いて以下のように変更しました。

  1. SPA生成(nuxt generate)時に出力したSPAのルートファイル(index.html)をCloud Functionにもデプロイ
  2. /quizzes/:quiz-id のようなOGP出力したいページにアクセスしたら、 Cloud Functionを起動して、 OGP metaタグを加えて、index.htmlをレスポンスする。

Cloud Functionでの実装のだいたいこんな感じです。

module.exports = functions.region('us-central1').https.onRequest(async function(req, res) {
  // SPAで生成したindex.htmlを読む
  const html = fs.readFileSync("./index.html", 'utf8');

  const dom = new JSDOM(html)
  const document = dom.window.document
  const headElement = document.hea

  /* ...(省略) JSDOMで metaタグを書き換える */

  const updateHTML = document.documentElement.outerHTML

  // 生成したHTMLを返却
  res.set("Cache-Control", "public, max-age=600, s-maxage=600");
  res.status(200).end(updateHTML)
}
   

こうしてやることで、OGP情報を加えた状態でSPA、フォールバックをすることが可能です。 相変わらずこんな方法で正しいのかという気はしますが、一応目指した挙動はしています。

課題

上記の方法により、前回はリダイレクトによるURLが変わってしまう問題は改善できました。(依然Cloud Functionでそこそこ遅いという問題がありますが) また、OGP情報が出力されたとは言え、Google からはほとんどクローリングしてもらえない状態なので、SEO対策は Rendertron などを 使用して別途対策する必要がありそうです(この方法はまた別の機会に書きたいと思います)。

AWS ElasticSearch Service の手動スナップショットを取得する

掲題の対応を実施したので、内容をメモしておきます。

AWSはElastic Search Serivceを使えば、Elastic Searchを簡単に起動でき、Kibanaも入っている状態でスタートできるのですが、 データの手動バックアップアップ、リストアに関してはひと手間かかります。

手順

以下の公式の手順を実施するだけで終わりなのですが、やや行間を埋める必要があったので、そこを埋めつつ記載します。

docs.aws.amazon.com

上記を見ると、自動スナップショットはよろしく取ってくれるようですが、手動スナップショットはいくつかの手順を踏む必要があると書いています。 自分は、Elastic Searchドメインを削除する前に、事前にバックアップを取得しておく必要があり、手動バックアップを行いました。

前提条件

なにやら実施する前にいくつかの前提条件があるそうですが、まとめると..

  • Elastic Searchのスナップショット格納先のS3バケットを作成する必要がある。(Glacierはだめ)
  • ElasticSearch Service に S3にデータを保存できるロールを作成し、付与する必要がある。
    • (IAMに慣れていないと分かりづらいかもですが)、ElasticSearch Serviceがこのロールを扱える(引き受けられる)ように IAMロールに Principal を設定する必要がある。
    • ロールの付与はElasticSearch Serviceのアクセスポリシーの設定から行う。

Principal の設定は以下のIAMロール画面の 信頼関係 タブから設定できます。

f:id:rso:20200622181423p:plain
IAMロール画面

設定したprincipal は以下のとおりです。(公式のそのまま)

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "",
    "Effect": "Allow",
    "Principal": {
      "Service": "es.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
  }]
}

このロールに、ポリシーを追加してやります。今回は面倒なのでインラインポリシーで追加しました。 S3バケット名を、バックアップ用に作成したバケット名に変更してやります。

{
  "Version": "2012-10-17",
  "Statement": [{
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::s3-es-backup"
      ]
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::s3-es-backup/*"
      ]
    }
  ]
}

さらに、次にElastic Search Serviceのアクセスポリシーを修正して、先程作成したロールの付与と、スナップショット作成のために HttpsPut を許可してやります。こちらはElasticSearch Serviceの画面から 修正します。 こちらは公式のものをコピペすると何故かエラーになり、 "Principal" を追加してやると変更できました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::xxxxxxx:role/ESTheSnapshotRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "es:ESHttpPut"
      ],
      "Resource": "arn:aws:es:ap-northeast-1:xxxxx:domain/es-target-domain/*"
    }
  ]
}

スナップショットリポジトリの登録

通常であれば PUT elasticsearch-domain-endpoint/_snapshot/my-snapshot-repo-name でスナップショットリポジトリを登録できるのですが、AWSの ElasticSearch ServiceはCurlでは対応できず、Pythonなどのライブラリを使用する必要があるようです。これもサンプルを指示通りに修正し、実行します。

import boto3
import requests
from requests_aws4auth import AWS4Auth

host = 'https://xxxxxxxx.ap-northeast-1.es.amazonaws.com/' # include https:// and trailing /
region = 'ap-northeast-1' # e.g. us-west-1
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)

# Register repository

path = '_snapshot/es-snapshot-repo-name' # the Elasticsearch API endpoint
url = host + path

payload = {
  "type": "s3",
  "settings": {
    "bucket": "s3-es-backup",
    "region": "ap-northeast-1", # for all other regions
    "role_arn": "arn:aws:iam::xxxxxxx:role/ESTheSnapshotRole"
  }
}

headers = {"Content-Type": "application/json"}

r = requests.put(url, auth=awsauth, json=payload, headers=headers)

print(r.status_code)
print(r.text)

上記を実行して {acknowledgement: true} が返ってきたので、作成できたようです。

スナップショット取得

スナップショット取得は上記に続き以下を実行します。

path = '_snapshot/es-snapshot-repo-name/last-snapshot'
url = host + path

r = requests.put(url, auth=awsauth)

これを実行した後、 GET /_snapshot/es-snapshot-repo-name/_all でスナップショット作成状態を確認し、 SUCCESS 担っていることを確認し、S3にも出力されていることを確認しました。

リストア

リストアに関しては、長くなってきたので、割愛します..

所感

いつも雰囲気でIAMを使用して理解していないせいもあり、少し予想より時間がかかってしまいました。 Elastic Search Serviceはとても簡単に利用開始できるのですが、こいう言った細かい運用は少し複雑な印象を受けます。このあたりももっとさくっとできればいいのですが...

WordpressのカスタムフィールドをREST API経由で更新する

掲題の対応方法をメモっておきます。 PHPは全然得意ではないのですが、Word Pressが世の中で流行っている以上、 なかなか逃げられず、調査したので、その内容を残しておきます。

課題

カスタムフィールド (今回は Smart Custom Fieldプラグインを使用)を REST API経由で投稿(Create)したい

調査内容

記事投稿なので、エンドポイントは /wp/v2/posts で良さそうです。 アクセストークン周りの認証の方法は以下の記事を参考にさせていただきました。ここでは割愛します。

web.plus-idea.net

これで通常のTitleやらContentは投稿できました。問題はカスタムフィールドです。

いろいろ調査すると、どうやら register_rest_field という関数を使用すると、既存のREST APIを拡張できるようです。

developer.wordpress.org

こいつをfunctions.php内で呼んでやります。

add_action( 'rest_api_init',  function() {
  register_rest_field(
    'post',        // post type
    'post_meta',   // rest-apiに追加するキー
    array(
      'get_callback'  => function(  $object, $field_name, $request  ) {
        ...
      },
        'update_callback' => function(  $value, $post, $field_name) {
        ...
        },
      'schema'          => null,
    )
  );
});

という風にかいてやれば、既存のpostエンドポイントを拡張し、post_meta情報を受け付けるようになるようです。 値の取得、更新はget_callback, update_callbackに処理を記述してやれば良さそうです。

細かい処理は説明すると煩雑になるので、実際にうごいたサンプルを備忘がてら載せておきます。

'get_callback'  => function(  $object, $field_name, $request  ) {
    // 出力したいカスタムフィールドのキーをここで定義
    $meta_fields = array(
      'field1',
      'field2'
    );
    $meta = array();
    foreach ( $meta_fields as $field ) {
        $meta[ $field ] = get_post_meta( $object[ 'id' ], $field, false );
    }
    return $meta;
},
'update_callback' => function(  $value, $post, $field_name) {
  if (!$value) {return;}
  foreach($value as $key => $data){
    if(is_array($data)){
      foreach($data as $record){
        add_post_meta($post->ID, $key, $record);
      }
    }else{
      add_post_meta($post->ID, $key, $data);
    }
  }
}

update_callbackはエラー判定などかなり端折っていますが、Smart Custom Fieldは繰り返し設定ができるので、 その場合配列が渡させるので処理を分けています。

所感

自分だけかもしれませんが、WordpressはREST APIのリファレンスはどうも分かりづらい気がしていて、 なかなかこういった情報が探しづらいなと言う印象を受けました。 あんまりREST API経由で カスタムフィールドを更新したいという要件自体レアかもしれないですが、残しておきました。

jsonwebtokenでAuth0で発行されたJWTを検証する

掲題の対応でWebで調べてもすぐに出なかったのでメモしておきます。

Auth0とFirebaseを連携させたいときに、まずCloud Functionで Auth0で発行されたJWTの検証を 行おうと思ったのですが、その手順と調べた内容です。

Auth0公式に方法がざっくり書いています。

auth0.com

大きく3つ方法があり、

  • ASP.NET Coreのようなミドルウェアを使う。
  • jwt.ioが提供しているライブラリを使う。
    • nodeを使うなら jsonwebtoken というライブラリでいいよ
  • 自前で検証処理を実装する。

とのことです。 今回はCloud Function上で検証がしたいので、node-jsonwebtoken のライブラリを使用することにします。

github.com

これを見ると、 ここjwt.verify(token, secretOrPublicKey ...) という関数が用意されているので、これを使用すれば良さそうです。 tokenが 検証したいJWT, secretOrPublicKeyは シークレットもしくは公開鍵を入れるようです。 今回JWTの生成アルゴリズムがRSA256だったので、公開鍵を入れる必要がありそうです。

この公開鍵は、Auth0 は JWKs という形式で配っているそうです。

auth0.com

↑を読むと、 https://YOUR_DOMAIN/.well-known/jwks.json にGETするとJWKsが返ってくるようです。

しかしこれはそのままでは公開鍵としては使えないようです。

適当にggるとどうやらjwkから公開鍵を生成してやらないと行けないようです。 jwk-to-pemというライブラリがあったので、そちらを使います。

const jsonwebtoken = require('jsonwebtoken')
const jwkToPem = require('jwk-to-pem');
const pem = jwkToPem(jwk) // jwk は auth0からGETしたjwksの1つ
const result = jsonwebtoken.verify(jwt, pem)

みたいな感じで無事検証できました。

追記

このメモを書いた後で見つけたのですが、 "jwks-rsa" というライブラリもあって、こちらを使ったほうが楽だったかもです。

github.com

所感

公式ではサラッと書いていましたが、 jwksがどこにあるのや、そこから公開鍵を生成しないといけないなど、 基本が分かっていなかったので、少々手間取りました。 似たような問題でつまづくケースがあるかもなので、残しておきました。

FireStoreの定期バックアップを実施したメモ

Firebaseを使用している新規Webサービスで、いい加減バックアップを取っておかないとまずいよね、ということがあり、バックアップを取ってみたんですが、その内容のメモになります。

前提

  • FirebaseのBlazeプラン以上でないと実行できません。

手順

以下の2つの公式の説明を見れば終わりなのですが、以下手順と所感です。

firebase.google.com

firebase.google.com

始めてみたとき、あれ、2つの方法があるのかなと思いましたが、よくよく見ると、1つ目の記事はgcloud コマンドラインツールで import / export する方法で、 2つ目の記事は exportを cloud function で定期実行する方法、ということになります。なので定期的なバックアップ取得〜リストアのテストまでやるには、上記の両方の記事を参照する必要があります。

定期的なバックアップ取得(Export)

ここでは詳細は手順を書いても公式と同じになってしまうので、ざっくり概要のみ書くと、

  • cloud functionを作成してデプロイする(Blazeプランじゃないとデプロイ時にエラーになる)
  • GCPから新しいバックアップ保存用のバケットをCloud Storageに作成する。(Firebaseコンソール画面からは見えない)
  • gcloud, gsutil コマンドで エクスポートして保存できる権限を付与する。(gcloud コマンドがインストールされてない場合インストールする。後のリストアでも使用する)
  • GCPのCloud Scheduler から、作成した定期バックアップを手動で起動して、 Cloud Storageに保存されることを確認する。

と行った感じです。普段の開発でFirebaseのみ使用している場合、GCPのツール入れたり管理画面に入ったりで、多少戸惑いがあるかもしれません(私はありました)

そして公式には明示的な記載は見つからなかったのですが、エクスポートされたデータはJSONのようにきれいなデータとして出力されません。なのでバックアップが正常に行われているかどうかは、リストアして確認する必要がありそうです。

リストア(import)

インポートは gcloud コマンドを使用してExportしたバケットとバックアップ時のPrefixを指定すればOKです。

ただ、リストア時には以下のような注意事項がありました。

  • データをインポートすると、データベースの現在のインデックス定義を使用して必要なインデックスが更新されます。エクスポートにインデックスの定義は含まれません。

  • インポートでは、新しいドキュメント ID が割り当てられません。インポートでは、エクスポート時に取得された ID を使用します。ドキュメントをインポートするときに、ドキュメントの ID が予約され、ID の競合が防止されます。同じ ID のドキュメントがすでに存在する場合、インポートを行うと既存のドキュメントが上書きされます。

  • データベース内のドキュメントがインポートの影響を受けない場合、そのドキュメントはインポート後もデータベースに維持されます。

特定のコレクションのみのリストアに関して

特定のコレクション グループのインポートがサポートされるのは、特定のコレクション グループをエクスポートした場合のみです。すべてのドキュメントのエクスポートから特定のコレクションをインポートすることはできません。

要するに、フルバックアップで取った場合は、全戻ししかできないようです。 また単純にドキュメントのIDで上書きなので、完全にバックアップ時の状態にそのまま戻るわけでは無さそうで、そうするには事前にコレクションの削除などが必要そうでした。

所感

FirebaseはかんたんなWebサービスならこれ一つでいい感じに開発が進められるのですが、FireStoreがMySQLなどのよくあるRDBとクセが違うのである程度慣れが必要なのですが、さらに運用面でも馴染んでおく必用がありそうです。小さなサービスならとりあえずRDBのダンプ取っておけばなんとかなりましたが、バックアップやリストアの検証してたら初回は時間がかかってしまいました。

こういった運用の準備や検証とかもいれると、いくらFirebaseがさくっと作れると言っても、初回は慣れている技術スタックのほうがやはり早いです。

一度設定して理解してしまえばそんなに難しいことはなく、次回同じことをやる際にはすんなり行けるとは思うので、本当にサクッと作りたいときにサクッと作るには、日頃からこういった細かいテストや検証を経て慣れ親しんでおく必要がありそうです。