Note

3年後の自分のために書いています

Docker で Windows 系 OS のイメージを使えるか?

急に気になったので調べた。

結論

ref. DockerのWindows対応についての整理#2-windowsベースイメージのコンテナを使うことができるか

元々のDockerはLinuxカーネルに依存したものでしたが、Docker Windows Containersの登場により可能になりました。

結論使えそう。その場合ホスト OS は Windows になるっぽい。(つまり Linuxカーネル共有みたいな感じで Windows カーネルを共有する)

Windows 系 OS のコンテナたち
-----
Docker Desktop for Windows の Engine
-----
Windows OS(Windows Server or Windows 10) ← ホスト OS

⚠️ 実際に試せておらず、ドキュメント読んで得た情報からの結論です

結論にいたるまでの軌跡

前提(という名の勘違い)

Docker は Linux コンテナという機能により、プロセスやユーザを隔離することで、アプリケーションをあたかも別のマシンで動かしてるようにみせており、ホスト OS のカーネルを共有するので、個別にゲスト OS を一から立ち上げる必要がないためホスト型仮想化 OS に比べて起動が速いのがメリット、その代わりに Linux 系の OS(UbuntuCentOS)のイメージしか動かすことができないのがデメリットと思ってた。

つまり Windows 系 OS のイメージは Docker で動かせないと思ってた。

調べた

大体以下の辺り読んだ。

Windows の土地勘がなく、ドキュメント読んでもわからなさすぎたので途中から Docker のソースコードを読もうとしてた。(結局イメージの生成部分だけ読んで満足した)

結局何を最後まで勘違いしてたかというと、自分の Docker の知識が会社にあった Docker実践入門――Linuxコンテナ技術の基礎から応用まで という 2015 年出版の本をベースにしてたもので、この本では「Docker は Linux コンテナという機能を使って実現されてる」という風に書いてあり、Docker のホスト OS は Linux でないとダメと勘違いしていたが、この情報が古かった。

確かに以前は Linux コンテナ(LXC) を使っていたが、現在は Go で実装された libcontainer によってコンテナ仮想化が行われているらしい。 see. https://ja.wikipedia.org/wiki/Docker#コンテナ仮想化

だから Linux カーネルへの依存がなくなり、上記の結論のようなことが可能になってるのだと思う。(自信ないけど)

libcontainer の実装見た感じ、Windows 対応はまだできてなさそうに見えるので今のところ 👆 は因果関係ないかもしれない。(だとすると Docker Desktop for Windows では libcontainer を使わずに別のソフトウェアでコンテナ仮想化を実現してるのかな :thinking_face_google: )

GitHub Apps を作る

Conclusion

Probot 使えばけっこう簡単にできる。

Motivation

project-bot という Issue/PR 作成時などに GitHub Projects にカード追加してくれる GitHub Apps があるが、ラベル追加時の挙動が微妙(すでにあるカードを移動してくれるのみ)で 機能追加の Issue も立ってる のだけど、1 年ほど開発が止まってるので、自分で作ろうと思った。

作ったやつ

App page: https://github.com/apps/pj-card-bot

What is Probot?

GitHub Apps を作るためのフレームワーク。 TypeScript または JavaScript で開発することが可能で、Webhook 受信や GitHub API 実行を簡単に利用できる仕組みを提供している。

内部的には express を使ってるっぽくて、probot を利用してWebhook イベントに対応するための API サーバを構築するような感じ。

:memo: Memo

最初に作られるテンプレは create-probot-app/templates にある。

開発環境では Webhook リクエストを受け取るのに smee.io を推奨されるけど、ngrok 使っても動くと思う。どっちもローカルホストの Web サーバを外部に公開するための proxy 機能を提供してくれるやつ。

二段階認証してると app.yml を用いた manifest flow が上手く動かないっぽい ので、permission や subscribe するイベント周りの設定は手動でする必要あり。

see. https://probot.github.io/docs/development/#manually-configuring-a-github-app

Basic of GitHub Apps

大体以下の流れ。

  1. GitHub 上のイベント発生(PR を作る、ラベル追加など)
  2. Webhook URL として登録したエンドポイントにリクエストがくる
    • Subscribe してるイベント発生時のみ(アプリ作成時に設定する)
  3. Webhook リクエストの payload の中にいろんな情報(作成された PR、ラベル、それに紐づくリポジトリの情報など)が入ってるので、それを用いて GitHub API を叩く
  4. 作業が自動化できる :clap:

