Swift で三次関数の実数解を求めるやつを書いていた。

概要

  • https://github.com/dnpp73/CubicEquationSolver

  • 2018年の10月の頭には見られるようにしてたんだけど忙しさを言い訳にしてブログに書いてなかったので整理した。

  • まさか三次関数のソルバを自前で実装する必要に迫られるなんて思ってもみなかったよ…。今でもそう思ってる。

詳細

動作が分かりやすいようにサンプルアプリを書いた。

Before

何に使うの。

よくあるアニメーションの EaseInEaseOut と似たような機能を作る必要があったのだけど、イージング関数として一般的に使われる

  • 始点が (0.0, 0.0)
  • 終点が (1.0, 1.0)
  • コントロールポイントが 2 つ

という三次ベジェ曲線で表現することにした。

コントロールポイントの座標 2 つだけを与えれば、そこそこ自由にイージング関数を表現出来るので、なるほどこれは確かに便利だ、という納得感があった。

あったのだが、しかしベジェ曲線の定義をよくよく見返すと媒介変数 t によって (x, y) を表示する形になっているので、 t を消去して y = f(x) の形に整理しなくてはならず、その過程で t についての三次関数の解を求める必要があることに気付いてしまう。

一通りライブラリがないか探したもののちょうど良さそうな規模感のものが見当たらず、しょうがないから色々なものを参考にしつつ書いた。

シンプルに三次関数の実数解だけが Double の配列で返ってきて欲しい。

"swift Polynomial" などでググると一応見付かるは見付かる。

例えば madbat/SwiftMathPolynomial.swift などがあって、見た感じ四次関数のソルバまで実装されていたが、残念なことに Swift 2 の時代で止まっていたりした。

また、数学方面の方々に Swift で代数学入門 などの記事を書いておられる人もいて、これはめちゃくちゃ参考になって読んでて楽しかったのだけど、僕が求めているものは (a: Double, b: Double, c: Double, d: Double) という三次関数の係数 4 つの Double を引数に渡したら [Double] の配列が返ってくる func であって、Swift の綺麗な世界で数学の綺麗さを表現しよう!みたいなものではなくて、もっとこう、泥臭く、現場で使うから必要なんや…みたいな、そういう気持ちが高まってしまって…。

用途的に虚数解は必要がないですし、そもそも浮動小数点での表現ですし。

ちなみに三次ベジェ曲線と三次関数について

A Primer on Bézier Curves に全てが纏まっていた。なんかすごかった。

その文書の 16 - Finding extremities: root finding の章のオマケで三次関数の根をカルダノの公式とビエタの解で求める JavaScript 実装がある。僕が書いたものはこれの Swift 移植です。

実際にカルダノの公式を用いて三次関数を代数的に解こうとすると実数解しか持たない関数だったとしても計算過程で虚数が出てくるが、ビエタの解による幾何学的な手法を用いると計算過程から虚数を排除し三角関数と冪乗だけで解くことが出来る。

そして現代のプログラミング言語で三次関数のソルバを実装する場合でも、虚数を排除して実数の三角関数と冪乗のみで書けた方が楽であることは間違いない。その辺りは標準ライブラリで使える関数として提供されている訳だし。

カルダノ から 450 年、 ビエタ から 400 年も後の時代に生きている我々からすれば、三角関数だろうが複素平面だろうがやってることは変わらないっぽいなと学の無い僕にでもなんとなく理解できる気はする。気がするだけかもしれない。

数学史を垣間見るのも面白いですね。僕も門外不出の解法を使って酒場の計算バトルで負け無しのやべーやつになってみたい人生だった。

締め

CAMediaTimingFunction あたりに便利メソッドがきっと実装されてるやろ〜とか思ってたけど全然そんなことはなくて、まさか自分で三次関数のソルバを書くことになるとは思わなかったし、職業プログラマをやってるとたまにこういうことがあるんだなという話でした。

EdgeRouter X をアップデートしたら Philips Hue が HomeKit から見えなくなったので何とかした。

先に結論

EdgeRouter X 内の /etc/avahi/avahi-daemon.conf を手動で弄っていたことをすっかり忘れていてそのままファームウェアアップデートして元に戻った結果、有線部分の bonjour 解決が死んでいたのが原因だった。

自宅のネットワーク的に無線部分は AirMac Time Capsule に全部繋がっていたからなのか、他の機器には強めのキャッシュが残っていたからなのか、もはや今となっては不明なのだが、一部の機器同士では bonjour による解決が成功していたため問題の原因を把握するのが遅れてしまった…。

詳細

EdgeRouter X のファームウェアを 1.10.5 にアップデートした辺りから Philips Hue が HomeKit 経由で操作出来なくなる現象が多発していた。

HomeKit 経由だけではなく、Google Home や Alexa からも操作が出来なくなことが稀に良くある、といった状況になってしまった。弊宅の照明は Hue で統一しているので、操作が出来ないとなるとこれは非常に困る。幸い Hue 公式アプリからは確実に操作が出来てはいたので生活は出来ていたのだが。

しかしたまに HomeKit 経由での操作が可能になることもあったし、何よりも HomeKit の設定をしていない Hue Tap 経由での操作も受け付けなくなる現象が多発していたので、最初は Hue Bridge の方が壊れたのだと考えていた。

タイミング的に Hue Bridge の二年保証が数週間前に切れたこともあって、保証期間が切れてすぐに壊れたのか…と悲しい気持ちになったりもしていたのだが、結果的には Hue Bridge は何も悪くなかった、疑ってごめんな。

しかし前述の通り自分はてっきり Hue Bridge が壊れたのだと思っていた訳で、辛い気持ちになりつつ仕事帰りにヨドバシに寄って新品の Bridge を買って帰り、結構大変な思いで Hue 公式アプリの設定や Apple HomeKit の設定や Google スマートホームの設定や Amazon Alexa の設定の引き継ぎなどをやったのだが、いざ新品の Bridge の設定を終え、しばらく使ってみるとまた操作が出来なくなる問題が発生し、問題は全く解決しなかった。

EdgeRouter X について。

公式はここ

YAMAHA ルータ欲しいけどリース品の期限が切れて中古市場に流れるタイミングもいつ来るか分からないし、欲しいけど凄く欲しいかと言われると別にそこまで困ってはないというか、あれば所有欲が満たされる位だよなぁ…それにしては高いよなぁ…とか思っていたところに 「EdgeRouter X がすごい」という記事を読んでいたらうっかり買ってしまったのが 2017 年の 12 月頃の話。

EdgeRouter X 以外にも製品があるが、これらの機器は EdgeOS が乗っている。EdgeOS とは何かと言うと Vyatta の fork だ。

Vyatta 自体は x86 上でしか動作しないが、これを MIPS で動作するように改造したのが EdgeOS だと言う理解をしている。

家庭用のスイッチングハブみたいな見た目なのに実際は LAN ケーブルが 5 つも刺さる Vyatta が動く何かだなと思えば、こんなに遊べるデバイスは他に無いなと思うじゃん。そりゃ買うよね。日本の Amazon で買っても 9999 円だし。

とは言え僕はネットワークの低レイヤ部分に関しては明るくないため、ゆっくり遊びつつ弄るか、くらいの気持ちではいたのだが。

EdgeOS の mdns reflector の実体は avahi-daemon

見出しの通りなのだけど、

sudo -i
configure
set service mdns reflector
commit
save

と打って mdns reflector を有効にすると

[ service mdns reflector ]
Starting system message bus: dbus.
Starting Avahi mDNS/DNS-SD Daemon: avahi-daemon.

と出力され、

sudo -i
configure
delete service mdns reflector
commit
save

と打って mdns reflector を無効にすると、

[ service mdns reflector ]
Stopping Avahi mDNS/DNS-SD Daemon: avahi-daemon.
Stopping system message bus: dbus.

と出力される。ここから内部的には avahi-daemon であることが推察される。

そしてすっかり忘れていたのだが、 /etc/avahi/avahi-daemon.conf を手動で弄っていたことを思い出した。

個人的な Evernote 内の作業メモを参照すると、どうやら以下を追記していた。

allow-interfaces=switch0, tun0
deny-interfaces=pppoe0
allow-point-to-point=yes
enable-wide-area=yes
enable-reflector=yes

EdgeRouter X のファームウェアのアップデートでこの辺りが消え去っていたため、手動で戻してしばらく様子を見たところ、Hue Bridge が操作出来なくなるという問題が根本から解決した。

解決はしたのだが Vyatta の流儀的に直接設定ファイルを弄るのは NG な様な気はしており、また忘れた頃に同じ問題が起きる気がしている。後で筋の良いやり方を調べておきたい。

