valid,invalid

関心を持てる事柄について

既存のRESTish APIエンドポイントにOpenAPI定義を足していく試み

昨年、とあるアプリケーションのフロントエンドリニューアルプロジェクト*1の際に取り組んだ課題について書きます。Ruby, Railsの話が出てきますがRESTish APIの定義をどのように管理するかAPI定義が存在しない既存アプリケーションにどのようにドキュメントを足していくかという課題については言語・フレームワークを問わない内容です。

同プロジェクトでは以下のような状況であり、既存のAPIエンドポイントに関する情報をなんらかの形で提供したいというのがモチベーションでした。

  • Single page applicationやモバイルアプリのバックエンドとして動作するRESTish API server (Rails) が存在する
  • API serverの各APIエンドポイントに関するドキュメントが存在しない (または存在するが実装と一致しているか不明で信頼性が低い)
  • フロントエンドエンジニアはAPIの仕様を知らない、かつAPIのコードリーディングには時間がかかる

欲しかったものは「APIのin/outについてきちんと"型"が定義されていること」「実装と乖離していないことの保証」「定義が常に参照可能であること」でした。

やったこと

OpenAPI 3の採用

フロントエンドエンジニアが最も慣れていて、かつ習得難易度とかエコシステムとか考慮して今後の運用にも耐えうるであろう記述形式としてOpen API 3を選択しました*2

OpenAPIはWeb APIの仕様の記述形式です。または関連する周辺ツールです。かつてSwaggerと名乗っており今もその名を関した周辺ツールがたくさん残っていますが、現在はOpenAPIという名前でバージョン3が公開されています。バージョン2と3でだいぶ差がありますが、本記事ではOpenAPIというときにバージョン3を指すことにします。

OpenAPI 3によるAPI specificationの定義

同プロジェクトで必要とされたAPI群の定義をゼロから手動で書き起こしました。

初っ端から非人道的な話ですみません。自動化の道を探ったほうが良さそうにも一見思いました…が、自分もまったく詳しくないエンドポイントたちの中身を知らずに提供するのは望ましくない状況だったので、各エンドポイントのコードリーディングをしたりリクエストを投げて挙動を確認したりしながら書き起こすことで理解を得ました。

また、その過程で既存APIのバグ改修やパフォーマンス改善を行うこともありました。

定義が実装と乖離しないようにする

もともと存在していたYARDコメントがまさにそうだったのですが、記述形式がなんであろうと実装と乖離することが保証されていなければ信頼性は保てません。定義通りにAPIが動くことを継続的に検証する必要があります。

Rubyではcommitteeというgemがあり、これを利用してOpenAPI定義 (JSON or YAMLのファイル) に違反する挙動を検出できます。

また、既存APIに対して後追いで人間が記述する場合、その暖かみのある定義が間違っていないかどうかの検証にもなります。

テストで実装を検証する

committeeにはrequest validation, coercion, test assertion, stub機能などありますが同プロジェクトで使った機能はCommittee::Middleware::ResponseValidationのみです。これは名前の通りRack middlewareであり、アプリケーション(この場合は我々が提供するAPI server)のレスポンスを検証します。

test assertionをすべてのテストに都度書いていくのは面倒なため、API定義が書かれているすべてのエンドポイントについて自動的に検証を走らせるようにしています。OpenAPIによる定義が書かれていない場合は検証は行われません。

# config/environments/test.rb
Rails.application.configure do
  config.middleware.use(Committee::Middleware::ResponseValidation, schema_path: Rails.root.join("openapi/index.yaml"), raise: true)
end

committeeではエラーを検出した際のハンドラを自前で書くこともできますが、ここではシンプルにraise: trueを指定することでテストが単にfailします。たとえばこのようにテスト中にとあるエンドポイントにリクエストを発行するケースがあるとします。

describe "#GET /:language/api/users/:id", type: :request do
  let(:user) { create(:user, username: "butcher") }

  subject { get "/ja/api/users/#{user.id}" }
  
  it "returns 200" do
    subject
    expect(response).to have_http_status(200) 
  end
end

テストケースとしてはstatus codeを検証しているのみですが、middlewareがレスポンスを検証し、違反を見つけたら以下のようなエラーを吐きます。

  1) #GET /:language/api/users/:id
     Failure/Error: subject { get "/ja/api/users/#{user.id}" }

     Committee::InvalidResponse:
       true class is TrueClass but it's not valid integer in #/components/schemas/User/properties/is_teacher
     # ./spec/requests/api/users_spec.rb:160:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # OpenAPIParser::ValidateError:
     #   true class is TrueClass but it's not valid integer in #/components/schemas/User/properties/is_teacher
     #   ./spec/requests/api/users_spec.rb:160:in `block (3 levels) in <top (required)>'

注意事項としては、この検証はrack middleware層で行っているのでcontroller specでは実行されないということです(もちろんrack middlewareレイヤも込みで動くrequest spec, feature specでは実行されます)。今どきcontroller spec…*3?とお思いかもですが古いアプリケーションらしく一部はcontroller specが生き残っていたのでこれを機にガッとrequest specへ書き換えたりもしました。

