retlat's blog

Dockerのポートバインドで使用するホストIPを指定する

普段はDockerを使った開発環境で、ホストとコンテナのポートをバインドして他の端末から繋いでいる
公衆無線LANの環境で作業する必要が出てきて、これ他の接続してるユーザーから見えてダメじゃんと思ったのでIPを縛るように変えてみる

とりあえず普段使う感じで確認
0.0.0.0 にバインドされているのがわかる

$ docker run --rm -d -p 80:80 nginx:alpine
$ docker ps --format "table {{.Image}}\t{{.Ports}}"
IMAGE               PORTS
nginx:alpine        0.0.0.0:80->80/tcp

リファレンスに書いてある通りホストのIPを指定する
これだとホストの外からはアクセスできない

$ docker run --rm -d -p 80:80 nginx:alpine
$ docker ps --format "table {{.Image}}\t{{.Ports}}"
IMAGE               PORTS
nginx:alpine        127.0.0.1:80->80/tcp

Docker Composeでも同じようにホストのIPを指定できるので、場所に応じて適切に使い分けたい

Array.prototype.mapとshallow copy

Arrayのdeep copyはどう作るのかなと思ってArray.prototype.map()を触っていたら、パッと思ったのと挙動が違うのでメモ
環境はSafari 13.0.5

まずArray.prototype.map()はMDNには以下のように記載されている

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。

ということで雑にcallbackFnで引数をそのまま返してみる

const a = [{a: 'aaa'}];
const b = a.map(v => v);

b[0].a = 'bbb';

console.log(a, b);
// Array (1)
//   0 {a: "bbb"}
// Array (1)
//   0 {a: "bbb"}

あれ? 新しいArrayが生成されるのに変数aの値も変わってる?
いや、Arrayとしては新しく生成されたけど、要素単位ではobjectだからshallow copyされてる?
ということでプリミティブ値のArrayで実験

const a = ['aaa'];
const b = a.map(v => v);

b[0] = 'bbb';

console.log(a, b);
// Array (1)
//   0 "aaa"
// Array (1)
//   0 "bbb"

変数aの方には影響が出なかったので、やっぱり要素がobjectだとそこだけはshallow copyみたい
ということは型の混ざるArrayだと?

const a = ['aaa', {a: 'aaa'}];
const b = a.map(v => v);

b[0] = 'bbb';
b[1].a = 'ccc';

console.log(a, b);
// Array (2)
//   0 "aaa"
//   1 {a: "ccc"}
// Array (2)
//   0 "bbb"
//   1 {a: "ccc"}

思った通りに混ざった
代入の仕様そのままだけど思い込みで違和感たっぷり

Laravel Mixのwebpack設定を出力する

Laravel Mixを使うとBabelのトランスパイルやSASSのコンパイルが簡単にできるけど、webpackが何してるかさっぱりなので設定を出力してみる
setup/webpack.mix.jsにAPIが書いてあったので以下の通りにすると出力される

const mix = require('laravel-mix');
const util = require('util');

mix.override(webpackConfig => {
  console.log(
    util.inspect(webpackConfig, false, null, true)
  );
});

IISでCache-Controlヘッダー出力

Vue.js + Vue Routerを普段使わないIISで配信する必要に迫られて、web.configでCache-Controlヘッダーの出力設定をしたのでメモ
Vue Routerはhashモードなのでrewriteはいらなかった

web.configのリファレンスはどこをどう探したらいいのかさっぱり分からない
ディレクトリごとの設定をまとめてできないのが不便

<configuration>
  <system.webServer>
    <defaultDocument enabled="true">
      <files>
        <clear />
        <add value="index.html" />
      </files>
    </defaultDocument>
    <httpProtocol>
      <customHeader>
        <add name="Cache-Control" value="no-cache" />
      </customHeader>
    </httpProtocol>
  </system.webServer>
  <location path="js">
    <system.webServer>
      <httpProtocol>
        <customHeader>
          <remove name="Cache-Control" />
          <add name="Cache-Control" value="max-age=86400" />
        </customHeader>
      </httpProtocol>
    </system.webServer>
  </location>
  <location path="css">
    <system.webServer>
      <httpProtocol>
        <customHeader>
          <remove name="Cache-Control" />
          <add name="Cache-Control" value="max-age=86400" />
        </customHeader>
      </httpProtocol>
    </system.webServer>
  </location>
  <location path="images">
    <system.webServer>
      <httpProtocol>
        <customHeader>
          <remove name="Cache-Control" />
          <add name="Cache-Control" value="max-age=86400" />
        </customHeader>
      </httpProtocol>
    </system.webServer>
  </location>