念の為だけど手動で弄っているのは /etc/avahi/avahi-daemon.conf だけです。はい…。それ以外はちゃんと流儀に乗ったやり方をしているはず。

一応 mdns repeater も有効にしておいた。

EdgeOS 1.8.0 からあったらしい。覚えていなかった。

自分の環境では eth0 を PPPoE として使っていて、 eth1 から eth4 までを switch にしているので

set service mdns repeater interface switch0
set service mdns repeater interface eth1
set service mdns repeater interface eth2
set service mdns repeater interface eth3
set service mdns repeater interface eth4

こんな感じになると思われる。しかしこの辺りの挙動はよく分かっていないのでちゃんと調べる必要がある。

HomeKit 側で何も設定していない Hue Tap も反応しなくなった件についての推測。

Hue Bridge に繋がっている何らかのセンサーが何らかを検知した際の挙動についての推測の話になる。Hue Tap ではボタンを押されたかどうか、人感センサーでは動きを検知したか、などが該当すると思われる。

これらのセンサーが直接繋がっているのは Hue Bridge であって、まずセンサーからのイベント自体は Hue Bridge まで正常に飛んでいそうではあった。確認する術がないのでこれも推測だが。

ここから Hue Bridge を HomeKit デバイスとしてペアリングしている場合と、そうで無い場合で挙動が分かれているように見える。

Hue Bridge が HomeKit デバイスとしてペアリングされている場合、センサーが検知したイベントをどう処理するか HomeKit 側にお伺いを立てている様に見受けられた。

そのため、Hue Bridge 側から HomeKit のハブである AppleTV や iPad が見えなかったり、逆に HomeKit ハブから Hue Bridge が見えなかったりすると失敗しているような挙動を示していた様に思う。

実際 EdgeRouter 側の設定が悪いと分かる前に試行錯誤してる途中で、 HomeKit デバイスとしてペアリングしてない場合とペアリングを解除した場合は Hue Tap を押して反応しなくなることはなかった様に思う。

Philips Hue のハードウェアの完成度は高い。

全体的には完成度が高いプロダクトではあるのだが、たまにアプリ側がやらかすことがある印象がある。

Hue 自体は 2012 年から続くプロダクトであり HomeKit 自体は 2014 年の iOS 8 からのもので、2016 年にリリースされた新 Bridge から HomeKit がサポートされたという経緯もあり、その影響もなくはなさそうではあるが…。

個人的には 2014 年からずっと Hue を使っているのだが、たまにアプリのアップデートで「これはやらかしたな…」と思った場面は何度かあった。その印象もあって、今回真っ先にどうせ Hue Bridge が壊れたんだろうなとちゃんと調査もせずに新品の Bridge を買い直してしまうという、まぁ、こちらがやらかしてしまったという訳で、適当な気持ちで疑ってごめんな…。

しかし今回色々弄ってみたところ、Hue アプリ内からの HomeKit の扱いが全体的に雑というか、例えば大量のバルブの名前をアプリの定義と HomeKit の定義で同期する機能ではエラーハンドリングを潰しているところが見受けられたし、もうちょっと適切にウェイトを入れつつちゃんと反映されたかどうかのチェックまで含めて丁寧に実装せいやとは思いました。

締め

HomeKit のトラブルシュートは本当に難しい。Bluetooth で素朴に繋がるものもあるし、WiFi や有線 LAN で繋がるパターンもある。今回も Hue だけが見えなくなり、それ以外に繋げていた Elgato Eve シリーズ類はずっと正常に繋がり続けていた。

とりあえず教訓としては、何かあったら bonjour 周りを最初に見るのが良いだろうということだろうか。

結果として壊れてないのに新品を買い直してしまったので、今まで使っていた Hue Bridge が余ってしまった。

メルカリにでも出すか、はたまたこのような記事も見付けたので、分解して遊ぶか悩み所。

GPD WIN 2 が届いていた。

概要

  • これを機に PC ゲームするぞ!

  • と思ったものの、現代の開発環境などを調べてたらゲームどころではなかった。

  • ちょっとゲームしてみたらうっかりサブで眠っていた自作機のグラボを強くしてしまった…。

  • むしろ開発環境を調べたり整えたりする方がゲーム感がある。

詳細

小中高生の頃から父親に Palm や CLIE など何か小さい計算機的なものを買い与えられるという教育をされていたのだが、そのまま大人になった結果こういう小さい計算機をうっかり買ってしまうようになってしまった。

前の世代の GPD WIN や GPD Pocket も興味はあるにはあったけど、なんとなく買ってなかったなーと思っていたところに indiegogo でクラウドファンディングが始まっていて、うっかり応援してしまったのが 2018 年の 1 月中頃の話。

最初の 1000 台なら $599 だったらしいけど、それには間に合わずウダウダしてたので $649 だった。クラウドファンディングなので応援した事すら忘れつつ気長に待っていると 2018 年の 6 月の中頃に国際便で到着した。

クラウドファンディングは通常の買い物と同じような気持ちでいてはいけないものだが、実績あるとこだから詐欺とか企画倒れの心配も薄いし、まぁ待ってりゃ届くだろという雑な気持ちでいたけど無事に届きました。よかったよかった。

2018/7/7 からは家電量販店などでも一般に販売開始されるみたいですね。税込 86184 円とのこと。

折角なので最近の Windows でのプログラミング環境を調べた。

僕は仕事では Web 屋と iOS アプリ屋をやっているので macOS ばっかり使っていて Windows でのプログラミングに関しては何も知らないよなと思い、ゲームは後でやるとして、とりあえず色々調べてみた。

Visual Studio

「とりあえず Visual Studio 入れときゃええやろ」という軽い気持ちで入れてみた。弄っているだけで中々楽しい。個人用途や OSS のためなら無償で使わせてくれる Microsoft の開発者支援とかスタートアップ支援なんかは本当に手厚い。ありがとうございます。

Docker for Windows

Hyper-V に依存しているため Windows 10 Pro でないと入らない。GPD WIN 2 の Windows 10 は Home なのでダメでした。

しかし Windows での Docker は難しい。そもそもの話として Linux Container と Windows Container がある。正直個人的には Linux Container さえ使えれば良いのだが、 Docker の技術的には別にどちらかに限定したものではないし、ざっくり調べた所感ではあるが「Windows で Docker 動かすなら Windows Container でしょ」という人も多いように感じた。Windows Server を普段から使っているような場合だと確かに便利そうだと感じたし多分そうなのだろう。

更に Windows Container には Windows Server Container と Hyper-V Container の 2 種類があるらしい。どちらも Windows Container という括りなので Docker image 自体は同じものではあるらしい。間に Hyper-V が入るかどうかの違いというか、コンテナから見えるカーネルがホストで直で動いているのか Hyper-V によって仮想化されているかの違いなようで、仮想化レイヤが一つ増える分 Hyper-V Container の方が重そうではある。これは自分で確認した訳ではないので伝聞形式でしか書けないけど。

Docker for Windows では Windows Container も Linux Container も扱える(排他的?)ようだが、調べてるだけで面倒臭くなり力尽きてしまった。少なくとも GPD WIN 2 では VirtualBox などの仮想化環境を入れてやらないと Docker で遊べないので、後で気が向いて他の環境を弄ることがあったら調べることにする。

Git for Windows

感覚的にも、あるいは、ちょっと調べただけでもすぐに Windows で Git は完全に相性悪そうだなという気持ちにはなったのだけど、とりあえず入れてみたところ予想外に色々便利なものも一緒に入ったのが印象的だった。Git Bash がすごい。

Git Bash を立ち上げるとなんか普通に bash が使えたので ps コマンドを打って確認してみたところ PID 1 は /usr/bin/mintty と出た。mintty は MSYS2 や Cygwin や PuTTY などでも使われているらしい。

Git Bash を弄って初めて mintty の事を知ったので、もののついでに色々調べたのだけど大変便利そうであった。この辺を弄っていて、自分が中学生くらいの頃、まだ Windows が XP であった時代に C 言語の入門書みたいなやつを買って遊んでいたことを思い出した。その当時は Cygwin を入れた覚えがある。今も Cygwin が便利な局面があることもあるのだろうけど、最近はあまり使われないのかなという感じがした。

何にせよ Windows で Git を使う場合、ファイルのアクセス権周りで微妙な使い辛さがあったりしてやはり相性は悪い。IDE に統合されているとまた別なのかもしれない。しかし僕は基本的に IDE に統合されたバージョン管理システムを信用していないというか、何されるか分かったもんじゃないので毎回必ず CUI で git コマンドを叩くようにしていて、繰り返しになるがそういう使い方だとやはり相性が悪い。

