2016年の思い出

しゃかいじんさんねんめ

去年は http://eagletmt.hateblo.jp/entry/2015/12/31/212651

仕事

hako の開発と環境作りがメインだった。 https://github.com/eagletmt/hako

Docker を使ったアプリケーションの動作環境作りは 去年もやってた んだけど、Amazon ECS が出てきたのでそれを利用した環境とそのためのデプロイツールの開発をしていた。 開発自体は去年の9月頃から始めていたんだけど、ツール面でもインフラ面でもちゃんと使える状態にして、実際に使われ始めたのが今年だった。 社内で新規に Web アプリケーションを作ったりバッチジョブを作ったりする上で標準的な選択肢になってきている。 http://techlife.cookpad.com/entry/2016/03/16/100043

発表

Rails Upgrade Casual Talks に誘われて発表してきた。発表は今年だけど内容的には 2年前のもの なのであんまり今年感は無い。 https://speakerdeck.com/eagletmt/activerecord-3-dot-2-4-dot-1

あとは hako の話を JAWS-UG おコンテナ支部 #5 でした。Amazon ECS を使ってこういう環境を用意して、そのために hako というツールを作っているという話。 https://speakerdeck.com/eagletmt/ecs-woli-yong-sitadepuroihuan-jing

趣味開発

envchain の Linux 対応 をしたのが自分にとって地味に一番インパクトあった気がする。個人でも仕事でも利用している。

あとは 録画スケジューラで試しに gRPC 使ってみたりmitamae を高速化してみたり かな…… こう書いてみるとあんまコード書いてない気はする。

VPS と自宅の間に Site-To-Site VPN はったり IPv6 対応したりして、ちょっとだけネットワークまわりの経験値をためた。 Docker のネットワークまわりの理解の助けにもなった気がする。iptables に対する苦手意識が減った。

アニメ

アイカツ終わってしまいましたね…… アイカツスターズ、最初はちょっとうーんと思っていたけど、劇場版がすばらしくてそこからテレビシリーズのほうの印象も一気によくなった。毎週楽しみです。

ViVid Strike! が魔法少女感はないもののやっぱりリリカルなのはだこれってかんじの話でとてもよかった。これとあわせてついに Reflection の上映日も発表されて本当に楽しみですね。

BD 買ったのはアイカツアイカツスターズプリズマイリヤドライ、ViVid Strike!、(まだきてないけど) ブレイブウィッチーズ、と結局代わり映えしないかんじになってた。

ゲーム

星のカービィロボボプラネット、スターフォックスゼロ世界樹の迷宮5、そしてポケモンサン・ムーンが買って面白かった。あと地味にピクロスにはまってピクロスeのシリーズを買ったりしていた。

ペルソナ5うたわれるもののために PS4 を買った。PlayStation 系のハードを買ったのはこれが初めて。友人にやらせてもらったり、友人から PSP を借りてなのはのゲームをやったことはあったけど。 どちらのゲームもたいへんよかった。

アイカツスターズの筐体デビューしてゲーセンに行く習慣ができた。 その結果、最近スクフェス AC をやって楽しんでる。EXTREME まではついていけるけど CHALLENGE はがんばりが必要そう。 音ゲー勢からはチュウニズムをすすめられているので来年は。

mitamae の高速化

itamae と比較して mitamae のメリットをシングルバイナリ以外にも作りたいなと思って mitamae の高速化を進めている。

高速化の方針は「できるだけ Ruby (mruby) のプロセス内で実行する」というもの。 itamae と mitamae の差の一つに SSH サポートや Docker サポートがあって、itamae は SSH 越しに実行したり Docker コンテナに対して実行したりできるが、mitamae はローカルのみをサポートしている。 mitamae では未実装なだけかと思ったが、既に一部の実装がローカル実行前提のようだったので、だったらその前提で外部コマンドを使わずに Ruby でファイルの存在などをチェックすることで高速化できるだろうと考えた。 実は itamae のときにも同じことをやろうとしていて実際に ちょっとだけ作ってみていた んだけど、うまく itamae 本体に組込む方法が思いつかずにそのまま放置していた。 このアイデアを mitamae に実際に実装して、1.2.x 系でいくつか取り込んでもらって高速化されている。

mitamae に入れた高速化は MItamae::InlineBackends という定数の下にクラスを作ると起動時に読み込まれてそれを使って specinfra のコマンドを一部乗っ取れるようにするという形で実現した。 mitamae 本体だけを考えるとちょっとまわりくどい方法だけど、こうすることによって MItamae::InlineBackends::NantokaBackend というクラスを提供する mrbgem を mitamae と一緒にビルドすることで、mrbgem を使って特定の環境に特化した高速化を入れることができる。 これを実際にやっているのが mitamae-pacman で、Arch Linux でこれと一緒に mitamae をビルドして使うと pacman -Q のかわりに libalpm*1 の関数を使って高速にインストール済みかどうかの判定ができるようになる。

https://github.com/eagletmt/mitamae-pacman

