デプロイのミスは多い。
例えば
などなど。
デプロイは必ず自動化しよう。デプロイのたびにFFFTPやTeraTermを立ち上げていてはいけない。間違えるからだ。
また、準本番や開発環境へのデプロイが手間だと動作確認を怠りバグの混入したソースをあげる人が出てくる。
簡単なことならGulpでも十分対応可能だ。
頑張ろう。
今日はインフラの設定を自動化しようという話だ。
インフラの設定とは例えば
などだ。
インフラの設定で大事な価値観は「再現できる」「再現の手順が修正できる」だ。
まず再現できるとは、今現在動いている環境と同じ環境を別のサーバーやローカルパソコンに再現できるということだ。もし、本番サーバーに何度もログインして設定を変更しており、その履歴を残していなければ同じ環境の再現ができなくなる。すると、異常が発生したときに原因の特定が遅くなる。また、新規機能のための開発環境がすぐに用意できなければ開発が滞る。だから、再現の手順がきちんと残っておかなければいけない。
次に、再現の手順が修正できる必要がある。各OSや仮想化ツールにはスナップショットを取る機能が存在するのでそれを使えば再現自体は簡単だ。しかし、それだけではだめだ。設定を追加することはできるが、既存の設定内容を変更することができない。例えば、PHP5をインストールしていたサーバーをPHP7にアップデートするとしよう。もちろんUpdate自体は可能だしPHP5をアンインストールすることもできる。しかし、どうせならOSをクリーンインストールしてPHP7をインストールしたいだろう。
まず、手始めにできることとして再現手順書を作ることだ。しかし、これでは手作業でサーバーを設定しなければいけないし、日本語力が求められる。
もっといい方法がある。DockerやVagrantやChef(僕は使ったことがない)やAnsibleを使うといい。どれも本質的な機能は異なるがサーバーを自動で設定してくれる点とその設定内容を簡単なテキストファイルで表現できる点が一緒だ。その設定用のテキストファイルだけをgitで管理すれば上記の要件を満たしたインフラ自動化が達成できる。
switch case文の中で変数に代入すると、非常に読みづらいコードになる。そのSwitch文の目的がわかりづらいしこのSwitch文がどの変数を書き換える可能性があるかを把握しづらいからだ。しかし、結局Switch文は何か値を返すために書いてある場合がほとんどである。であればその値を返す関数を作って欲しい。下のコードを見て欲しい。これは英語で入って来た曜日を日本語でPrintするコードだ。
void printJAString(String week) {
String ja;
switch (week) {
case "monday":
ja = "月";
break;
case "tuesday":
ja = "火";
break;
case "wednesday":
ja = "水";
break;
case "thursday":
ja = "木";
break;
case "friday":
ja = "金";
break;
case "saturday":
ja = "土";
break;
case "sunday":
ja = "日";
break;
}
print(ja);
}
なんどもjaというコードに代入している。もしコードがもっと複雑になり、ja以外のいろんな変数に代入していたらどうなるだろうか。それよりはこのように最終的に一つの値を返す関数にして欲しい。
void printJAString(String week) {
String ja = toJAString(week);
print(ja);
}
String toJAString(String week) {
switch (week) {
case "monday":
return "月";
case "tuesday":
return "火";
case "wednesday":
return "水";
case "thursday":
return "木";
case "friday":
return "金";
case "saturday":
return "土";
case "sunday":
return "日";
}
}
このようにすることで「どうせtoJAString関数は値を一個返すだけだ。ということは読み飛ばしてもいい」と判断できる。
ぜひ、Switch文で変数を代入するのではなく、Switch文を包んだ関数で値を返そう。
本題と関係ないので省略したが、switch文のdefaultは必ず書こう。
時刻によって動作が変わるプログラムを書いた時、どうやってテストしているだろうか。手作業で書き換えているかもしれない。
String getAisatu() {
// var date = new DateTime.now();
var date = new DateTime(2017, 10, 28, 18, 0);
if (date.hour > 17) {
return "こんばんは";
}
return "こんにちは";
}
もっといい方法を紹介しよう。プログラム言語のinterface機能を使ったことがあるだろうか。あったら申し訳ない。このブログは初心者を対象にしている。interfaceプログラミングの詳細な説明は省く。とりあえずおなじinterfaceを実装していれば中身をすげ替えてもいいというルールがある。これを使おう。CurrentTImeService interfaceと現在時刻を返すクラスとダミー日付を返すクラスを作ろう。Dartでinterfaceはabstruct classという宣言をすることになっているが、適宜読み替えて欲しい。
// 共通のinterface
abstract class CurrentTimeService {
DateTime getDateTime();
}
// 本当に現在時刻を返す
class ProdCurrentTimeService implements CurrentTimeService {
@override
DateTime getDateTime() {
return new DateTime.now();
}
}
// ダミー日付を返すclass DummyCurrentTimeService implements CurrentTimeService {
DateTime dateTime;
DummyCurrentTimeService(DateTime dateTime){
this.dateTime = dateTime;
}
@override
DateTime getDateTime() {
return dateTime;
}
}
そして、現在時刻を取得する関数は引数にCurrentTimeServiceを取り、それに現在時刻を問い合わせるようにしよう。
String getAisatu(CurrentTimeService currentTimeService) {
var date = currentTimeService.getDateTime();
if (date.hour > 17) {
return "こんばんは";
}
return "こんにちは";
}
そして、呼び出し側でProdとDummyを差し替えられるようにしておこう。環境変数なので自動で切り替えられるようにしておくといい。
void main() {
var currentTimeService = new DummyCurrentTimeService(new DateTime(2017, 10, 28, 18, 0));
// var currentTimeService = new ProdCurrentTimeService();
print(getAisatu(currentTimeService));
}
これで時刻を色々指定して動作確認ができるようになった。この手法はHTTPリクエストや乱数の生成など、挙動が予想できないメソッドのテストに有効だ。
REST APIは、要求を受理するか判定する部分と、要求に従ってデータを返信したりDBに書き込んだりする部分に分かれる。前者の受理判定部分はあらゆる情報を使っていい。しかし、後者の処理部分はリクエストに明記されたもの以外を使ってはいけない。使っていいのはURLとBodyだけだ。
例えば/myprofileというAPIがあったとしよう。このサーバーはCookieを使ってアクセスした人を特定できる。そこで /myprofile ではCookieの情報をもとにその人のプロフィール情報を返してあげていた。これはよくない設計だ。このように直してほしい。まずURLは/profiles/:user_id (:user_idは変数)に変更しよう。そして、メソッドの冒頭にCookieと:user_idを比較して一致しなければエラーを出してほしい。
void onMyProfile(HttpRequest req) {
// これで /users/:user_id の:user_idの部分が取れる
var userID = req.uri.pathSegments[1];
if (!isUserIDMatch(req.cookies, userID)) {
req.response.statusCode = HttpStatus.BAD_REQUEST;
return;
}
req.response
..statusCode = HttpStatus.OK
..write(getProfile(userID));
}
前者と後者の違いは、前者はサーバーに忖度を求めているのに対し後者はサーバーにしてほしいことを明確に指定している。後者の考え方でシステム全体を統一することで設計の不明瞭さがなくなる。どのAPIも挙動が予想しやすく、変更をかけやすく、機能追加したい人がどうしたらいいかわかりやすい。もう一つの違いは、前者は処理が始まってから複数の成功パターンに分岐していくのに対して、後者は一種類の成功パターン以外はすべてエラーになるということだ。後者のほうがコードと仕様がきれいになる。
他に例を挙げると、掲示板の書き込みAPIはPostのBodyにMessageとUserIDを取ろう。Messageだけを受け取りUserIDをCookieから割り出してはいけない。
いい例
{
"message": "こんにちは",
"user_id": "user1"
}
悪い例{
"message": "こんにちは"
}
また、1年以上前から会員登録してくれていた人向けに特別サービスをするときのAPIについても考えよう。二種類考えられる。
案1
案2
この場合も、面倒だが案2を選ぼう。
前回、元データとキャッシュが1:1で対応するときのコツを書いた。今回は全体を集計した結果のキャッシュの話をする。難易度が1段上がる。
まずどのようなキャッシュが該当するか考えよう。月ごとの売上を合計して画面に表示するプログラムがあったとしよう。このようなSQLになる。
select month , sum(price) as month_sum from sales group by month;
売上件数が多ければ結構な時間がかかってしまう。そこで、別テーブルに集計しておき、次回はそこからデータをとってくることにした。
create table cache_month_sales as select month , sum(price) as month_sum from sales group by month;
この場合、salesテーブルのどの行がキャッシュテーブルのどの行に対応するか明確に決まっていない。このキャッシュは元データ全体に対するキャッシュだ。
このキャッシュの難しさは以下の二点だ
1は、先述の例は簡単すぎたが、もっと複雑な集計をすることもある。例えば各月のランクもキャッシュしていたなら、salesテーブルを1行変えただけで順位の入れ代わりを計算することになる。
2は、元データからすぐに集計結果を出せるわけではないので、キャッシュが存在しなければ元データからデータを取り出そうという実装ができない。
では、このようなキャッシュを作るうえで気をつけたいことを挙げる。まず、元データに更新がかかったらキャッシュを全部作り直すか部分的に更新するか決めよう。先の例であれば、部分的に更新するSQLはこうだ。
insert into sales values( ? , ? );
update cache_month_sales set month_sum + ? where month = ?;
全部一気に更新するならこうだ。
drop table if exists cache_month_sales;
create table cache_month_sales as select month , sum(price) as month_sum from sales group by month;
これらは主に読み込みと書き込みの頻度を元に決める。書き込みが頻繁なら部分アップデートをしなければいけないがたまにしか書き込まれないなら全体を洗いがえてもいい。部分を更新するほうを選ぶときは、それなりに覚悟を持って。次に、どちらを選ぶにせよ全部一気に更新する方法を用意しよう。最後に、前回も話したが全部一気に更新する間違えにくいUIを用意しておこう。あなたが一番あせってるときに間違えないUIだ。
PostgreSQLを使っている場合、Materialized Viewというそのまんまの機能があるのでそっちを使おう。
正規化したTableとキャッシュ用のテーブルの区別がつくように、キャッシュ用のテーブルにはすべてcache_やmareriarized_などのPrefixをつけよう。
全部一気に更新するときにdrop tableしてから create tableしたが、truncateからのinsert selectのほうがいいかも。
create tableしたら alter add indexをしたほうがいい場合があるので忘れないように。
元データとキャッシュデータが1:1で対応するとは、キャッシュ層に元データの一部を置いてある状態である。この場合、キャッシュのデータは元データのどの部分かはっきりしている。例えば、47都道府県の市町村リストがあり、その中で茨城と島根のデータをローカルにキャッシュしてあるような状況を想像してほしい。このようなキャッシュは、元データが変更されたらキャッシュデータの何を更新しないといけないかわかりやすい。比較的難易度が低いキャッシュと言える。
では、どのようにキャッシュを使おうか考えよう。
まずキャッシュがあってもなくても同じ動きになるようにしよう。get()メソッドの中で
String get(String id) {
if (cache.hasCache(id)) {
return cache.get(id);
} else {
return httpHandler.get(id);
}
}
のように自動的にキャッシュを使うかどうか判断してくれるとありがたい。こうしておけばいざという時はキャッシュを全部消せば良いので心配が減る。ただ、このライブラリが自動でキャッシュを使うということがライブラリ利用者側伝わりづらくトラブルの元になりかねない。意味もなくuseCacheをtrueにさせるという一手間をかけさせると「あ、キャッシュするんだな」と伝わって良い。たとえfalseという選択肢がなかったとしても。
class Api {
Api({bool useCache: true}) {
if (!useCache) {
throw "そんな選択肢はない!";
}
}
}
void main() {
var api = new API(useCache: true);
}
次に、元データ管理層からキャッシュの更新か破棄を指示できるようにしておこう。キャッシュ層が自分の管理するサーバー内にあるならそれようのプロトコルを作っておこう。WebシステムならURLの末尾にタイムスタンプをつけておくとCDNと併用できて手軽だ。
var modificateAt = <?php print $modificateAt; ?>;
$.ajax("/resources.json?" + modificateAt);
最後に、キャッシュを手動で破棄する方法を用意しよう。管理画面にデカデカとキャッシュクリアボタンを用意しておくと良い。いついかなる理由でキャッシュをクリアしなければいけないか未知なので必ず設けよう。だいたいキャッシュをクリアしたい時は超絶焦っている時だ。「SSH接続してこのファイルを削除ー」とかはやめよう。