Git GUI から Generate Key ボタンを押すだけで SSH Key pair を作れそうだったが、今の時代は別途 Microsoft から公式に OpenSSH を入れられそうだったので、折角だしそちらの ssh-keygen を使って鍵ペアを作ることにした。

OpenSSH

Windows で SSH といえば PuTTY というところで知識が止まっていたので今回色々と調べたところ RLogin とか TeraTerm とかも目に付いたは付いたが、やはり Microsoft から公式にインストールすることが出来るようになっていたものを使うべきかなという感じがした。

インストールしてやると PowerShell から ssh が叩けるようになる。正確には ssh.exe だけど。実体は C:\Windows\System32\OpenSSH にあるようだ。

ssh-keygen.exe -t rsa -b 4096

とりあえずこれで鍵ペアは作れる。この段階で既に「本当に openssh じゃん…」という感動があった。そして出来上がった鍵ペアから公開鍵を各種サーバーに設定したらあっさり通った。PowerShell から ssh が出来るのはかなり衝撃があった。

ここまでやると PowerShell から ssh 経由の git pull 等も出来るようになる。これもかなり衝撃がある。

Windows Subsystem for Linux

Bash on Windows と呼ばれていたものをなんとなく知っていたので調べたところ名前が変わっていた。

気持ちで Ubuntu を入れてみたところ「なるほど…これは Ubuntu ですわ…便利…」となったものの、やはり ファイルのアクセス権周りで Windows と Linux の思想の違いによる辛さがある。/mnt/c に C ドライブがマウントされて全てのファイルが弄れるのだけど、見た感じ 777 なので精神的によろしくない。統一されて 777 という訳でもなく、場所によってアクセス権が結構適当だったりして、 ls コマンドで見たときの色分けで「ウッ」ってなる。

ここまでやって気付いたのだが、デバイス内に複数の openssl やら openssh が存在することになってしまった。

  • openssl
    • WSL で入れた Ubuntu の中
    • Git for Windows で入れた C:\Program Files\Git\usr\bin\openssl.exe
  • openssh
    • WSL で入れた Ubuntu の中
    • Git for Windows で入れた C:\Program Files\Git\usr\bin\ssh.exe
    • C:\Windows\System32\OpenSSH

これはこれで微妙な気持ちではある。

源真ゴシック

僕は等幅フォントに関してはもう長いこと Source Code Pro を使っている。日本語を含めると Source Han Code JP (源ノ角ゴシック Code JP) を使っていて、まぁこれは等幅ではなく 2/3 字幅ではあるのだが、個人的にはこちらの方が読みやすい。

もちろん Source Han Code JP はすぐにインストールしたのだが、なんと Windows のコンソールなどでは OpenType を認識してくれない。

ちょろっと調べたところ TrueType に変換しつつ細部を微調整したフォントとして源真ゴシックというものがあるらしく入れてみた。

2/3 字幅が好きなのに等幅のものしか認識しないしフォントレンダリングも macOS からすると微妙なため幸福度が著しく下がっている。こういうとこだぞ。

折角なので PC ゲーム環境も整えた。

PC ゲームをするために GPD WIN 2 を買った訳で、さぁやるぞと意気込んで実際に Steam でいくつか買ったのだが、そうするとやっぱり良いグラボの乗ったパソコンが欲しくなるというか、まぁそれは自然な流れであって、ずっとサブ PC として眠っていた 10 年前に作った自作機を久々に開腹してグラボだけ GeForce GTX1060 に換装するみたいなことをしてしまい、むしろそっちがそこそこ快適な PC ゲーム環境になってしまった。

10 年前のマザボ (ASUS P5Q Pro) と 10 年前の CPU (Intel Core2Quad Q8400@3.2GHz Over Clocked) に現代のグラボ(GeForce GTX1060)が挿さることも、そしてそのまま動くことも驚きではあるけど、過去の自分の変なこだわりのおかげで強くて効率の良い電源や速い SSD を積んでいたため何とかなっている。PCIe2.0 と PCIe3.0 の互換性に感謝。帯域は半分だが正直ゲームをやる場合のボトルネックはそこではないので問題になっていない。

結果として Rise of the Tomb Raider とか FINAL FANTASY XV WINDOWS EDITION などが遊べてしまっている。

ゲーム用にパソコンを強化した結果 3D モデリングとかもそこそこ快適に出来るようになったのでは?と気付いたので、時間を作ってそちらの世界でも遊んでみたい。

締め

開発環境を構築したり調べたりするのがゲームみたいなもん。

git push したらブログが更新されるようにした話の続き。

概要

詳細

元々 git push origin release するだけで Google Cloud Container Builder が docker image build してくれるようにはしていたのだけど、出来上がった docker image を撒く作業を手動でやってたのでそれを自動化した。

Container Builder の使い方などはググれば色んな記事がヒットするので適当に見れば良いのだが、やはり 公式ドキュメント を見るのが一番良いと思われる。便利な変数一覧もある。

開発中に毎回 git push しないと挙動を確認出来なくて不便…なんていうことはなくて、

gcloud container builds submit --config=cloudbuild.yaml .

と打つと build request が飛ぶので、実際に動かして確認することが出来る。

ただし手動で実行した場合は git push による hook で来るときだけ使えそうな変数には何も入っていない。注意が必要ですね。

Cloud Container Builder がビルドを初めたタイミングとかを知りたい。

cloud-builderscurl が追加されてたので、steps の中で curl を叩くことにした。

非同期でビルドしておいて waitFor で待つなど、通知のタイミングを完全にコントロール出来るという利点がある。

steps:
  - id: 'notification_start'
    name: 'gcr.io/cloud-builders/curl'
    args: ['-X', 'POST', 'https://example.com/hook', '-s', '--max-time', '2', '--retry', '0', '-d', '始まったよ〜。']

# [...] (中略)

  - id: 'notification_finish'
    name: 'gcr.io/cloud-builders/curl'
    args: ['-X', 'POST', 'https://example.com/hook', '-s', '--max-time', '2', '--retry', '0', '-d', '終わったよ〜。']
    waitFor: ['app1_push', 'app2_push']

しかし Cloud Functions とかをちゃんと調べて使えるようになるのが正しいという気はしている。後でちゃんとやります。

docker image push を終えたタイミングを知りたい。

以下の様に stepsimages を書くと取れなくなってしまう。

steps:
  - id: 'app1_build'
    name: 'gcr.io/cloud-builders/docker'
    args: ['image', 'build', '--no-cache=true', '--pull=true', '-t', 'gcr.io/dnpp-org/my_project/app1:latest', 'app1']
    waitFor: ['-']

  - id: 'app2_build'
    name: 'gcr.io/cloud-builders/docker'
    args: ['image', 'build', '--no-cache=true', '--pull=true', '-t', 'gcr.io/dnpp-org/my_project/app2:latest', 'app2']
    waitFor: ['-']

images:
  - 'gcr.io/dnpp-org/my_project/app1:latest'
  - 'gcr.io/dnpp-org/my_project/app2:latest'

こうすると steps で指定した手順が全て正常終了した後に gcr.io へ docker image push をしてくれて便利なのだが、今回は cloud-builders にある curlsteps 内で使って通知をしたいため、このままでは docker image push が終了したタイミングを取ることが出来ない。

しょうがないので steps 内で愚直に docker image push を行うように cloudbuild.yaml に記述することで解決した。

締め

gcloud docker は Docker 18.03 以降をサポートしないらしい。

概要

  • gcloud auth configure-docker してやると、今まで gcloud docker -- image pull してたところを docker image pull でよくなる。

  • CoreOS で Google Cloud SDK を Docker から利用しているとややこしい。

  • gcloud auth print-access-token でトークンを取って docker login するしかなさそう。

  • (追記 : 2018/5/28) docker-credential-gcloud を自前で書いて PATH の通った /opt/bin に用意すると全てが綺麗に解決する。

詳細

4月くらいには気付いていたんだけど時間がなくて直せてなかったものを直した。

いつものように

gcloud docker -- image pull gcr.io/my-project/my_image:latest

などとしていたところ

WARNING: `gcloud docker` will not be supported for Docker client versions above 18.03.
Please use `gcloud auth configure-docker` to configure `docker` to use `gcloud` as a credential helper, then use `docker` as you would for non-GCR registries, e.g. `docker pull gcr.io/project-id/my-image`.
Add `--verbosity=error` to silence this warning, e.g. `gcloud docker --verbosity=error -- pull gcr.io/project-id/my-image`.
See: https://cloud.google.com/container-registry/docs/support/deprecation-notices#gcloud-docker

というエラーメッセージが出た。エラーメッセージが出るだけでまだ普通に動きはする。

察するに gcloud docker は deprecated となるのだろう。

gcloud auth configure-docker しろと書いてあるので素直にしてやると、