</configuration>

Nginxで静的ファイルをリバースプロキシキャッシュ

Nginxのリバースプロキシで他のサーバにあるファイルをキャッシュする

まずhttpブロックでキャッシュファイルを保存するパスとキャッシュのパラメータを設定する

http {
    proxy_cache_path /var/nginx/cache keys_zone=static_assets:1m max_size=1g;
}

keys_zoneが1MBで8,000個のデータに関する情報を保存できるので, 1ファイル100KBと仮定すると800MB位になる
そんなに大きくないのでmax_sizeを全て格納できそうな1GBにする

次にserverブロックにリバースプロキシの設定を記述していく

upstream resource_server {
    server resource.example.com;
}
server {
    location /(css|js)/ {
        proxy_pass http://resource_server;
        proxy_cache static_assets;
        proxy_cache_valid 200 10m;
        add_header X-Cache-Hit $upstream_cache_status;
    }
}

proxy_cache_pathの keys_zone で指定したzone名を proxy_cache に設定し, proxy_passに記述したサーバからのレスポンスを格納する
proxy_cache_validではレスポンスのステータスコードごとにキャッシュ時間の制御ができる
ここでは200が返って来たら10分間キャッシュするように設定している
キャッシュが使われたか等の情報は $upstream_cache_status 変数が持っているのでヘッダーとして出力すると確認できる
ドキュメントには値の意味が見当たらなかったが, Nginxのブログには書いてあった

AndroidからJDBC DriverでPostgreSQLに接続

ちょっと作りたいものがあるので, AndroidからDocker Desktop for Macで動かしているPostgreSQLにJDBC Driverで接続できるか実験

環境

  • Android Studio 3.5.2
  • Android Emulator 29.2.1
  • Nexus 5
  • PostgreSQL JDBC Driver 42.2.8
  • postgres:11.5 Docker image

実験用ソース

Android Studioの Start a new Android Studio Project から, Empty ActivityでMinimum API level 23なプロジェクトを作る
これに必要な分をちょっとだけいれたものを使う
まずはapp/build.gradleのdependenciesにドライバを追加する

dependencies {
    // 略
    implementation 'org.postgresql:postgresql:42.2.8'
    // 略
}

次に適当な接続してSQLを実行するコードを, MainActivity.ktに追加する

override fun onResume() {
    super.onResume()

    thread {
        val c = DriverManager.getConnection("jdbc:postgresql://10.0.2.2/postgres?user=postgres&password=password")
        val s = c.createStatement()
        val r = s.executeQuery("CREATE TABLE IF NOT EXISTS sample (id SERIAL PRIMARY KEY, number INTEGER)")
        r.close()
        s.close()
        c.close()
    }
}

ネットワークアクセスするのでパーミッションの記述をAndroidManifest.xmlに追加する

+ <uses-permission android:name="android.permission.INTERNET" />

Emulator と実機の比較

最近の端末を持っていないのでエミュレータを使うとして, 実機と同じように動作するかテストする
Android 6.0.1がインストールされたNexusとAPI 23のエミュレータで実行したところ, 同じように以下のExceptionが出力された
カスタマイズして出荷されている可能性があるので一概に言えないが, とりあえずエミュレータが実機と同じように動作すると判断する

E/AndroidRuntime: FATAL EXCEPTION: Thread-147
    Process: com.example.sample, PID: 2812
    java.sql.SQLException: No suitable driver
        at java.sql.DriverManager.getConnection(DriverManager.java:186)
        at java.sql.DriverManager.getConnection(DriverManager.java:144)
        at com.example.sample.MainActivity$onResume$1.invoke(MainActivity.kt:19)
        at com.example.sample.MainActivity$onResume$1.invoke(MainActivity.kt:8)
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

実験

いろいろなバージョンで試してみる
とりあえず上の実験でAPI 23はダメだったので, それ以降を順次試す
API 24, 25はクラスが見つからないのでダメ

