type holyshared = Engineer<mixed>

技術的なことなど色々

ハラ ミュージアム アークに行って来た

前から行きたかったハラ ミュージアム アークに行ってきました 場所は渋川なので、高崎で一泊して午前中に行くことにしました

ホテル周辺

ホテルにチェックイン後は暇だったので本買ったり、近くの高崎市美術館で時間を潰したりして楽しかったです
駅周辺は栄えていてめちゃくちゃ便利そうでした、大宮とか町田の駅前に近い感じです

夕食

夕飯はラーメンを食べたかったので、駅近くのOPAにあるラーメン屋で食べました
ここ数ヶ月全くラーメンを食べてなかったのでめちゃくちゃ上手く感じました

翌日は渋川まで電車でいき、後はタクシーでハラ ミュージアム アークまで行きました
かなり山の麓に近いところで、高原みたいな所で風景がめちゃくちゃ良かったです

帰りは渋川駅まで歩いて帰りましたが、ずっと緩い下り坂が続くのでめちゃくちゃ大変でした
普段7kmぐらい歩くのは平気だったので、いけるかなと思っていましたがバスかタクシーで帰ればよかったです

次来る時は、渋川の付近のホテルに泊まろうかなと思います

Cloudinayの新プラン移行

Cloudinayのプラン上限に達したので、プランを変更してみました。
プランはClassic free planだったのをFree planにしました。

CloudinayのアカウントはHerokuのアドオンで指定していたプランだったので、Cloudinayのアカウントを新規に作成してのプランを変更しました。

作業手順としては下記の通りに進めました。
1. 新規アカウントを作成する
2. 新規アカウントでURLのマッピングを旧アカウントの設定に合わせる
3. S3のバケットの.wellknown/cloudinary/に新アカウントのCloudNameをファイル名にした空のファイルを作成する
4. 新しいURLで画像が変換されるのを確認する
5. herokuの環境変数を変更する
6. アプリケーションで画像が正常に表示されるか確認する
7. S3から旧CloudNameのファイルを消す
8. 旧アカウントを削除する

プラン変更は無事終了しました。
これでしばらく運用できそうです。

css-loaderのバージョンを上げた

webpackのcss-loaderのバージョンを5.xから6.xに変えました。
それに伴いcss内の画像指定が期待した通り変換されなくなったのでwebpackの設定ファイルを変えることで対応しました。

具体的にはwebpackの設定のrulesにasset/resourceを追加する作業です。
この設定によりcss-loaderがcssファイル内で参照している画像を置き換えてくれます。

module.exports = [
  {
    module: {
      rules: [
        {
          test: /\.scss$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader
            },
            {
              loader: "css-loader",
              options: {
                url: true,
                importLoaders: 2,
              }
            },
            "sass-loader"
          ]
        },
        {
          test: /\.(jpg|jpeg|png|svg|gif|ico)$/,
          type: "asset/resource",
        }
      ],
    },
    plugins: [
      new MiniCssExtractPlugin({}),
    ],
    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
    },
    stats: 'errors-only',
  }
];

asset/resourceをasset/inlineにすると、画像のパスをData URIに置き換えてCSSに画像データを埋め込んでくれます。
ファイルのサイズが小さい場合はこちらでも良いかもしれません。

BigQueryのテーブルに複数のログファイルをロードする

Google Cloud Storageに保存されている複数のファイルをBigQueryのテーブルに読み込もうとして、エラーが発生するので調査してみました。
エラーはテーブルにレコードが挿入できないエラーで、フォーマットが当初おかしいのかと思っていました。

Error while reading data, error message: JSON table encountered too many errors, giving up. Rows: 1; errors: 1. Please look into the errors[] collection for more details.
Error while reading data, error message: JSON processing encountered too many errors, giving up. Rows: 1; errors: 1; max bad: 0; error percent: 0
Error while reading data, error message: JSON parsing error in row starting at position 0: Could not convert value to string. Field: sign_in_count; Value: 12

しかしファイルを調べてみてもおかしいところはありません。
調べた結果、スキーマの構造が最初に処理するファイルによってスキーマが決定されていそうでした。

例えば、次のようなログファイルがあった場合、sign_int_countがNullableであることはわかりますが、INTEGERというデータ型まで特定するのは困難です。
代わりにデータ型はNullableなSTRING扱いになるようです。

{ "id": "a", "name": "demo1",  sign_in_count: null, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }

次のようだとおそらくNullableなINTEGERであることは推測できます。
なぜなら、sign_in_countに2が含まれていて、数値であることがわかるからです。

{ "id": "a", "name": "demo1",  sign_in_count: null, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }
{ "id": "b", "name": "demo2",  sign_in_count: 2, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }

今までだと単一のファイルで読み込むことが多く、問題が出ませんでした。
それは単一のファイルだとフィールドのデータパターンが全部収集できるので、フィールドのデータ型が推測しやすかったからだと思います。

解決方法

この問題を解決する為に、読み込み時にスキーマを明示的に指定するようにしました。
明示的に指定することで、正しく読み込むことができます。
200ファイル近くのファイル読み込んでみましたが特に問題は起きませんでした。

その代わりautodetectのメリットがなくなります。

import { BigQuery } from '@google-cloud/bigquery'
import { Storage, File } from '@google-cloud/storage'

const storage = new Storage({ projectId: process.env.GCP_PROJECT_ID })
const bigQuery = new BigQuery({ projectId: process.env.GCP_PROJECT_ID })

const logFilesOf = async (logDate: string) => {
  const prefix = `logs/${logDate}/`
  const bucket = storage.bucket(process.env.GCP_BACKUP_BUCKET_NAME!)
  const [files] = await bucket.getFiles({ prefix })
  return files
    .filter((file: File) => file.name !== prefix)
    .filter((file: File) => {
      const size = Number(file.metadata.size)
      return size > 0
    })
}

const loadFilesOf = async (tableId: string, logDate: string) => {
  const files = await logFilesOf(logDate)

  if (files.length <= 0) {
    return
  }

  const table = bigQuery.dataset('example_dataset').table(tableId)

  return table.createLoadJob(files as any, {
    sourceFormat: 'NEWLINE_DELIMITED_JSON',
    autodetect: true,
    writeDisposition: 'WRITE_TRUNCATE',
    schema: {
      fields: [
        {
          name: 'id',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'name',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'sign_in_count',
          type: 'INTEGER',
        },
        {
          name: 'createdAt',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'updatedAt',
          type: 'STRING',
        },
      ],
    },
  })
}

loadFilesOf('example', '2021-08-09')
  .then(() => {
    console.log('done')
  })
  .catch((err: Error) => {
    console.log(err.stack)
  })

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周りのパッケージをアップグレード

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