The following settings will be added to your Docker config file
located at [/Users/dnpp/.docker/config.json]:
 {
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud"
  }
}

Do you want to continue (Y/n)?

と出て、 Y してやれば ~/.docker/config.json に上記が追記され、素直に docker コマンドを使えるようになる。

これにて macOS 環境で gcloud docker を使うために無効にせざるを得なかった Docker for Mac の Securely store Docker logins in macOS keychain オプションを有効に出来るようになった。

めでたしめでたし。

しかし話はそんなに簡単ではない。

上記の話は、

  • macOS High Sierra 10.13.4
  • Docker for Mac
  • Google Cloud SDK を curl https://sdk.cloud.google.com | bash で直接インストール

の場合の話である。もうちょい詳細な環境は、

$ docker --version
Docker version 18.05.0-ce-rc1, build 33f00ce

$ which gcloud
/Users/dnpp/google-cloud-sdk/bin/gcloud

$ gcloud --version
Google Cloud SDK 200.0.0
beta 2017.09.15
bq 2.0.33
core 2018.04.30
gcloud
gsutil 4.31

こんな感じ。

手元にある MacBook Pro Retina 13-inch Late 2013 と Mac mini Late 2012 で 2 台とも同じ環境だったが、これは言われた通りにやれば上手く動く。大きな問題はない。

試してはいないが、恐らく gcloud が実体として PATH の通った場所に実行できる形で存在すれば他の環境でも問題はないものと思われる。

本題はここからである。

CoreOS で Google Cloud SDK を Docker から利用しているとややこしい。

Google Cloud SDK は Docker image でも配布されている。

docker image pull google/cloud-sdk:latest

そして CoreOS ではデフォルトでこちらを使うようになっている。

GCE で CoreOS のインスタンスを立ち上げると初期状態で gcloud コマンドが使えて結構驚くのだが、実体は alias のようだ。

alias gcloud="(docker images google/cloud-sdk || docker pull google/cloud-sdk) > /dev/null;docker run -ti --rm --net=host -v $HOME/.config:/root/.config -v /var/run/docker.sock:/var/run/docker.sock google/cloud-sdk gcloud"

実装自体は、昔調べた 2017/9 頃の話だと coreos/ignitioninternal/oem/oem.go にされていた。

2018/5 にこの記事を書くにあたって調べ直したところ、 coreos/coreos-overlay
coreos-base/oem-gce/files/files/google-cloud-sdk.sh の方に移動した様だ。

この間に、

  • -v $HOME/.config:/.config から /root が抜けていたバグ。
  • -v /var/run/docker.sock:/var/run/doker.sock にあった doker という typo
  • run--rm 指定がないので docker ps で見たときにゴミだらけになってた問題。

などの問題が解決されており、着実に進歩していた模様。

2017/9 頃の話なのだが、 gcloud docker -- image pulldoker という typo を直して /var/run/docker.sock をマウントするだけでは動かず、

  • /usr/bin/docker
  • /run/metadata
  • /run/torcx

をマウントするように追記する必要があった。

詳細は Ignition's "gcloud" alias doesn't work with torcx-ified docker #2112 にある。

しかし 2018/5 にこの記事を書くにあたって再び検証してみたところ /var/run/docker.sock のみのマウントで gcloud docker -- image pull が出来るようになっていたので、細かいところで色々な改修が行われているのだなぁという気持ちになった。

どうして動くようになったのか、余裕があれば後で追っておきたい。

本題

  • CoreOS から gcloud を何も考えずに使えるのは alias になっているからであり、コマンドではない。
    • Docker の credential helper として "gcloud" と設定したところで、シェルの alias なので永遠に見付けることができない。
  • alias で設定されている gcloud には -v $HOME/.docker:/root/.docker の指定がない。
    • つまり gcloud auth configure-docker の結果書き出される ~/.docker/config.json がホスト側に永続化されない。

といった問題があり、このままでは全く使えない。

失敗例

結論から言うとこれではダメだったが、とりあえず PATH の通ってる /opt/bin にラッパーシェルスクリプトを置けば良いのではと思ってやってみた。

CoreOS でそのような用途で使っても良いとされているのが /opt 以下で、 /opt/bin にはデフォルトで PATH が通っている。

/opt/bin/gcloud

#!/usr/bin/env bash

docker container run -it --rm --net=host \
-v $HOME/.config:/root/.config \
-v $HOME/.docker:/root/.docker \
-v /var/run/docker.sock:/var/run/docker.sock \
google/cloud-sdk gcloud "$@"

シェルスクリプトを書くときに $@ とか $* とかそれらを "" で囲ったときの違いが怖くて毎回ググってしまうが、ラッパーとして使うには多分これで良いはず。

この状態で gcloud auth configure-docker してやると ~/.docker/config.jsoncredHelpers が追記される。

もっとも gcloud auth configure-docker するだけなら ~/.docker/config.json に書き込み権限があるだけで良いので、

  • -v $HOME/.config:/root/.config
  • -v $HOME/.docker:/root/.docker

とマウントして、

docker container run -it --rm \
-v $HOME/.config:/root/.config \
-v $HOME/.docker:/root/.docker \
google/cloud-sdk gcloud auth configure-docker

これでも良さそうではあった。

とりあえず

  • gcloud ラッパースクリプトが存在している
  • それを "credHelpers" に指定している

という状況で docker image pull をしてみたが、これが失敗する。

$ docker image pull gcr.io/my-project/my_image:latest
Error response from daemon: unauthorized:
You don't have the needed permissions to perform this operation, and you may have invalid credentials.
To authenticate your request, follow the steps in:
https://cloud.google.com/container-registry/docs/advanced-authentication

途中試行錯誤しつつ、

  • gcloud docker と打つたびに変更される ~/.docker/config.json のアクセス権が正しいか確認したり、
  • ラッパースクリプトから環境変数 $HOME が読めるか確認したり、
  • "credHelpers" に直接 sudo docker container run ... などと書いてみたり、
  • そもそも "credHelpers" に指定した gcloud が呼ばれているのか確認したのだが、

そもそも呼ばれてないことが分かった。

マジかよ。僕が勝手に思ってた "credHelpers" の挙動と違うじゃん。

試行錯誤してる中で見付けたのだが、 ~/.docker/config.json への書き込み権限がある状態で gcloud docker と打つと、サブコマンドなど一切打ってなくても ~/.docker/config.json"auths" が勝手に追記され、ついでにアクセス権も root:root600 になり、アクセス権を正しくしてやると、それ以降は docker image pull に成功するということも分かり、とにかく完全につらい気持ちになった。

成功例 その1 (諦めて docker login する)

エラーメッセージにも書いてあるので、素直に 公式ドキュメント を読みにいくと docker login する方法が書いてあった。

gcloud auth application-default print-access-token

これでトークンが取れると書いてあるが以下のエラーが出る。

ERROR: (gcloud.auth.application-default.print-access-token)
The Application Default Credentials are not available. They are available if running in Google Compute Engine.
Otherwise, the environment variable GOOGLE_APPLICATION_CREDENTIALS must be defined pointing to a file defining the credentials.
See https://developers.google.com/accounts/docs/application-default-credentials for more information.

色々見ていると application-default は別に無くても良さそうなので

gcloud auth print-access-token

とすると取れた。

実際には docker container run --rm -v $HOME/.config:/root/.config google/cloud-sdk という prefix が付いている。

GCLOUD_ACCESS_TOKEN=$(docker container run --rm \
-v $HOME/.config:/root/.config \
google/cloud-sdk \
gcloud auth print-access-token)
$ docker login -u oauth2accesstoken -p "$GCLOUD_ACCESS_TOKEN" 'https://gcr.io'

WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /home/core/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Are you sure you want to proceed? [y/N]

-p でパスワード指定をすると「やめろ!」って言われつつ [y/N] のプロンプトが出てしまう。

言われた通りに --password-stdin してやると二行目の「暗号化されずに保存されるぞ!」という警告は出るが、 [y/N] のプロンプトは出ずにそのままログインが成功する。

echo "$GCLOUD_ACCESS_TOKEN" | docker login -u 'oauth2accesstoken' --password-stdin 'https://gcr.io'

-u 'oauth2accesstoken' は現状では固定の様だ。それ以外の値を指定するとログインに失敗する。

あと gcloud docker と打つだけで生成される ~/.docker/config.json には "email": "not@val.id" とあったので試しに -e 'not@val.id' としてみたら

unknown shorthand flag: 'e' in -e
See 'docker login --help'.

と怒られる。email の時代は終わりです。

まとめるとこうなる。

gcr-login.sh

#!/usr/bin/env bash

