type holyshared = Engineer<mixed>

技術的なことなど色々

Cloud CDNでの署名付きCookie、署名付きURL

Cloud Storageに格納したファイルをCDNで配信する時にアクセスを限定したい場合があります
その際に署名付きCookie、署名付きURLが使えます

設定自体はterraformなどで、自動で設定して置けるようにすると楽です

resource "google_storage_bucket" "public-images" {
  project  = var.project_id
  name     = "public-images-${var.project_id}"
  location = "asia-northeast1"

  force_destroy = true

  cors {
    origin          = ["https://${var.domain}"]
    method          = ["GET"]
    response_header = ["*"]
  }
}

resource "google_storage_bucket_iam_member" "signed-url-user" {
  bucket = google_storage_bucket.public-images.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:service-${var.project_number}@cloud-cdn-fill.iam.gserviceaccount.com"

  depends_on = [
    google_compute_backend_bucket_signed_url_key.backend_key
  ]
}

resource "google_compute_backend_bucket" "cdn-backend-bucket" {
  project     = var.project_id
  name        = "backend-${google_storage_bucket.public-images.name}"
  bucket_name = "public-images-${var.project_id}"
  enable_cdn  = true
}

resource "random_id" "url_signature" {
  byte_length = 16
}

resource "google_compute_backend_bucket_signed_url_key" "backend_key" {
  project        = var.project_id
  name           = "public-backend-bucket-key"
  key_value      = random_id.url_signature.b64_url
  backend_bucket = google_compute_backend_bucket.cdn-backend-bucket.name
}

resource "google_compute_url_map" "cdn" {
  project         = var.project_id
  name            = "public-images-cdn"
  default_service = google_compute_backend_bucket.cdn-backend-bucket.id
}

resource "google_compute_target_https_proxy" "cdn-proxy" {
  project          = var.project_id
  name             = "proxy-${var.project_id}"
  url_map          = google_compute_url_map.cdn.id
  ssl_certificates = [google_compute_managed_ssl_certificate.tls.id]
}

resource "google_compute_managed_ssl_certificate" "tls" {
  project  = var.project_id
  provider = google-beta
  name     = "public-images-${var.project_id}"
  managed {
    domains = [var.domain]
  }
}

resource "google_compute_global_forwarding_rule" "cdn" {
  project    = var.project_id
  name       = "public-images-${var.project_id}"
  target     = google_compute_target_https_proxy.cdn-proxy.self_link
  port_range = 443
}

生成された鍵はSecretManagerなどに登録しておくといいと思います

署名付きCookie

cloud.google.com

署名付きCookieは署名付きリクエストの鍵を使ってCookieに署名します
生成した値はCloud-CDN-Cookieに指定して利用できます

import crypto from 'crypto';
import dayjs from 'dayjs';