java.lang.NoClassDefFoundError: Failed resolution of: Ljava/time/Duration;
 Caused by: java.lang.ClassNotFoundException: Didn't find class "java.time.Duration" on path: DexPathList[[zip file "/data/app/com.example.sample-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.sample-1/lib/x86_64, /system/lib64, /vendor/lib64]]

API 26から29はExceptionがthrowされているものの, SQLを実行した結果なのでOK

org.postgresql.util.PSQLException: No results were returned by the query.

追加実験 (追記: 2019/12/30)

API 24のExceptionはJava 8およびAPI 26で追加された java.time.Duration を使っているからダメなだけで, DriverをJava 7用にしたら問題なかったのではと思い立ったので実験
app/build.gradleのdependenciesを以下のように書き換える

  dependencies {
      // 略
-     implementation 'org.postgresql:postgresql:42.2.8'
+     implementation 'org.postgresql:postgresql:42.2.8.jre7'
      // 略
  }

他は変更せずに動かしてみるとAPI 26以降と同じ org.postgresql.util.PSQLException が投げられたので接続OK

結論

Android 7.0 NougatからJDBC DriverでPostgreSQLに接続できる
なおAndroid 7.0, 7.1 はJava 7用を使う必要があるが, Android 8.0 OreoからはJava 8用で構わない
ただし端末による

GAE/Go 1.12 の静的ファイル配信

ネット上でGoogle App EngineのGo 1.9やPython 2.7 Runtimeの記事を見かけたが, Go 1.12ではどうにも思った通りにhandlerが設定できないのでいろいろ実験した記録

達成したいのは http://sample.appspot.com/ へのアクセスに対し, app.yamlからの相対パスで public/index.html を返すこと
ドキュメントのサンプルに従えば以下のようにしておけばできるはず

handlers:
- url: /$
  static_files: public/index\.html
  upload: public/.*

- url: /(.*)
  script: auto

ここではscript: autoのプログラムは Response From Go と返し, index.htmlは Static File と記載しているものとする
デプロイしてアクセスしてみると Response From Go と表示される
おかしい…

デプロイしたものがおかしいのかGCPのウェブコンソールでApp Engineのバージョンページに行き, 設定を表示してみる

handlers:
- url: /$
  static_files: public/index\.html
  require_matching_file: false
  upload: public/.*
- url: /(.*)
  script: auto
- url: .*
  script: auto

デプロイしたものに require_matching_file: false が書き足されている
urlの正規表現がマッチしたら評価するが, 該当するファイルがなければ他のhandlerに処理を流すという要素なのだろうか…?
ドキュメントにもない要素でよくわからないので環境の方の調査をしてみる
Stackdriver Loggingでログを見てみると, /var/log/google_init.log に Waiting for network connection open. Subject:"nginx" Address:127.0.0.1:8080" と記述がある
app.yamlはNginxの設定ファイルに変換されていそうなので実験してみる

今回は static_files を指定していて url は正規表現パターンマッチになるため, NginxではLocationディレクティブの正規表現パターンマッチと考えられる
static_files はアップロードしたファイルパスの関連付けを行うためrootかaliasどちらかのディレクティブになるが, 正規表現でキャプチャした値を使用できるためaliasが該当する
そして実際の挙動と require_matching_file という名称から try_files でファイル指定と内部リダイレクトをしていると推測される

以上を元に以下のように変換した設定をDocker ComposeでNginxとGolangコンテナを使って実験してみる

http {
    location ~ /$ {
        alias public/index\.html;
        try_files $uri @fallback;
    }
    location ~ /(.*)$ {
        proxy_pass http://golang:8080;
    }
    location @fallback {
        proxy_pass http://golang:8080;
    }
}

App Engineと同じ出力になるため変換をしているという点は的外れではなさそう

意図しない挙動をした部分について踏み込んでみる
try_files には他に適当な変数が見当たらないためとりあえず$uriを設定したが, 実際の値がわからないためログに出してみると / になっている
変数を展開すると以下のようになることから, 該当するファイルが見つけられず @fallback に内部リダイレクトされているものと思われる

try_files / @fallback;

$uriも含めて変換が上記の実験通りであれば意図した通りの動作に設定できない気がするので, 結局 http.FileServer() で解決することにした
Nginxのaliasとtry_filesの部分を設定できるようになれば, App Engineのインスタンスを起動しなくてよくなるのに

LighthouseをDockerで動かす