GCLOUD_ACCESS_TOKEN=$(docker container run --rm -v $HOME/.config:/root/.config google/cloud-sdk gcloud auth print-access-token)

HOSTS=('https://gcr.io' 'https://asia.gcr.io' 'https://eu.gcr.io' 'https://us.gcr.io' 'https://staging-k8s.gcr.io')

for HOST in ${HOSTS[@]}; do
    echo "$GCLOUD_ACCESS_TOKEN" | docker login -u 'oauth2accesstoken' --password-stdin "$HOST"
done

とりあえずこれで docker login してしまえば、力技ではあるが一応問題は解決する。

成功例 その2 (正しくラップして credHelpers に指定する)(追記:2018/5/28)

docker login 回りの 公式ドキュメント を読んでいたところ、

Keys specify the registry domain, and values specify the suffix of the program to use (i.e. everything after docker-credential-).

という記述を見付けた。

要するにこれは credHelpersgcloud と指定した場合は docker-credential-gcloud を見に行くということなのではないかと気付いた。

試しに、 .docker/config.jsonauths の記述がなくても動いている Mac での環境で調べたらところ

$ which docker-credential-gcloud
/Users/dnpp/google-cloud-sdk/bin/docker-credential-gcloud

と出てきて、あっこれだわ、っとなった。

幸いシェルスクリプトだったので中を見ると

docker-credential-gcloud

#!/bin/sh
#
# Copyright 2017 Google Inc. All Rights Reserved.
#

# [...] (中略) 実行環境のチェックをして変数に放り込んだり export したりするコードが続いてる。

"${CLOUDSDK_ROOT_DIR}/bin/gcloud" auth docker-helper "$@"

となっていた。

つまり gcloud auth docker-helper "$@" という内容のラッパースクリプトを /opt/bin/docker-credential-gcloud に置けば綺麗に解決するのではないかと思い、やってみたところ見事に解決した。

docker-credential-gcloud

#!/usr/bin/env bash

docker container run --rm -i \
-v $HOME/.config:/root/.config \
google/cloud-sdk \
gcloud auth docker-helper "$@"

これを /opt/bin に放り込んで 755 にしてやって gcloud auth configure-docker してやれば良い。

ポイントは docker -i でコンテナの STDIN にアタッチしてやること。データのやり取りは標準入力経由でされている。

もしかしたら $HOME ではなく /home/core を直接指定してしまった方が、 sudo で実行した場合に /root になってしまって gcloud の認証情報が取れずにコケるのを回避することが出来て便利かもしれない。

echo "https://gcr.io" | docker-credential-gcloud get

上手くいけば上記コマンドでそれっぽい JSON が返ってくるようになり、gcr.io に向けた docker image pull などが成功するようになる。

多分これが一番綺麗なやり方だと思う。

一応環境のメモ

Container Linux by CoreOS alpha (1758.0.0)
Container Linux by CoreOS alpha (1772.0.0)
$ docker --version
Docker version 18.04.0-ce, build 3d479c0

$ docker container run -it --rm google/cloud-sdk gcloud --version
Google Cloud SDK 198.0.0
alpha 2018.04.13
app-engine-go
app-engine-java 1.9.63
app-engine-python 1.9.69
beta 2018.04.13
bigtable
bq 2.0.32
cbt
cloud-datastore-emulator 1.4.1
core 2018.04.13
datalab 20180213
gsutil 4.30
pubsub-emulator 2018.04.13

締め

そもそもの話として gcloud docker コマンドを使うには -- と指定しなくてはいけないのだが、僕にはこれが美しくないと感じてしまう。

引数とオプションを使う側が明確に区別するためのものだが、「先頭に gcloud と付けるだけで docker image pull できますよ!」という触れ込みで触ったのに、一瞬で裏切られたなぁという気持ちは常にあった。

# エラー
$ gcloud docker image pull gcr.io/my-project/my_image:latest
ERROR: (gcloud.docker) unrecognized arguments:
  image
  pull
  gcr.io/my-project/my_image:latest

# -- を付けると通る
$ gcloud docker -- image pull gcr.io/my-project/my_image:latest

あと gcloud docker -- --versiondocker --version で出力が違ったりもする。マジかよって思った。

$ docker container run -it --rm -v $HOME/.config:/root/.config google/cloud-sdk gcloud docker -- --version
Docker version 17.12.0-ce, build c97c6d6

$ docker --version
Docker version 18.04.0-ce, build 3d479c0

中で docker コマンドを別に持ってるのか…。そうか…。

gcloud コマンドはそこそこお行儀が悪いので Docker 内に閉じ込めておきたいという気持ちはかなり強く、 Docker image として配布されているのはとても好印象ではある。

好印象ではあるのだが、これもまたそもそもの問題として google/cloud-sdk の Docker image が 1.65 GB なので、細い回線と細いマシンで docker pull したくないという気持ちも強い。というか Docker image の容量に関してはもうちょっと節約できるやろ、と毎回思っている。

要するに gcloud は邪悪。

5/13 くらいに書き始めたものの、途中で仕事が忙しくなったり、事実関係の調査とか検証を重ねていった結果時間を食われてしまい、出すまで遂に一週間以上掛かってしまった。難しいですね。

今回調べられてない以下の実装について後で調べておきたいなという気持ちだけ残しておきたい。

シェルスクリプトで便利な git の使い方

概要

git コマンドはとにかく便利で、大体なんでも取れる。

SHA-1 Hash を取る。

予想外に色々なやり方がある。

git rev-parse HEAD

git rev-parse --short HEAD # short

git rev-list --max-count=1 HEAD

git show -s --format=%H HEAD

git show -s --format=%h HEAD # short

git show-ref --hash --head HEAD

検証はしていないけど、多分 git rev-parse が速そうな空気感を醸し出している。

Commit Message を取る。

git show -s --format=%B HEAD

例は HEAD だけど勿論 Branch や Hash を直で指定も出来る。

Commit したときの UNIX time を取る。

git show -s --format=%ct HEAD

UNIX time が取れると便利で、例えば「HEAD より過去のコミットが指定された時は無視する」みたいな機能を割と簡単に実装することが出来る。

指定された Hash が指すコミットが存在するのか調べる。

git show の終了コードを見れば良い。

#!/usr/bin/env bash

HASH="0123456789abcdef0123456789abcdef01234567"

git show "$HASH" > /dev/null 2>&1
if [ 0 -ne $? ]; then
    echo 'not found.' 1>&2
    exit 1
fi

バリデーション用途や、既に存在してれば git fetch をスキップするなどの用途などが考えられる。

diff があるかチェックする。

git diff--exit-code を付けると diff コマンドと同じような終了コードで終わってくれるので判定に使うことが出来る。

ついでに --quiet を付ければ > /dev/null 2>&1 する必要もなくなり見通しも良い。

#!/usr/bin/env bash

TARGET="path/to/file_or_dir"
HASH_1="0123456789abcdef0123456789abcdef01234567"
HASH_2="fedcba9876543210fedcba9876543210fedcba98"

git diff --exit-code --quiet "$HASH_1" "$HASH_2" "$TARGET"
if [ 0 -ne $? ]; then
    echo 'diff exists.'
fi

「このファイルが変わってるときだけ再起動する」とか「このディレクトリに変化があるときだけキャッシュを飛ばす」などの用途に使える。

念のため remote の URL をきっちり指定し直す。

拠所無い事情から「ssh を使って欲しくて user も git に指定して port も 22 番を指定するように origin の remote url を直したい」みたいなときがあって、そういうときは git remote set-url でなんとかなる。

git remote set-url origin ssh://git@github.com:22/user/repository.git

などとすれば良い。良いのだけど、しかし、こういう場合は ~/.gitconfig

[url "ssh://git@github.com:22/"]
    insteadOf = git@github.com:
    insteadOf = http://github.com/
    insteadOf = https://github.com/

などと書いたりする方が健全かなとも思う。

SHA-1 っぽい文字列かどうかチェックする。

これは git に関係ないけど、 grep -E^[0-9a-f]{40}$ にヒットするか調べて終了コードを見れば良い。

#!/usr/bin/env bash

HASH="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

echo "$HASH" | grep -E '^[0-9a-f]{40}$' > /dev/null 2>&1
if [ 0 -ne $? ]; then
    echo 'invalid format.' 1>&2
    exit 1
fi

MD5 なら 32 文字で SHA-256 なら 64 文字になるはず。

締め

GIT_SSH_COMMAND が便利だった話もあり、シェルスクリプトはいくつになっても書く機会がある。

Bot に発言させる Slack のメッセージをお洒落にするのが若干の沼。

概要

git push したらブログが更新されるようにしたのだけど、Slack に投げてた通知が味気ないなと思い、お洒落にしようとしたら沼だった。