これでテスト実行時にOpen API specificationと実装の乖離に気づけるようになりました。

…本当でしょうか?

実はこの検証だけでは実装との乖離を完全に防ぐことはできません。用意されたテストデータに対してしかテストを行っていないためです。たとえば上記コードでは user というテストデータを生成してそのデータに基づいてレスポンスを返すAPIを検証していますが、もしこのデータが変わったとしてもAPIが定義通りに動くことは保証しません。開発者が想定した正常系ド真ん中のテストデータによってしか動かない可能性もあります。

ステージング環境での違反検出と通知機構の整備

開発者が想定していないデータをカバーしたいときや、レスポンスのパターンが複雑でテストデータを用意しきれないようなときはどうすれば良いでしょうか。

そのような場合はテストですべてをカバーしようとするのではなく、実際に動くアプリケーション or "リアル"に近いデータ or "リアル"に近いリクエストで検証するアプローチが有効です。

一例として、QAやプレビューとして利用するステージング環境*4Committee::Middleware::ResponseValidationを有効にします。ただし、違反検出時にstatus code 500を返すと他の開発者や社内ステークホルダー諸氏が困るので「定義違反は検知するがレスポンスには手を加えず素通りさせる」「API開発者が気付けるように検知した違反の通知機構を作る」ようにします。

# config/environments/production.rb
Rails.application.configure do
  if ENV["USE_COMMITTEE_RESPONSE_VALIDATION"]
    config.middleware.use(
      Committee::Middleware::ResponseValidation, schema_path: Rails.root.join("openapi/index.yaml"),
      error_handler: -> (ex, env) { Raven.capture_exception(ex, tags: { committee_response_validation: true }) },
      ignore_error: true,
    )
  end
end

ステージング環境はRAILS_ENV=productionの前提でconfig/environments/production.rbに設定を記述しています。middlewareのoptionとしてignore_error: trueによって「定義違反は検知するがレスポンスには手を加えず素通りさせる」を、error_handler: -> (ex, env) { Raven.capture_exception(ex, tags: { committee_response_validation: true }) }によって「API開発者が気付けるように検知した違反の通知」を実現しています。この例ではSentryにエラーを通知しています。

f:id:ohbarye:20200428214538p:plain
Sentryへの通知をSlackに流す

ステージングとはいえ用意されたテストデータよりも遥かにバリエーションがあるのでこれにより実装のバグやOpen API定義の間違いなどに気づくことができました。

書き忘れましたが、ステージング環境のDBのデータがproductionのdumpをリストアしてマスクしたものであればさらに盤石です。テストする環境が本番に近ければ近いほどより信頼性の高いテストができるというわけです。

Kent C. Dodds先生の発言を思い起こさせます。

Swagger UIでいつでも参照できる状態にした

記述したAPI定義の参照容易性は大事です。

JSONまたはYAMLは定義そのものですが人間にとって可読性が高いものでは有りません。なのでdevelop branchが自動的にデプロイされるたびにSwagger UI (Open APIのviewer) で最新の定義がCOOOLな感じで見られるようにしました。

f:id:ohbarye:20200428215837p:plain
Swagger UIはこういうやつです

swaggerapi/swagger-uiのdocker imageを使っています。

おわりに

当初のゴールに対して以下のようにアプローチを行い、既存のRESTish API serverのドキュメント事情が"無"だったところを整備しました。

  • APIのin/outについてきちんと"型"が定義されていること」 <= 何もなかったところにOpenAPI定義を書いた
  • 「実装と乖離していないことの保証」 <= テストで実装を検証する + ステージング環境での違反検出と通知機構の整備
  • 「定義が常に参照可能であること」 <= OpenAPI viewerの提供

そして次にゼロからRESTish APIサーバを立ち上げるプロジェクトがあればSchema Firstでやっていく強い気持ちを得ました。

また、同プロジェクトにおいてやってないこと、今後やれそうなこととしてはコードの自動生成、OpenAPI specificationの自動生成 (from YARD to OpenAPI, grape-swagger)、検知した違反の自動修正等が挙げられます。これらも面白そうですがまた別のお話。

*1:https://speakerdeck.com/ohbarye/migration-from-react-native-to-pwa に詳しいです

*2:実質RESTish APIに関する形式としてはデファクトといっても良いと思います。API Blueprint, RAML...懐かしい名だ

*3:https://everydayrails.com/2016/08/29/replace-rspec-controller-tests.html

*4:production環境で検証するというハードコアな道もありますが一歩間違えると大惨事なのでまずはステージング環境から始めると良いと思います

C言語でSQLiteのクローンを作るチュートリアルやった

2019年12月の冬休みに1週間程かけて"Let's Build a Simple Database"という、C言語SQLiteのクローンを作るチュートリアルをやりました。この存在を教えてくれた同僚に感謝 :pray:

cstack.github.io

チュートリアルの内容

Richard Feynman先生の“What I cannot create, I do not understand.”という言葉が掲げられているように、データベースを作ることでデータベースをより深く理解することに主眼が置かれているチュートリアルです。