結果としては、個人で mitamae で管理しているとあるサーバにおいて mitamae v1.1.2 だと dry-run に7.8秒くらいかかっていたものが mitamae v1.2.4 + mitamae-pacman v0.2.0 だと1.2秒くらいになった。 これだけ差があると体感でログが流れる速度が変わっているのがわかる。

*1:Arch Linux のパッケージマネージャ pacman のバックエンドとなるライブラリ

mitamae 向けの itamae-secrets を書いた

構成管理ツールを使う上で悩ましい問題として秘匿値の管理があり、それに対する解決策の一つとして itamae 向けには itamae-secrets というものがあった。 自分も秘匿値の管理にこの itamae-secrets を使っていたが、この度 mitamae に移行してみようと思い、mitamae で使える itamae-secrets として mitamae-secrets というものを書いた。

https://github.com/eagletmt/mitamae-secrets

mitamae-secrets の暗号化・復号を実装するためには OpenSSL が必要だが、mruby 向けの OpenSSL バインディングはまだ存在していないようだったので、必要な部分だけ OpenSSL の関数を使いながら C で実装する形にした。 EVP という高レベルの API をそのまま使えたので C 実装部分はわりとすっきり書けた。

mitamae で mitamae-secrets を使うには、まず mitamae を mitamae-secrets と一緒にビルドする必要がある。ここがちょっとめんどくさい点ではある。 自分の場合はこういう build_config.rb を用意してビルドした mitamae バイナリを使っている。 https://aur.archlinux.org/cgit/aur.git/tree/?h=mitamae

node[:secrets] = Itamae::Secrets::Store.new(path) と書いていたものを node[:secrets] = MitamaeSecrets::Store.new(path) に変えればだいたいそのまま動くと思う。 少なくとも自分の使い方ではそのまま動いている。 itamae-secrets set とか itamae-secrets newkey に使う itamae-secrets コマンドの代わりとなる mitamae-secrets コマンドも用意している。 ただし .itamae-secrets.yml を読む機能は現時点ではまだ実装してないので、とりあえず毎回 --base を明示するかんじで……

mitamae 移行の感想

既に itamae を使っているところから mitamae への移行を実際にやってみた感想としては、itamae-secrets 以外の点では Ruby と mruby の標準添付ライブラリの差が地味につらかった。 普通に Ruby を使っている itamae では IPAddr とか Digest とかが使えたのに、mitamae では使えない *1 ので、そこを書き直す必要があった。 個人の環境なのでそこまで大変ではなかったとはいえ無視できないコストはかかっていて、mitamae に移行するメリットが本当にあるのか今もちょっと疑問だけど、今後メリットを自分でも作っていきたい気持ちでしばらくは mitamae を使っていこうと思っている。 とりあえず、外部コマンドを実行しすぎで遅い問題を mitamae で解決していきたい……

*1:それぞれ mrbgem としては mruby-ipaddr や mruby-digest はあります

IPv6 と Docker と NAT

IPv6 のアドレスが1つしかない状況で、ネットワークが分離されたコンテナから IPv6 で通信しようとすると IPv6 だろうと NAT が必要になる。 このへん Docker がどう扱うのかよくわかってなくて、結局 Docker の外側で docker0 とか ip6tables を管理することで動いた……

まず docker0 に使うレンジを決める。ここでは fdbb:3f26:ceda::/4810.11.0.0/16 とする。 docker0 を systemd-networkd で作る。

% cat /etc/systemd/network/docker0.netdev
[NetDev]
Name=docker0
Kind=bridge
% cat /etc/systemd/network/docker0.network
[Match]
Name=docker0

[Network]
Address=10.11.0.1/16
Address=fdbb:3f26:ceda::1/48

dockerd を起動する。このときネットワーク系のオプションを切っておく。

% cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=fdbb:3f26:ceda::/48 --bridge=docker0 --ip-forward=false --ip-masq=false --iptables=false -H fd://
# systemctl start docker.service

デフォルトだと dockerd が IPv4 でやっていたことを、IPv4IPv6 の両方について自分でやる。

% cat /etc/sysctl.d/30-ip-forward.conf
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.forwarding = 1
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
# iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# iptables -A FORWARD -s 10.11.0.0/16 -i docker0 ! -o docker0 -j ACCEPT
# iptables -A POSTROUTING -s 10.11.0.0/16 ! -o docker0 -j MASQUERADE
# ip6tables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# ip6tables -A FORWARD -s fdbb:3f26:ceda::/48 -i docker0 ! -o docker0 -j ACCEPT
# ip6tables -A POSTROUTING -s fdbb:3f26:ceda::/48 ! -o docker0 -j MASQUERADE

これで Docker コンテナ内から IPv6 で通信できるようになる。