Before:

Before

After:

After

詳細

と言っても公式ドキュメントを見るのが一番良い。

知見としては

  • 絵文字で手軽にお洒落になる。

  • "attachments" の中の "fields""short": true で要素を放り込むとお洒落になる。

  • 面倒臭がらずにリンクを張るようにするとお洒落になる。

  • 細かい要素 ("author_name" とか "author_icon" とか) を面倒臭がらずに入れるとお洒落になる。

シェルスクリプト小ネタ

なんか結局シェルスクリプトで色々実装してしまったので微妙な知見が貯まった。

SHA-1 の 40 文字から先頭 7 文字だけ取り出す。

  • cut コマンドを使う。
  • bash の変数展開を使う。

あたりが考えられる。

#!/usr/bin/env bash

REVISION="0123456789abcdef0123456789abcdef01234567"

echo "${REVISION:0:7}" # 0123456

echo "$REVISION" | cut -c 1-7 # 0123456

パイプで繋げて cut コマンドを立ち上げるよりは bash 依存の変数展開の方が速そうではあるけど、経験的に変数展開を駆使すると未来の自分が読めなくなることを知っているのでやめた。

どんな環境でも絶対に JST で日付を出力したい。

TZ を指定しつつ起動してやれば良い。

TZ='JST-09:00' date '+%Y/%-m/%-d %-H:%M:%S %Z' # 2018/5/10 1:49:00 JST

alpine 等の環境で tzdata が入ってないような場合でも 'JST-09:00' と指定すれば意図した通りの挙動になる。

重たいコマンドを実行した場合の経過秒数を出したいけど別に精度は要らない。

date コマンドで UNIX time を保持しておけば引き算で出せる。

#!/usr/bin/env bash

BEGIN_UNIX_TIME=$(date '+%s')
function elapsed_sec() {
    CURRENT=$(date '+%s')
    ELAPSED=$(( $CURRENT - $BEGIN_UNIX_TIME ))
    echo "$ELAPSED"
}

sleep 10
echo "$(elapsed_sec) sec" # 10 sec
sleep 5
echo "$(elapsed_sec) sec" # 15 sec

time コマンドでも実行時間は測れるけど、別にそういう正確な情報が欲しい訳じゃなくて、ステップ毎に「何秒経過しました」みたいなのを雑に出すにはこういうのもあるかなと。あと、そもそも bash 組み込みの time は単純に扱い辛い。

しかしこの辺まで来ると段々気が狂ってきて「expr コマンドより $(( )) の二重括弧の方が速いっしょ」みたいな気持ちで実装しがちになる。別にループ内で使う訳でもないから誤差の範囲になるのだけど。

締め

最初の段階で jq コマンドが便利すぎた結果、それに合わせる形で curl で API を叩いて遊ぶみたいなことしてしまって、うっかり最後までシェルスクリプトで実装してしまったみたいな話でした。

cloud-builderscurl が追加されてた。

詳細

タイトルの通りなんだけど Google Cloud Container Builder の公式イメージのリポジトリ github.com/GoogleCloudPlatform/cloud-builders を眺めてたら curl が追加されてた。

プルリクは これ。2018年3月終わりくらいに切られてて、4月終わりにマージされてた模様。

気持ち

今までは wget しかなかったけど curl も使えるようになって選択肢が増えて心が豊かになった。

GIT_SSH_COMMAND が便利。

概要

  • 環境変数を指定して起動してやると git コマンドの挙動を制御することが出来る。

  • ssh が絡む部分だと GIT_SSHGIT_SSH_COMMANDGIT_SSH_VARIANT などがある模様。

  • ~/.ssh/configgitconfig を弄ることが難しい場合や、環境に依存しないシェルスクリプトを書く場合などに役に立ちそう。

詳細

ドキュメントここ を読めば良い。

ssh が絡む部分だと git fetchgit pullgit clonegit push あたりだろうが、僕は git fetch で上手く動いてることしか確認していない。

GIT_SSH_COMMAND が使えるのは 2.3 以降

Git 2.3 has been released を見るに 2.3 以降でないと使えないらしい。

使えない場合は GIT_SSH の方が昔からあるので、それを使ったラッパースクリプトを書くやり方があるようだ。

使い所

-i で秘密鍵を指定したり -Fssh_config を指定したり -o で好きなようにオプションを投げるような感じの使われ方が想定されているのだと思われる。

GIT_SSH_COMMAND="\
ssh \
-i /path/to/secret_access_key \
-F /path/to/ssh_config \
-o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no" \
git fetch origin

締め

gitssh も毎日使ってるのに知らない便利機能が毎日出てきて凄い。

git push したらブログが更新されるようにした話。

概要

こんなややこしいことしなくてもいいなとも思うけど、こういうのが趣味なのでやっているという話。

詳細

更新の作業が面倒だと記事を全然書かなくなるであろうことは最初から分かりきっていたので後でなんとかしようと思ってたのだけど、遂になんとかした。

このサイトのリポジトリは bitbucket の無料プライベート枠で管理しており、 Webhook からのアクセスによって自分自身を更新できるような機能を実装すれば git push するだけで更新出来るようになるのでその様にした。

sinatra で bitbucket からの webhook を受ける。

特に言うことはない。任意に設定した URL に POST で JSON が飛んでくるので、

post '/webhook' do
  payload = JSON.parse(request.body.read)
  # payload から必要な値を使ったり適当な処理をして、適当な status code と body を返す。
end

とすれば良い。仕様は Manage webhooksEvent Payloads を見れば良い。

ただこの sinatra アプリケーション自体は Docker 内に閉じ込めているので、ホスト側に良い感じに通知して、通知を受けたホスト側が良い感じになんとかするような実装をする必要があった。

通知パターンの設計の勘所だけど、登場人物がお互いの事を完全に知っている必要があると容易に破綻してしまうので、そこだけは注意する必要がある。

それに bitbucket から飛んできた Webhook のリクエストにはさっさとレスポンスを返しておきたい。更新の成否を bitbucket 側に返す必要も今回の用途だと無い。

シェルスクリプトで何とかする。

悩んだ結果、Webhook からのアクセスが来たら Docker Volume でマウントしたホスト側にファイルを作るだけ作ったらさっさとレスポンスを返しつつ、別に tail -F で受けておいてその中でゆっくり処理するみたいな感じにした。

tail -F /path/to/file | while read -r LINE; do
    # $LINE を使ってなんか処理
    echo $LINE
done

こんな感じで tail -F にパイプで while read に食わせてやればコマンドに流せるようだ。シェルスクリプトの世界は奥が深い…。

実際に使うには、各所のエラーハンドリングとか、重複で起動しないようにしたりとか、大量の Webhook が来ても大丈夫なようにしたりとか、シェルスクリプト自身が変更されないように、あるいは変更されても大丈夫なようにしたりとか、成否は Slack に通知したいよねとか、まぁ考えることが結構あるので実装はやや複雑にはなる。

シェル芸をやりだすとキリがないし未来の自分が読めなくなるのであまりトリッキーな書き方はしないようにしているが、基礎的かつ嵌るポイントみたいなのはちゃんと押さえておきたい。

上記の while read -r を付けた話で言うと、例えば以下のワンライナーを実行した場合、

sleep 10 | echo 'print'

これは 10 秒経った後に print と表示される訳ではない。即座に print と表示され、10 秒後に操作が返ってくるという挙動を示す。

複数のコマンドをパイプで繋げたとき、プロセスはそれぞれ同時に実行されており、ただ単に入出力が繋がっているだけ、という理解を個人的にはしている。

tail -F からの出力が来る度にそれを利用する処理を実行したい、といった場合は間に while read を挟んでやるといったテクニックが定石なようだ。

しかしこの手法では行毎にプロセスが立ち上がるはずなので、大量のデータが流れてくる状況で出来るだけ短時間で処理する必要がある用途には向かないと思われる。まぁ把握だけしてれば良いだろう。

締め

今回は docker image の更新の必要がないように予め作っていた部分を利用して作ったため、docker image の更新が伴う更新は実現出来てない。

ただ docker image の build には Google Cloud Container Builder を使っているのでなんとでもなるというか、その内ちゃんと作ります。

(追記:2018/6/1)作った。

Docker に Gemfile.lock を含む Ruby アプリケーションを良い感じに乗せる。

概要

  • docker image build で毎回 bundle install が走ると遅くなりがちなので、なるだけキャッシュを効かせたい。

  • よく変更されるアプリケーション部分の COPY よりも先に Gemfile と Gemfile.lock だけ COPY しておいて RUN bundle install するような Dockerfile にしておく手法が良さそうだと思う。

  • 先に Gemfile.lock を生成しておく必要があるので、bundle install を走らせて Gemfile.lock を取り出すスクリプトを書いた。

  • alpine ベースのイメージを使うと軽い。罠も無くは無いけど。

  • あと細かい知見とかを書いた。

