Let's Encrypt でサイトをHTTPS化する

Let's Encrypt が Public Beta になり事前申請なしで証明書の発行ができるようになったので、試しに使ってみました。

環境

事前準備

証明書を取得する

Let's Encrypt のドキュメントの通り、letsencrypt-auto スクリプト一発で証明書を取得することが出来ます。

# EC2 内において
$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto --debug certonly

letsencrypt-auto を実行すると、Linux の設定画面のような TUI の青い画面が表示され、対話形式でメールアドレスやドメイン名を入力していきます。

この間に裏では ACME プロトコルによるチャレンジが行われており、問題なく終了すると /etc/letsencrypt/live/{domain}/ 以下に証明書等が作られます。

作られた証明書を見てみる

$ sudo ls /etc/letsencrypt/live/{domain}
cert.pem  chain.pem  fullchain.pem  privkey.pem
  • cert.pem : 証明書
  • chain.pem : 中間証明書
  • fullchain.pem : cert.pem と chain.pem がくっついたもの
  • privkey.pem : 秘密鍵

実際に取得した証明書を見てみると、確かに Let's Encrypt の CA による署名が付いていることがわかります。 証明書の有効期限は3ヶ月と短いですね。

$ sudo openssl x509 -in /etc/letsencrypt/live/{domain}/cert.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            01:45:4d:37:ef:ab:46:11:f5:b9:8d:e5:d0:55:97:af:02:cc
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X1
        Validity
            Not Before: Dec 26 06:00:00 2015 GMT
            Not After : Mar 25 06:00:00 2016 GMT
    ...
    ...
    ...

nginx に設定する

nginx.conf の server ディレクティブに証明書と秘密鍵のパスを指定します。 nginx をリロードして、ブラウザから https でアクセスできれば成功です。

server {
    listen 443;
    server_name localhost;
    root /var/www/html;
    
    ssl on;
    ssl_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem;
    location / {
    }
}

Let's Encrypt を使うと、手作業でも最初の git clone からものの 10 分ほどでサイトをHTTPS化することができました。 いやー簡単だなー。

MySQL で JWT の中身を確認する

MySQLJSON Web Token (JWT) をデコードして表示してくれるプラグインを作りました。

addsict/mysql_jwt

こんな風に使えます。

> SET @token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cDovL2FwcC5leGFtcGxlLmNvbSIsImV4cCI6MTQyNDQzNzQ5MSwiaWF0IjoxNDI0NDM2NTkxLCJqdGkiOiIxMjM0NTY3ODkwIn0.V0SEo1Y1kurWp2bSYU9gEQ2K9nweII_RNIlYEBRHdWY';
> SELECT decode_jwt(@token)\G
*************************** 1. row ***************************
decode_jwt(@token): {"iss":"http://example.com","sub":"1234567890","aud":"http://app.example.com","exp":1424437491,"iat":1424436591,"jti":"1234567890"}
1 row in set (0.00 sec)

decode_jwt()の第二引数に claim を指定することもできます。

> SELECT decode_jwt(@token, 'iss');
+---------------------------+
| decode_jwt(@token, 'iss') |
+---------------------------+
| http://example.com        |
+---------------------------+
1 row in set (0.00 sec)

> SELECT FROM_UNIXTIME(decode_jwt(@token, 'exp'));
+------------------------------------------+
| FROM_UNIXTIME(decode_jwt(@token, 'exp')) |
+------------------------------------------+
| 2015-02-20 22:04:51                      |
+------------------------------------------+
1 row in set (0.00 sec)

DB に格納されてる JWT の中身をささっと見たい時に便利かなと思うので、是非使ってみてください。

Web アプリケーションに HAL を適用する

HAL (Hypertext Application Language) とは Web API でやり取りされるリソースを、ハイパーメディアとしても扱えるようにするための仕様です。 ハイパーメディアとは HTML のアンカータグをイメージするとわかりやすいですが、ドキュメントといったメディアがリンクを介して繋がっている状態のことを指します。 例えば通常の Web API で取得できるリソース同士は特別何かで繋がっているわけではなく、お互いのリソースがある意味孤立した状態にありますが、そこにリンクを含めてあげることでお互いのリソース間を行き来することが出来るようになります。