これは重要事項説明かつタイトル詐欺に関する謝罪なのですが… 残念ながらこのチュートリアルは完成しておらず、Part 13が2017-11-26に公開されたのを最後に更新が止まってしまっており、以下の13章しかありません。

  • Part 1 - Introduction and Setting up the REPL
  • Part 2 - World’s Simplest SQL Compiler and Virtual Machine
  • Part 3 - An In-Memory, Append-Only, Single-Table Database
  • Part 4 - Our First Tests (and Bugs)
  • Part 5 - Persistence to Disk
  • Part 6 - The Cursor Abstraction
  • Part 7 - Introduction to the B-Tree
  • Part 8 - B-Tree Leaf Node Format
  • Part 9 - Binary Search and Duplicate Keys
  • Part 10 - Splitting a Leaf Node
  • Part 11 - Recursively Searching the B-Tree
  • Part 12 - Scanning a Multi-Level B-Tree
  • Part 13 - Updating Parent Node After a Split

なのでデータベースに関して網羅的に学べるわけではなく、内容は以下に限られます。SQLiteのクローンも完成しません…。

  • ちょっとしたREPLの作り方
  • In memoryでのデータ保存とディスクへの永続化
  • page, pager, cursorといった抽象化
  • B Tree (B+ Tree) がなぜデータベースのテーブルやインデックスに使われるのかの説明とその実装

とはいえ、データベースの内部実装でもとりわけ重要なB Tree周りの解説とハンズオンに大部分が割かれているのでB Treeを理解りたい人や、なんとなく理解った気持ちでいるけど実際にテーブルをB Treeで実装してみて深く知りたい人におすすめです。(ただ、チュートリアルの後半のNodeの分割や木の更新に関わる処理は地道な実装が多くてけっこう辛めでした)

また、スクラッチ巨大なアプリケーションを書くときに「小さく作る」「常に動く状態を保つ」といったプラクティスが実践されているのも良い点です。

登場するコードの全体像はすべてGitHubで確認できるのでC言語を書けなくても写経しながら読んでいけばだいたいなんとかなります。また、実装に対するE2EテストはRubyRSpecで書かれています。

学んだこと

実施している最中のメモはこちら: Let's Build a Simple Database - ohbarye

B Tree (B+ Tree) がなぜデータベースのテーブルやインデックスに使われるのか

検索すれば以下のような先人の良い記事がたくさん見つかるのですが、B Tree自体の理解が浅かったときに読んでもいまいちピンと来ていませんでした。チュートリアルを通して手を動かしてB Treeの構造を実装してみることで具体的な探索処理などがイメージできるようになりました。

チュートリアルではB+ Treeを導入する際に高速な検索・削除・挿入といったメリットだけでなく、B+ TreeのNodeとして持つべき内部データ・メタデータなどのオーバーヘッドでfile sizeが肥大化するデメリットも併せて説明しています。

SQLiteではB-Treeがテーブルとインデックスの両方に用いられている*1」という"知識"を持っていても、「なぜ他の選択肢、たとえば要素の参照・追加・削除がすべて平均的にはO(1)で行なえるハッシュテーブルではいけないのか」「どのようなトレードオフがあるのか」を説明できる程度までは"理解"していなかったことが浮き彫りになりました。

メモリレイアウトの工夫

ふだん高級言語でWebアプリケーションを書いているとあまり気にされないレベルのメモリの使い方について。

たとえば、ページサイズを4096 bytesにする。この数字はほとんどのコンピューターアーキテクチャ仮想メモリシステムで使用されるページと同じサイズであり、これらが同じおかげで余計な分割処理を行うことなくメモリに出し入れできる。他にもチュートリアルの簡便さのためにページ上限を決めたり、ページを超えて行を保存することを禁止することでページをまたいでのread / writeを避けたりしてるのですが、それがなぜかということも解説されています(ページをまたぐとメモリアドレスが隣接しない可能性が高いので大変になる)。

他にも、pagerにpage number Xを要求する => メモリに乗っているキャッシュから読むことを試みる => キャッシュミスが発生する => データベースファイルを読み取ることでデータをディスクからメモリにコピーする のようなフォールバック処理を書くことで、「あ〜、こういうときに処理が低速になるのか」と体感できたりします。

こうしたローレベルの工夫によりデータベース製品は性能や品質を実現しているという理解が得られました。

Vimをhex editorとして使う

作成するデータベースは終了時にfileにデータをdumpします。dumpされたデータファイルはただのバイナリファイルなので以下のようにして中を覗くことができます。

$ vim -b test.db

:%!xxd

f:id:ohbarye:20200417161306p:plain
insertしたデータがちゃんと入っていることや、想定したレイアウト担っていることが一応確認できる

パイプを使ったREPLのテスト

これも本筋ではないのですがREPLのテストをパイプを使っているのを初めて見たので学びがありました。