詳細

/path/to/your/great/app が以下のような構成になってるとして

$ tree /path/to/your/great/app
/path/to/your/great/app
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── app
│   └── app.rb
└── docker-entrypoint.sh

Dockerfile をこう書いたとする。

FROM ruby:latest

ENV APP_ROOT /path/to/your/great/app

RUN mkdir -p $APP_ROOT
WORKDIR $APP_ROOT

COPY ./ .

RUN bundle install --path vendor/bundle

ENTRYPOINT ["./docker-entrypoint.sh"]

COPY ./ . の後に RUN bundle install があるので、Gemfile を弄ってなくてもアプリをちょっと変更する度に docker image build のキャッシュが無効になってしまい、毎回 bundle install が走ってしまう。

毎回 native extension の build が走ったりするのは個人的には許容出来ない。時間も掛かるし、エコでもない。

そうすると大体以下の様に先に Gemfile と Gemfile.lock を COPY して、先に RUN bundle install するような Dockerfile を書くようになると思う。

FROM ruby:latest

ENV APP_ROOT /path/to/your/great/app

RUN mkdir -p $APP_ROOT
WORKDIR $APP_ROOT

COPY ./Gemfile      Gemfile
COPY ./Gemfile.lock Gemfile.lock
COPY ./.bundle      .bundle

RUN bundle install --path vendor/bundle

COPY ./ .

ENTRYPOINT ["./docker-entrypoint.sh"]

Gemfile.lock を先に生成するには。

  1. 使い捨てのコンテナを作る。
  2. ホストから Gemfile をコンテナに docker cp する。
  3. 使い捨てコンテナの中で bundle installdocker exec する。
  4. コンテナ内に生成された Gemfile.lock を docker cp でホストに回収して終わり。

という内容のシェルスクリプトを書いて使っている。

本当は引数を取るように書いた方がいいんだろうけど面倒で毎回中を書き換えて使ってる。後で何とかします。

今気付いたけど .bundle ディレクトリ取り出してなかった。後で何とかします。

#!/usr/bin/env bash

RUBY_IMAGE_TAG="2.5.0"
GEMFILE_DIR="/path/to/your/great/app"

if [ ! -s "$GEMFILE_DIR"/Gemfile ]; then
    echo "$GEMFILE_DIR""/Gemfile not found." 1>&2
    exit 1
fi

NAME="gemfile_lock_generator_$(date '+%Y%m%d_%H%M%S')"
W_DIR="/opt/app/gemfile_lock_generator"

docker container create -it --name "$NAME" --rm ruby:"$RUBY_IMAGE_TAG"
docker container start "$NAME"
docker container exec -t "$NAME" mkdir -p "$W_DIR"
docker container cp "$GEMFILE_DIR"/Gemfile "$NAME":"$W_DIR"/Gemfile
docker container exec -t --workdir "$W_DIR" "$NAME" bundle install --path vendor/bundle
docker container cp "$NAME":"$W_DIR"/Gemfile.lock "$GEMFILE_DIR"/Gemfile.lock
docker container stop "$NAME"

いつの頃からか Docker にサブコマンドが導入されて本当に分かりやすくなった。

ターミナルでコマンドを直打ちするときは手癖で docker ps -a とか docker images とか docker rmi とか打ってしまうけど、シェルスクリプトにまとめるときなどは意識的にサブコマンドを書くようにしている。

bundle platform 問題の話。

Gemfile.lock を生成したマシンと実行するマシンの bundle platform が違っていると bundle install 時のなんらかの native extension ビルドでコケる事例がある。

# コンテナ内に入って実行
$ bundle platform
Your platform is: x86_64-linux
# macOS 上で直接実行
$ bundle platform
Your platform is: x86_64-darwin16

ホストの macOS で生成された Gemfile.lock を安易に持っていくと死ぬ場合があるので大人しく同じ環境で生成している。

Mac 上で開発してた Rails アプリを本番サーバーに持っていこうとしたらなんか上手くいかないみたいなやつの Docker 版みたいなことが起こる訳だ。

本来 Docker 自体は x86_64-linux に限定されたものではなく、armv7l な Raspberry Pi などでも動くには動くのだが、狭義の Docker というと暗黙の了解で x86_64-linux が前提であることが多い。

そのため Docker Hub に置いてある Raspberry Pi 向けのイメージには rpi- などの prefix を付けるような文化になっていて涙ぐましい気持ちになったりもする。

ベースイメージのタグでメジャーバージョン指定しておく。

実際に使うには FROM で引いてくるイメージのタグは詳細に指定した方が良いと思われる。latest だとうっかりメジャーバージョンが上がってしまう。

指定に有効なタグ一覧は公式 https://hub.docker.com/_/ruby/ を見ましょう。

基本的にはバージョンアップには追従する姿勢だけど、CI で Docker イメージビルドを自動化してるような環境だと稀に事故が起こる。

alpine ベースにすると軽い。

Docker あるあるとして alpine ベースのイメージがあればそちらを使うことによって容量をかなり節約出来る。

$ docker images
REPOSITORY  TAG              IMAGE ID      CREATED     SIZE
ruby        2.5.0-alpine3.7  42abb49f8ada  7 days ago  60.7MB
ruby        2.5.0            e8d112ff7588  7 days ago  869MB

通常だとベースイメージで 869 MB もあるところが 60.7 MB になる訳なので効果は大きい。

しかしまぁ alpine ベースにすると色々なものがなくて不便なこともある。

例えば tzdata などが入っていないため、 ENV['TZ'] = 'Asia/Tokyo' の様な指定が出来なくなる。この場合は tzdata を自前で入れるか、諦めて ENV['TZ'] = 'GMT-9' などと記述すれば良い。

個人的にはタイムゾーン周りは政治と歴史の都合で完全な闇だと思っているので 'GMT-9' みたいな指定の方が事故らないのではと思う。最近も Qiita で タイムゾーン呪いの書 などがバズっていたけど、ここまでまとまってる情報を前にただただ圧巻という他なかったです。すごい。

他にも bashmakegcc が入っていない。これらを入れておかないと native extension のビルドが必要な場合の bundle install でコケる。

gcc libc-dev musl-dev g++ あたりは必要だったり不要だったりアンインストールしてはいけない場合があるので、最小構成で作ってみてエラーが出たら必要なのを足すみたいな感じにしてる。

という訳で最近雛形として使ってる Dockerfile はこんな感じです。

FROM ruby:2.5.0-alpine3.7

ENV APP_ROOT /path/to/your/great/app

RUN mkdir -p $APP_ROOT
WORKDIR $APP_ROOT

COPY ./Gemfile      Gemfile
COPY ./Gemfile.lock Gemfile.lock
COPY ./.bundle      .bundle

RUN apk add --no-cache --virtual .bundledeps musl-dev make gcc libc-dev && \
    apk add --no-cache g++ bash && \
    bundle install --path vendor/bundle && \
    apk del .bundledeps && \
    rm -rf /var/cache

COPY ./ .

ENTRYPOINT ["./docker-entrypoint.sh"]

締め

Docker の世界観にも色々ややこしさはあるものの、各種 LL を管理するために入れた なんちゃらenv 系は壊れるときによくシステムを巻き込むことを考えると、環境を Docker に閉じ込められる方が幸せだと感じている。

特に LL の環境構築のベストプラクティスを理解することは昔は難しかったと思うのだが、今は Docker Hub にある公式の Dockerfile を見れば良くなった。ありがたいことです。

Docker for Mac は結局 macOS の中で仮想マシンを動かしてるし、ホストとゲスト間のファイル共有が死ぬほど遅い問題のせいでボリューム機能が死ぬほど遅くて辛い話とかもあって、それなら virtualbox + vagrant で良いじゃんみたいな気持ちも無くは無いんだけど、まぁ時と場合によって両方使ってます。

他にも /(?:機械|強化|深層)*学習/ 系のタスクを解くためにマシンパワーを限界まで使いたいみたいな時は、仮想化レイヤやコンテナレイヤを挟むことによるオーバーヘッドが無視できない場合もあって、そういう時は大人しくホストの環境を汚しましょう。

sass でネストの深いリストを良い感じにスタイリングする。

先に概要

  • sass では @function が書けるが、再帰で呼び出すと比較的早い段階で SystemStackError - stack level too deep と怒られる。

  • プログラミング言語だと思って見ると sass での変数の扱いと配列の扱いが難しい。

  • redcarpet の問題なのか、リストを 6 段階以上ネストさせると出力が消える。正しい仕様と挙動が分からないので、ちゃんと検証したら issue を立てたい。

