2017.05.23 / web middleman
前回このブログをHTTP2対応させた話 をしましたが、その際にMovableTypeからmiddleman-blogに移行しています。 移行そのものの話も書きたいのですが、今回はmiddleman-blogのエントリをAMP 対応させたのでその話を書きます。
AMP対応の細かなフローは公式サイトのチュートリアル を見れば理解できると思います。要は、
AMP用のJavaScriptを読み込む 自前のJavaScriptはロード、実行できない 外部スタイルシートはロードできない 自作スタイルシートはhead要素内に差し込む imgタグはamp-imgタグなど独自のタグに置き換える あたりがポイントになります。この対応をmiddleman-blogのエントリに対して適用させます。
AMP対応が完了するとValidationが通り、このような結果になります。
全体設計 カテゴリやアーカイブページなど、AMP対応はやろうと思えばいくらでもできますが、今回は固有の記事エントリのみをAMP対応させることにしました。 本ブログは、固有記事エントリのパーマリンクは
の形式ですが、AMP対応のページは
と、拡張子をampにさせています。 実際は、この命名はなんでもよくて、 /amp/
のようにディレクトリをURLの中で切る方法もあると思いますが、今回は「amp形式のリソース」と見なして拡張子に付けることにしています。
あとは、この固有記事とAMP対応ページをlink要素で相互に関連付けます。
固有ページ <link href='https://blog.katsuma.tv/2017/05/http2.html.amp' rel='amphtml'>
AMP対応ページ <link href='https://blog.katsuma.tv/2017/05/http2.html' rel='canonical'>
config.rb AMP対応ページを作るということは、構造としてはmiddleman-blogのpermalinkと似たものを別にもう1つ作ることになります。 最初はmiddleman-blogそのものに手をいれることや、Middlemanの拡張を作ることも考えましたが、今回は config.rb
のみを操作して次の戦略で実現しました。
ready
ブロックでpermalink URLを持つリソースを抽出 proxy
を利用して同一リソースで別templateを利用するページを作成 after_build
ブロックで2.のページをビルド 1, 2 では例えばこのような形で実現できます。
amp_paths = []
ready do
sitemap.resources.select { |resource|
resource.path.end_with?(".html") && resource.is_a?(Middleman::Blog::BlogArticle)
}.each do |article|
proxy_path = "#{article.date.year}/#{article.date.month.to_s.rjust(2, '0')}/#{article.destination_path.split("/").last}.amp"
proxy proxy_path, "layout_amp.html", locals: { article: article }
amp_paths << proxy_path
end
end
amp_paths
はビルド対象なパスを保存しておき、build
フェーズ(3)で再利用します。
after_build do
amp_paths.each do |path|
modify_html_as_amp_format("build/#{path}")
end
end
modify_html_as_amp_format
は
img
要素をamp-img
要素に書き換え 外部CSSをhead内に挿入 の2つの処理を行います。
def modify_html_as_amp_format(path)
html = File.read(path)
html = use_amp_img(html)
html = use_inline_style(html)
File.write(path, html)
end
(このエントリ書いてて思ったけどデバッグ面倒くさいので、buildフェーズではなくてrender前あたりで実施してもいいかも、と思い始めた。。)
amp-img要素への書き換え img
要素のsrc属性値からFastImageを利用して画像のサイズを取得 amp-img
要素に書き換え、src属性を置換、1.で取得したサイズを利用 のような形で書き換えています。
IMG_LINK_REGEXP = /<img\s[^>]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/i
def use_amp_img(html)
html.scan(IMG_LINK_REGEXP).each do |img_sources|
src = img_sources[0]
scanned_src = src.start_with?('/') ? 'build' + src : src
sizes = FastImage.size(scanned_src)
if sizes
html.gsub!(IMG_LINK_REGEXP, "<amp-img src='#{src}' with='#{sizes[0]}' height='#{sizes[1]}' />")
end
end
html
end
ポイントはamp-img
要素はレンダリングの高速化のために画像サイズを事前に伝える必要があります。 (一応回避する方法もありますが、ここでは言及しません ) そこで、今回はFastImageを利用してサイズを取得しています。 ただし、指定画像が外部ドメインに存在する場合は、buildのたびにインターネットを介した通信が走るので、キャッシュさせるなり高速化の手法は考える余地はありますね。
CSSのインライン化 AMPページではCSSのロードが許可されないので、head内で定義しておく必要があります。 amp用のCSSを用意することも可能ですが、今回は非AMPページでも利用しているCSSを利用することにしました。
やるべきことは単純でCSSを呼び出しているlink要素を参照しているCSSの中身で差し替えます。
STYLE = File.read("build/stylesheets/bundle.css")
STYLESHEET_LINK_REGEXP = /<link href="\/stylesheets\/bundle\.css" rel="stylesheet" \/>/
def use_inline_style(html)
html.gsub(STYLESHEET_LINK_REGEXP, "<style amp-custom>#{STYLE}</style>")
end
上の例ではCSSを1ファイルにかためている前提なので単純な書き方ですが、 複数のlink要素が存在する場合は、正規表現でCSSファイルをマッチさせ、そのたびにhead要素に実体を差し込む必要がありますね。
CSSの容量 ちなみに、AMP対応ページではhead内に存在できるCSSは上限が50000 bytes
です。 この上限を超えた内容を差し込むとValidationエラーになるので注意が必要です。
The author stylesheet specified in tag 'style amp-custom' is too long - we saw 96941 bytes whereas the limit is 50000 bytes. AUTHOR_STYLESHEET_PROBLEM
line 5, column 13
本ブログはCSSフレームワークとしてBulmaを利用していますが、この容量制限にひっかかったので、 利用に必要な最小限のCSS moduleのみをロード させることでこの容量問題を回避させました。
nginxの設定 このままだと本番環境(今回はnginx)にデプロイしたところで、ブラウザからアクセスすると .amp
ファイルがダウンロードされることになります。 そこでmime.types
に手をいれて .amp
拡張子をHTML扱いさせることにします。
types {
text/html html htm shtml amp;
...
}
まとめ config.rbに手をいれることで、middleman-blogのpermalinkページをAMP対応させました。
実際にはこの記事を執筆化している時点ではGoogleにインデクシング化されていないので、効果が本当にあるかどうかは不明ですが、 この記事がmiddlemanユーザーにとって大まかな指針の1つになれば、と期待しています。
2017.05.17 / web
またもや1年ぶりの更新ですが、唐突に本ブログをHTTP2対応させました。それに伴いURLが変更になっています。といってもhttpからhttpsへの変更ですね。
経緯としては、ずっとMovableTypeで運用していたこのブログを、Middlemanに移してgithubでコード管理に変更したいな、、とずっと思っていたのですが 今年のGWにようやくまとまった時間がとれたので勢いで移行することができました。 その際に、どうせ自分のサーバ(さくらのVPS)で運用させるのなら手を入れられるだけとことん入れてみたいな、、と思い調査をしはじめました。
https対応 2017年におけるSSL証明書の相場感もよくわかっていなかったのですが、今は無料で手に入るのですね。いい時代。 今回はLet’s Encrypt を利用することにしました。 慣習的に /usr/local
以下に入れるパターンが多そうなので従ってます。
cd /usr/local/
git clone https://github.com/certbot/certbot
cd certbot/
./certbot-auto -n なお、証明書発行の際には、DNSを設定して発行対象のドメイン=作業環境になっている必要があります。 DNSが浸透する前に証明書発行処理を行ったことで発行に失敗し、少しハマることがありました。
発行処理は、nginxなどWebサーバプロセスが起動していない状態であれば、スタンドアローンモードで実行すれば特に何も迷わずに進めることができるかと思います。
./certbot-auto certonly --standalone -t 実行完了すると /etc/letsencrypt/
以下に証明書がドメイン毎に保存されます。 今回はWebサーバとしてnginxを利用したので、以下のような設定で発行された証明書を指定できます。
server {
server_name blog.katsuma.tv;
root /path/to/root/blog.katsuma.tv/public;
# ここを指定
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/blog.katsuma.tv/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.katsuma.tv/privkey.pem;
access_log /path/to/blog.katsuma.tv/logs/access.log main;
error_log /path/to/blog.katsuma.tv/logs/logs/error.log warn;
} ついでにhttpの従来のリクエストはhttpsのプロトコルへリダイレクトさせます。
server {
server_name blog.katsuma.tv;
listen 80;
return 301 https://$host$request_uri;
} http2対応 これだけでもhttps対応は可能なのですが、ついでにhttp2対応も行います。 listen
の箇所を変更するだけでOKです。
listen 443 ssl http2; おお、便利。。と思いきや実はこれだけではhttp2対応できません。
Chrome51からTLS上のネゴシエーションプロトコルがALPN に対応していないとhttp2をしゃべってくれなくなりました。こんなの初めて知ったぞ。。
ALPNは、OpenSSL1.0.2以上がインストールされていればOKです。 たしかにOKなのですが、さくらのVPSの環境ではyumで入るOpenSSLは1.0.1eが最新で、1.0.2に上げるためにはソースコードからコンパイルが必要になります。 ここで、コンパイルしてインストールしなおしを選んでもいいのですが、OpenSSLのようにいろんなソフトウェアから利用されているものをいきなりバージョン上げるのはあまり意欲がわきません。
LibreSSLを利用したnginxのビルド そこでOpenSSLのバージョンを上げるのはやめて、LibreSSL を利用することにします。 LibreSSLはニュースサイトで一度見たくらいでスルーしていたのですが、安定性も特に問題なさそうとのことなのと、 OpenSSLと分離することで最悪問題がおきても被害を最小限に留められるかと考えました。
実際はnginxコンパイル時にwith-openssl
オプションでLibreSSLを指定すればOKです。 nginxのビルドはミスっても最悪yumで入れ直せばいいのでリスク的には許容できそう。
ひとまずnginxのビルドオプションはyumで入っているもの(/path/to/nginx -V
で確認できます)に基本あわせて、 with-openssl
の箇所だけを変更します。ちなみに今回ビルドしなおしたnginxのバージョンは 1.13.0
です。
cd nginx-1.30.0
./configure \
--prefix=/usr/local/nginx \
--modules-path=/usr/lib64/nginx/modules \
--conf-path=/usr/local/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nginx \
--group=nginx \
--with-compat \
--with-file-aio \
--with-threads \
--with-http_addition_module \
--with-http_auth_request_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_mp4_module \
--with-http_random_index_module \
--with-http_realip_module \
--with-http_secure_link_module \
--with-http_slice_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--with-mail \
--with-mail_ssl_module \
--with-stream \
--with-stream_realip_module \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-openssl=../libressl-2.5.3 \
--with-ld-opt='-lrt'
make
sudo make install これでようやくhttp2対応ができました。
安全性をより強化 SSL Labs では特定の環境の安全性を診断してくれます。 どうせなら高得点を目指したい。以下、そのためのTips。
HSTSの対応 Strict-Transport-Security
ヘッダをサーバから送ることでHTTPSでの通信を強制させることができます。
add_header Strict-Transport-Security "max-age=15768000; includeSubdomains"; 認証方式の指定 SSLv2, v3あたりの認証方式は古いので明示的に無効にさせます。 また、強度が弱い暗号化方式を無効化させ、ForwardSecrecyに対応させるためにECDHを指定させます。
ssl_ciphers 'ECDH !aNULL !eNULL !SSLv2 !SSLv3'; まとめるとこんなかんじです。
server {
server_name blog.katsuma.tv;
listen 80;
return 301 https://$host$request_uri;
}
server {
server_name blog.katsuma.tv;
root /path/to/root/blog.katsuma.tv/public;
listen 443 ssl http2;
ssl_ciphers 'ECDH !aNULL !eNULL !SSLv2 !SSLv3';
add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";
ssl_certificate /etc/letsencrypt/live/blog.katsuma.tv/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.katsuma.tv/privkey.pem;
access_log /path/to/blog.katsuma.tv/logs/access.log main;
error_log /path/to/blog.katsuma.tv/logs/logs/error.log warn;
} これで本ブログサイトもhttp2対応した上で、安全性もA+のスコア を取ることができました。
まとめ Let’s encryptを利用して証明書を無料で取得した LibreSSLを利用してnginxをビルドしなおした HSTS対応、認証方式の見直しを行った 結果、http2対応した上でより安全性を向上させる環境を構築することができた 今回、久々にインフラ触りました。 後半のSSL Labのスコア上げは正直惰性でしたが、http2環境構築のための作業はなかなか大変なところはあったものの、かなり勉強になって楽しかったですね。
2016.07.18 / tensorflow
(2016.07.18 19:00 追記: 誤解のある表現が多かったのでタイトル含め加筆しています)
唐突ですが、1年ぶりの更新を機に、最近興味を持って触っているTensorFlow / TF Learn(skflow) の話をします。
背景 2016.06.27にTensorFlow v0.9がリリースされています。
「モバイルサポートが充実したよ〜」が今回のウリなのですが、v0.8から本体に梱包されてるTF Learn(skflow)において、 シンプルなDeep Neural NetworkモデルのClassifierを扱うときに便利なDNNClassifier
周辺で 罠が多いことが分かったので備忘録としてメモを残しておきます。 ちなみに、下記のコードや調査はすべてTensorFlow Learn(TF Learn) ベースのものです。 いろいろな罠の話をしていますが、TensorFlow本体の話では無いのでご注意ください。
結論からいうとv0.9は実用段階では無さそうです。
v0.9はmodelをsave/restoreできない いきなり致命的な問題です。要するに学習したmodelを使いまわせないというもの。どうしてこうなったのか。。 当然のように、IssueやStackOverflowでは同様の質問が乱立しています。
これはv0.8まであったTensorFlowDNNClassifier
がv0.9からDeprecatedになって DNNClassifier
の利用を推奨される流れで入ったバグのようです。問題を整理すると、
v0.9のDNNClassifier
は TensorFlowDNNClassifier
にあったsave
, restore
メソッドが落ちてる v0.9のTensorFlowDNNClassifier
のsave
, restore
メソッドはv0.8から引き続き生きている ただし、v0.9のTensorFlowDNNClassifier.save
はcheckpointファイルを作るものの、modelファイルを保存しないバグがある 結果としてv0.9のTensorFlowDNNClassifier.save
の結果をrestore
することができない 詰む というわけで、modelのsave, restoreをする必要がある場合、2016/07/18時点ではv0.8の利用が必要になります。 ただし、いろんなIssueで話題になってるので、この問題は近いうちに修正されるでしょう。
v0.9のDNNClassifierはパフォーマンスが悪い じゃぁsave, restoreはおいといて、ひとまずコードをTensorFlowDNNClassifier
からDNNClassifier
に 移そうか、、と思うのですが、v0.9のDNNClassifier
はパフォーマンスがかなり悪いです。
DNNClassifier
, TensorFlowClassifier
で学習とテストデータ、およびstepを固定し、 fitにかかる時間とclassification_reportを計測してみると以下のような結果に。
DNNClassifier
precision recall f1-score support
0 0.77 0.96 0.86 4265
1 0.86 0.45 0.59 2231
avg / total 0.80 0.79 0.76 6496
elapsed_time: 7359.658695936203 [sec]
TensorFlowDNNClassifier
precision recall f1-score support
0 0.77 0.92 0.84 4265
1 0.75 0.47 0.58 2231
avg / total 0.76 0.76 0.75 6496
elapsed_time: 68.16537308692932 [sec]
まず、DNNClassifier
はTensorFlowDNNClassifier
と比較してprecisionが10%ほど向上。 おそらくデフォルトのハイパーパラメータが一部異なるのでしょう。原因は不明確ですが、ひとまず結果が良くなる分はまだ良いです。
問題は、elapsed_timeが68秒から7359秒とハチャメチャに長くなってる点。なんだこれは。。 これだと使い物にならなさすぎるので、Stackoverflowに投げてみたものの、2016/07/18時点ではまだ回答はありません。
このとおり、TensorFlowDNNClassifier
は近い将来DNNClassifier
に乗り換える必要があるものの、 このパフォーマンス差はだと乗り換えは辛いです。100倍以上遅くなってるけど、これどうなるんでしょ・・・? ちなみに、DNNClassifier
は、CPU使用率もTensorFlowDNNClassifier
と比較すると20%くらい高くて、 正直何もいいことが無い印象です。
v0.8のTensorFlowDNNClassifierはv0.9と比較すると遅い これまでの通り、v0.9は辛い状態かつmodelをsaveできない状態なので、 v0.9でチューニングしたハイパーパラメータでv0.8を利用してmodelをsaveさせることにします。
ところが、v0.8のTensorFlowDNNClassifier
はv0.9と比較すると 約3倍遅い 結果に。
つまり、v0.9では実はTensorFlowDNNClassifier
はDeprecatedになりながらも 内部では全体的にパフォーマンスが向上してるようですね。 もう、ここまでくるとDeprecaedにするのもやめてくれ、、、と思い始めます。
v0.8のTensorFlowDNNClassifierはOptimizerを指定しているとmodelをsaveできない v0.8でsave/restoreの調査を進めてて気づいた問題なのですが、
optimizer = tf.train.AdagradOptimizer(learning_rate=learning_rate)
classifier = learn.TensorFlowDNNClassifier(hidden_units=units, n_classes=n_classes, steps=steps, optimizer=optimizer)
classifier.fit(features, labels)
classifier.save(model_dir)
こんなかんじのコードをv0.8で実行すると、最後のsave時にJSONのシリアライズに失敗して ValueError("Circular reference detected")
が出てコケます。 ちなみにsaveを呼び出さない場合はコケないのでこれも地味に辛いです。
回避方法としては、「Optimizerは使わない」を選ぶしか無いのかな。この場合、learning rateを調整できないのが辛いですね。
classifier = learn.TensorFlowDNNClassifier(hidden_units=units, n_classes=n_classes, steps=steps)
ちなみにv0.9ではOptimizerを指定したTensorFlowDNNClassifier
でsave
を呼び出してもエラーにはなりません。 ただし、modelデータの保存もできてないので、このバグが修正されたときにOptimizerのバグも治っているかどうかは不明です。
まとめ トラップだらけなのですが、modelのsave/restoreが必要な場合、
v0.8 + TensorFlowDNNClassifier
を利用 save/restoreのバグが修正されるであろう、v0.9.1相当のリリースを待つ DNNClassifier
のパフォーマンスが上がってたらTensorFlowDNNClassifier
から乗り換える。上がってなかったらしばらく使う のような感じでしょうか。
まだまだTensorFlowの知識は少ないので、誤った情報がある場合はぜひ教えていただきたいです。