def run_script(commands)
  raw_output = nil
  IO.popen("./db test.db", "r+") do |pipe| # REPLを起動し、そのプロセスに繋がるpipeが得られる
    commands.each do |command|
      begin
        pipe.puts command # pipe越しにinsertとかselectのSQL commandを流し込む
      rescue Errno::EPIPE
        break
      end
    end

    pipe.close_write

    raw_output = pipe.gets(nil) # REPLプロセスの出力 (文字列) を得る
  end
  raw_output.split("\n")
end

it 'inserts and retrieves a row' do
  result = run_script([
    "insert 1 user1 person1@example.com",
    "select",
    ".exit",
  ])
  expect(result).to match_array([
    "db > Executed.",
    "db > (1, user1, person1@example.com)",
    "Executed.",
    "db > ",
  ])
end

さらなる課題

B Treeやデータベースの知識がつくと以下のような実践的記事や実製品の解説が力強く読めるようになりました。

また、まだ実践していないのですが次のステップとしていくつか良さそうな教材があるのでメモしておきます。

*1:正確にはインデックスにはB-Tree、テーブルにはB+ Treeが使われている

「Coinhive 事件」についての意見書を書きました

高裁で逆転有罪になった「Coinhive 事件」についての意見書を書きました。なお、先に自分の意見は「同件にて被告の無罪を望む」側だということをはじめにことわっておきます。

意見書って何?

コインハイブ事件弁護団主任弁護人の平野先生のコメントを引用します。

このたび、ウェブやセキュリティ関連企業をはじめ、IT業界でご活躍の皆様に、意見書の執筆をお願いいたしたく存じます。意見書は上告趣意書と合わせて最高裁判所に提出します。

目的は「業界内の声を直接届けること」です。高裁判決に示された規範が先例となってしまうとどのような不利益が生じるか、不正指令電磁的記録があいまいに解釈適用されていくことがどれほどソフトウェアの開発を萎縮させるか、現場や経営の立場から、実情をもとにご意見をお寄せいただければと思っています。

詳しくは引用元のページをご覧ください。

www.hacker.or.jp

「業界内の声を直接届ける」役割に微力ながら貢献できればと思います。

締め切りはWebのフォーム提出が2020年4月1日午前0時まで。原本送付はこの2週間後までです。

やり方

手順は池澤あやかさんのNoteの記事を参考にしました。

note.com

だいたい同じですが自分の場合は以下のやり方で進めました。

  1. 意見書の提出フォームからWordファイルをダウンロード
  2. 手元のMacで内容を編集
  3. 印刷して署名。捺印不要とのことで捺印なし。これが原本となる
  4. スキャンしてPDF化。
  5. 意見書の提出フォームにPDFを送付
  6. 原本を封筒に入れ、ハッカー協会指定の住所に送付。切手はコンビニで購入してそのまま貼付けた

意見書の記述から発送までトータルで90分ほどかかりました。

感想

正直言うと意見書募集を見かけてからこれまで、手順がちょっと面倒なので後回しにしていました。あまりふだんしないタイプの作文をしたり原本を送付したり…。

僕と同じように高裁の判決に異議を持っていたり、モロさんを応援したいと思いつつも意見書の作成・提出を億劫だと感じている人も少なくないと思いますが、少しの手間を惜しんだことを最高裁判所で望まない結果が出た後で後悔するよりは遥かにマシだと自分は思いました。

繰り返しますが締め切りはWebのフォーム提出が2020年4月1日午前0時まで、原本送付はこの2週間後までです。思い立ったら後回しにせずに行動されると良さそうです。

参考にした記事/サイト


最後に参考までに自分の意見書の内容を貼っておきます。

法に疎いので頓珍漢なことを書いているかもしれませんが自分の意見として以下を主張しました。

  • 高裁の判決に反対であるということ
  • その理由は判決が刑法の拡大解釈であり、このような解釈がまかり通るとソフトウェア産業のみならず公益を害するような影響も起こり得るという予見

意見書の内容

私は日本国内にて勤務するソフトウェアエンジニアです。業務においてはWebサービスを開発し、趣味でもWebサービスの制作やオープンソースソフトウェアの開発に携わっています。業務・趣味のどちらにおいてもJavaScriptを用いています。

私自身はCoinhiveを使ったことはないのですが、報道で一連の事件を知り、自分の関わる業務および業界の行末と無関係ではないと考え、意見を申し上げます。

弊社の製品のみならず2020年の一般的なWebアプリケーションにおいてはごく一部を除いてJavaScriptが使用されております。警察庁のWebサイト https://www.npa.go.jp/最高裁判所のWebサイト https://www.courts.go.jp/saikosai/index.html においても使用されております。こうした状況においてCoinhive事件が有罪となった時に与える社会的な影響について述べます。

刑法第168条の2の中にて「閲覧者の意図に反する動作をさせるもの」(反意図性が認められるもの)が不正指令電磁的記録にあたると定義されていますが、Webサイトの閲覧者が訪問先のWebサイトにて供されるJavaScriptの動作を意図することは私のようなソフトウェアの専門家でも極めて困難です。