HTML と HAL をハイパーメディアの観点で言うと、このような感じになるでしょうか。

  • HTML: 人間が操作するためのハイパーメディア
  • HAL: 機械が操作するためのハイパーメディア

具体的にリソースを HAL + JSON で表すとこんな感じになります。

{
    "userId": 10,
    "lastOrderDate": "Mon, 07 Jul 2014 18:26:21 GMT",
    "_links": {
        "self": { "href": "/users/10/orders" },
        "next": { "href": "/users/10/orders?page=2" }
    },
    "_embedded": {
        "orders": [{
            "orderId": 12,
            "orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
            "_links": {
                "self": { "href": "/users/10/orders/12" }
            }
        }, {
            "orderId": 11,
            "orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
            "_links": {
                "self": { "href": "/users/10/orders/11" }
            }
        }]
    }
}

userIdlastOrderDate といったリソースの情報と共に、それに付随したリンクが _links というプロパティに含まれています。 このリソースを使用するクライアントはそのリンクを「辿る」ことにより、別のリソースの操作を行おうという試みです。

こういったハイパーメディアを Web API に利用する考えは HATEOASREST Level3 など、昨今注目を集めているように思えます。

ブラウザと Web API の相性の悪さ

ここで少しブラウザと Web API*1 の関係について考えてみます。 ブラウザは人間が操作するものであり、HTML 上のリンクをクリックして別のドキュメントに遷移したりします。 しかし昨今の Web アプリはそういった画面遷移の他にも、JavaScript で Web API 用の URL を組み立て、Ajax を使って呼び出したりすることが多くなっています。 しかしそういった Ajax を使った Web API の呼び出しは、それまで HTML で表せていたハイパーメディアとしての領域をはみ出してしまいます。 リソース同士の繋がりを HTML とは別のもの(例えばクライアントサイドで定義したURLなど)を利用して表現しなければならないからです。 このように Web API が絡んでくると、クライアントサイドは既存のハイパーメディアの上にただ単純に乗っかっていれば済む話ではなくなるため、ブラウザとの相性が悪いと個人的には思っています。

ブラウザで HAL を使う

そこで、前述した HAL をブラウザでも使いたい!というのが本エントリーの主旨です。 具体的に言うと、HTML の他に HAL 形式のリソースを同時にブラウザに渡し、JavaScript ではその HAL 形式のリソースを参照します。 同じリソースを表しているのに、HTML と HAL という2つの表現として渡されてしまうという欠点はありますが、クライアントサイドは HAL に書かれているものだけを使えばいいので、サーバサイドとクライアントサイドが疎結合アーキテクチャになります。

例えば以下の様な HTML があるとします(HAL で表したリソースを type="application/hal+json" の script タグに埋め込んでおきます)。

<html>
<head>
    <script type="application/hal+json" id="hal-resource">
    {
        "_links": {
            "self": { "href": "http://example.com/users/10/orders" },
            "next": { "href": "http://example.com/users/10/orders?page=2" }
        },
        "_embedded": [{
            "id": "order-1",
            "_links": {
                "self": { "href": "http://example.com/users/10/orders/1" }
            }
        }, {
            "id": "order-2",
            "_links": {
                "self": { "href": "http://example.com/users/10/orders/2" }
            }
        }]
    }
    </script>
</head>
<body>
    <ol>
        <li id="order-1">Order 1</li>
        <li id="order-2">Order 2</li>
    </ol>
</body>
</html>

ここで次のページを Ajax で取得したいといった時、普通のやり方だと JS で URL を組み立ててそれを使ったりするでしょう。

var userId = 10;
var nextPage = 2;
var url = "/users/" + userId + "/orders?page=" + nextPage;
$.ajax({
    type: "GET",
    url: url,
    success: function (...) { ... },
    error: function (...) { ... },
});