Note

  • 多分ちゃんと運用するとなったら本番用と開発用の二つの GitHub Apps を作って、権限設定など同じにして本番用の Webhook URL は Heroku などのデプロイ先、開発用は smee.io の URL にするのが良さそう
  • デプロイタイミングは作者の自由
  • 一度インストールしてもらえれば、途中でアイコンや description 変えても、何度デプロイしても、ユーザに通知が行くことはないが、permission 変えた時だけ変更リクエストがいき、ユーザの許可を求められる(そしてユーザが許可したかどうかは作者からは分からない)

References

XMLHttpRequest と fetch の違い

構文的な違いではなく挙動的に違いがあるのか知りたい。 特に、XMLHttpRequest にできて、fetch にできないことがあるのかが気になっている。

以下、XMLHttpRequest は XHR と略す。

結論

XHR にできて、fetch にできないことはほとんどないと言って良さそう。

XHR にあって fetch がサポートしてない機能もあるけど、他の API と組み合わせれば(コードが複雑になる可能性はあるが)再現可能。

ただ、IE では fetch は使えないので、そこだけ注意が必要。(その場合 polyfill 使う必要あり)

fetch と jQuery.ajax() の違い

MDN より引用。 jQuery.ajax() = XHR と読み替えてもよいと思う(ソースコードこの辺

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

fetch の仕様は jQuery.ajax() とは主に二つの点で異なっています。

  • fetch() から返される Promise は レスポンスが HTTP 404 や 500 を返して HTTP エラーステータスの場合でも拒否されません。代わりに (ok ステータスが false にセットされて) 正常に解決し、拒否されるのはネットワークのエラーや、何かがリクエストの完了を妨げた場合のみです。
  • 既定では、 fetch はサーバーとの間で cookies を送受信しないため、サイトがユーザーセッションの維持に頼っている場合は未認証のリクエストになります (cookie を送るには、認証情報の init オプションを設定しておく必要があります)。 2017年8月25日に、既定の認証情報のポリシーが same-origin に変更になり、 Firefox は 61.0b13 から変更しました。

:qmark: Ajax(XHR)の場合はいつでも cookie 送るということ? :amark: XHR では同一オリジンなら自動で cookie が送られ、クロスオリジンの場合は fetch と同じように xhr.withCredentials = true; が必要(構文はちょっと違うけど)、なので cookie に関する挙動は fetch とあんまり変わらないはず

「fetch xmlhttprequest difference」 でググった結果

XMLHttpRequest vs the Fetch API: What’s Best for Ajax in 2019?

👆 の記事が良さそうだったのでまとめてみる(他の記事は構文的な違いの言及ばかりだった)

以下、XHR を全て fetch に置き換えられない理由。

Browser Support

IE の全てのバージョンで fetch はサポートされていない。

Cookieless by Default

XMLHttpRequest とは異なり、Fetch のすべての実装が Cookie を送信するわけではないので、アプリケーションの認証に失敗する可能性があります。

って書いてあるけど、これは上述の通り 2017年8月25日 に fetch でも 既定の認証情報のポリシーが same-origin に変更になって同一オリジンには cookie 送られるようになったし、クロスオリジンに cookie 送るには XHR でも fetch でもオプションが必要なので、変わらないと思う。

Errors Are Not Rejected

404 Page Not Foundや500 Internal Server ErrorのようなHTTPエラーは、Fetch Promiseを reject する原因にはならず .catch() は決して実行されません。これは通常、response.ok ステータスが false に設定された状態で解決します。 reject はリクエストが完了できない場合にのみ発生します。

だから、fetch だとエラーハンドリングが複雑になるよ、という話。

Timeouts Are Not Supported

fetch はタイムアウトをサポートしていないって話。

Aborting a Fetch

fetch の中断、数年前までできなかった、けど今は AbortController 使えばできるとのこと。

No Progress

fetch では progress events をサポートしてないので、進捗状況が分からないって話。

XMLHttpRequest vs the Fetch API?

XHR の方が低レベルの API なので単純なリクエストなら fetch の方が扱いやすい。(XHR の場合はラッパー関数が必要になる場合が多い)

結論、用途によって fetch でも XHR でも好きな方選べばいいが、プログレスバーを要求するような IE のクライアントがある場合は fetch だとキツい。(一応 polyfill もある けど)

axios と fetch の違い

Axios or fetch(): Which should you use?

LogRocket が axios と fetch の比較記事を書いてたのでまとめる(axios は内部的には XHR 使ってる)

Response timeout

タイムアウトの話題、axios ならオプションつけるだけで簡単に設定できる。 こちらの記事では fetch でも AbortController と setTimeout 使えば実装できるよ、と書いてある。

Automatic JSON data transformation

JSON データの変換を axios なら自動でやってくれるけど fetch は response.json() しないといけないので一ステップ余分にかかるね、という話。

HTTP interceptors

HTTP インターセプタ機能が axios にはデフォルトでついてる。 fetch もグローバルな fetch メソッドを上書きすれば実装は可能。

Download progress

XHR にあった onprogress ハンドラが fetch にはないが、 fetch の場合も ReadableStream を使えば一応プログレスバーを実装可能。 axios の場合は他ライブラリと組み合わせればさらに簡単に実装できる。

Simultaneous requests

複数のリクエストを同時に行うには axios の場合は axios.all() メソッドを使用する。 fetch の場合も組み込みの Promise.all() 使えば可能。

Conclusion

上述の通り axios の機能は全て fetch でも再現可能なので、ネイティブ API にこだわるならば、fetch 使ってもいい。

結論、好きな方使ったらよい。

メモ

参考

Fetch API の仕様と各ブラウザの実装

なんだか急に気になったのでメモ。

仕様

IETF (RFC) とも W3C とも別の WHATWG なるコミュニティによるもの。

実装

多分ここだろう、という箇所たち。

それぞれレンダリングエンジンの中に実装があるっぽい。 HTML レンダリングエンジンっていうぐらいだから、HTML の解析とか(?)が主で HTTP 通信に関する実装はないだろうと思い違いしていた…。

:firefox: Firefox (Servo)

FirefoxGecko から Servo に置き換わりつつあるとのこと。

:safari: Safari (WebKit)

参考

Rails の API モードで静的ファイルをホスティングする

環境

結論

以下、二つの方法がある。

config.public_file_server.enabled を true にして public ディレクトリ以下に置く

静的な HTML ファイルを表示させたいだけなら config.public_file_server.enabled を true に設定した環境で public/index.html を作成すれば、ルーティングやコントローラを実装せずとも表示される。(config.public_file_server.enabled は development 環境などでは デフォルトで true

コントローラで render :file する

以下のどちらかをしないと API モードでは render :file できない。

  • コントローラの継承元を ActionController::API から ActionController::Base に変更する
  • ActionView::Rendering モジュールをインクルードする

説明

Rails ガイドより、

ref. https://railsguides.jp/asset_pipeline.html#アセットパイプラインの使用方法

アセットは引き続きpublicディレクトリ以下に置くことも可能です。config.public_file_server.enabledがtrueに設定されていると、publicディレクトリ以下に置かれているあらゆるアセットはアプリケーションまたはWebサーバーによって静的なファイルとして取り扱われます。

とある通り、config.public_file_server.enabled が true になっている場合は public ディレクトリに置かれているファイルは自動的にホストされる。

development 環境の config.public_file_server.enabled は以下のコマンドなどで確認できる。

$ rails r -e development 'p Rails.application.config.public_file_server.enabled'

参考

Rails の API モードでセッションを有効にする

巷には config/application.rb 内で Rails.app_class.config.api_only = false にすればできる、的な記事が溢れているが、不要な middleware まで読み込みたくない。

環境

差分

デフォルトの CookieStore を使う場合

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CookieStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end

config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware が必要というのが分からずハマった。

👆 しないでも _session_id は保存されたが、session[:user_id] などの情報が保持されなかった。(再度リクエストした時に取得できなかった)

Rails と Rack - Railsガイド

  • ActionDispatch::ContentSecurityPolicy::Middleware

Content-Security-Policyヘッダー設定用のDSLを提供します。

とのこと。これを使うことで _session_id が暗号化(?) されていた。

api_only = false にすると {app_name}_session というキーに暗号化された値が入る(_session_id にも入るけど)

CacheStore を使う場合 ~memory_store の場合~

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CacheStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index dc78c00..cbbc46a 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -14,18 +14,11 @@ Rails.application.configure do
   # Show full error reports.
   config.consider_all_requests_local = true

-  # Enable/disable caching. By default caching is disabled.
-  # Run rails dev:cache to toggle caching.
-  if Rails.root.join('tmp', 'caching-dev.txt').exist?
-    config.cache_store = :memory_store
-    config.public_file_server.headers = {
-      'Cache-Control' => "public, max-age=#{2.days.to_i}"
-    }
-  else
-    config.action_controller.perform_caching = false
-
-    config.cache_store = :null_store
-  end
+  # Enable caching.
+  config.cache_store = :memory_store
+  config.public_file_server.headers = {
+    'Cache-Control' => "public, max-age=#{2.hour.to_i}"
+  }

   # Print deprecation notices to the Rails logger.
   config.active_support.deprecation = :log

Rails.app_class.config.cache_store に何も指定しなければ、デフォルトの :file_store になり、 path には "#{root}/tmp/cache/" が使われる。(Rails.app_class.config.cache_store = :file_store と書いた場合は明示的に path の指定が必要になる)

Redis を使いたい場合はここを :redis_cache_store にすれば OK。

api_only の設定による middleware の差分

Rails.app_class.config.api_only が true の場合と false の場合の差分。

$ rails middleware で確認できる。

false の方がたくさん middleware が使われている。

--- tmp/api_true.txt 2020-05-05 05:16:23.000000000 +0900
+++ tmp/api_false.txt 2020-05-05 05:18:06.000000000 +0900
@@ -3,6 +3,7 @@
 use ActionDispatch::Static
 use ActionDispatch::Executor
 use Rack::Runtime
+use Rack::MethodOverride
 use ActionDispatch::RequestId
 use ActionDispatch::RemoteIp
 use Rails::Rack::Logger
@@ -12,7 +13,12 @@
 use ActionDispatch::Reloader
 use ActionDispatch::Callbacks
 use ActiveRecord::Migration::CheckPending
+use ActionDispatch::Cookies
+use ActionDispatch::Session::CookieStore
+use ActionDispatch::Flash
+use ActionDispatch::ContentSecurityPolicy::Middleware
 use Rack::Head
 use Rack::ConditionalGet
 use Rack::ETag
+use Rack::TempfileReaper
 run RailsCsrfProtectionApi::Application.routes

セッションストレージについて

Rails 的には CookieStore 推奨っぽい

あらゆるセッションは、セッション固有のIDをcookieに保存します (注意: RailsでセッションIDをURLで渡すことはセキュリティ上の危険があるため許可されません。セッションIDは必ずcookieで渡さなくてはなりません)。

多くのセッションストアでは、このIDは単にサーバー上のセッションデータ (データベーステーブルなど) を検索するために使われています。ただしCookieStoreは例外的にcookie自身にすべてのセッションデータを保存します(必要であればセッションIDも利用できます)。そしてRailsではCookieStoreがデフォルトで使われ、かつRailsでの推奨セッションストアでもあります。CookieStoreの利点は、非常に軽量であることと、新規Webアプリケーションでセッションを利用するための準備がまったく不要である点です。cookieデータは改竄防止のために暗号署名が与えられています。さらにcookie自身も暗号化されているので、内容を他人に読まれないようになっています。(改ざんされたcookieRailsが拒否します)

以下も参考になりそう。

Rails セキュリティガイド#セッションストレージ

参考

WebRTC + Firebase のチュートリアルやってみた

See. https://webrtc.org/getting-started/firebase-rtc-codelab

ほぼ写経しただけ、👆 のチュートリアルの通りに書いても動かなかったので、サンプルリポジトリ の方を参考にした。

成果物

ブラウザで動くシンプルな 1 対 1 のビデオ通話アプリケーション。 チュートリアルではローカルでの動作確認までしか書いてないけど、せっかくなので Firebase Hosting にデプロイまでした。

使い方

  1. Open camera & microphone でカメラとマイクを許可
  2. Create room を押下すると Room ID が発行される
  3. 別の PC で Join room を押下し、Room ID を入力すると通話ができる
  4. Hang up を押下すると通話終了し、その Room ID は使えなくなる

同一 LAN 内じゃないと動作しなかった :sob:

いろいろ試した結果以下のような結果になった。

最初は OS やブラウザのせいかと思っていたが、途中からもしかしてネットワーク周りが原因では?と思い、最終的に家用 Wifi ルータとモバイルルータ使って LAN を分けて試した結果、Mac + Chrome x Mac + Chrome 環境でも LAN が違うと通信できないことが分かった。

現状 Google が提供している STUN サーバなるものを利用しているが、LAN 外のホストと通信するには TURN サーバなるものを用意しないといけないかもしれない。

ref. そして壁の向こうへ。 NAT/Firewallを越えて通信しよう―WebRTC入門2016

STUNでNATを越えられないとき

NATにはグローバルIPアドレスを共有するだけでなく、セキュリティ対策としての役割もあります。内部の端末を隠したり、通信できるポートを制限したり、一種の簡易Firewallとして利用されているケースもあります。その場合はFirewallの場合と同じく、次に説明するTURNを利用する必要があります。

またNATの構造によっては、接続先によって(今回の場合、STUNサーバーとPeer-to-Peerの通信相手)別のポートが割り当てられる Symmetric NAT という物があるようです。この場合もSTUNの仕組みでは通信することができません。やはりTURNの出番ということになります。

メモ

  • PC(Mac)同士だと動いたけどスマホiOS)では相手の画面が表示されない不具合が出た
  • Firestore は Room ID と offer の管理に使ってるだけ、あとはほぼ JS から呼べる MediaDevices と WebRTC の API で実装できてる
  • ICE と SDP あとで調べる

参考