例えば上記サイトの警察庁のWebサイトに私が「台風19号の情報を知りたい」という意図で訪問したとします。するとGoogle AdSense(オンライン コンテンツから収益を得ることができるサイト運営者向けのサービス)に関するJavaScriptファイル( https://cse.google.com/adsense/search/async-ads.js )がWebブラウザに読み込まれ、訪問履歴が第三者に提供されます。これは訪問者の利便性とサイト運営者の収益を両立させるための仕組みではありますが、不正指令電磁的記録の拡大解釈をすれば、私の意図とは反する挙動をするため反意図性があり、また、この同Webサイトにアクセスした私のコンピュータ資源を私に断ることなく利用して収益を得ている可能性があるので反社会性がある、不正指令電磁的記録にあたると指摘をすることができます。高裁による判決はこのようなものであったと私は理解しております。

しかしこのようなことはあってはならないと私は考えます。上述のようにWebサイトにて動作するJavaScriptの大半は閲覧者の利便性とサイト運営者の収益・事業継続などを両立させるための仕組みとして存在しているからです。意図しない動作をしたからといって、コンピュータ資源を断りなく使用して収益を得たからといって、こうした仕組みを廃する方向に舵を切れば現代のWebサイトの大部分はその用を成すことができなくなり、ソフトウェア事業の多くが立ち行かなくなります。選挙や災害等の国民の生活に必要な情報をWebサイト等を通じて届けることもできなければ、COVID-19における感染拡大防止のために自宅で業務を行なったりオンライン学習したりすることもできません。私が従事する産業だけの問題ではなくなると考えております。

最後に私の意見をまとめます。

・刑法第168条の2および3の恣意的な運用、曖昧な基準や拡大解釈による処罰の横行について強く反対する ・犯罪とされる行為の内容、及びそれに対して科される刑罰を予め、明確に規定していない状態でのCoinhive有罪判決は罪刑法定主義に反すると考える ・これらの刑法が制定された本来の目的である悪質なコンピューターウイルスやハッキングの取り締まりに立ち返るためにも、最高裁では適正な判決を望む

以上

Quipperを"退職"ります

近況報告です。4年7ヶ月勤続したQuipperを"退職"ります。

退職エントリですか?

これは退職報告ではありますが、一般的に期待されるような"退職エントリ"ではない気がしています。もう少しちゃんとしたやつは後ほど書くつもりです。

@ohbarye です。"広島の粗大ごみ"と呼ばれることもあります。

在職中はRuby on Railsによるサーバサイド、React, TypeScriptによるフロントエンドを中心に学習サービスの開発をやってきました。長らくBtoC領域に携わっており、登録導線や決済システムやカスタマーサポート周りの開発・運用経験が長いです。また、必要に応じてスクラムマスターのような動きもした時もありました。

ここ2年半ほどは上記のような動きをしつつ、Engineering Managerとして組織設計・採用プロセス整備・評価制度策定・表出した組織課題への対応・会社主催のイベント運営などを行いました。その一環としてEngineering Manager Meetupというイベントを主催したりEOF 2019の運営にも携わらせてもらいました。

会社ブログにそこそこ記事も書きました。

あとは興味ある方向けに諸々のアカウントを貼っておきます。

正確な最終出社日と退職日

  • 最終出社日 2020-03-31
  • 退職日 2020-05-25

from / to

  • from Quipper
  • to 無職

!?

次は何をするのか

ワーキングホリデー制度を利用して6月頃からドイツへ移り、欧州を旅したり現地の企業でエンジニアとして働こうと計画しています…いや、いました。

そう、"計画"してきたのですが…。昨今の"COVID-19 -コロナウイルス-"の影響がさすがに無視できなくなってきました。

f:id:ohbarye:20200309012510p:plain
2020-03-09時点

Coronavirus Update (Live): 5,041,620 Cases and 327,062 Deaths from COVID-19 Virus Pandemic - Worldometer

もともとは4,5月にぼちぼちオンラインで面接を受け始め、5月後半にドコミに行きつつ旅行がてらオフサイトの面接を受けられれば良いなと思っていました。

が、COVID-19起点で影響を受ける要素がいくつかあり、これらに関して数カ月先の状況が読めなくて悩んでいます。

ビザ関連の特別措置

発行済みの査証(ビザ)が無効になる事例が既にいくつかあります。

日本が中韓に対して行なったビザ無効化は大きく話題になりました。しかし受け入れだけでなく日本から出ていくほうにも制限がかかってきています。インドが日本人向けのビザを無効にする対応を取ったり、複数カ国からのビザの免除停止も始まっています。

日本からの入国制限、27カ国 新型コロナ感染に各国警戒:時事ドットコム

タイミングによっては日本の現在居住する物件を引き払った直後などに無効化されたりすると辛いですね…。

入国規制

永遠に入国拒否というのは今の現時点では無さそうですが既に各国が行なっているように14日間の隔離措置が取られる可能性はあります。

隔離措置を行っていない別の国を経由して入国するなどの方法はありそうです。

欧州のアジア人差別

人間の感情や行動をコントロールすることはできない…。

いよいよドイツもパニックか 買い占めにアジア人差別 日本人も被害に | ワールド | 最新記事 | ニューズウィーク日本版 オフィシャルサイト

現地の肌感覚がまったくないのでどの程度なのかわからない、というのが不安要素です。

現地で一次情報が手に入らない / 入りにくい

これは現地に住む時点で避けようのないことなのですが、緊急性の高いニュースがドイツ語で発信されたときに即座にキャッチアップできないという問題があります。まぁこれぐらいはその国を選んでいるんだから頑張れという話ではありますが…。

少なくとも英語で手に入る情報にある程度速報性があれば助かりますがどうなんでしょうね。

とりあえず今は

外務省 海外安全ホームページ|現地大使館・総領事館からの安全情報 詳細を見たり、ドイツ在住のYouTuberからの情報発信を見たりしています(これはN=1だ…と言い聞かせつつ)


まあ個人的には個々の問題はそれなりに対処していけるのではないかという楽観もあるのですが、被扶養者も同行するので家族の安心安全を守れる状況かというと断定はできないので悩んでいる状況です。このまま行くと退職後は無職です。

もしドイツ現地にお住まいで情報提供いただける方、もしくはそのような知人をお持ちの方がいたらご紹介いただけると大変ありがたいです。ご相談させていただくかもしれません。

渡独を断念する場合は機を見つつ国内で活動していく…かもですが今のところなんとも言えません。

いずれにせよ年齢制限によるビザの申請期限があるので5月末ぐらいまでには決めるつもりです。

もっとこう…あるだろう!

現職に関する思い出などについては別途、遅くとも退職日までには感情のこもった記事を書き上げたいと思います。

react-use-kana v2.2.0 ユーザーの入力文字列からひらがなを抽出する

react-use-kanaという、フォームでのよみがな入力支援のための npm package の内部実装をガッと書き換え、入力文字列からよみがなを抽出する処理を自前で書いてみた。

背景

同 package を作って公開したことは以前にも記事に書いた。

ReactのContextとHooksで日本語のふりがな入力を支援するコンポーネント書いた - valid,invalid

ReactのuseStateで日本語のふりがな入力を支援するhook書いた - valid,invalid

2019年9月に publish した際には React hooks としてのインタフェースだけ提供し、入力文字列をひらがなに変換する処理は historykana という package に移譲していた。しかしある時から、同 package の依存する別 package に脆弱性が見つかり security alert が GitHub repository 上に表示され続けてしまっていた。

f:id:ohbarye:20200308194537p:plain
忌々しい黄色の帯

放置するのも精神衛生上よくない(と言いつつ半年近く放置したが…)ので入力文字列をひらがなに変換する処理を自前で書いてみることにした。

f:id:ohbarye:20200308193917g:plain
動くようす

実装

基本的なコンセプトとしては「入力文字列と直前の入力文字列を比較して"ひらがな"と"ひらがな以外"の対応表を作る」という方針。

たとえば"やまだ"という入力の次に"山田"と続けば{ "山田": "やまだ" }と記憶しておき、この対応表を使って入力文字列をひらがなに置換する。

react-testing-library を使って書いた hooks の test case を見るとイメージが伝わりやすいと思う。実際にユーザーが入力する順をエミュレートしている。

describe('when several kanas are converted to kanjis at once', () => {
  test('returns kana based on user input', () => {
    const { result } = renderHook(() => useKana());

    expect(result.current.kana).toEqual('');
    ['や', 'やm', 'やま', 'やまd', 'やまだ', '山田'].forEach(value => {
      act(() => {
        result.current.setKanaSource(value);
      });
    });
    expect(result.current.kana).toEqual('やまだ');
  });
});

これだけであれば実装はかんたんに見えるし実際にそう思ったのだが、"ひらがな"と"ひらがな以外"が混在するケースなどもあり、その場合にどの文字とどのひらがなが対応するのかを判別するロジックを一般化するのに苦労した。

変換パターンの組み合わせ

組み合わせを整理してみると 16 patterns ほどあった(変換前と変換後それぞれにひらがなと非ひらがなが含まれるかどうか? 2 ** 4 = 16)。とはいえ、すべてに対応する実装が逐一必要なわけではなかった。一般化すれば2通りの実装でおおよそをカバーできた。

f:id:ohbarye:20200308204750p:plain
組み合わせを整理した表

漢字の連続変換

厄介だったのが '山だ' => '山多' => '山田' のように非ひらがな(この場合は漢字)の変換を繰り返すケース。直前との差分では漢字同士なのでひらがなとの対応表を作れないのだが、{ "田": "だ" }のように以前に入力されたひらがなと対応づけたい。この問題は、「直前に発生したひらがな => 非ひらがなの変換で使用した事前の入力(前述の例では"山だ")」を保存しておき、再利用して比較することで対応した('山だ' => '山田'と変換されたとして対応表を作る)。

できないこと

フォームに値を直接貼り付ける場合など、ひらがなを知り得ない状況では当然ながらひらがなを導出できない。

また、おそらく必要ないと思うが、 "山田"を貼り付けたあとにひらがなの"やまだ"に変換しなおすようなケースも実装を省いている。

感想

security alertが消えてすっきりした。

ところで、React時代に皆さんがどのようによみがなの自動入力機能を作っているのか気になっている。もしかして、令和ではもう誰もよみがなを入力させるようなフォームなんて作っていない…?

PostgreSQL docker imageがPOSTGRES_PASSWORD未設定だと起動しない

一次情報は https://github.com/docker-library/postgres/issues/681 参照。

事象

PostgreSQL公式のdocker imageが2020-02-15 07:00 JST頃に更新され、(おそらく) 9.4以上すべてのlatest minor versionで、PostgreSQLが起動せずにexitすることがある。エラーメッセージは以下。

code:shell
   Error: Database is uninitialized and superuser password is not specified.
          You must specify POSTGRES_PASSWORD for the superuser. Use
          "-e POSTGRES_PASSWORD=password" to set it in "docker run".
   
          You may also use POSTGRES_HOST_AUTH_METHOD=trust to allow all connections
          without a password. This is *not* recommended. See PostgreSQL
          documentation about "trust":
          https://www.postgresql.org/docs/current/auth-trust.html
   
   Exited with code 1

このエラーメッセージは https://github.com/docker-library/postgres/blob/05ac2d3ba69fc62c7acf6f689d3a93e5e862f738/docker-entrypoint.sh#L112-L119 から来ている

発生条件

発生条件は以下のいずれかにあてはまる場合

  • POSTGRES_PASSWORD に長さ1以上の文字列を設定していない
  • POSTGRES_HOST_AUTH_METHOD: "trust"を設定していない

理由

セキュリティ向上のため。変更が入った https://github.com/docker-library/postgres/pull/658 に詳しい。password未設定の場合は誰でもアクセスできてしまうので、それを防ぐため。

MySQLのdocker imageはdefaultではpasswordが必須になっているのでそちらと合わせるとのこと。

対応

以下のいずれか。エラーメッセージにあるように公式の推奨は1。

  1. POSTGRES_PASSWORD に長さ1以上の文字列を設定する (空文字だと [ -z $POSTGRES_PASSWORD ] で未設定と判断される)
  2. POSTGRES_HOST_AUTH_METHOD: "trust"を設定する (MySQLでいう MYSQL_ALLOW_EMPTY_PASSWORD に該当する)
  3. latest minor versionではなく過去のversionを指定する 過去のversionのimmutable identifierを指定する

本番でこのimageを使い、かつpassword設定してない人はほとんどいないと思いますが、CIでPOSTGRES_PASSWORD指定せずに立ち上げて落ちる人はそこそこいるのではないかと思います(というか僕も落ちたので気づいた)

postgres:9.6のようにmajor versionまでしか指定してない場合、minor versionはpullするtimingで変わるので、まさに「何もしてないのに壊れた」ように見えそうです。

(2020-02-17 追記)

同僚の @mtsmfm さんから指摘いただいたので追記。

  • docker image のありとあらゆるタグは上書きされる可能性があり、patch まで指定していても更新されることはよくあります
  • なんでも上書きされる可能性があるのは git もそうなんですが、docker image はかなり雰囲気でどんどん更新されます
  • 本当に厳密にやりたければ一応コミットハッシュみたいなので指定する方法があります https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier

(2020-02-17 追記おわり)

Now.sh v1 to v2 & Next.js v7 to v9 アップグレード記

Goofiの利用しているNow.shをv1からv2に移行し、その過程でNext.js versionも7.0.2から9.1.1へupgradeした。 Now.shもNext.jsもしばらく全く追いかけていなかったのでやや苦労した。

変更内容は https://github.com/ohbarye/goofi/pull/172

背景: Now v2

もはや一年以上前のことだが、Now.shが2.0になったという発表があった。

2018年の夏頃に自分が開発していたGoofiはNow v1を利用していたので「差分はよう知らないけどそのうち移行しなきゃ」ぐらいに捉えていた…が、結果1年ぐらい放置してしまっていた。

最初はnow.json*1をちょっと書き換えたら動くやろと思って、特に何のリファレンスも見ずにNow.shをupgradeしてみた。localでPaaSの挙動は試せないと思っててきとうにproductionでもエイヤっと上げてしまったがそこからが大変だった。

Next.js upgrade

まず、serverプロセスが立ち上がらない。Now.shのdashboard on webを見るとクラッシュしている。調べてみたところ当時のGoofiの構成(以下)はNow v2ではもはや動かないとのこと。

  • Next.jsとExpress.jsを組み合わせ、serverとしてのプロセスをNow platform上で立ち上げる
  • initial requestでSSRしてresponseを返す
  • 以降はAPI serverとして振る舞う

v2声明文にあるようにNow v2はserverlessを志向しており、利用者のコードを静的ファイルに変換する、またはリクエスト毎に実行可能なserverless functionに変換し、そのplatform上にdeployする。

Behind the scenes, Now 2.0 works like an extensible build system and compiler, capable of transforming your sources into static files and serverless functions (lambdas) for production.

つまりserverプロセス立ち上げっぱなしというのがそもそもNow v2の思想からは外れていそうだということ。

幸い、というか必然*2にしてNext.jsもv8からserverless対応しているので、platformの変化に合わせてupgradeしてやれば良さそうだった。

pages/ directory以下にページごとの実装を書いていくのはNext.js v7までと同じだが、v8からはserverless modeというのが追加されており、pages/以下のファイルがserverless functionに変換可能になる。

The serverless target will output a single lambda per page. This file is completely standalone and does not require any dependencies to run:

また、v9からはAPI Routesという機能が追加されており、pages/api/ directory以下に以下のようなfunctionを書くだけでserverless functionになる。面白い。

import { NextApiRequest, NextApiResponse } from "next";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { id } = req.query;
  const result = await doAsyncSomething({ id });
  res.setHeader("Content-Type", "application/json; charset=utf-8");
  res.send(result);
};

