一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

社内に周知しているパソコン購入・パソコン交換のルール

qiita.com

この記事は、一休.com Advent Calendar 2019の6日目の記事です。

こんにちは、nakashunです。

普段は情シスみたいなことをやっています。

image

今年のAdvent Calendarについて、Slackでこんなご意見を頂いたので書いてみます。

意外と表に出てこない、入社時に支給されるパソコンに加え

追加で購入する場合・交換する場合のルールも公開してみようと思います。

パソコンの購入・交換ルールの基本スタンス

パソコンの購入・交換のルールについては、Qiita:teamで告知しています。

社員はQiita:teamを参照し、自分のパソコンを追加購入するのか・交換するのかを判断します。

上長の承認を得た後、情シスが購入手続きを行う流れになっています。

ルールを簡単にまとめると

  • 故障修理・故障交換などを除く全てのPC購入にこのルールが該当するよ
  • 購入するPCのモデル・スペックについてはこの記事で定めたものに限定するよ
  • それ以外のメーカー・スペックのPCは購入しないよ
  • 特異な事情がある場合は上長の承認があれば購入するよ

という内容になっています。

入社される方に支給しているパソコン

一休では基本的に、新品・最新モデルのパソコンを支給します。

職種により、macOS / Windowsどちらかを選択することができます。

予め、採用の時点でどちらを希望するかをヒアリングしています。

エンジニア

エンジニアは、ThinkPad X1 Carbon / MacBook Proどちらかを支給しています。

デフォルト モデル CPU RAM ストレージ 備考 価格 納期
X1 Carbon Core i7 16GB 1TB SSD 指紋センサー/赤外線カメラ/WWAN ---円 2ヶ月
MacBook Pro Core i7 16GB 512GB SSD TouchBar搭載 ---円 2週間
MacBook Pro(フルスペック) Core i9 32GB 512GB SSD TouchBar搭載 ---円 2週間

また、ThinkPad X1 Carbonでは、メモリ 16GBが上限です。(2019年11月現在)

16GBでは、メモリを多く消費する開発環境では不足する場合があります。

必要な方には、メモリを多く積んだデスクトップマシンを追加で支給しています。

デフォルト モデル CPU RAM ストレージ 備考 価格 納期
OptiPlex 5070 Core i7 64GB 512GB M.2 SSD ---円 2ヶ月

エンジニア以外の方

エンジニア以外の方は、ThinkPad X280,X390 / X1 Carbonどちらかを支給しています。

デフォルト モデル CPU RAM ストレージ 備考 価格 納期
X280,X390 Core i5 16GB 256GB M.2 SSD 指紋センサー/赤外線カメラ/WWAN ---円 2ヶ月
X1 Carbon Core i5 8GB 256GB M.2 SSD 指紋センサー/赤外線カメラ/WWAN ---円 2ヶ月

また、外出する方には別途 プライバシー保護フィルタなどを支給しています。

追加でパソコンを購入するルール

一休のQiita:teamを抜粋しました。

エンジニア・デザイナー向けには、高スペックデスクトップPCを用意しています。
ノートPCのリプレースを検討している & ノートPCのスペック不足を感じる
という方は、高スペックデスクトップPCを追加することをご検討下さい。

まず、基本的にパソコンを追加したい場合は

高スペックのデスクトップパソコンを検討してもらっています。

上長の承認があれば、それ以外のパソコンを購入することが可能です。

その際は、希望者・上長・情シスが議論の上適切なパソコンを選択します。

前例として、パーツからすべて組み立てたケースもあります。

パソコンを交換するルール(2年)

一休では、パソコンの交換サイクルを2年と定めています。

書くのが面倒なのでQiita:teamから適当に画像を貼ります。

image

※ 2018年のルール定義で、サイクルを4年→2年に変更しました。

社員は、2年経過したタイミングでパソコンの動作が遅すぎるなど

業務に支障があると判断できる場合は交換が可能です。

上長(技術的な視点を含める必要がある為、上長がエンジニアでない場合はCTO)

が業務に支障があるかどうかを判断します。

また、動作が遅い原因がハードウェア起因でない場合もあります。

情シスは、技術的観点で改善できるところがないか などの相談窓口になっています。

パソコンを強制交換するルール(4年)

上記、2年ルールで交換しなかった場合には

4年が経過したタイミングで情シスからパソコンの交換を要求しています。

image

これは、特殊なルールだと思います。

目的は古いパソコンを社内に置いておかないことです。

近年、驚くべきスピードで進化するIT技術を我々は利用しています。

パソコンに利用されるあらゆるパーツは完璧ではなく

4年もあれば脆弱性がいくつも発見され、攻撃の対象になるケースもあります。

社員に安全なパソコンを利用してもらう為、交換をお願いしています。

あとがきみたいなやつ

ざっくりとパソコンの支給・購入・交換ルールを公開してみました。

恐らく、パソコンに関するルールを社外に公開するということが稀なケースかなと思っています。

書きながら、他社のPC支給・購入・交換ルールはどうなっているんだろう という疑問も湧いてきました。

もし機会があれば、公開可能な他のルールもオープンにしていきたいと思います。

VeeValidate 2から3へのアップデート

f:id:igatea:20191202001546p:plain

この記事は、一休.com Advent Calendar 2019の3日目の記事です。

qiita.com

宿泊事業本部のいがにんこと山口です。id:igatea
UIUXチームでフロントエンドをメインに開発しています。

一休の宿泊予約サイト の一部のフォームではVue.js、およびVeeValidateを用いてフォームのバリデーションを実装しています。
そのVeeValidateのバージョンを 2.2.15 から 3.0.11 へ移行しました。
VeeValidateはメジャーバージョンが2と3では大きく仕様が変わり、破壊的変更が多数入っています。
この記事ではVeeValidateのV2とV3の記述の比較を行い、VeeValidateのアップデートの参考になる情報をまとめたいと思います。

ライブラリバージョン情報

  • Vue.js 2.6.10
  • VueValidate 2.2.15 → 3.0.11

また先日、公式からもマイグレーションガイドが公開されました。
そちらも併せてご覧ください。 logaretm.github.io

破壊的変更

まずV2からV3に移行するにあたり破壊的変更によって影響を受けるものを挙げていきましょう。

  • Validatorクラスの廃止
  • v-validateの廃止
  • data-vv-asの廃止
  • ErrorBagクラスの廃止
  • $validatorプロパティの廃止

今までVeeValidateを使用していた方はかなり驚くのではないかと思います。
破壊的変更にあわせて関数ベースのAPIが公開されています。
パフォーマンス改善、コンポーネントの可読性、メンテナンス性の改善のためにこのような変更が行われました。
詳しくはバージョンアップのIssueで作成者が説明されています。
VeeValidate v3.0 🚀 · Issue #2191 · logaretm/vee-validate · GitHub

バリデーションルールの定義

基本形

V2

V2ではValidator.extendを使用してバリデーションルールの登録を行っていました。
またバリデーションエラー時のメッセージはgetMessageという関数を定義しています。

// 各バリデーション定義 ./Validations/hoge.js
export default {
  validate,
  getMessage: field => `${field}の形式が正しくありません。`,
};

// 登録側 ./index.js
import VeeValidate from 'vee-validate';
import hoge from './Validations/hoge';

VeeValidate.Validator.extend('hoge', hoge);

V3

V3ではextend関数を使用してバリデーションルールの登録を行います。
またバリデーションエラー時のメッセージはmessageという名前で定義するようになりました。

// 各バリデーション定義 ./Validations/hoge.js
export default {
  validate,
  message: field => `${field}の形式が正しくありません。`,
};

// 登録側 ./index.js
import { extend } from 'vee-validate';
import hoge from './Validations/hoge';

extend('hoge', hoge);

値を受け取るパターン

V2

export default {
  validate,
  getMessage: (field, maxByte) =>
    `${field}は全角${maxByte / 2}文字以内で入力してください`,
};

V3

V3では値を受け取るときにparamsの指定をする必要があります。

export default {
  params: ['max'],
  validate: (value, { max }) => validate(value, max),
  message: (field, { max }) =>
    `${field}は全角${max / 2}文字以内で入力してください`,
};