一方、HAL を使うと、JS 側では渡されたリソースのリンクを辿るだけで次のページを取得できます。

JavaScript で HAL を扱うには halfred(https://github.com/basti1302/halfred) というライブラリがおすすめです。

var hal = $("#hal-resource").html();
var resource = halfred.parse(JSON.parse(hal));
var url = resource.link("next").href;

$.ajax({
    type: "GET",
    url: url,
    success: function (...) { ... },
    error: function (...) { ... },
});

このように単純にブラウザを HAL のクライアントとみなすことで、Web API との親和性も高くなります。

さいごに

HAL を Web アプリケーションに使用した場合の一番わかりやすい利点は、エンドポイントの管理がサーバサイドで完結し疎結合アーキテクチャになる点です。 もちろん HAL なんて使わなくても同様のことは簡単に実現できますが、標準に乗っかることでブラウザ以外の別のクライアントでも同じやり方を採用できたりするので、色々ハッピーかなと思ったりする年の瀬でした。

参考

*1:ここでは WebAPI を「HTTP プロトコルを利用してリソースを操作するためのコンピュータ用のインターフェース」として考えています。

Canonical な JSON エンコーディング

あるデータ表現を JSONエンコーディングする際、常に同じキーの順番でエンコーディングして欲しいことがあります。 例えば JSON Web Token (参考) は JSON 文字列をトークンとして用いるので(正確には更に Base64 エンコーディングしたもの)、 中身は同じ内容でも JSON へのエンコーディングの仕方によっては最終的なトークン文字列が異なるものになってしまいます。 特に Perl の場合は Perl v5.18 からハッシュのランダム化 がされるので、 JSON エンコーディングする度に異なる文字列が得られる可能性が高いです。

Canonical JSON

常に同じキーの順番でエンコーディングされる JSON のことを Canonical (正準) JSON と言ったりするそうです。 Perl では JSON.pm の canonical メソッドCanonical モードにすることが出来ます。

use JSON;

my $json = JSON->new->canonical;

Example

#!/usr/bin/env perl

use strict;
use warnings;
use v5.18;

use JSON;

for (1..3) {
    my $hash = +{ foo => 1, bar => 2, baz => 3 };
    say JSON->new->encode($hash);
}

say '';

for (1..3) {
    my $hash = +{ foo => 1, bar => 2, baz => 3 };
    say JSON->new->canonical->encode($hash);
}

結果

{"bar":2,"baz":3,"foo":1}
{"foo":1,"baz":3,"bar":2}
{"baz":3,"bar":2,"foo":1}

{"bar":2,"baz":3,"foo":1}
{"bar":2,"baz":3,"foo":1}
{"bar":2,"baz":3,"foo":1}

CanonicalJSON は常にキーでソートされた順番でエンコーディングされていることがわかると思います。 JSON Web Token など常に同じ JSON 文字列を期待する場面では Canonical JSON を使用しましょう。

ISUCON4 予選参加しました

ISUCON4の予選二日目に参加してきました。

結果は惨敗です。最終スコアは12,500くらい。 ランキング上位陣と同じようにインメモリなデータの持ち方でスコアを稼ごうとしましたが、 バグが取りきれず、最終的にはDBにインデックスを張ったりSQL最適化をしたりで終わってしまいました。

以下反省点

問題の切り分けができなかった

Redisを使ったコードへの書き換えを行いましたが、一発でベンチマークが通るわけもなく、デバッグをすることに。しかし、そもそもオリジナルのコードの仕様把握で間違えているのか、書き換えたコードにバグあるのか、その辺の切り分けができていなく、無駄に時間が過ぎていく一方でした。

改善案としては、

  1. クエリ最適化などを手始めに行い堅実にスコアを伸ばしつつ、仕様を正しく把握していく
  2. 全てを一気に書き換えるのではなく、インクリメンタルに書き換えて検証を行う

例えばDBの書き込みはそのまま残しつつ、少しずつ参照部分をRedisの方に向けていく、なんてやり方がよかったのかもしれません。

推測ではなく計測

今回計測方法としてはdstatでCPUやI/O使用率を見るということくらいしかしていなく、あとはほとんど「ここが重そうだよね」程度の推測で動いてしまいました。上位陣は適切にリクエスト時間やDBアクセス時間を計測し、地道に最適化していたみたいです。やはり推測ではなく計測大事ですね。

地味な最適化を怠らない

2日目暫定トップのfujiwara組のブログエントリーを読みましたが、こんな所まで最適化していたのか(base.txの削除やCSS参照削減など)と驚かずにはいられませんでした。特に印象に残ったのは、

100msかかっていたモノを99msにしてもスコアは1%しかあがらないですが、 2msかかっていたモノを1msにすると同じ1msの短縮でもスコア自体は2倍になる

という言葉です。考えてみれば当たり前のことですが、確かにその通りだなぁと。 今回はそういうマイクロチューニング的なことをする以前の段階で終わってしまいましたが、 日々の業務や、来年のISUCON(あるかな?)出場時には意識したいポイントです。

最後に

結果としては何も残せませんでしたが、参加して得られたものは大きいです。 後日公開されるであろう本選のISUCON問題、今回の反省点を生かして取り組むのが今から楽しみです!

Perlでカジュアルに単体テストのフィクスチャを扱う

PerlでDBを使った単体テストを行う時は Test::mysqldHarrietを使ってDBのプロセスを立ち上げたりしているのですが、 テスト毎にフィクスチャを定義するカジュアルなやり方がないなーと感じてました。 そこで__DATA__セクションにSQLをそのまま書きフィクスチャデータとして扱える Data::Section::Fixtureというモジュールを作ったので紹介します。

Data::Section::Fixture

インストール

$ cpanm Data::Section::Fixture

使い方

まず以下のサンプルコードのように、テストの__DATA__セクションにsetupとteardown用のSQLをベタ書きします。 このSQLwith_fixture関数に渡したコードが実行される前と後に呼ばれて、フィクスチャをセットアップすることができます。 あとはテスト本体でwith_fixtureを使うと、その関数の中だけフィクスチャデータにアクセスできるようになります。

use Test::More;
use DBI;
use Data::Section::Fixture qw(with_fixture);

my $dbh = DBI->connect('dbi:mysql:test', 'root', '');

subtest 'get all data' => sub {
    with_fixture($dbh, sub {
        my $rows = get_all_from_db();
        is_deeply $rows, [1, 2, 3];
    });
};

subtest 'delete all data' => sub {
    with_fixture($dbh, sub {
        delete_all_from_db();
        my $rows = get_all_from_db();
        is_deeply $rows, [];
    });
};

__DATA__
@@ setup
CREATE TABLE t (a int);
INSERT INTO t (a) VALUES (1), (2), (3);

@@ teardown
DROP TABLE t;

ちなみにwith_fixtureが呼ばれるたびにフィクスチャデータはリフレッシュされるので、他のテストの影響を受けることなく各テストを実行できて便利です。

おわりに

各テストが安心・安全にフィクスチャを扱えるモジュールの紹介でした。ガンガン単体テストを書いていきましょう。

InnoDBが実際に読んだレコード数を取得する

MySQLサーバが返すレコード数でなく、InnoDBが実際に読んだレコード数知りたいなーと思ってたら、普通にステータス変数の Innodb_rows_read で取れました。

SHOW SESSION STATUS LIKE 'Innodb_rows_read';

Innodb_rows_read は今までの累積値なので、クエリ実行前と後の差分を見るといいでしょう。 (FLUSH STATUS;を実行してもInnodb_%関係のステータス変数はリセットされないみたいです。サーバ再起動しかリセット方法はないのでしょうか?)

EXPLAIN結果のrows列が想定値よりずれてる時に見てみるといいと思います。