f:id:ohbarye:20191208005021p:plain
Next.jsのレールに乗ってserverless functionなどがbuildされるようす

Next.jsはこのようにしてv9までupgradeしていった。

その他、細かい課題と変更

大きくはNext.jsのupgrade対応に時間を費やしたのだが、その他にも諸々の変更が必要だったので思い出せるものをメモしておく。

TypeScript対応

Next.js v9からはTypeScriptがデフォルトでサポートされ、@zeit/next-typescriptが不要になっていた。このあたりも多少のコード修正は必要だったがTypeScript化の恩恵を感じて体験が良かった。

publicRuntimeConfigが利用不可

next.config.jsに書いていたpublicRuntimeConfigがNow v2では利用不可になっていた。

https://github.com/zeit/next.js/blob/master/errors/serverless-publicRuntimeConfig.md

実行時ではなくbuild時に値を埋め込むことは可能なのでその方向で修正し、値にはprocess.env.*でアクセスするようにした。

https://nextjs.org/docs#build-time-configuration

Expressやめる

上述の通り、serverless modeではExpressサーバプロセスを起動しっぱなしにすることはできない。 Expressサーバに実装していたAPI endpointsは pages/api/ に処理を移した。

npm script

Expressを使っていた時はyarn devでlocal開発を開始していたが、next commandを使うことになった。