また、そのままvalidateメソッドからエラーメッセージを返すことが可能です。
{_field_} というフィールド名を表示するプレースホルダーも使用可能です。

export default function(value) {
  const invalidStr = checkStrXSS(value);
  if (invalidStr) {
    return `{_field_}に禁止文字${invalidStr}が含まれています。`;
  }
  return true;
}

標準バリデーターの再実装

V3ではライブラリの容量削減のため下記の標準バリデーターが削除されています。

  • before
  • credit_card
  • date_between
  • date_format
  • decimal
  • ip
  • ip_or_fqdn
  • url

例えばクレジットカードのバリデーターなどが削除されているので、適宜再実装の必要があります。
V2の内部では validator.js というものを使用しておりそのライブラリを使って同じものが再実装できます。
vee-validate/credit_card.js at 2.2.15 · logaretm/vee-validate · GitHub

import isCreditCard from 'validator/lib/isCreditCard';
/**
 * クレジットカードのバリデーション
 */
const validate = value => isCreditCard(String(value));

export default {
  validate,
  message: field => `${field}が正しくありません`,
};

v-validateを全てValidationProviderに

これが一番大規模な作業が必要になるところだと思います。
v-validateを使用しているところを全てValidationProviderに置き換えます。
ValidationProviderはバージョン2.1のときに実装されたバリデーションコンポーネントです。
バージョンが3になりv-validateが廃止された今、唯一のバリデーションを実行するための方法となっています。
またエラーの表示方法によってはValidationObserverも組み合わせる必要があるでしょう。

V2

<input v-validate="'required'" data-vv-as="姓" name="lastName" v-model="lastName" type="text">
<span>{{ errors.first('lastName') }}</span>

V3

ValidationProviderではv-validateはrulesに、data-vv-asはnameとなります。
エラーはScoped slot dataから取得するようになりました。
v-slot="{ errors }" というところですね。グローバルなerrorsを使用しなくなりました。
ValidationProvider内のエラーがすべてそのまま配列に入っています。
これでシンプルに errros[0] という記述だけでエラーを取れるようになり、nameを引数に指定して取る必要はなくなりました。

<validation-provider name="姓" rules="required" v-slot="{ errors }">
  <input name="lastName" v-model="lastName" type="text">
  <span>{{ errors[0] }}</span>
</validation-provider>

親子コンポーネントでのバリデーション結果共有

親コンポーネントと子コンポーネントでバリデーション結果を共有するということがあると思います。
そういう時V2では$validatorをinjectに設定して実装していました。
しかしV3では$validatorは存在しません。
その代わりにValidationObserverで親コンポーネントを囲むことで実現できます。

<!-- 親 -->
<template>
  <validation-observer ref="validationObserver" tag="div" v-slot="{ errors }">
    <!-- バリデーションがある子コンポーネント -->
    <child-component />
  </validation-observer>
</template>

<!-- 子 -->
<template>
  <validation-provider rules="required">
    <input type="text" v-model="hoge">
  </validation-provider>
</template>

子コンポーネントも含めたエラーがerrorsに入ります。
子コンポーネントで親コンポーネントのエラーを使用するという場合はpropsとして送る必要があります。

validateメソッド

$validator が廃止になったので全てのバリデーション結果を取得するメソッドも変わっています。

V2

const isValid = this.$validator.validateAll();

V3

V3ではValiationObserverを$refsで取得することで、そのコンポーネント内のValidationObserver、ValidationProviderのエラーの有無を以下のメソッドで検知することができます。

const isValid = this.$refs.validationObserver.validate();

VueSFCの例

<template>
  <validation-observer ref="validationObserver" tag="div">
    <validation-provider rules="required">
      <input type="text" v-model="hoge">
    </validation-provider>
    <validation-provider rules="required">
      <input type="text" v-model="fuga">
    </validation-provider>
  </validation-observer>
</template>
<script>
export default {
  methods: {
    submit() {
      const isValid = this.$refs.validationObserver.validate();
      // 後続処理
    } 
  },
}
</script>

ErrorBag廃止の対応

V2ではエラーが ErrorBag というもので返ってきていました。
ですがV3の v-slot="{ errors }" で取得したエラーは { [エラーのフィールド名]: [エラーメッセージの配列] } という形のオブジェクトとして格納されます。
注意として、ここのエラーフィールド名というのはinputのnameではなくValidationProviderに指定したnameとなります。
なのでErrorBagからinputのnameが取得することができなくなりエラーからはinputを特定するといったことが困難になりました。
特定のエラー情報を元にそのinputにフォーカスするということがそのままだとできないわけです。
ここではその対応の一例としてclassを振って判定可能にする方法を紹介します。
エラーになったフィールドには特定のクラスを割り当ててそれを活用してフォーカスするようにします。

errorKeyがValidationProviderのnameに指定した値(バリデーションエラー時に表示されるフィールド名)、errorIndexがそのコンポーネント内での何番目のエラーかを表します。

// componentのmethods内に定義
focusError({ errorKey, errorIndex }) {
  const validationProvider = this.$refs.validationObserver.refs[errorKey];

  if (validationProvider) {
    validationProvider.$el
      .getElementsByClassName('errorField')
      [errorIndex].focus();
    return;
  }

  // ValidationObserverで囲われたValidationProviderがエラーの場合、observersにValidationObserverが格納されその内部のエラーとなっている入力欄を探す
  // エラーとなっている入力欄をどちらにフォーカスするかは配列の順番に従う
  const validationObserver = this.$refs.validationObserver.observers.find(
    observer => observer.id === errorKey,
  );
  if (validationObserver) {
    validationObserver.$el
      .getElementsByClassName('errorField')
      [errorIndex].focus();
  }
},

observers などは公式ガイドに載っていないAPIなのでそれを踏まえたうえで使用してください。

エラーが出力されているかのテスト

V2

V2ではvalidateメソッドを叩いてバリデーションをかけ、$validatorからエラーを取り出してテストをしていました。

it('フリガナの必須項目チェックが機能しているか', async () => {
  const wrapper = shallowMount(HogeComponent, {
    sync: false,
  });
  await wrapper.vm.$validator.validate();
  assert.strictEqual(wrapper.vm.$validator.errors.has('kanaSei'), true);
  assert.strictEqual(wrapper.vm.$validator.errors.has('kanaMei'), true);
});

V3

V3ではinputに値を入力して、DOM更新を行って、クラスから要素を取得することによってエラーがあるかどうかを検知するようになりました。
また、v-slotを使用してValidationObserver、ValidationProvider内にエラーが出力されるので、shallowMountのときはVeeValidateのコンポーネントがスタブにならないようにstubsにValidationObserver、ValidationProviderを指定する必要があります。

import flushPromises from 'flush-promises';
import { shallowMount } from '@vue/test-utils';
import { ValidationObserver, ValidationProvider } from 'vee-validate';

it('フリガナの必須項目チェックが機能しているか', async () => {
  const wrapper = shallowMount(HogeComponent, {
    sync: false,
    stubs: { ValidationObserver, ValidationProvider },
  });
  wrapper.find('input[name="kanaSei"]').setValue('');
  wrapper.find('input[name="kanaMei"]').setValue('');
  await flushPromises();
  const errors = wrapper.findAll('.errorText');
  assert.strictEqual(errors.length, 2);
});

個別のバリデーションメッセージをテストする

バリデーションがこのように定義されていたとしたら、フィールド名が適用された状態のエラーメッセージをテストすることができません。

export default function(value) {
  const invalidStr = checkStrXSS(value);
  if (invalidStr) {
    return `{_field_}に禁止文字${invalidStr}が含まれています。`;
  }
  return true;
}

VeeValidateのルールにのっとってフィールド名も正しく入っているかをテストしたい場合は以下のように書くことで実現できます。

import { extend, validate } from 'vee-validate';