% cat Dockerfile
FROM ubuntu:16.04
RUN apt update && apt install -y curl
% docker build -t ubuntu-curl .
% docker run --rm ubuntu-curl curl -sv -o /dev/null https://ipv6.google.com/
*   Trying 2404:6800:4004:816::200e...
* Connected to ipv6.google.com (2404:6800:4004:816::200e) port 443 (#0)
(snip)

ただ、ULA なアドレスを使っているせいで A も AAAA も持っているようなドメインの場合、デフォルトだと IPv4 が優先されてしまう。

% docker run --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 172.217.25.78...
* Connected to google.com (172.217.25.78) port 443 (#0)
(snip)

この優先度は /etc/gai.conf で決められているので、とりあえずそこを変えれば IPv6 が優先されるようになる *1

% cat gai.conf
label ::1/128       0
label ::/0          1
label 2002::/16     2
label ::/96         3
label ::ffff:0:0/96 4
% docker run -v $PWD/gai.conf:/etc/gai.conf:ro --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 2404:6800:4004:819::200e...
* Connected to google.com (2404:6800:4004:819::200e) port 443 (#0)
(snip)

今回は Docker でやったけど、systemd-nspawn (machinectl) を使うときもやることは同じ。 docker0 と同じ要領で br0 を作って systemd-nspawn --network-bridge=br0 で起動する。 systemd-nspawn の場合は勝手にアドレスを割り当てたりしてくれないので、それはコンテナ内の systemd-netword でやるようにする。 このときデフォルトだと host0 に対して余計な設定があるので、コンテナ内で ln -s /dev/null /etc/systemd/network/80-container-host0.network で切っておく。

*1:curl の場合は -6 で IPv6 を強制できるけど

Ruby で実行時に引数のクラスを推測するやつ

https://gist.github.com/eagletmt/3e064fcbe2935a8356bc8658c8e472c1

require_relative '../infer_type'
RSpec.configure do |config|
  config.before(:suite) { @infer_type = InferType.new; @infer_type.start }
  config.after(:suite) { @infer_type.finish }
end

たとえば上記のように RSpec のフックに仕掛けて INFER_TYPE_TARGET=YourAwesomeApp:: bundle exec rspec とかやるとテストから適当に推測されたクラスが表示される。 とりあえず nullable かどうかと、TrueClass と FalseClass を Boolean と解釈するように調整を入れていて、このへんをがんばるともう少しいいかんじのクラスを表示できそう。

これと同じようなかんじで、YARD で書かれた型と一致するかどうかチェックできると便利そう。

tmux 2.2 以降で East Asian Ambiguous Width Character を正しく表示させる方法

これまで tmux は文字幅を得るために独自のテーブルを持っていて、その独自テーブルでは East Asian Ambiguous Width というものを一切考慮していないので、CJK な環境ではパッチをあてて使うことがよく行われていた (tmux cjk patch とかでググるといろいろ出てくると思う)。

tmux 2.2 からは wcwidth(1) を使うようになり、独自テーブルをやめてロケールの情報から文字幅を得るようになった https://github.com/tmux/tmux/commit/26945d7956bf1f160fba72677082e1a9c6968e0c 。 が、このコミットをよく見ると setlocale(LC_CTYPE, "en_US.UTF-8") で固定されており、LC_ALL や LC_CTYPE に関係なく en_US.UTF-8 が使われる。 tmux は UTF-8 を前提としており、そこを固定したい気持ちは分からなくもないが……

なので対応としては、en_US.UTF-8 ロケールで文字幅を変更してやれば、パッチなしで East Asian Ambiguous Width の問題を回避できる。 http://eagletmt.hateblo.jp/entry/2016/03/23/020117 に書いたような方法で /usr/share/i18n/charmaps/UTF-8-CJK.gz を生成し、/etc/locale.gen で en_US.UTF-8 UTF-8-CJK にして locale-gen したところうまくいった。 普段 en_US.UTF-8 と ja_JP.UTF-8 を使い分けているような人はこの方法だと厳しいけど、基本 ja_JP.UTF-8 しか使ってない人は en_US.UTF-8 で文字幅を変えても悪影響は無いと思う。

fluentd のバッファファイルを直接加工する

たとえば変なレコードが混じってしまったせいで何度リトライしてもバッファのフラッシュに失敗するようなときに、バッファファイル (buffer_type file で作られるやつ) を使っていれば、そのファイルをいじることで応急処置ができる。

バッファファイルは [tag, time, record] という三つ組の列を msgpack でシリアライズした形式になっていて、v0.12.x と v0.14.x で time の型が違う *1 けど、どちらのバージョンでも以下のようなコードでバッファファイルを読み書きできそう。 なお v0.12.x において Fluent::Engine.msgpack_factory が追加されたのは v0.12.17 から なので注意。

require 'fluent/engine'

in_path = '/path/to/some-buffer.q0123456789abcdef.log'
out_path = '/path/to/modified-buffer.log'

def modify(tag, time, record)
  # do something
  [tag, time, log]
end

File.open(in_path) do |fin|
  unpacker = Fluent::Engine.msgpack_factory.unpacker(fin)
  File.open(out_path, 'w') do |fout|
    packer = Fluent::Engine.msgpack_factory.packer(fout)
    unpacker.each do |triplet|
      packer.write(modify(*triplet))
    end
  end
end

一旦 fluentd を停止してバッファファイルが触られないようにしてから、上のようなスクリプトで変更を加えたバッファファイルを作成し、 mv /path/to/modified-buffer.log /path/to/some-buffer.q0123456789abcdef.log で上書きしてから fluentd を起動するとよさそう。