詳細

Markdown では以下の様に書くと

- list1
- list2

次の様に出力される。

  • list1
  • list2

簡単にリストが書けて大変便利でよく使う訳だけど、これはネストさせることが出来る。

- list1
    - list2
- list3
    - list4
        - list5
    - list6
        - list7
            - list8
                - list9

するとこんな感じになる。

  • list1
    • list2
  • list3
    • list4
      • list5
    • list6
      • list7
        • list8
          • list9

いくらでも階層を深くさせられるので大変便利。

ここに良い感じな css をあてたいと考えてしまった。

具体的には

  • 1 段階目は
  • 2 段階目は
  • 3 段階目は
  • 以下ループ

リストには順序付きのものもある。

  • 1 段階目は 1.
  • 2 段階目は i.
  • 3 段階目は a.
  • 以下ループ

となるように list-style-type を調整したい。

要するに <ul><ol> 要素に対して、それぞれの親要素の <ul><ol> を数え上げて 3 で剰余を求めて 0, 1, 2 で場合分けして style を指定出来れば良い。

そんな便利な指定方法はない。(ないよね?)

いっちょやったろうと思って sass@function などを駆使して実装してみた。

実装

sass でプログラミングをするのは初めてなので、とりあえず適当な Int を放り込むと剰余から該当する <ul> 用の list-style-type が返ってくる君を実装してみた。

// $i % 3 == 1 -> disc
// $i % 3 == 2 -> circle
// $i % 3 == 0 -> square
@function ul_style_for($i) {
    $ul_styles: (square, disc, circle); // scss の Index は 1 始まりなので、配列の方をずらすと上手く行く。
    $index: ($i % length($ul_styles) + 1);
    @return nth($ul_styles, $index);
}

この段階で割と後悔が強い。まず配列が難しい。

$list: "1.1" "1.2" "1.3", "2.1" "2.2" "2.3", "3.1" "3.2" "3.3";

こう書くと二次元配列として扱えるらしい。なるほど。

というか sass の世界では配列と変数の区別があるのか疑わしい。見た感じ無さそうだなと思う。配列というか単にデリミタとして空白とカンマを使っているだけのようにも見える。調べてはいない。

そして for のループカウンタや配列の index は 1 から始まる。確かに css を書くような局面では 1 から始まった方が都合が良い場合が多いのかもしれない。

数値の扱いも難しい。よく考えたら sass では 100px * 2 などが実現されているのだから難しいに決まっているのであった。

しれっと出てきたが、配列から要素を取り出すには nth() を使うことになっている。

めげずに <ol> 用のものも実装する。

// $i % 3 == 1 -> decimal
// $i % 3 == 2 -> lower-roman
// $i % 3 == 0 -> lower-alpha
@function ol_style_for($i) {
    $ol_styles: (lower-alpha, decimal, lower-roman);
    $index: ($i % length($ol_styles) + 1);
    @return nth($ol_styles, $index);
}

とりあえず上記で動いてることは地味な print デバッグなどを駆使してなんとか確認出来た。なぜこんなことを始めてしまったのか。

損切りのタイミングをミスったため最後までやっていく。

最終的には愚直に書かれた css をコンパイルして出力することになるため、無限を無限のまま扱うことは出来ない。無限のネストに対する指定は諦め、現実的な深さのネストに対する style の指定をすることになる。

そうするとネスト数を指定できるように書いた方が都合が良いので下記の様になった。

// $i: 1 -> ""
// $i: 2 -> ul, ol
// $i: 3 -> ul ul, ul ol, ol ul, ol ol
// $i: 4 -> ul ul ul, ul ol ul, ul ol ul, ul ol ol, ol ul ul, ol ol ul, ol ol ul, ol ol ol
// $i: 5 -> (略)
$list_tags: (ul, ol);
@function xl_prefixes_for($i) {
    @if ($i == 1) {
        @return ""; // ここは空配列じゃなく空文字を返す。
    }
    @else if ($i == 2) {
        @return $list_tags;
    }
    @else {
        $p: (); // ここは空文字ではなく空配列にする。
        @each $prefix in xl_prefixes_for($i - 1) {
            @each $list_tag in $list_tags {
                $p: append($p, #{$list_tag $prefix});
            }
        }
        @return $p;
    }
}

これを実際に使って 6 段目までのネストに対応する css を出力するには以下の様に書けばいい。

@for $i from 1 through 6 {
    @each $prefix in xl_prefixes_for($i) {
        #{$prefix} ul {
            list-style-type: ul_style_for($i);
        }
        #{$prefix} ol {
            list-style-type: ol_style_for($i);
        }
    }
}

@function xl_prefixes_for($i) で再帰が使われている。

現状は 6 にしてあるが 9 くらいまでは行ける。自分の環境ではこのコードで 10 にすると sass コンパイル時に SystemStackError - stack level too deep の例外を吐いて死ぬ。

このコードは O(2^n) な訳で、そらそうだなという気持ちになった。

どうせ nginx 側で gzip されるし、圧縮率の良さそうな出力だなぁ…という感想です。

締め

一応このページの css に使われているものの、こんなことに頑張ってもしょうがなかったなという気持ちになった。

なおデバッグ中に Markdown のネスト表記をガンガン深くしていったところ、6 段目あたりで redcarpet からの出力が消えることが確認されたのだが、環境依存なのか、仕様なのかは調べていないので分からない。余力があれば調べてなんとかしたい。

文章を書いておける場所を作った。

ブログのようなものを作った。ここです。

アウトプットしていかないとダメなのでは…と急に意識が高まったので、雑に Markdown を書くだけで色々と良い感じなるような Web アプリケーションをしたためた次第です。

作るにあたって考えたこと

お金を掛けないようにしたい。

運用に必要なお金は固定費になるし、そういうのは節約すべきかなと。

幸い Google Cloud Platform の Always Free 枠で f1-micro を使っておけば本当にタダなので、それで出来る範囲のものを逆算して作っておけば良さそうだと思い至った。

転送量とか Cloud DNS 部分とかドメイン管理はお金かかるけど、ドメイン周りは必要経費だし、個人サイトの転送量などは微々たるものだろう。

インタラクティブにしたくない。

ブラウザはドキュメントビューワであってアプリケーションの実行環境では無い派なので、なるだけ 1 リクエストで描画に必要なものが全て返ってくるというシンプルさを突き詰めていきたい。

というか GCP の f1-micro はマジで死ぬほど弱いインスタンスなので、せいぜい静的ファイルを返すくらいしか出来ないんだよね。極力リクエスト数を抑えたいというのが大きい。

しかし考えてみればブログなんて https で html を返すだけだし、最低限 nginx が ssl の終端とキャッシュをこなしてくれれば良くて、裏の Web アプリケーションが重くても最初の 1 回だけで済む訳で、これは用途としては丁度良いのではという感じになった。

技術的なこと

なんやかんや便利だし書き慣れているのでアプリケーション部分は sinatra で書いている。

Markdown のレンダリングには redcarpet だし、シンタックスハイライトには rouge だし、テンプレート部分は slim + sass と、そんな変な作りではないのではと思ってる。

しかし sass で再帰を実装したら辛くなった話とかもあって、ここに書いた。

そうやって組んだ Web アプリケーションを Docker 化して CoreOS 上で動かしてる。

主に bundler のせいなのだけど、 Ruby で書いたアプリケーションを綺麗に Docker に乗せるのは若干めんどくさい。バッドノウハウもそこそこ貯まりつつある気はする。そういうことも書いておきたいと思ったので、一応ここに残した。

どちらかと言うと CoreOS を単体の f1-micro インスタンスで使ってる方が微妙ではあって、アプリケーションを Docker 化してるなら GCP にも AWS にもマネージドなコンテナ管理サービスあるじゃんという気持ちはあり、完全に仰る通りなのだが、それらは極限まで金を使わないような貧乏臭い使い方をすることが出来ない。仕方無くこうなってる。

おかげで systemd とか cloud-config 周りで地獄は見られるんだけど、勉強にもなって良いかなとも思う。

しかし一番時間が掛かったのは css を弄るところ。仕様が確定したデザインを粛々と実装に落とし込む作業自体はそこまで大変ではない。面倒臭い場合はあるけど。パソコンからでもスマートフォンからでも綺麗に見えそうなレイアウトを考える方が大変だった。世の中のデザイナさん達は凄い。いつもありがとうございます。

締め

redcarpet は Markdown から HTML を出力することをレンダリングと言っていて、sass は css を出力することをコンパイルと言っている。

この辺の言語感覚は面白いとも思うし難しいなとも思う。違和感の無い表現をしていきたい。

Recents