extend('noxss', noxss);
it('validate() - JavaScriptっぽい文字列が含まれないようバリデーション', async () => {
  // OK
  const ok = await validate('ふつうのコメント', 'noxss');
  assert(!ok.errors[0]);

  // NG
  const ng = await validate(
    '<Script>//JSっぽい文字列が含まれるコメント</Script>',
    'noxss',
  );
  assert(ng.errors[0]);
});

最後に

この記事がVeeValidateのアップデート時に皆様の役に立てば幸いです。
また今回書かせていただいたVeeValidateの話を12月23日のRoppongi.vueでお話させていただきます。
弊社一休が会場提供させていただくのでこの機会にぜひお越しください!

roppongi-vue.connpass.com

一休.comにService Worker(Workbox)を導入しました

f:id:ryo-utsunomiya:20191128104547p:plain

こんにちは。宿泊事業本部の宇都宮です。

この記事は、一休.com Advent Calendar 2019の2日目の記事です。

今日は、一休.com( https://www.ikyu.com )にService Worker + Workboxを導入した件について書きます。

Service Workerとは

Service Workerはブラウザのバックグラウンドで動作するJavaScriptで、PWA(Progressive Web Apps)の基盤技術です。

Service Worker の紹介 https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja

はじめてのプログレッシブウェブアプリ https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja

Service Workerを導入することには2つの意義があると考えています。

(1) PWAの機能を提供するための前提となる (2) プログラマブルなブラウザキャッシュ機構の導入によるパフォーマンス改善ポイントの追加

一休.comでの導入内容

一休.comでも、Service Workerを導入しました。ただし、ミニマムに始めるため、サイトの既存の動作に極力影響しない形で導入しました。

  • PWAモードは無効化
    • したがって、Add to Home Screen(A2HS)なし
  • オフラインページ( https://www.ikyu.com/offline.html )の追加
  • Service Workerによるキャッシュはstyleのみで実験的に開始 => script, image, fontにも拡大
    • 今後、静的ページのキャッシュを追加予定

実装の詳細

service workerのエントリーポイントとなるスクリプトは、webpackでバンドルしたjsに含めています。ほとんどの画面ではこのスクリプトが呼ばれます。

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').then(
      registration => {
        console.log(
          `ServiceWorker registration successful with scope: ${registration.scope}`,
        );
      },
      err => { /* エラーハンドリング */ },
    );
  });
}

このスクリプトは、ブラウザがService Workerを利用可能な場合には、sw.js をService Workerに登録します。実際にService Workerで実行されるスクリプトは https://www.ikyu.com/sw.js にあります。

sw.js では、Workboxという、Service Workerでキャッシュ管理を宣言的に行えるようにするライブラリを使っています。

オフラインページ