また、productionでサーバプロセスを起動することはないので用意していたyarn startは不要になった。

// package.json
  "scripts": {
-    "dev": "node ./server/index.js",
+    "dev": "next",
    "build": "next build",
-    "start": "NODE_ENV=production node ./server/index.js",
    "type-check": "tsc"
  },

apollo-server-express to apollo-server-micro

これもExpressやめたことに伴う対応。 apollo-server-expressではなくapollo-server-microを利用するようにした。

microもZEITによるOSS。よくわかってないけどGitHubのREADME読む感じ、ZEITの志向性がバリバリ出ている。

x-now-deployment-url

Nowはデプロイ時に一意となるIDを発行し、それをURLに利用している (例: goofi-3ejlofj11.now.sh)。productionはgoofi.now.sh固定で良いがstagingのURLは毎回変化するため、この値を動的に取得しないと、SPA側のクライアントからAPI serverへのリクエストを行えない。

Now v1はで環境変数からprocess.env.NOW_URLのように取得できたが、v2からはrequest headerにあるx-now-deployment-urlを参照しなければいけない。

class MyApp extends App<Props> {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    const nowUrl = `https://${ctx.req.headers["x-now-deployment-url"]}`;

    return { pageProps, nowUrl };
  }
}

deprecation warning対応