Lighthouseでサイトの評価は見たかったものの, なんとなくChromeを端末にインストールしたくなかったのでコマンドラインで使ってみた
せっかくなのでコンテナとして動かしてみた
環境は

  • macOS 10.15
  • Docker Desktop 2.1.0.4

実践

LighthouseのCLIはnpmで提供されているので, とりあえず以下のDockerfileを使って

FROM node:10.17.0

RUN npm install -g lighthouse

こんなDocker Composeの構成にして

version: '3'
services:
  nginx:
    image: nginx:1.17.5
  lighthouse:
    build: ./
    tty: yes

使ってみる

$ docker-compose up -d
$ docker-compose exec lighthouse lighthouse http://nginx/
~~~ 略 ~~~
Runtime error encountered: The environment variable CHROME_PATH must be set to executable of a build of Chromium version 54.0 or later.
~~~ 略 ~~~

実行にはChromeが必要だったので, ChromiumをインストールするようにDockerfileを修正し

- RUN npm install -g lighthouse
+ RUN apt-get update && \
+     apt-get install -y chromium && \
+     npm install -g lighthouse

リトライ

$ docker-compose down
$ docker rmi ${ディレクトリ名}_lighthouse:latest
$ docker-compose up -d
$ docker-compose exec lighthouse lighthouse http://nginx/
~~~ 略 ~~~
  ChromeLauncher Waiting for browser....................................................................................................... +503ms
  ChromeLauncher:error connect ECONNREFUSED 127.0.0.1:34953 +1ms
  ChromeLauncher:error Logging contents of /tmp/lighthouse.luZObZw/chrome-err.log +0ms
  ChromeLauncher:error [23:23:1027/054400.061948:ERROR:zygote_host_impl_linux.cc(89)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.
  ChromeLauncher:error  +0ms

Chromeをrootで使う場合のオプション不足とエラーに出ているので追加してリトライ
ついでにGUIで起動しようとして失敗していると思ったのでheadlessのフラグも追加

$ docker-compose exec lighthouse lighthouse --chrome-flags="--no-sandbox --headless" http://nginx/
~~~ 略 ~~~
  ChromeLauncher Waiting for browser.....✓ +3ms
  config:warn IFrameElements gatherer requested, however no audit requires it. +171ms
  config:warn MainDocumentContent gatherer requested, however no audit requires it. +1ms
  status Connecting to browser +10ms
  status Resetting state with about:blank +17ms
  status Benchmarking machine +55ms
  status Initializing… +512ms
  status Resetting state with about:blank +44ms
  status Setting up network for the pass trace +15ms
  status Cleaning browser cache +4ms
  status Beginning devtoolsLog and trace +10ms
  method <= browser ERR:error Tracing.start  +2ms
  status Disconnecting from browser... +2ms
  ChromeLauncher Killing Chrome instance 22 +1ms
Runtime error encountered: Protocol error (Tracing.start): 'Tracing.start' wasn't found
Error: Protocol error (Tracing.start): 'Tracing.start' wasn't found
~~~ 略 ~~~

Chromeが起動して実行できたけどエラーが出る
検索してみるとDebianの問題とのこと
Node.jsのコンテナイメージがDebianベースなので, Ubuntuベースにした以下のDockerfileに切り替えて

FROM ubuntu:bionic-20191010

RUN apt-get update && \
    apt-get install -y curl ca-certificates && \
    curl -sL https://deb.nodesource.com/setup_10.x | bash - && \
    apt-get install -y nodejs chromium-browser && \
    npm install -g lighthouse

再チャレンジ

$ docker-compose down
$ docker rmi ${ディレクトリ名}_lighthouse:latest
$ docker-compose up -d
$ docker-compose exec lighthouse lighthouse --chrome-flags="--no-sandbox --headless" http://nginx/
~~~ 略 ~~~
  status Generating results... +1ms
  Printer html output written to /nginx_2019-10-28_20-00-00.report.html +121ms
  CLI Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser +0ms
  ChromeLauncher Killing Chrome instance 32 +0ms

正常終了したので結果のHTMLをコンテナから回収して見てみるとちゃんと評価できていた

$ docker-compose exec lighthouse cat /nginx_2019-10-28_20-00-00.report.html > ./report.html

感想

雑にDockerfileを書いた割にNode.jsのイメージよりコンテナが小さかったのは意外