export const ENCODED_URL_PREFIX = (Buffer.from(process.env.URL_PREFIX))
  .toString("base64")
  .replace(/\+/g, "-")
  .replace(/\//g, "_");

export const signedCookie = (seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();
  const input = `URLPrefix=${process.env.ENCODED_URL_PREFIX}:Expires=${unixTimestamp}:KeyName=${process.env.SIGNED_URL_KEY_NAME}`;
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(input)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  return `${input}:Signature=${signature}`;
}

署名付きCookieの場合はCookieのパスの指定ミスなどで期待通りブラウザがCookie送ってくれなかったり、原因の切り分けを早くできた方がいいので、 実装が済んでしまったら先に正しい署名がCookieについているか確認しておくと良いと思います

curl --cookie='Cloud-CDN-Cookie=[生成した値]' \
  -D dump-headers.txt \
  --output=example.jpg \
  https://cdn.example.com/example.jpg

署名付きURL

cloud.google.com

署名付きURLはURLに対して署名を付加できるものです
Cookieと違って対象のURLが多い場合面倒ですが、実装自体は簡単なので導入しやすいです

import crypto from 'crypto';
import dayjs from 'dayjs';

export const signedURL = (url: string, seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();

  const segments = [
    `?Expires=${unixTimestamp}`,
    `&KeyName=${process.env.SIGNED_URL_KEY_NAME}`
  ];

  const signURL = `${url}${segments.join('')}`
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(signURL)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  segments.push(`&Signature=${signature}`);

  const singedQuery = segments.join('');

  return `${url}${singedQuery}`;
}

こちらも実装が済んだら検証しておきます

curl --output=example.jpg [署名付きURL]

SOLOVAIRの新しいカラーのブーツ買った

新しく青系のカラーが3色追加されたみたいなので、一足買ってみました。
SOLOVAIRのアストロノーツブーツを一足持っていて、サイズ感はわかっていたので、特に困ることはなかったです。

f:id:holyshared:20210618111638j:plain:w500
SOLOVAIR Neon Blue 8Eye Boot

また、自分は履く靴によってソックスを変えているのですが、アルパカの毛が使用されているソックスが気になったのでそれも一緒に購入しました。

f:id:holyshared:20210618111735j:plain:w500
SOLOVAIR Socks

アルパカのソックスを履いて歩いてみましたが、チクチクしないニットぽい感触で特に不快感もなく過ごせました。 ブーツ履く用に使用して行きたいと思います。

SentryのSDK問題

SentryのjavascriptSDKを6.5.1にアップグレードしたら初期化に失敗するようになった。

options._metadata.sdkがないのでエラーになるようだ。
ただ内部的に付与されるぽいプロパティな感じがするので、何か必須のパラメータがあるのかなと思ったけどそれらしいものはなさそうでした。

一応強制的に指定するようにしたら出なくなり、一応動くようになった。
しかし気持ち悪い。

import * as Sentry from "@sentry/node";
import * as Tracing from "@sentry/tracing";

Sentry.init({
  dsn: 'real dsn',
  _metadata: { sdk: null }, // これを追加した
  integrations: [new Tracing.Integrations.Express()],
});

追記

手元で最小構成で試してみたら再現できなかったので、SentryのSDKの問題ではないことがわかった。
アプリケーションはwebpackでbuildしていたので、buildし直してみたら発生しなくなった。

解決した理由は下記のどれかと思われる。 * typescript周りのパッケージをアップグレード * babel周りのパッケージをアップグレード

一旦応急処置はしたのでパッケージのアップグレードでもするかーとやっていたら直った感じ

Oura Ringのバッテリーの問題

Oura Ringを半年くらい使用していたのだけど、突然充電できなくなりました。 大体10分くらいで充電が終わるのだけど全然できなくて困ってました。

多分故障だろうと思って新しく買わないとダメなのかなと思っていたら、購入から2年以内は交換できるぽかったので、アプリから問い合わせてみました。

サポートから1日ぐらいで連絡が来て、バッテリーの問題ぽいので、注文のIDとか住所を聞かれ、回答したら新しいやつを送ってくれることになりました。

サポートの対応が早くてめちゃくちゃ助かりました。

今日届いて、新しいリングも無事ペアリングでき半日つけてみた感じ、消費カロリーとか記録もトラッキングできてるぽいです。

同じように困っている人がいたら問い合わせてみた方が良いかもしれません、

画像を投稿した後にレポートを出す

投稿後の結果
レポート

自作アプリで写真を投稿後に今の集計値を確認したくてページ遷移することがあり不便だったので投稿後に集計を表示するように変えた

デプロイしてから、数日使って見ている感じストレスが減ったので実装してよかったです。   この前進捗状況を表示するように変えたけど、それも表示したい。

履いた靴の進捗状況を表示するようにした

靴をできるだけ均一に履くようにしていると、どれが今月履いたのかわからなくなるので、ダッシュボードに進捗を表示するように変えてみた。

進捗状況
進捗状況

ダッシュボードには履いてない靴を4件リストに出していたけど、後どのくらい残っているのかはわからなかったので結構不便だった。

なのでリファクタリングのついでに新機能として追加してみだけど、結構便利。

大体毎月2回はループするので、今何周目とかも追加したい。
もしは進捗バーの色変えるとか。

loadable-componentsのheroku対応

loadable-componentsを使用していて、Server side renderingをheroku環境で行う場合ビルドされるloadable-stats.jsonのパスが実行環境と違うため、chunkファイルが読み込めないという問題があります。

これはビルド時は​​­/tmp/build_[id]/​のようなテンポラリディレクトリに成果物が出力されるが、実際のファイルは/appの下に配置されるため、パスが参照できなくなる為です。

この為ローカルでは正常に動いても、heroku環境ではファイルの読み込みエラーが発生します。

import * as path from 'path';
import { ChunkExtractor } from '@loadable/server';

export const statsFile = path.resolve('./dist/loadable-stats.json');

export const nodeExtractor = new ChunkExtractor({
  statsFile
});

これを回避するには、ChunkExtractorのオプションにpublicPathoutputPathを明示的に指定します。
通常この設定はloadable-stats.jsonの指定を参照するのですが、オプションを指定してあげるとそちらを使用します。

import * as path from 'path';
import { ChunkExtractor } from '@loadable/server';

export const statsFile = path.resolve('./dist/loadable-stats.json');

export const nodeExtractor = new ChunkExtractor({
  statsFile,
  publicPath: path.resolve('./dist/'), // statsファイルのpublicPathの代わり
  outputPath: path.resolve('./dist/'), // statsファイルのoutputPathの代わり
});

こうすることで、サーバー側でchunkファイルを読み込めるようになるので、正常にレスポンスを返せるようになります。