build時または実行時のログにいくつかのdeprecation warningが見えた。 エラーログにURLが含まれており、リンク先に理由と対処法が書いてあったのでそのとおりに対応した。

https://github.com/zeit/next.js/blob/master/errors/static-dir-deprecated.md https://github.com/zeit/next.js/blob/master/errors/app-container-deprecated.md https://github.com/zeit/next.js/blob/master/errors/no-document-title.md

Now v1 => v2移行は不可逆

一番焦った問題はNow v2にあげてしまうとv1には戻れないという点で、上記の対応をしている間Goofiはずっと止まっていた…。

Unable to deploy to 1.0 · Issue #1805 · zeit/now · GitHub

Goofiは個人開発の趣味アプリケーションなので良かったが、他にも困っている人がおり、うっかり本番で上げてたら死んでた…。

所感

手数が多くて大変だったものの、(特にZEITが志向する)serverlessのパラダイムシフトを感じるupgrade作業だった。

先日のJSConf2019 JPに行くまで知らなかったのだがJAMstackと呼ばれるアーキテクチャがあり、今回の対応もこれに追従するようなもの。

事前にこの対応をしたおかげか、同カンファレンスで行われたGuillermo Rauch*3によるBuilding and deploying for the modern web with JAMstackの内容がより立体的に感じられて良かった。

*1:Now platform上の挙動などを制御する設定ファイル https://zeit.co/docs/configuration/

*2:Now.shもNext.jsも開発元が同一のZEITだから

*3:the founder of ZEIT