オフラインページ( https://www.ikyu.com/offline.html )は、↓のようなスクリプトでキャッシュできます。

const OFFLINE_PAGE = '/offline.html';
workbox.precaching.precacheAndRoute([
  OFFLINE_PAGE,
  '/dg/image/logo/neologo2.gif', // オフラインページで使ってるロゴ
]);

workbox.routing.setCatchHandler(({ event }) => {
  switch (event.request.destination) {
    case 'document':
      return caches.match(OFFLINE_PAGE);
    default:
      return Response.error();
  }
});

これによって、ネット接続が切れている場合でも静的ページを表示できます(↓は機内モードなのでネット接続なし)。

f:id:ryo-utsunomiya:20191128102816p:plain:w320

実行時キャッシュ

実行時にキャッシュさせるリソースは以下のように宣言します。

workbox.routing.registerRoute(({ url, request }) => {
  const hostnames = [
    // キャッシュを許可するドメイン名のリスト
    'www.ikyu.com',
    'www.img-ikyu.com',
  ];
  const types = [
    // キャッシュを許可するリソースの種別
    'font',
    'script',
    'style',
    'image',
  ];
  return (
    hostnames.some(hostname => url.hostname === hostname) &&
    types.some(type => request.destination === type)
  );
}, new workbox.strategies.StaleWhileRevalidate());

ここでは fetch standardの request.destination を使って、リソースの種別によってキャッシュの可否を決めています。 https://fetch.spec.whatwg.org/#concept-request-destination

このキャッシュはブラウザのデフォルトキャッシュに優先されます。また、Stale While Revalidate ストラテジーでキャッシュが管理されるため、リソースが更新されている場合は、次回リクエスト時には新しいリソースに差し替わります。

参考:Stale-While-Revalidate ヘッダによるブラウザキャッシュの非同期更新 https://blog.jxck.io/entries/2016-04-16/stale-while-revalidate.html

また、デフォルトは NetworkOnly になっていて、キャッシュ対象でないリソースの取得時には、Service Workerは何もしません。

// デフォルトはNetworkOnly(service workerは何もしない)
workbox.routing.setDefaultHandler(new workbox.strategies.NetworkOnly());

キャッシュの確認方法

Chrome DevToolsのApplicationタブで Cache > Cache Storage > workbox-xxx という項目を見ると、Service Workerがキャッシュしているファイルを確認できます。

f:id:ryo-utsunomiya:20191128103128p:plain:w375

Developer Toolsの注意点

Service Worker(Workbox)を入れると、ネットワークリクエストをService Workerが中継するようになるため、Developer ToolsのNetworkタブの見方が変わります。

通常のネットワークリクエストのログに加えて、Service Workerがネットワークリクエストを中継したことを示すfetchのログが出るようになります(Networkタブのログに、実際のリクエストのログとService Workerのログの両方が出るようになります)。

↓のようにログが2行出ていても、2回リクエストが飛んでいるわけではありません。

f:id:ryo-utsunomiya:20191128103444p:plain:w375

⚙️(歯車)のついているリクエストは、Service Workerが中継したことを示しているだけで、無視して良いです。

また、以下のように、cssや画像などのService WorkerログもXHR(XHR and Fetch)タブに登場します。実際のリクエストログは CSS や Img といった専用タブにあります。

f:id:ryo-utsunomiya:20191128103531p:plain:w375

これらの影響で、Networkタブがかなりノイジーになるので、Service Workerのログをフィルタリングしたいところですが、今のところChrome/Firefoxではフィルタリング機能は提供されていないようです。

Service Workerのデバッグ

DevToolsの Application > Service Workes にはService Worker関係のデバッグ機能が用意されています。たとえば、「Bypass for Network」を使うと、Service Workerをバイパスする(ブラウザにネットワークアクセスを強制する)ことができます。

f:id:ryo-utsunomiya:20191128154234p:plain:w375

Progressive Web App のデバッグ https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps

Web App Manifest

Service Workerとは直接関係ないですが、PWA絡みでWeb App Manifestについても触れておきます。

Web App Manifestは、PWAが動作するための要件の一つで、PWAとしての動作モードなどを指定します。

一休.com のmanifestは https://www.ikyu.com/manifest.json にあります。

Web App Manifestで一番重要な設定は "display" で、これによって動作モードが変わります。 https://developers.google.com/web/fundamentals/web-app-manifest

一休.com では現在 "display": "browser" を使用しており、これはPWAとしては動作しないモードです。 このようにしているのは、 (1) 一休ユーザの5割(モバイルでは7割)を占める Safari では、PWAの体験が良くないこと (2) 2017年頃に一休レストランでA2HSを試したところ、ほとんど使われなかったこと が理由です。

SafariのPWAモードが改善したり、A2HSを促すための良いタイミングが見つかったりしたら、"display": "standalone" などPWAとして動作するモードに切り替えようと思っています。。

今後の展望

Service Worker、今後しばらくはキャッシュ強化などのパフォーマンス改善目的で使用し、将来的にPWA化を進めたくなった時に備える、という感じで、引き続きやっていきます。

謝辞

一休.comのService Worker導入に当たっては、Googleの id:sisidovski さんに多大なご協力をいただきました。この場を借りてお礼申し上げます。

CSSフレームワークBulmaについて

フロントエンドエンジニアのid:ninjinkunです。この記事は一休.comアドベントカレンダーの1日目の記事です。

一休.comレストランの管理画面リニューアルプロジェクトにおいて、CSSフレームワークのBulmaを導入しました。結論としては、採用して良かったと思っています。

このエントリではBulmaを選定した理由と、採用後に見えたPros / Consについて述べたいと思います。

なお今回リニューアルした一休.comレストランの管理画面の概要は以下の通りです。

  • レストラン店舗向けの管理画面
    • 店舗の方と一休スタッフの両方が使う
    • DAUは数千の規模
  • 主な用途は在庫の管理と、プラン(コース)や席の管理
  • 現在は店舗を限定してリリース済み

具体的には以下のような画面で構成されています。

f:id:ninjinkun:20191201171421p:plain

f:id:ninjinkun:20191201171439p:plain

UIフレームワークは必要か?

まずそもそもUIフレームワークは必要かという議論があります。

今回のプロジェクトにはデザイナーがおらず、エンジニアの自分がUIデザインを担当していたので、ゼロからきちんとしたビジュアルデザインを設計するのが荷が重かったというのが1つ目の理由。

また、作る画面も20画面弱というそこそこのボリュームで、担当するエンジニアそれぞれがマークアップを行っていたため、スタイルの統一が必要だったというのが二つ目の理由です。

なぜBulmaなのか?

Bulmaの特徴は以下の通りです。

  • CSSのみ、JSなし
  • Flexboxベースのグリッドシステム
  • レスポンシブデザイン対応
  • SCSSでカスタマイズ可能

今回のプロジェクトではVue.jsとサーバーサイドテンプレートのJinja2を適材適所で使うハイブリッド構成だったため、Vueベースのフレームワークはまず選択肢から外れました。

そうなるとCSSベースのフレームワークがターゲットになります。Bulma以外にもBootstrap、UIKit、Materializeなどを検討しましたが、それぞれ以下の理由で見送りました。

  • Bootstrap
    • jQuery依存
    • アップデートで苦しんでいる人を多数観測
  • UIKit
    • JSを含んではいるがサイト自体がVueで作られていたりして親和性が高そうなのはGood
    • コンポーネントが多く、分厚い印象
    • ちょっとお洒落すぎる
  • Materialize
    • マテリアルデザインは既存の管理画面のテイストと全く違うので、移行した人が混乱する可能性を懸念

また、CSSのみで実装されているフレームワークとしてはTailwindCSS、Pureなどがありますが、以下の理由で採用を見送りました。

  • TailwindCSS
    • 細かすぎる
      • マークアップが得意なら良さそうだが、サーバーサイドエンジニアには辛そう
  • Pure
    • 簡素すぎる

そして最終的には以下の理由でBulmaを選びました。

  • 必要なパーツがそこそこ揃っている
  • コードもそこそこ薄くて読みやすい
    • フレームワークは使っていくと結局コードを読む羽目になる
      • であれば極力薄いフレームワークが良い
  • Flexboxベースのレイアウトは挙動が理解しやすい
  • カスタマイズすればテイストを旧管理画面に近づけられそう

Bulmaを使った感想

4ヶ月ほどBulmaを使ってきましたが、総評としては採用して良かったと思っています。

以下に細かいPros/Consを書き出してみました。

Pros

  • VueとJijna2両方でスタイルを統一するという用途にはとても合っていた
  • ビジュアルが良い案配で成立する
    • めちゃくちゃお洒落という感じにはならないが、管理画面には合っている
  • ドキュメントが読みやすい
  • フレームワークのコードが読みやすい
  • 実装中にレイアウトが崩れても、DOMインスペクタでCSSクラスを見れば何が悪かったすぐ分かる
    • マジックがないのが良い
  • カスタマイズが容易
  • 今回の要件ではiPadからPCまでの画面サイズをカバーしたが、問題無く使える

Cons

  • コンポーネントにツールチップがない
    • 管理画面ではツールチップを使いたいところが多いので、地味に困るところ。自前で実装している
  • モーダルなどの実装は自分でJSを書いて動きを付ける必要がある
    • Vueで実装するときは全てJSなのでこっちの方が良いのだが、Jinja2で実装しているときはJSを書き出すのが億劫…
  • これはどんなUIフレームワークでもそうだが、「エンジニアが作ったUI」感がどうしても出てしまう
  • ドロップダウンがIE11対応されていない
    • 他のコンポーネントは問題無く動くので、最近追加された実装で壊れた模様

まとめ

BulmaはCSSのみで構成され、適度に薄く、適度にレイアウトが揃うので、今回の管理画面リニューアルの用途には合っていました。

管理画面リニューアルプロジェクトはまだまだ進行中なので、今後もBulmaを活用していく予定です。

WordPressで爆速Canonical AMPサイトを構築した方法と3つの理由

文責

新規プロダクト開発部の伊勢( id:hayatoise )です。

新規プロダクト開発部は一休の新規事業の開発とデザインを担当する部署です。現在、新規プロダクト開発部は主に『一休.comスパ』、『一休コンシェルジュ』および『KIWAMINO』を担当しています。

はじめに

エグゼクティブや秘書の方々が会食先を探す際のお悩みを解消するためのオウンドメディア『KIWAMINO』をローンチしました。

『KIWAMINO』は WordPress をベースに Canonical AMP サイトにしました。AMP とは、ウェブ高速化のための HTML フレームワークです。そして、Canonical AMP サイトとは、全てのページが AMP で構成されているサイトのことです。

こちらのツイートから分かる通り、WordPress と Google が提供する AMP プラグイン(以下、AMP プラグイン)で Canonical AMP サイトを構成することはまだ事例としては少ないようです。

そこで、『KIWAMINO』をどうやって構築したのか紹介します。加えて、なぜ WordPress と AMP プラグインで Canonical AMP サイトを構成したのかについても説明します。

『KIWAMINO』をどうやって構築したのか

さくらの VPS 上に Docker で Nginx、WordPress ( PHP-FPM ) および MariaDB のコンテナを作成しました。そして、AMP プラグインで Canonical AMP サイトにしました。CDN は Fastly と imgIX を導入しています。

WordPress と AMP プラグインで Canonical AMP サイトを構成した方法

WordPress の構築もさることながら AMP プラグインで Canonical AMP サイトにすることもカンタンです。

まず WordPress を構築し、Twenty Ten 以降 Twenty Nineteen までのコアテーマを有効にします。そして、AMP プラグインも有効にします。

AMP プラグインの設定画面
AMP プラグインの設定画面

AMP プラグインの設定画面の Experiences 項目の Website にチェックを入れます。次に、Website Mode 項目を Standard にすると Canonical AMP サイトになります。以上です。

前述した通り、AMP HTML を書く必要はありません。AMP プラグインが HTML を自動で変換するからです。

<img src="https://resq.img-ikyu.com/asset/image/about/bn_about.png" alt="KIWAMINOについて" >

例えば、img タグを記述すると...

<amp-img src="https://resq.img-ikyu.com/asset/image/about/sd_bn_about.png" alt="KIWAMINOについて" width="640" height="400" class="amp-wp-unknown-size amp-wp-unknown-width amp-wp-unknown-height amp-wp-enforced-sizes i-amphtml-element i-amphtml-layout-intrinsic i-amphtml-layout-size-defined i-amphtml-layout" layout="intrinsic" i-amphtml-layout="intrinsic">
    <i-amphtml-sizer class="i-amphtml-sizer">
        <img alt="" role="presentation" aria-hidden="true" class="i-amphtml-intrinsic-sizer" src="data:image/svg+xml;charset=utf-8,<svg height=&quot;400px&quot; width=&quot;640px&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;/>">
    </i-amphtml-sizer>
    <noscript>
        <img src="https://resq.img-ikyu.com/asset/image/about/sd_bn_about.png" alt="KIWAMINOについて" width="640" height="400" class="amp-wp-unknown-size amp-wp-unknown-width amp-wp-unknown-height">
    </noscript>
    <img decoding="async" alt="KIWAMINOについて" src="https://resq.img-ikyu.com/asset/image/about/sd_bn_about.png" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>

表示する際は上記のような AMP HTML に自動で変換されます。

AMP プラグインの AMP Stories 作成画面
AMP プラグインの AMP Stories 作成画面

AMP プラグインの設定画面の Experiences 項目の Stories にチェックを入れると、開発コストを掛けることなく記事と同様に WYSIWYG で AMP Stories を作成することが可能になります。

インフラ

  • さくらの VPS( 2G プラン )
  • Fastly
  • imgIX( 画像に特化した CDN です。詳しくはこちら

ミドルウェア

  • Docker
  • Nginx
  • PHP-FPM
  • MariaDB

さくらの VPS 上に WordPress を構築せず、Docker を使用した理由は同じ環境を構築しやすいからです。

もう一つのオウンドメディア『一休コンシェルジュ』はローカル、ステージングおよびプロダクション環境が揃っておらず、ステージング環境でテストしたのにも関わらず何度か事故を起こしてしまいました。

この経験から各環境を統一することが比較的容易な Docker を採用しました。

WordPress

  • WordPress Core( 常に最新版を使用 )
  • AMP( HTML を AMP HTML に自動で変換 )
  • Fastly( コンテンツの投稿、更新および削除を検知して自動でキャッシュをパージ )
  • Media Cloud( S3 と imgIX の設定と連携を一括で可能に )
  • Yoast SEO( SEO 関連の設定を最適化 )
  • Glue for Yoast SEO & AMP( Yoast SEO の AMP 対応版 )
  • WP Pusher( 任意のブランチへのマージを検知して自動で差分をデプロイ )

WordPress Core のメジャーバージョンを除いて、全てのアップデートを自動に設定しています。

Lighthouse

『KIWAMINO』の Lighthouse のスコア
『KIWAMINO』の Lighthouse のスコア

修正できる点はいくつか残っていますが、Lighthouse の Performance スコアは常に 99 前後です。

なぜ WordPress と AMP プラグインで Canonical AMP サイトを構成したのか

WordPress と AMP プラグインで Canonical AMP サイトを構成した理由は以下の 3 つです。

(1) AMP の制約によって、サイトスピードが速くなるから

(2) エンジニア・デザイナーの学習および開発コストが低いから

(3) 巨大な組織・コミュニティの恩恵を受けられるから

(1) AMP の制約によって、サイトスピードが速くなるから

AMP の制約上、CSS および JavaScript のファイルサイズは軽くせざるを得ないので、それらがサイトスピードを必然的に妨げることがなくなります。

AMP は CSS の総量を 50 KB 以下にしなければなりません。また、JavaScript に関しては AMP が提供する公式のコンポーネントしか使えません。

したがって、CSS および JavaScript ファイルは必然的に軽量化されます。その結果、半強制的にサイトスピードを保つことができます。

(2) エンジニア・デザイナーの学習および開発コストが低いから

AMP プラグインが HTML を AMP HTML に自動で変換するため、デザイナーの学習コストは殆ど掛かりません。さらに Canonical AMP サイトにすると通常ページと AMP ページの二重管理がなくなるので、開発コストも低下します。

(3) 巨大な組織・コミュニティの恩恵を受けられるから

インターネット上の約 3 分の 1 のサイトは WordPress で動いているので、情報が沢山あります。かゆいところに手が届かないこともありますが、基本的に検索すれば大抵の技術的問題を解決することができます。したがって、エンジニアであれば誰でも担当できると思います。

当然、外注する場合も候補になり得る人材が沢山いるので、依頼しやすいかと思います。プラグインも豊富なので、その気になれば非エンジニアが新機能を追加してみることも可能です。

オウンドメディアを運営する上で欲しいと感じる機能は全てプラグインとして提供されていると言っても過言ではありません。巨大なコミュニティの元だからこそ得られるメリットが多々あります。

大半のオウンドメディアは Google 経由のトラフィックに依存しているかと思います。その良し悪しは別として AMP を採用することで、Google 経由の多くのユーザの体験が良くなります。その結果、それが SEO 対策になり、流入数の向上も見込めます。

さらに、AMP の制約を守り続けることでサイトスピードが維持されるため、サイト全体の離脱率も高まりにくいです。AMP 提供元の Google がプラグインを開発しているので、安心してプラグインに頼ることができます。サイトを放置しても自動でアップデートされるので、細かい仕様変更でエンジニアの手を借りる必要がなくなります。

おわりに

『KIWAMINO』は WordPress と AMP プラグインで Canonical AMP サイトにしました。WordPress と Canonical AMP を採用した理由は 3 つです。

(1) AMP の制約によって、サイトスピードが速くなるから

(2) エンジニア・デザイナーの学習および開発コストが低いから

(3) 巨大な組織・コミュニティの恩恵を受けられるから

以下、実際に運用してみた結果です。

(1) 数ヶ月間、実運用しましたが、問題なくサイトスピードは保たれています。少し困っていることは AMP プラグインが新しい JavaScript ファイルを検知した際、管理画面でそのファイルを許可するまで AMP が無効になることです。それ以外は特に困っていません。

(2) 予想通り、エンジニア・デザイナーの学習および開発コストは低かったです。

『KIWAMINO』のデザイナーさんに質問
『KIWAMINO』のデザイナーさんに質問

『KIWAMINO』のデザインを担当した方に技術ブログに掲載することは伝えず、Slack の DM で質問しました。

hayatoise「AMP HTML は一切書いたことがなかったという認識で OK ですね?!」

designer「YES! でも今も書いてる認識はないですけどね〜うふふ」

AMP プラグインは HTML を AMP HTML に自動で変換してくれます。なので、デザイナーの方は一切 AMP HTML を書く必要がないです。HTML と CSS の知識だけでコーディングが可能です。本人も一切書いてる認識はないようです。新しい技術を採用しましたが、デザイナーの学習および開発コストは殆ど高まっていません。

また、エンジニアの学習および開発コストも殆ど高まっていません。一度、AMP プラグインと他のプラグインが競合し、AMP エラーが発生しました。しかし、WordPress に関する情報量が多かったため、あまり詰まること無く問題を解決できました。オウンドメディアを開発する程度だと、AMP の学習コストは殆ど必要ないことも分かりました。

(3) 前述した通り、WordPress に関する情報量は非常に多いです。大抵の問題の解決方法は検索すれば見つかります。今のところ、あまり詰まったことはありません。

また、ちょっと機能を試したい時、WordPress プラグインに頼れることは非常に有り難いです。例えば、記事内でアンケートを取りたいと言われた場合、それを可能とするプラグインが存在するので、インストールするだけでビジネスサイドに提供できます。

結論、WordPress, Twenty Nineteen & AMP Plugin で Canonical AMP サイトは良いぞ

採用情報

hrmos.co

【イベント告知】一休のデータドリブン経営を"超具体的"に解剖

f:id:ikyuinouen:20190903140458p:plain

■応募方法
応募はこちらから

一休 × SmartNews イベント応募ページ

■イベントについて

一休はデータ活用を最大限にレバレッジした「データドリブン経営」を実践し、 第二創業期に入った現在も成長を続けています。 一休におけるデータサイエンティスト・マーケターは、経営を補完する役割ではなく 「経営・事業を動かす最重要な役割」を担っています。 自らもデータサイエンティストの代表・榊を中心としたチームで取り組んでいます。

「データドリブン経営」の最前線にいらっしゃるスマートニュース社 西口一希様と共に、 一休の実データを踏まえた"超具体的"な解剖や、強いデータサイエンティスト・マーケター になるためのポイントなどをセッションします。

■具体的には

・強いデータサイエンティスト、マーケターになるためのポイントとは
・「PL責任をもつ、ビジネス感度を上げる」といった、本当の意味で活躍できるデータサインティストについて
また当日は一休の具体的なデータを用い、セッションを行います。
■応募方法

すべての説明をお読みの上、下記「応募フォーム」よりお申し込みください。

一休 × SmartNews イベント応募ページ

■タイムテーブル

時間  内容
18:20 受付開始
19:00 一休のデータドリブン経営の紹介&トークセッション
20:40 懇親会(自由参加)
22:00 終了

■注意事項

①名刺2枚お持ちください
②電源設備のご用意がございません。ご注意下さい。
③参加人数に限りがございますので、事前にご参加が難しくなった際はお早めにキャンセルのご協力をお願いいたします
その他、ご不明点がある方は下記メールアドレスよりご連絡下さい。
【問い合わせ先】business_event@ikyu.com

■榊の過去資料

bdash-marketing.com

logmi.jp

AWS Elastic beanstalkからAmazon EKSへ移行する

以前の記事でも簡単に紹介した通り、一休では、アプリケーションのAWS Elastic beanstalkからAmazon EKSへの移行を進めています。

user-first.ikyu.co.jp

この記事では、その背景や、実際の設計、実際にAmazon EKSを活用してみて気付いた点、困った点、今後の展望を紹介したいと思います。

AWS Elastic beanstalkの辛い点

新しい環境の構築や運用が大変

  • 一休ではAWSのリソースをTerraformを使って管理しています。新しくウェブアプリケーションを立ち上げて、Elastic beanstalkで動かす場合、以下の作業をする必要があります。

  • Terraformで、Elastic beanstalkの定義を作ってリリースする。

  • 新しいアプリケーションのデプロイを通知するように自前で作ったAWS lambdaを修正。
  • アプリケーションのCI/CDの構築。
  • (必要に応じて)Route53の調整。

これを検証環境と本番環境の両方で実施する必要があります。面倒です。 さらに、TerraformとElastic beanstalkはあまり相性がよくないようで、意図しない変更差分が発生してしまったりします。 また、新しいインスタンスタイプが出てきたときに、環境によっては、完全に再作成しないと使えない場合があります。 実際、一休では、c5系やt3系のインスタンスが既存環境では使えずに、かなりの工数をかけて環境を再構築しました。 EC2とALBやAutoscalingをなまで使うよりElastic beanstalkを使うほうがはるかに楽なのは間違いないのですが、もっと楽に環境構築や運用ができる方法があれば、そっちに移行したい。

計算リソースを最適に使えていない

Elastic Beanstalkの場合、↓のようなアプリ配置をしなければならず、その結果、計算リソースの余剰を抱え込まざるを得ないです。

  • どんなに小さいアプリケーションでも可用性を確保するため、2台のec2インスタンスを割り当てている。
    • 2台ないとデプロイするときにダウンタイムが発生してしまいます。
  • ひとつのECインスタンスでひとつのアプリケーションだけを動かす。
    • 厳密にいえば複数のアプリを動かすことも可能ですが、設計的な無理が生じます。

実際に本番環境で動作しているすべてのEC2のCPU利用率の平均を算出してみたのですが、大半が使われていないことがわかりました。メモリも同様です。 オートスケールに依存しない設計にしているので、リソースはある程度余裕をもって割り当てています。なので、計算リソースの余剰がある程度あるのは設計通り、なのですが、さすがにかなりもったいない。

Amazon EKSへの移行による解決

Amazon EKSへ移行することで上述の2点は以下のように解決されると考えました。

  • 環境構築のTerraformから脱却しアプリケーションの構成や運用に関する定義はなるべくひとつのリポジトリに集約する。
  • コンテナオーケストレーション基盤で動かすことで、ECインスタンスとコンテナの関係が、1 対 1から 多 対 多になり、計算リソースを効率的に使える。

なぜAmazon EKSにしたのか

AWSの場合、ECSを使うという選択肢もあります。ECSを使うか、EKSを使うかの2択になりますが、EKSを選びました。 KSはKubernetesという業界標準であり今後も大きく進化していく仕組みを提供するため、ECSよりもコミュニティ、業界による改善の恩恵を受けやすい、と考えたからです。

構成と利用しているツールやアドオン

構成は下図の通りです。

f:id:s-tokutake:20190826124302p:plain

  • クラスタをふたつ構築し、Spinnakerを使い、同じアプリをデプロイし、Fastlyでロードバランシングします。
  • AWS ALB Ingress Controller とexternal-dns(https://github.com/kubernetes-incubator/external-dns)を使うことで、ロードバランサの定義とroute53の設定をKubernetesの管理下に置きます。
  • DockerイメージはAWS ECRに置きます。

eksクラスタの作成には、eksctl を利用しました。定義は以下の通りです。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: cluster01
  region: ap-northeast-1
  version: "1.13"

vpc:
  id: "vpc-xxxxxx
  cidr: "xx.xx.xx.xx/16"
  subnets:
    private:
      ap-northeast-1a:
        id: "subnet-aaaaaaaa"
        cidr: "xx.xx.xx.xx/22"

      ap-northeast-1c:
        id: "subnet-bbbbbbbb"
        cidr: "xx.xx.xx.xx/22"

      ap-northeast-1d:
        id: "subnet-ccccccccc"
        cidr: "xx.xx.xx.xx/22"

nodeGroups:
  - name: ng1
    labels: {role: workers}
    tags: {Stack: production, Role: eks-node, k8s.io/cluster-autoscaler/cluster01: owned, k8s.io/cluster-autoscaler/enabled: "true"}
    instanceType: c5.2xlarge
    desiredCapacity: 5
    maxSize: 8
    volumeSize: 100
    privateNetworking: true
    securityGroups:
      attachIDs: [sg-xxxx,sg-xxxx]
      withShared: true
    ssh:
      allow: true
      publicKeyPath: xxxxxxx
  • EKS作成時に新規作成される専用VPCではなく既存のVPCを利用します。
    • 新規で作成される専用VPCを使うと既存のVPCとの接続や設計上の衝突の解決など付随するタスクが多く発生すると考えたからです。
    • サブネットも同様です。
  • デフォルトのままだとディスクのサイズが小さいので volumeSize: 100 にすることで、ある程度の大きさを確保します。

利用したhelmチャート

helmはKubernetesのパッケージ(=チャート)管理ツール。helmでインストールできるチャートはなるべくhelmでインストールし、helmfileで宣言的な記述にして管理しています。 利用しているチャートは以下の通りです。

  • AWS ALB Ingress Controller

    • KubernetesのIngressとして、ALB を使えるようにするコントローラです。
  • external dns

  • kube2iam

    • Podに対してIAMポリシーを適用する仕組みを提供します。
  • datadog

    • 一休は、モニタリングにDatadogを使っています。また、APMやログもすべてDatadogを使うよう、現在移行作業を実施中です。Kubernetesでも引き続き使っていきます。
      • ログについては全部Datadog Logsに送信するとコストが高くついてしまうので、日々の運用で検索や分析に使うログだけをDatadog Logsに送りつつ、すべてのログをfluentdを使ってS3に送り、何かあったときに後から調査できるようにしてあります。 fluentdはDeamonSetで動かしています。

CI/CDをどのように構築するか

クラスタを運用するにあたって、管理する必要のある各種定義ファイルは以下の通りです。

  • 上述のeksctlのパラメータとなるクラスタの定義ファイル
  • helmチャートなどすべてのクラスタに共通の設定
  • Kubernetesのマニュフェストファイル
  • Spinnakerのパイプライン定義

このうち、eksctlのパラメータとなるクラスタの定義ファイルはそれほど頻繁にapplyするものではないので、Githubで管理しつつCI/CDの仕組みは構築しませんでした。 helmチャートは、クラスタに共有の設定になります。これは、ひとつのリポジトリにまとめて、circleciで CI/CDを構築しました。 KubernetesのリソースのマニュフェストファイルとSpinnakerのパイプライン定義はすべてのアプリケーションの定義をひとつのリポジトリににまとめてCI/CDを構築しました。 アプリケーションごとに別々のリポジトリにする設計やアプリケーションのリポジトリに入れてしまうというやり方も検討しましたが、まずはまとめて管理してみて、難点が出てきたら再度検討する、ということにしました。 また、アプリケーションのデプロイはコンテナリポジトリへのPushをトリガにするように、Spinnakerのトリガを設定しています。これによって、アプリケーション側はEKSを意識する必要をなくしました。

f:id:s-tokutake:20190828123537p:plain

苦労した点、気づいた点

DNS関連の課題

CoreDNSのポッドが落ちました。幸い少量のトラフィックを流してテストしている時期だったので大事には至りませんでした。 原因はわからないのですが、調査してみると、以下の記事が見つかりました。欧州のファッションサイトで発生した、DNS起因のkubernetes障害の振り返りの記事です。

kubernetes-on-aws/jan-2019-dns-outage.md at dev · zalando-incubator/kubernetes-on-aws · GitHub

DNS関連に次のような課題があることがわかりました。

"ndots 5 problem"

このコメント に詳しいのですが、kubernetesで動かしているPodのデフォルトの/etc/resolv.confは、以下の通り、ndots:5 が指定されます。

nameserver 172.20.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-1.compute.internal
options ndots:5

この場合、解決対象の名前に含まれているドットの数が5より小さいと、search に含まれているドメインを順に探して、見つからなかった場合に、最後に与えられた名前を完全修飾名として、探します。 例えば、www.ikyu.com を解決するなら、www.ikyu.com.default.svc.cluster.local を探す => ない => www.ikyu.com.svc.cluster.local cluster.local を探す => ない ... を繰り返して、search に書かれているドメインで見つからない => www.ikyu.com で探す => みつかった。という流れになります。その結果、ひとつのクラスタ外の名前を解決するだけで想定以上の名前解決リクエストが発生します。

この問題は、Podの(dnsConfig)https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-configndots:1 になるように記述をして対処しました。 PodのdnsConfigと(dnsPolicy)https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policyを記述することで、/etc/resolv.confをカスタマイズすることができます。

CoreDNS自体の信頼性

Amazon EKS(kubernetes 1.13)が使うCoreDNSのバージョンは、1.2.6で、最新のバージョンと比べると古いです。 configmapを覗いて、Corefileの中身を見てみると、proxy プラグインを使っているのがわかります。 で、この proxy プラグインは、CoreDNSの最新のバージョンではソースコードごとなくなっています。 経緯は次のissueに詳しいです。

deprecate plugin/proxy · Issue #1443 · coredns/coredns · GitHub

  • proxy はバックエンドに対するヘルスチェックに問題がある。
  • forward プラグインのほうがコードもシンプルでソケットをキャッシュするので高速に動作する。

kubernetesも1.14から forward を使うようになっているようです。公式のライフサイクルから判断すると、Amazon EKSが1.14に対応するのは9月。この問題についてはバージョンアップするしか解決のしようがなさそうなので、EKSで1.14が使えるようになったら速やかにバージョンアップすることにしました。

また、EKSではデフォルトのCoreDNSのPodの数は2つになっています。処理自体は2つで十分捌けるのですが、Podが落ちるという現象に当たってしまったので安全を見てPodの数を増やしました。

また、以下を参考にしながら、Nodelocal DNS Cacheを導入してみたのですが、なぜか名前解決の速度が劣化してしまい、導入を断念しました。また、詳しく調査してチャレンジしてみたいと思います。

kubernetes/cluster/addons/dns/nodelocaldns at master · kubernetes/kubernetes · GitHub

Descheduler は入れたほうがいい

運用を開始してみるとPodがノード上で偏り、あるノードは50%を超えてメモリを使っているのに別のノードはスカスカ、というようなことが起きました。 対策としてdescheduler をCronJobで動かすことで、定期的にPodの再配置を行い、ノードのリソース利用状況を平準化しています。 descheduler自体はとても簡単に導入できます。

負荷試験はやったほうがいい

最初は、現状のElastic Beanstalkのリソースの利用状況やリクエスト数を見ながら計算をしてPod数やcpu/memoryの requests/limits を割り出していたのですが、実際にリリースしてみると想定通りリクエストを捌けませんでした。ある程度、トラフィックのあるアプリケーションの場合は、机上計算だけではなくきちんと負荷試験をやったほうがよさそうです。

SIGTERMを受けたらグレースフルにシャットダウンするアプリケーションにしておく

Podの終了については以下の記事が大変詳しいです。

Kubernetes: 詳解 Pods の終了 - Qiita

KubernetesはPod内のプロセスにSIGTERMを送信することでPodを止めます。 SIGTERMを受けたらグレースフルにシャットダウンするアプリケーションにしておく必要があります。

当初の目的は達せそうか

冒頭に書いた通り、以下のふたつがAmazon EKSへの移行で解決したい課題でした。

  1. アプリケーションの動作環境の構築を簡単にする。
  2. 計算リソースをより効率的に使えるようにする。

1.については、Elastic beanstalkに比べてはるかに簡単かつ素早くに環境が作れるようになりました。が、そう感じるには慣れと習熟が必要なのも確かです。 多くの人が書いていますが、マニュフェストファイルを理解するのはどうしても時間がかかります。 すべてのエンジニアが簡単に環境を構築できるようにするのなら、なんらかのscaffolding的なものが必要だと感じました。

計算リソースの利用の効率化は手ごたえを感じています。Elastic beanstalkからすべてをEKSへ移行できたら大きくコストダウンできそうです。

まとめと今後の展望

現時点で、Amazon EKSで動いているのは、pythonのwebアプリケーションとgoのgrpcサーバです。その他の、一休で動かしているLinux系のアプリケーションは、Elastic beanstalk時代からDockerで動いていたため、Amazon EKSへの移行はスムーズにできそうで、すでに、移行の目途が立っています。

あとは、Windows系のアプリケーションをどうするか、ですが、kubernetes 1.14からWindowsのコンテナがサポートされます。 一休のWindows系のアプリケーションはコンテナでは動いていませんが、これを機にコンテナ化にチャレンジしたいと考えています。 kubernetesは複数のクラウドプロバイダの差異を抽象化するレイヤとして進化しているように思います。 例えば、あるクラウドプロバイダで大規模な障害が起こって、マルチクラウドにデプロイせよ!!となったときに、kubernetesで動作しているアプリケーションなら、クラウドプロバイダの違いを意識せずに、アプリケーションのデプロイができるはずです。実際に動作するどうかは別問題ですが。 そう考えると、Windows系のアプリケーションもコンテナ化してkubernetesで動かせるようにすることで、今後のクラウド/コンテナ技術の恩恵をしっかり受けれるようにしておきたいと感じています。 また、バッチ実行基盤もkubernetesベースのジョブエンジンに切り替えることで、可用性や信頼性を改善し、効率的にリソースを使えるようにする、ということにも取り組んでいきたいです。

採用情報

一休では、クラウド/コンテナ技術に経験がある方 or 興味がある方やSREやDevopsを通じて価値あるサービスを世に届けたい方を募集しております。

hrmos.co

hrmos.co


この記事の筆者について

  • システム本部CTO室所属の 徳武 です。
  • サービスの技術基盤の開発運用、開発支援、SREを行なっています。

Amazon EKSでgRPCサーバを運用する

以前の記事でも紹介した通り、一休では、gRPCを使ったサービスを導入し始めています。

user-first.ikyu.co.jp

この記事では、このサービスをAmazon EKSで提供するための設計や気をつけたポイントについて紹介します。

背景

一休では、ウェブアプリケーションの実行環境としてAWS Elastic Beanstalkを採用しています。 そして、この4月からElastic BeanstalkをAmazon EKSへ移行するプロジェクトを進めています。 このgRPCサービスもElastic Beanstalkで運用をしていましたが、以下の問題を抱えていました。

  • 適切にロードバランシングできない。
    • Elastic BeanstalkでgRPCサービスを運用しようとするとNetwork Load Balancer(NLB)を使うことになります。NLBはレイヤ4のロードバランサです。一方で、gRPCはhttp/2で動作し、ひとつのコネクションを使いまわします。このため、NLBでは、特定のサーバに負荷が偏ってしまう場合があります。

また、EC2サーバのdockerdのcpu利用率が突然上昇する、アプリケーションのデプロイに時間がかかるというような問題も起き始めていたため、Amazon EKSへ移行することでこれらの問題の解決を試みました。

KubernetesでgRPCサーバを動かす場合の注意点

Amazon EKSは、AWSが提供するマネージドなKubernetesです。KubernetesでgRPCサーバ動かす場合の注意点は上述のNLBの場合と同様、以下の点になります。

  • KubernetesのServiceはL4のロードバランサであり適切に負荷分散できない。

このため、KubernetesのServiceではなく別の仕組みを使って負荷分散を実現する必要あります。

Envoyを導入する

多くの事例でEnvoyを使ってこの問題を解決しています。

www.envoyproxy.io

AWSのApplication Load Balancer(ALB)がhttp/2に対応しているのはクライアントとALB間の通信でありALBからバックエンドへの通信はhttp/2に対応していない、だから、gRPCサーバの場合はNLBを使う必要がある。そしてNLBを使うと上述の問題にぶつかる、、、ということでしたが、Envoyは、公式サイトに書かれている通り、クライアントとEnvoyの間もEnvoyとバックエンドサーバの間も両方ともHTTP/2とgRPCをサポートします。一休でもEnvoyを使うことに決めました。

構成

GitHub - GoogleCloudPlatform/grpc-gke-nlb-tutorial: gRPC load-balancing on GKE using Envoy

このGKEでの事例を参考にしつつ、以下のような配置構成にしました。

f:id:s-tokutake:20190826085145p:plain

Kubernetesクラスタ外部からのアクセスを受ける必要があるため、type: LoadBalancer のService経由でEnvoyが外からのアクセスを受けます。EnvoyのバックエンドはHeadless Serviceにします。Headless Serviceを使うと背後にあるPodに直接アクセスできるレコードがクラスタ内部DNSに作成されます。EnvoyはこのDNSレコードからPodのIPを取得し、Envoy側の構成にしたがって負荷分散を行います。

Envoyの設定

Envoyの構成情報はKubernetesのComfigMapで設定します。内容な以下の通り。

apiVersion: v1
kind: ConfigMap
metadata:
  name: grpc-service-proxy
data:
  envoy.yaml: |
    static_resources:
      listeners:
      - address:
          socket_address:
            address: 0.0.0.0
            port_value: 50051
        filter_chains:
        - filters:
          - name: envoy.http_connection_manager
            config:
              access_log:
              - name: envoy.file_access_log
                config:
                  path: "/dev/stdout"
              codec_type: AUTO
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: https
                  domains:
                  - "*"
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: grpc-service
              http_filters:
              - name: envoy.health_check
                config:
                  pass_through_mode: false
                  headers:
                  - name: ":path"
                    exact_match: "/healthz"
                  - name: "x-envoy-livenessprobe"
                    exact_match: "healthz"
              - name: envoy.router
                config: {}
      clusters:
      - name: grpc-service
        connect_timeout: 0.5s
        type: STRICT_DNS
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        drain_connections_on_host_removal: true
        http2_protocol_options: {}
        hosts:
        - socket_address:
            address: grpc-service.default.svc.cluster.local
            port_value: 50051
        health_checks:
          timeout: 3s
          interval: 5s
          unhealthy_threshold: 2
          healthy_threshold: 2
          grpc_health_check: {
            service_name: Check
          }
    admin:
      access_log_path: /dev/null
      address:
        socket_address:
          address: 127.0.0.1
          port_value: 8001

注意点としては、以下の通りです。

  • drain_connections_on_host_removal: true を設定すること。
    • この設定がない場合、バックエンド側のServiceからPodが外れても、そのPodのヘルスチェックが失敗するまで、Envoyは外れたPodにトラフィックを流し続けます。その結果、デプロイ時にリクエストを落としてしまう可能性が発生します。
  • grpc_health_check を指定すること。
    • バックエンド側がgRPCサービスなのでこれを設定します。

gRPCサービス側の構成

gRPCサービスはElastic BeanstalkでもDockerを使って動かしていました。したがって、EKSに動かすために大きな変更は必要ありませんでした。唯一必要だったのがヘルスチェックです。 上述の通りNLBではgRPCサービスのヘルスチェックをすることができません。NLBのヘルスチェックはTCPのポートの疎通確認ですが、厳密にいえばこれはアプリケーションの死活監視になりません。一方、Kubernetesではヘルスチェック( livenessprobereadinessprobe)は重要な役割を果たしますので、設定する必要があります。 gRPCサービスに対するヘルスチェックについては以下の記事に方法が書いてあります。

Health checking gRPC servers on Kubernetes - Kubernetes

この記事に従って、以下の2点を行いました。

  • gRPCサービスにヘルスチェックのエンドポイントを実装する。
  • Dockerイメージ内にgrpc-health-probeを同梱する。

そして、gRPCサービスのPodの定義を以下のようにしました。

...略...
spec:
      containers:
        - name: grpc-service
          image: ["ImageName"]
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 50051
          readinessProbe:
            exec:
              command: ["/bin/grpc_health_probe", "-addr=:50051","-service=Check"]
            initialDelaySeconds: 5
          livenessProbe:
            exec:
              command: ["/bin/grpc_health_probe", "-addr=:50051","-service=Check"]
            initialDelaySeconds: 10

...略...

リクエストを落とさずにデプロイをする

ここまで構築してリクエストを流してみると、ひとつのコネクションで多数のリクエストを送信しても、きちんと複数のPodにトラフィックが分散されることがわかりました。

あとは、EnvoyもgRPCサービスもリクエストを落とさずにデプロイできるように設定する必要があります。

Envoyの場合

Envoyの場合は、以下のissueがとても参考になりました。

Graceful HTTP Connection Draining During Shutdown? · Issue #7841 · envoyproxy/envoy · GitHub

このissueによれば、/healthcheck/fail というエンドポイントにPOSTリクエストをすることでEnvoyはそれ以後受け付けたリクエストの接続をクローズするようです。これを活用して、EnvoyのPodの定義のlifecycleに以下の通り書けばリクエストを落とさずにPodの入れ替えができそうです。

...略...
lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - '-c'
                  - >-
                    wget -qO- --post-data=''
                    http://127.0.0.1:8001/healthcheck/fail && sleep 10
...略...

preStopフックで wgetを使ってhttp://127.0.0.1:8001/healthcheck/failにPOSTリクエストを行い、その後10秒待っています。 127.0.0.1:8001 には Envoyの管理用のAPIが立ち上がっています。 この設定によって、Podが停止する流れは大まかに以下のようになります。

  1. PodがTerminating 状態になる。同時にpreStopフックが動き、healthcheck/fail にPOSTリクエストがされたあと、10秒待つ。
  2. Podには、リクエストは来るが、レスポンスを返すときにコネクションは閉じられる。
  3. 10秒待っている間に、ServiceからPodが外れる。以後、新しいリクエストは来なくなる。接続中のリクエストも10秒間ですベて処理される、はず。
  4. 10秒停止後、SIGTERMで、コンテナが停止される。すでに接続がない状態なので、リクエストは落とさない。

この設定をして、トラフィックを流しつつ、Envoyのデプロイを行ってみたところリクエストを落とさずにPodが入れ替わることが確認できました。

gRPCサーバの場合

注意点は以下のふたつです。

  • 上述の通り、Envoyの構成で drain_connections_on_host_removal: true の設定をする。
    • この設定をしないとEnvoyはヘルスチェックが通り続ける限り、古いReplicaSetに属するPodにもトラフィックを流し続けます。
  • アプリケーションはSIGTERMを受け取ったら、グレースフルにシャットダウンする処理を実行する。
    • 今回のサービスはGoのgRPCサービスなので、GracefulStop()を呼び出すようにしています。

このふたつをやっておけば、ローリングアップデートでもBlue/greenでもリクエストを落とさずにPodの入れ替えができることを確認できました。

まとめ

今回は、gRPCサービスのEKSでの動かした方を紹介しました。 一休では、現在、主要サービスのAmazon EKSへの移行を進めています。この移行の全体像については、別途、紹介するつもりです。

参考資料

文中に紹介したリンク以外にも以下のサイトを参考にさせて頂きました。

Kubernetes上でgRPCサービスを動かす | SOTA

kubernetesでgRPCするときにenvoy挟んでみたよ - Qiita

Kubernetes: 詳解 Pods の終了 - Qiita

spinnaker/solutions/bluegreen at master · spinnaker/spinnaker · GitHub

Manage Traffic Using Kubernetes Manifests - Spinnaker


この記事の筆者について

  • システム本部CTO室所属の 徳武 です。
  • サービスの技術基盤の開発運用、開発支援、SREを行なっています。