一休.com Developers Blog

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

Storybook を自作して「フロントエンドビルドが遅い問題」に立ち向かう

この記事は一休.comアドベントカレンダー2018の10日目です。

qiita.com


こんにちは。レストラン事業部の所澤です。 WEBアプリケーションエンジニアとしてフロント/サーバー問わず機能開発を行っています。

今回は一休.com レストランの旧アプリケーションのフロントエンド開発環境改善についてお話します。

※ この記事の執筆時点では以下の内容は master に取り込まれていません。同僚のフロントエンドエンジニア(ガチ勢)から何か指摘があったら追記します。

この記事の概要

  • 一休.com レストランの旧WEBアプリケーション(以下 restaurant1 )はなぜかフロントエンドビルドが超遅い。
  • Storybook のようなアプリと切り離された、高速でビルドできる環境があればもっと快適に開発できるのではないか?
  • Storybook だと vue-devtools が使えないので Storybook (の最低限の機能を持つ)小さいアプリケーションを作ってみた。

一休.com レストランの開発環境

新アーキテクチャへの移行状況

何度かこのブログでも取り上げていますが、現在(2018年12月)、一休.com レストランは新旧ふたつのWEBアプリケーションが並行する形で運用されています。 古くから稼働している VBScript で書かれたアプリケーションは restaurant1、リニューアル後の Python で書かれた新しいアプリケーションは restaurant2 と呼ばれています。 着々と restaurant2 への移行は順調に進んでいますが、依然として restaurant1 の上に乗っている部分も多く残っています。

f:id:shozawa:20181210145104p:plain:w300

たとえばスマートフォン版の店舗トップ画面は機能追加の機会が多いページですが、まだ restaurant2 への移行が完了していません。 新アーキテクチャだけを触ればOK、という状態まではまだ少しかかりそうだというのが現状です。

restaurant1 のフロントエンドについて

さて、"レガシー"などと言ってしまいましたが、実はフロントエンドに限って言えば旧アプリケーションもそこまで古くはありません。 jQueryでゴリゴリ書かれたページもありますが、主要ページに関しては ES2015+ と Vue.js で開発できる環境が整っています。 restaurant2 のピカピカのコードに比べると若干見劣りはしますが十分モダンだと言っていいでしょう。

問題は、フロントエンドビルドが とてつもなく遅い ことです。 フルビルドに時間がかかることに関して良いとしても watch しているときの差分ビルドも 1分以上 かかります。(Core i7 の開発機で)

遅い原因は特定できていないのですが、

  • アセットの肥大化
  • そもそも Windows だとビルドが遅い( restaurant1 は ASP で書かれているので Windows 必須です)
  • セキュリティのために入れているファイル監視ソフトの相性の問題

など、いろいろな可能性が考えられます。

さて本来であれば根本原因を特定して解決するのが筋ですが、どうにも問題の切り分けがうまくいかないので別の解決方法を考えてみます。

restaurant1 に Storybook を導入してみる

いままではユニットテストを書いてブラウザを使った動作確認の回数を減らし、なるべくこの問題を意識しなくて済むように気をつけていました。 しかしやはり新規のコンポーネントを0から作るときやデザインの微調整をする際はどうしてもビルドの遅さが気になります。

restaurant2 や宿泊のサイトでは 既に Storybook を導入済みだったこともあり、"開発用の Playground として" restaurant1 にも Storybook を入れてみようと試してみました。

f:id:shozawa:20181210150410p:plain:w300

※ restaurant2 では"デザイナーとの協働をスムーズにする" という目的で Storybook が活用されています。詳細はまたいつか。

Storybook で十分、か?

さて冒頭でも書きましたが結局 Storybook を導入することは見送りました。理由は vue-devtools が使えないからです。

今回はデザインシステムとしてではなく、開発用の Playground として Storybook を使いたいので開発ツールがうまく動かない点は致命的な問題です。

(最初に気付けよ、という感じですが私自身はそれまであまりちゃんと Storybook を使ったことなかったので...)

Github の issue を見るとワークアラウンドがありそうですが...。すでに導入でだいぶ消耗していて、これ以上の yak shaving をする気は起きなかったので別の方法を検討することにしました。

(追記: 無理やりですが iframe を別タブで開くと vue-devtools が使えます)

Storybook 相当のアプリケーションを作ってみる

よく考えれば今回の用途に限って言えば Storybook の全機能が使える必要はありません。やりたいことは至ってシンプルです。

要求・仕様

  • アプリケーションと独立した環境で動作確認しながらコンポーネントを開発できる
  • vue-devtools が使える
  • LiveReload
  • Mac でも開発できる
  • コードのコピペなどせずに restaurant1 のコードがそのまま確認できる
  • Storybook 風にストーリーが書ける

この仕様を満たす小さなアプリケーションを書いてみることにしました。 ※ あくまで Playground として使い、最終確認はアプリケーションに組み込んでやる前提。

まずは使い方をご紹介

  • stories.js にStorybook 風のAPIでストーリーを追加していく
storiesOf('sample')
  .add('hello', h => h('h3', ['hello. this is my story.'], {}));

storiesOf('DatePicker')
  .add('select', h => h(CustomDatePicker, { props: { value: '2018-12-01' } }));
  • restaurant1 内で yarn play を実行してサーバーを起動

f:id:shozawa:20181210144606g:plain

UIがダサいのはご容赦ください...。

これだけですが、「アプリケーションと切り離された環境でコンポーネントを開発する」ということは実現できています。

こだわりポイント

ここからは蛇足な気がしますが、せっかくなのでこだわりポイントをご紹介します。

  • とにかくシンプルに!
  • 新しいライブラリを追加しない
  • Storybook like な API

以上の3点を心がけて実装しました。

このツールを使う人やツールの機能を拡張しようとしてコードを読む人の負担が最小限になるように気をつけています。

まずは何よりコードが小さく、シンプルになるように心がけました。また不用意に新しいライブラリを追加すると、でコードを読んだ人に負担もかけてしまうので restaurant1 に追加済みのライブラリのみを使用することにしました。

webpack-dev-server でホスト

今回はアプリケーションの中に playground ディレクトリを作りそこに関連ファイルを格納し、 webpack-dev-server でホストしています。 今回のモチベーションが「ビルドの速度改善」なので 本アプリの webpack の設定を使い回すことはせずに新しく playground 以下に数十行のシンプルな設定ファイル( playground/webpack.config.js )を追加しました。

// package.json
{
  // 略
  "scripts": {
    // 略
    "play": "webpack-dev-server --config playground/webpack.config.js"
  }
}
// playground/webpack.config.js
{
  // 略
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    port: 9000,
  },
}

9000番ポートで dist ディレクトリの内容をホストします。

storiesOf 関数

storiesOf 関数の実装はこれだけです。 storiesOf と add でオブジェクトにコンポーネントを登録していきます。

// playground.js
const stories = {}; // Component を格納
const tableOfContents = {}; // ナビゲーション用

// TODO: HMR
function storiesOf(title) {
  tableOfContents[title] = tableOfContents[title] || {};
  return {
    add(scenario, value) {
      const key = `${title}:${scenario}`;
      tableOfContents[title][scenario] = key;
      playground[key] = { render: value };
    },
  };
}

const getStories = () => stories;
const getTableOfContents = () => tableOfContents;

export {
  storiesOf,
  getStories,
  getTableOfContents,
};

※ vue-play の実装を参考にさせていただきました。

github.com

プレビュー機能

import { getStories } from './playground';
export default {
  name: 'PlaygroundPreview',
  data() {
    return {
      scenario: '',
      stories: {},
    };
  },
  methods: {
    setScenario() {
      const hash = decodeURI(window.location.hash);
      this.scenario = hash.replace('#', '');
    },
  },
  computed: {
    current() {
      return this.stories[this.scenario];
    },
  },
  created() {
    this.stories = getStories();
    this.setScenario();
    window.addEventListener('hashchange', this.setScenario);
  },
  render(h) {
    return h(this.current, [], {});
  },
};

ストーリーとして登録したコンポーネントのプレビュー部分です。 URL のハッシュ部分にコンポーネントのキーを入れるようにし、ハッシュの変更によってプレビューされるコンポーネントが切り替わるようにしています。

http://localhost:9000/#{ストーリーのタイトル}:{ストーリーの小見出し}

今後の展望

webpack の設定ファイルを含め、250行程度のコードでここまでの内容が実現できました。

シンプルな実装で最低限やりたいことはできた、と思っています。

  • WEBフォントの読み込みができていない
  • Vuex と連携するコンポーネントの動作確認ができない
  • DefinePlugin の対応
  • async/await を使っているコードでエラーが出るので webpack の設定を見直し
  • UIがイケてない

などすでにいくつか課題は見つかっているのですが、プレゼンテーションだけに責任を持つシンプルなコンポーネントの開発であれば十分に活用できるかと思います。

実はデモ用にいくつか実際に使われているコンポーネントを追加しようと思ったのですが、ほとんどの主要コンポーネントが Vuex に依存していてうまく追加できませでした。

よく言われていることですが、あらためて Presentation Component と Container Component の分離が重要ですね。

もう少しブラッシュアップして、良さそうであれば master に取り込もうと思います。

Rundeck in practice [運用編]

この記事は一休.comアドベントカレンダー2018の9日目です。

qiita.com


導入編に続き、運用編です。
ここ2年間 Rundeckを運用してきて発生したトラブルとその対処について書きます。
※この記事で言及するRundeckはバージョン2.6.9です。
トラブルはふたつありました。

  • データベースが高負荷になり動作が不安定になった
  • なぜかジョブが起動しない

データベースが高負荷になり動作が不安定になった

原因は複数ありました。

データベース(AWS RDS)のインスタンスタイプが小さすぎた

完全にサイジングのミスでした。動作確認で複数のジョブを大量に動かしたときでも、t2.smallのインスタンスで十分に動作したので、t2.smallで大丈夫だろうと、そのまま本番導入したのですが、運用開始して2ヶ月くらいで、高負荷になりました。速やかにt2.mediumにスペックアップしました。

コネクションプールの設定が漏れていた。

Rundeckは、rundeck-config.propertiesというファイルにデータベースの接続情報を記述します。
デフォルトでは、H2 Databaseを使用する前提の接続情報になっています。
これをRDS(MySQL)を使うように修正したのですが、その際、コネクションプールの設定が漏れていました。そのため、接続が一切プールされない、という状態になっていました。
以下の記述をrundeck-config.propertiesに追加することで適切にプールをするように修正しました。

dataSource.pooled=true
dataSource.properties.removeAbandoned=true
dataSource.properties.removeAbandonedTimeout=60

データベースのインデックス不足

このissueで議論されていますが、RundeckのデータベースにはGUIの性能を大きく改善できるインデックスがいくつかあります。これらのインデックスはデフォルトでは付与されていないようです。運用開始後、GitHubやGoogle Groupに投稿されている情報を調査し、このissueで紹介されているインデックスや#1547で紹介されているインデックスを付与することで性能を改善できました。

実行ログがたまり続ける

これは運用を開始する前からわかっていた課題だったので、データベースの高負荷の原因にはなりませんでしたが、対処が必要な課題ではありました。
Rundeckはデータベース上の実行ログを削除しません。長期間運用して実行履歴が大量に溜まった段階で実行履歴の検索を行なった場合、データベースの負荷が高まる可能性があるので、定期的に削除する仕組みが必要だと判断しました。
削除する実行ログの条件は以下の通りです。

  • ジョブは最新の1万件の実行ログを保持する。1万件を超えたら古い順に削除される。

月次1回だけ実行されるジョブもあれば、1時間以内に複数回実行されるジョブもあります。また、調査のために古い履歴を調べることがあるかもしれません。このような前提を考慮して、上記のようなルールにしました。

そして、Rundeckのデータベースの構造を理解してどのテーブルのデータを削除すればよいのかを見つけ、定期的に削除するプログラムを作成しました。

調査したところ以下のdelete文の実行すれば良さそうです。

delete from log_file_storage_request where execution_id in ( @jobhistoryids )
delete from execution where id in ( @jobhistoryids )
delete from base_report where jc_exec_id in (@jobhistoryids )

@jobhistoryidsは、base_reportテーブルのjc_exec_id列の値です。
あとは、上記の条件に合致するジョブのIdと削除件数を特定する必要があります。
それは次のSQLで取得できました。

select
    inntable.jc_job_id as JobId,
    inntable.counts - 10000 as DeleteCount
from
    (
        SELECT
            jc_job_id,
            count(jc_exec_id) counts
        FROM
            base_report
        group by
            jc_job_id
    ) inntable
    inner join
        scheduled_execution se
    on  se.id = inntable.jc_job_id
where
    se.execution_enabled = 1
and inntable.counts > 10000
order by
    inntable.counts desc

このselect文で、実行回数が1万回を超えているジョブのIdと超過回数がわかります。
※例えば10300回実行されたいたら300回が超過回数になります。

そして、次のSQLで削除対象のjc_exec_idを特定します。

SELECT
    jc_exec_id
FROM
    base_report
where
    jc_job_id = @jobId -- 上のselect文で見つかった JobId
order by
    date_completed asc -- 完了日時で昇順でソートすることで1万件を超過した実行ログのIdを特定できる
limit @deleteCount -- 上のselect文で計算した削除対象件数 DeleteCount

あとは、このselect文で取得できたjc_exec_idをパラメータにして上述した3つのdelete文を実行すれば、削除完了です。

なぜかジョブが起動しない

データベースのトラブルが治った後はしばらく順調に動作していました。しかし、指定した時間なのにバッチに起動しない、という現象が発生するようになりました。
詳しく状況を見てみると、バッチ実行が遅延しているようでした。
いろいろと調べてみると、Rundeckが内部で使っているジョブスケジューラライブラリのQuartzのパラメータが原因でした。
Rundeckの公式ドキュメントによれば、The maximum number of threads used by Rundeck for concurrent jobs by default is set to 10と書いてある通り、デフォルトでは最大で10本のジョブの同時実行が可能です。
一方、一休では利用が促進された結果、タイミングによっては10以上のジョブが同時に実行されるような状況になっており、その結果、実行が遅延するようになっていました。この設定を変えるには、以下の記述をrundeck-config.propertiesに追加します。

quartz.props.threadPool.threadCount=30

この記述によって最大30まで同時実行できるようになり、問題が起きなくなりました。

終わりに

今回は実際に運用してきて発生したトラブルとその対処について紹介しました。参考になれば幸いです。 一休ではWindowsのタスクスケジューラからRundeckへ移行しました。Rundeckは未知のツールだったので苦労する点もありましたが、起こった問題は調査すれば解決策が見つかるものばかりだったので、移行は十分成功したと感じています。

おまけ

GUIの日本語化

管理画面が英語だとわかりにくいので、一休ではガイドラインにしたがって、主要な部分だけですが日本語にしています。 部分的なローカライゼーションではありますが、ないよりマシ、なレベルではあるので、本家の方にも導入できるようにPRを送っています。次のバージョンで取り込まれるかもしれません^ - ^

この記事の筆者について

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

ネット断食におすすめ!日帰り温泉・サウナも楽しめるSPA15選

この記事は一休.comアドベントカレンダー2018の7日目です。

こんにちは。スパ事業部 デザイナーの東根です。

約1年かけて10月25日にローンチした一休.com スパ即時予約サービス をご紹介したいと思います。

一休.com スパ

SPAとは?

エンジニアのみなさまは「SPA」と聞いてまず 「Single Page Application(シングルページアプリケーション)」 を思い浮かべたかもしれませんが、罠でした。すみません。

ここでご紹介するのは、 日帰り温浴やサウナのほかリラクゼーションマッサージの施術が受けられる 「デイスパ(Day spa)」や「ホテルスパ」のことです。

最近では「サ道」「サウナー」も増えているそうですが、 温浴施設で身体を温めてからアロマトリートメントなどの施術を受けると、 血行やリンパの流れが良くなりより効果が高まります。

「Day spa」のwiki英語版によると、

A day spa is a business that provides a variety of services for the purpose of improving health, beauty and relaxation through personal care treatments such as hair, massages and facials. A day spa is different from a beauty salon in that it contains facilities such as a sauna, pool, steam room, or whirlpool that guests may use in addition to their treatment. ...

一休 .com スパの特徴・UIUXのポイント

つまり、一休 .com スパが厳選するスパは、
街中にあるリラクサロンやエステサロンとは違い

  • バスサウナなどの温浴施設
  • プールフィットネスジムなどの運動施設
  • バスローブのまま寛げるリラクゼーションラウンジ

などの施術前後に使える付帯施設があったり、

  • 一般には流通しないこだわりの化粧品ブランド

を使っていたり、といった魅力をアピールしていく必要があります。

UIUXのポイント① 施設の魅力を伝える

f:id:higashinek:20181206134036j:plain

・・・

f:id:higashinek:20181206144238j:plain

施設の魅力といえば、何と言っても写真です。
一休.comの他のサービスにも共通しますが、高級感のある美しい写真をできるだけ大きく見れるとユーザーにその施設を訪れたときのイメージを持ってもらえます。

なので、登録するのに写真は必須。また、お部屋のスペック(広さやスパスイートかどうか、完全個室であるかetc)や温浴施設の内容(ホットバス、コールドバス、ドライサウナ、スチームサウナ、温泉、岩盤浴があるか)、プールやフィットネスジムは水着やトレーニングウェアをレンタルできるかどうかをシンプルなテキストアイコンで表現しました。

UIUXのポイント② プランの魅力を伝える

f:id:higashinek:20181206152217j:plain

ここからはスマホ版のキャプチャで説明します。 プラン一覧およびプラン概要では、タイトル・利用できる付帯施設・滞在時間目安・料金がわかるようになっています。ちなみに、タイトルにある分数純粋なトリートメントの時間で、カウンセリングや施術前後に使える付帯施設の利用時間を含んでいません。そのかわりに前後の付帯施設利用時間を含めた全体の滞在目安の時間を別に表示することで、どのくらい時間に余裕があればこのプランをしっかり体験できるかがわかります。

f:id:higashinek:20181206153816j:plain

プラン内容ではトリートメントの詳細とともにどんなお部屋で施術受けるのか、付帯施設はそれぞれいつ(施術前か後か)どのくらいの時間、どんな施設を使えるのかわかります。

UIUXのポイント③ 空き時間・予約時間をわかりやすく

f:id:higashinek:20181206162455g:plain

プランが決まったら「予約時間を確認する」ボタンから空き時間を探してみます。 まずカレンダーで「日付」を選択、次に「施術スタート時間」を選びます。 時間はレストランの予約とは異なり来店時間ではありません。*印で「付帯施設は、施術スタートの○分前から利用できます」と表示していますので、それを参考に15分刻みの時間ボタンを選んで予約フォームに進みます。

UIUXのポイント④ 予約の前に利用条件をしっかり説明

f:id:higashinek:20181206164036g:plain

注意文言に入れて読んでるハズとしてしまうのではなく
女性限定マタニティ不可のプランなどで利用できない条件を選ぶと 予約完了できないようになっています。

その他、施設スタッフから事前にユーザーへ質問・確認ができるようになっています。

おすすめのホテルスパ15選

ということで、 一休 .com スパの中から以下のポイントでおすすめの施設をセレクトして15選ご紹介します。

  • ホテルスパ、旅館・リゾートスパである
  • 温浴施設利用付きプランがある
  • 男性も女性も利用できる

東京のホテルスパ

1. ザ・ペニンシュラ スパ

東京・日比谷/ザ・ペニンシュラ東京 6階 https://spa.ikyu.com/day_spas/660025/?ikac18i ★こちら実際に体験してきました!私の中で満足度1位★

お風呂はなくサウナのみですが、しっかり暑いドライサウナと ほどよい暖かさのスチームサウナ、アロマの香りがするシャワーのほか アイスファウンテンがあるのでサウナーの方も満足いただけるはず。

アロマテラピーの施術がとっても気持ちが良いのはもちろん、 この写真のリラクゼーションルームではよく冷えたグレープフルーツジュースと マンゴージュースがいただけます。 またこのチェアではヘッドホンで音楽を聞くことができたり、読書灯で 雑誌を読むこともできるので、ネット断食にはぴったり。ゆったり寛げます。

2. ザ スパ アット マンダリン オリエンタル東京

東京・三越前駅直結/マンダリン オリエンタル 東京 37階 https://spa.ikyu.com/day_spas/660038/?ikac18i

おそらく一休.com スパで一番お高いプランのあるホテルスパ。 でも、ローンチしてすぐにペアでXmas近くに予約が入りました。すごい。 とても人気なのでいつか行ってみたい!

完全個室の贅を極めたスパスイートのほか、お風呂やサウナからも眺望が楽しめます。

3. アマン・スパ/アマン東京

東京・大手町駅直結/アマン東京 34階 https://spa.ikyu.com/day_spas/660030/?ikac18i

見てください、このプール!左端には富士山が見えます。 迷わずトップページのメインビジュアルに採用してしまいました。

プールのほかフィットネスジム、大浴場、トリートメントルーム毎の リラクゼーションエリアからも眺望が素晴らしいです。

4. AO スパ&クラブ/アンダーズ 東京

東京・虎ノ門ヒルズ/アンダーズ 東京 37階 https://spa.ikyu.com/day_spas/660032/?ikac18i

白を貴重とした洗練された空間。木のぬくもり溢れるトリートメントルームからも高層階ならではの青空や皇居を望む素晴らしい眺望が楽しめます。

全トリートメントルームにはプライベートロッカー、シャワールームがあり他のゲストの目が気になりません。 ロッカーエリア併設の温浴エリアではお風呂、シャワーの他、男性はドライサウナと水風呂、女性はスチームサウナと360°シャワーを完備されています。

5. スイス・パーフェクション スパ キオイ/ザ・プリンスギャラリー 東京紀尾井町

東京・赤坂見附・永田町/ザ・プリンスギャラリー 東京紀尾井町 30階 https://spa.ikyu.com/day_spas/660019/?ikac18i

世界中のセレブから愛されるスイス製植物性セルラー化粧品「スイス・パーフェクション」を使用する国内初の直営サロンで、エイジングケアの先端技術を結集したトリートメントを堪能できます。

写真はスパスイート・ペアルーム。角部屋のパノラマの眺望が素敵ですね。 スパスイートのプランではこのお部屋でアフターティーをいただくことができます。

6. スパ&ウェルネス ジュール/ハイアット リージェンシー 東京

東京・新宿西口/ハイアット リージェンシー 東京 28階 https://spa.ikyu.com/day_spas/660005/?ikac18i

著名デザイナーが手掛けたスタイリッシュな空間は、木などの天然素材の温もりを感じさせながらコンテンポラリーな雰囲気が漂います。

トリートメント前にはプールやフィットネスジムも利用可能。写真のとおり、プールにはジャグジーやウォームルームを備え、プールサイドは居心地のよいデッキチェアとテーブルを配したウッドデッキとなっています。

7. フォルトゥーナ/ホテルニューオータニ

東京・赤坂見附/ホテルニューオータニ ガーデンタワー 3階 https://spa.ikyu.com/day_spas/660003/?ikac18i

100℃前後の乾燥した高温で発汗を促すドライサウナ、50℃前後の温度で肌や体に負担がかかりにくいスチームサウナの2種類をラインナップ。 バイブラとジェットの機能を持つ浴槽や、体をゆったりと休められるラウンジも併設します。

プール・フィットネスジム付きのプランなら水着やトレーニングウェアを無料でレンタルできるので手ぶらで利用できます。

8. SPA THE SAKURA/ザ・プリンス さくらタワー東京

東京・品川/ザ・プリンス さくらタワー東京 B1階 https://spa.ikyu.com/day_spas/660020/?ikac18i

都会のなかにたたずむ静寂と広い空間のなかで、日本古来の伝統的な香りに包まれるトリートメントルームです。

トリートメント後、アフターティーをいただけるラウンジは、竹林にたたずむような静けさに包まれながら、ゆっくりと過ごすことができる空間になっています。

9. 庵スパ TOKYO/ヒルトン東京お台場

東京・台場/ヒルトン東京お台場 5階 https://spa.ikyu.com/day_spas/660015/?ikac18i

レインボーブリッジ、東京湾ビューが楽しめる大浴場は、プールを併設する水着着用エリアにあります。(有料レンタルあり)

海を眺めるテラスがついた開放的なフィットネスセンター。 各種マシーンを備え、ご宿泊者は無料で24時間利用できます。

東京から2時間以内で行ける旅館・リゾート

10. 赤沢スパ/赤沢迎賓館

静岡・伊東市/赤沢温泉郷 https://spa.ikyu.com/day_spas/660047/?ikac18i

壮大な緑に囲まれた赤沢温泉郷内に、美と健康、リラクゼーションの場所として誕生。 フランス発祥の海洋療法「タラソテラピー」の発想をもとに生まれた海洋深層水のプールには、赤沢沖の深海800mから汲み上げた新鮮な海水を温めて使用されています。

エステエリアには、特徴の異なる3つのドーム(サウナ)と温・冷の足湯などを完備。 海洋深層水のプールで代謝をアップさせた後には、完全個室のアロマの香りに包まれたお部屋でトリートメントを受けられます。またご希望のお客様には、プラス料金で生花のバラを100輪浮べたバラ風呂をご用意することも可能です。

11. GINYU SPA(ギンユウスパ)/箱根吟遊

神奈川・箱根・宮ノ下 https://spa.ikyu.com/day_spas/660048/?ikac18i

日本一予約の取れない宿として噂の旅館「箱根吟遊」その静寂な杜の空気に包まれた「Ginyu Spa」

トリートメント前後に ウォーターガーデンの向こうに望む雄大な自然を眺めていただきき源泉から湧き出る大地の力で、五感を解き放つことができます。 セラピストのテクニックにより、体の疲れをとることだけでなく、自分本来のバランスを整え健康や美しさへと導きます。

12. 庵スパ KARUIZAWA/軽井沢マリオットホテル

長野・軽井沢/軽井沢マリオットホテル B1階 https://spa.ikyu.com/day_spas/660017/?ikac18i

こちらのお風呂は「小瀬温泉」泉質はナトリウム-炭酸水素塩泉。 “美肌の湯”とも呼ばれ、肌の不要な角質や毛穴の汚れを取ってくれる女性に嬉しい効能がたくさん。湯上がりがさっぱりするので、スポーツやアクティビティで汗を流した後などにもおすすめです。

和のエッセンスを随所に取り入れたヒーリング空間「庵スパ KARUIZAWA」。 日本人ならではの繊細で丁寧な施術と、日本由来の贅沢な粧材を使用し、心と身体を解きほぐしていきます。

大阪・京都のホテルスパ

13. CONRAD SPA/コンラッド大阪

大阪・中之島・梅田/コンラッド大阪 38階 https://spa.ikyu.com/day_spas/660006/?ikac18i

淀川側を望むトリートメント全室からは、地上200mから望むスカイラインが刻一刻と表情を変える景色を眺めながら、安らぎの時間をお過ごしいただけます。シャワーブース、トイレ、パウダーコーナーが完備された完全プライベートな空間です。

温浴施設はサウナ、ジェットバス完備。
男性:ホットバス / コールドバス / ドライサウナ
女性:ホットバス / スチームサウナ

ロッカールーム内にはダイナミックな景色を眺めながら、ゆっくりとした時間をお過ごしいただけるラウンジも併設。ドリンクコーナーには、季節のフルーツウォーター、温かいお茶が用意されています。

14. MEGURI SPA & WELLNESS/インターコンチネンタルホテル大阪

大阪・梅田・グランフロント大阪/インターコンチネンタルホテル大阪 4階 https://spa.ikyu.com/day_spas/660021/?ikac18i

全室がクローゼット、シャワー、トイレ、ドレッサーを備え、お着替えからトリートメント後のお支度まで、全て個室内で行えるスパスイート。

温浴施設はまるで高級旅館の温泉を彷彿させる日本式浴場。 大都会の中心で味わう、想像を越えた極上のリフレッシュ&リラクゼーションジャーニーを楽しめます。

15. ザ スパ アット フォーシーズンズホテル京都

京都・東山周辺/フォーシーズンズホテル京都 https://spa.ikyu.com/day_spas/660044/?ikac18i

インドアプールは、京都屈指のゆったりとした広さを確保しております。20メートルの広さに加え、2つのジャグジーも完備。リゾート感溢れる雰囲気のなか、贅沢なひとときをお楽しみください。

お風呂(温浴・冷浴)・サウナをお楽しみいただけます。
男性:ドライサウナ
女性:スチームサウナ

入念に選び抜かれた自然の力とラグジュアリーが融合したスキンケアブランドと、深いヒーリング効果をもたらす京都ならではの素材、それらを使用したトリートメントは、本物のくつろぎと新鮮な安らぎをもたらします。

おわりに

みなさん毎日PC仕事やネットサーフィンで目を酷使しているので 肩こり・腰痛持ちの方も多いのではないでしょうか?
(わたしは慢性的な肩こりの解消にアロマテラピーにはまりました。)
たまには自分の身体もメンテナンスしないと高いパフォーマンスを出せません。

一休.com スパではただいまXmasまでのウィンターセールを開催中です。 この機会にぜひ心身を癒やす贅沢な体験をしてみませんか?

https://spa.ikyu.com/specials/timesale/?ikac18i

Rundeck in practice [導入編]

この記事は一休.comアドベントカレンダー2018の6日目です。

qiita.com


一休では、2016年の10月からRundeckを使ってバッチジョブの実行管理を行なっています。
導入からおおよそ2年たちました。
その間にデータセンターからAWSへの移行やいくつかの運用トラブルなどを経験しました。知見が溜まってきたので導入編と運用編の2つの記事に分けて紹介したいと思います。

今回はまず、導入編として、導入の背景と実際の導入作業で工夫した点、苦労した点を紹介します。また、Rundeckを導入したことで得られた改善についても紹介します。

Rundeckとは

f:id:s-tokutake:20181206084400j:plain

  • Rundeck社が提供するOSSのジョブ管理ソフトウェア。有償版もある。
  • ジョブフロー構築、失敗の自動リトライ、開始終了に対する通知フックなど、 一般的なジョブエンジンの機能を持つ。
  • Java + Groovy + Grailsで実装されている。
  • スケジューリングの定義は、cron形式。
  • 管理画面はWebブラウザで操作できる。
  • SSH経由でリモートのコマンドを実行できる。
  • SSHが通るマシンであれば、OSを問わず、どのマシンのプログラムでも定期実行できる。

脱Windows タスクスケジューラ

Rundeckを導入する前、一休ではサービス運用に必要なバッチ処理をWIndows タスクスケジューラで実行していました。登録されたタスクの数は、100を超えていました。
タスクスケジューラで100を超えたタスクを管理するのはとても辛いです。なにが辛いかというと、

タスクスケジューラのGUIは大量のタスクを管理するのに向いていない。

タスクスケジューラのGUIはフィルタや検索ができません。タスクの数が少なければ問題ないのですが、100以上のタスクを管理しようとするとかなり苦労します。例えば、手動でタスクを動かす必要がある場合、タスク一覧を目視で舐めて目的のタスクを見つけるという辛い作業をしていました。

ジョブフローが組めない。

  • 100以上のタスクがあれば、実行順序に依存関係のあるタスクもあります。しかし、タスクスケジューラでは、「タスクAが正常に実行を完了したらタスクBを動かす」というようなジョブフローが組めません。

なぜRundeckを選んだのか

脱Windows タスクスケジューラを目指すため次の4つの要件を満たすジョブエンジンを探しました。

  • ジョブフローが組めること
  • GUIがわかりやすいこと。ブラウザでアクセスできること。
  • WindowsとLinux両方で動くこと。
    • 一休はWindows系の技術スタックをメインに使っていますので、Windowsで動くことが必要ですが、Linuxサーバも使いますので、Linuxもサポートする必要があります。
  • OSSであること。
    • プロプライエタリな製品だと設計や運用でライセンスを気にする必要が出てきます。また、機能検証しにくいこともあるかもしれないので、まずは、OSSで探して、良さそうなものが見つからなければ市場調査をしようと考えました。

この基準で判断した結果、以下のふたつが、候補になりました。

決め手のひとつはGUIのシンプルさでした。SOS JobschedulerもブラウザでアクセスできるGUIを持っていますが、Rundeckの方がわかりやすいです。 また、日本語の情報もRundeckの方が多かったため、Rundeckを選択しました。
そして最大の決め手は、「SSHさえ疎通すれば、どんなマシンのどんなコマンドでもcron実行できる」という柔軟さです。これによって、ジョブ管理をするサーバとジョブを実行するサーバを分離できます。ジョブ実行に必要なリソースが足りなくなったら、ジョブ実行をするサーバだけ増やせば済みます。そして、ジョブを実行するサーバはOSを問いません。

構成

現在の構成は以下の通りです。

image.png

RundeckサーバもジョブサーバもすべてEC2です。導入当時はデータセンターの物理マシンでしたが、構成自体は上の図とほとんど変わりません。 ジョブの実行ログはS3に保存し、RundeckのデータベースにはRDSを利用しています。AWSのサービスを最大限利用した構成にしました。 現時点では、RundeckサーバもジョブサーバもWindows Serverです。

導入にあたって、工夫した点、苦労した点を紹介します。

管理画面の認証

Rundeck自身がユーザー管理の機能を持っています。しかし、50人近くいる開発者全員のアカウントをRundeckに登録して適切に管理するのは大変です。外部の認証機構と連携する必要があるのは自明でした。 RundeckにはActive Directoryと連携する機能があります。また、oauth2_proxyに対応しているので外部のOAuth 2.0 サービスプロバイダーとも連携できます。 一休ではデータセンターにRundeckがあった時代は、AD連携機能を使って認証を行なっていました。クラウドに移行したときに、Rundeckサーバから社内のADが見えなくなってしまったので、oauth2_proxyとGitHubのOauth2の仕組みを使って、GitHubアカウントでログインできるようにしました。

oauth2_proxy は bitly社が開発しているOSSで、リバースプロキシとして動作し、oauthプロバイダとのやりとりを代理してくれる便利なツールです。Goで開発されているのでWindowsでも問題なく動作します。

※ このoauth2_proxy+GitHubでの認証の仕組みの構築には、minamijoyoさんの記事を参考にさせていただきました。ありがとうございます!

EC2プラグイン

RundeckにはEC2プラグインがあります。このプラグインを使うと特定のタグがついているEC2インスタンスを自動的にRundeckのジョブサーバにできます。

image.png

これによって、なんらかのメンテナンスでジョブサーバの入れ替えや再構築が必要なときも簡単に対応できます。 また、動的にジョブサーバを追加することもできます。例えば、常時動作しているバッチサーバのスペックでは処理しきれないような大規模バッチ処理がある場合、高スペックなEC2インスタンスを起動して処理をさせ、完了したら、そのEC2インスタンスを停止する、という一連の流れを手動操作を介在させることなく実現できます。

sshサーバ

Linuxサーバなら悩む必要はないのですが、一休ではジョブを実行するサーバはWindowsサーバです。環境を構築した時点では、公式のWindows環境のOpenSSHの実装は、動作が不安定で使えませんでした。そこで、Windows環境で動作するSSHサーバを有償無償問わず、調査したところ、bitvise ssh server 最適と判断しました。

  • 有償ですが、安い。ライセンス買い切り。
  • 設定が簡単でシンプル。

当初は、無償のfreesshdで進めようと思っていたのですが、動作が安定せず、断念しました。

タスク移行

タスクスケジューラ上の100以上あるタスクを手動でRundeckのジョブとして移行していたら絶対にミスをします。また、当然、移行作業中も通常のサービス開発は行われています。新しいバッチ処理が追加されているかもしれません。移行作業と開発との間の齟齬が起きないようにする必要がありました。 そこでタスクスケジューラからxml形式でタスクをエクスポートし、Rundeckのジョブ定義xmlに変換するプログラムを書き、そのジョブ定義xmlをRundeckにインポートすることで、スムーズかつ齟齬がないように移行しました。

ジョブのエラー通知

Rundeckには、タスクの完了(成功、失敗)を通知する仕組みがあります。メール通知、webフックの呼び出しができます。また、プラグインを利用することでslackにも通知が飛ばせます。一休ではサービスのエラー通知はすべてslackに飛ばしています。なので、プラグインを使ってslackに通知しようと考えました。しかし、以下のふたつの理由でこのプラグインを使ってのslack通知はやめました。

  • ひとつひとつのジョブに設定しなければならないのが面倒。新しく追加したジョブに設定漏れが起きそう。
  • 通知内容が少ない。特にエラーになった場合は、実行したコマンドのステータスコードや標準出力の内容も通知したい。

そこで、以下のような方法にしました。

  • S3プラグインを使ってジョブの実行ログをS3に出力する。
  • S3へのログのPutをトリガにして動作するAWS Lamdbaを実装する。
  • このLamdbaはログの中身をみてエラーだったら、エラー内容(ステータスコードや標準出力)をSlackに通知する。成功の場合は通知しない。

このようにすることで、ジョブの通知設定に関わらず全てのエラーをslackに通知することができました。

ansibleを使って環境構築

oauth2_proxyのインストールやRundeckのインストール、各種構成ファイルの設定、監視の設定はすべてansibleで行うようにしました。こうすることでトライアンドエラーを繰り返しながら上述したような技術検証ができました。また、ansibleがWindows環境でも問題なく使えることがわかったのも収穫でした。

改善ポイント

  • 当初導入によって目論んでいた改善はほばすべて達成しました。
  • ブラウザから管理画面にアクセスできるようになったのでタスクスケジューラよりもはるかに簡単に管理できるようになりました。ジョブのフィルタもできます。また、タスクスケジューラ時代はさまざまな事情がありジョブの登録や変更は特権を持った特定のエンジニアしかできないようになっていました。このルールもRundeck移行によって見直すことができました。

  • ジョブフローも活用されています。導入当初は、使われていませんでしたが、数ヶ月経つと、特定のエンジニアが導入を推進する、ということをしなくても、自然と使われるようになっていきました。

  • また、当初見込んでいた改善ではないですが、タスクスケジューラを使ったバッチ処理よりもインフラの可用性は大きく向上しました。バッチの実行管理をするRundeckサーバと実行の定義のストアであるRDS、そして実際にバッチを実行するバッチサーバの3つを分離できました。この3つそれぞれ別々に障害対策を考えればいいので運用がしやすいです。フェールオーバの手順はタスクスケジューラ時代の障害時フェールオーバの手順よりもかなりシンプルになりました。

終わりに

今回紹介した内容は1年半くらい前に実施したことなので少し情報が古いかもしれません。しかし、LinuxのcronではなくWindowsのタスクスケジューラからRundeckに移行した例はあまりないのでは、と考え、紹介しました。RundeckはWindows環境でも十分に活用できます。 次回は、運用編として、この2年間で起こった運用トラブルとその対処について、紹介したいと思います。

この記事の筆者について

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

Amazon Connect の導入と自社システムを連携した話

この記事は一休.comアドベントカレンダー2018の5日目です。

qiita.com


こんにちは。 CTO室の村石です。

一休ではAmazon Connect を導入し、カスタマーサービスの一部コールセンターを新しい体制へと変えました。

今回は導入した Amazon Connect に関して、概要から導入後の運用まで幅広く話をしようと思います。

Amazon Connect とは

AWSが提供するクラウド型のCTI*1システムのことで、コールセンターを簡単に作れることが魅力のサービスです。 また、Webサービスになっているため、ブラウザがあれば利用できます。

aws.amazon.com

コールセンターではよくある以下のようなことも、少しの設定で実現出来ることは大きな魅力だと思います。

  • 音声ガイダンスを流す
  • 案内で番号をプッシュさせる
  • 特定の担当部署に電話をつなげる

料金については、従量課金制となっており、通話が多いほど高くなります。 一方、オペレータ数などは料金の対象にならないため、柔軟に変更が可能です。

Amazon Connect は現在東京リージョンで提供されていませんが、近い将来提供されることが発表されていて今後が楽しみなサービスです。

aws.amazon.com

Amazon Connect の導入方法については割愛しますので、AWSの各種ドキュメントをご覧ください。

Amazon Connectの特徴

個人的に特に気になった特徴を紹介します。

短期間、初期費用不要で構築が可能

Amazon Connect インスタンスを作ってから、単純に電話できるようになるまでは、慣れれば数分で実現出来ます。 実際の運用環境を整えるには別途時間がかかりますが、時間のかかる電話線工事などがカットできるのは大きいです。

各種設定変更が楽

ここでいう各種設定とは以下のようなことです。(あくまで一部です。)

  • オペレータの追加
  • コールフローの編集
  • 営業時間の変更

Amazon Connect では、これらを管理サイトから変更することができます。 コールセンターの構築にベンダーを使うと、依頼が必要があったりと、オペレーションコストかかってしまいます。 時間がかかりがちの調整をせずとも、すぐに設定変更を行うことが出来るのは大きなポイントだと思います。

(現時点で)シドニーリージョン

前項でも述べたように、まだ日本で提供されていません。 今すぐ利用するためにはシドニーリージョンでの利用となります。 懸念として音声データの遅延が挙げられる思いますが、弊社が検証したところ、業務に差し支えるほどの遅延は確認できませんでした。

また、リージョンが海外のため、内部的には国際電話となります。 料金はそれほど気になりませんが、電話をかける際のダイヤルには気をつけないといけません。 しかし、東京リージョンでサービス開始されたとして、ダイヤルする際は国際電話でない保証はありません。

Amazon Connect の専用管理サイトが存在

AWS コンソール上の運用になるイメージを持つかもしれませんが、違います。 AWS コンソールでAmazon Connect のインスタンスを作成すると、専用の管理サイトが作成され、コールセンター業務は作成されたサイトで管理することになります。

電話を受けるオペレータのアカウントは、管理サイトから発行することになり、AWS アカウントとは別に存在します。

一通り構築が出来たらエンジニアは運用業務から離れることが出来る、というのも一つ特徴かと思います。

Amazon Connect のシステム

システム構成

電話のフローから見る構成図です。

システム構成-電話
システム構成-電話

オペレータはブラウザからWebサービスである Amazon Connect にログインして待機します。 Amazon Connect が着信を受け取り、実際にオペレータに繋がるまでに、いくつかの工程を通ります。 コールフロー / キュー / ルーティングプロファイル、この3点については、Amazon Connect を使う上で重要な項目のため少し詳しく説明します。

コールフロー

コールフローは、「着信した電話をどのように処理するか」を制御する重要な場所です。 電話がかかってきた時は、最初にコールフローに入ります。

コールフロー設定画面
コールフロー設定画面

上図のように、GUIを用いて設定することになっていて、シーケンスフローを組み立てる感覚で設定できます。

紹介しているのは、通話内容の録音設定を行い、後続のキューに転送するだけの単純な構成ですが、以下のように複雑な制御も設定可能です。

  • 営業時間内であるかチェックして、時間外なら音声ガイダンスを流して終了する
  • Lambda と連携し、任意の処理を走らせる
  • 発信者に番号プッシュの自動案内を行い、入力値によって応答するオペレータを変える

他にも、一般的なコールセンターで出来ることは一通り揃っていて、それらはすべてコールフローで設定することになります。

キュー

「着信した電話をため込む場所」です。 コールフローの終了後は基本的にキューに入り、オペレータと繋がるのを待つ状態になります。 待ちになった電話はFIFOのアルゴリズムに則り、順次オペレーターに繋がります。

ルーティングプロファイル

「キューとオペレータの橋渡し役」です。 キューに入った電話は、ルーティングプロファイルによって、電話可能のオペレータと結び付けられます。

ルーティングプロファイルとオペレータが「多対1」の関係であるのに対し、ルーティングプロファイルとキューは「多対多」の関係になっている、という点は混乱しやすいので注意が必要です。 一般的には、部署毎にルーティングプロファイルを作り、1対1の関係でキューを作るのが基本かと思いますので、気にならないかもしれません。

AWS の他サービスとの連携

Amazon Connect が取り巻く周辺のAWS事情です。 今後も更に連携できるものが増えていきそうです。

システム構成-AWS
システム構成-AWS

自社システムとの連携について

ここからは Amazon Connect と自社システムとの連携方法についてです。 連携の方法は大きく分けて2つの方法が考えられます。

  • コールフローの中で Lambda を使って連携
  • オペレータが電話を受けたタイミングで連携

ここでは、弊社で行った Lambda を使った連携を紹介します。 弊社が作成したのは、「Lambda を使って会員の情報を取得し、着信と同時にオペレータのブラウザに表示する」というものです。 簡単に処理フローを図示したものです。

Lambda連携-フロー図
Lambda連携-フロー図

これを実現するには、Lambda の他に amazon-connect-streams という JavaScript のライブラリも必要になります。 順を追って説明していきます。

Lambda 編

Lambda との連携はコールフローから行います。 まずはコールフローの中で Lambda と連携するための設定が必要です。 実際にコールフローを設定した内容です。

Lambda連携-コールフロー設定
Lambda連携-コールフロー設定

Amazon Connect と Lambda を繋げる

「AWS Lambda 関数を呼び出す」の設定項目には、呼び出したい Lambda のARNを設定します。

Lambda連携-関数を呼び出す
Lambda連携-関数を呼び出す

Lambda の返却値を Amazon Connect で設定する

「問い合わせ属性の設定」の設定項目には、Lambda 関数が返却した値を設定します。 この工程を行わないと、電話と共にデータが伝搬されません。

Lambda連携-問い合わせ属性の設定
Lambda連携-問い合わせ属性の設定

Lambda の処理

こちらは Lambda に記述する実際のソースコードになります。 python を用いています。 辞書型で渡される event 引数に発信者電話番号が入っています。 注意点としては、返却値のvalueは必ず文字列/数値/真偽値のいずれかである辞書型にする必要があります。

def lambda_handler(event, context):

    logger.info(event)
    
    end_point = event["Details"]["ContactData"]["CustomerEndpoint"]

    # ここで自社システムと連携して任意の処理を行う
    
    return {
        "response": json.dumps(end_point)
    }

引数 event 対するログ出力内容はこのようになっています。

{
    'Details': {
        'ContactData': {
            'Attributes': {},
            'Channel': 'VOICE',
            'ContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'CustomerEndpoint': {
                'Address': '+81xxxxxxxxxx',
                'Type': 'TELEPHONE_NUMBER'
            },
            'InitialContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'InitiationMethod': 'INBOUND',
            'InstanceARN': 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
            'MediaStreams': {
                'Customer': {
                    'Audio': None
                }
            },
            'PreviousContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'Queue': {
                'ARN': 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
                'Name': 'test-queue',
                'OutboundCallerId': {
                    'Address': '+81xxxxxxxxxx',
                    'Type': 'TELEPHONE_NUMBER'
                }
            },
            'SystemEndpoint': {
                'Address': '+81xxxxxxxxxx',
                'Type': 'TELEPHONE_NUMBER'
            }
        },
        'Parameters': {}
    },
    'Name': 'ContactFlowEvent'
}

amazon-connect-streams編

Lambda で連携したデータを電話を受けた時に表示するために必要になります。

amazon-connect-streams とは

Amazon Connect のオペレータ画面で使用する JavaScript ライブラリです。 実際に電話を受ける、かける、などの操作ができます。簡単に言うと電話機です。 これを使うことで、電話機を自作のWebサイトで利用することができます。

github.com

実際に amazon-connect-streams を使うには、AWS でのIPホワイトリストの設定と、Node.jsのビルドが必要です。 導入方法については、ここでは割愛しますので、上記ドキュメントをご覧ください。

CCP (Contact Control Panel)の表示

CCP とは、amazon-connect-streams の中で使われる機能の一部で、電話機のUIを指します。 以下のソースコードが示すように表示したい場所にdivタグを用意し、DOMを渡すことでその場所にiframeが展開されます。

let containerDiv = document.getElementById("containerDiv");
connect.core.initCCP(containerDiv, {
   ccpUrl: "https://[インスタンスエイリアス].awsapps.com" // 実際の管理サイトのURL
});

表示されるCCPです。

Amazon Connect CCP
CCP

着信時のデータ受け取り

// initCCPの後

connect.contact((contact) => {
    contact.onConnecting((conectingContact) => {
        let attributes = conectingContact.getAttributes();
        console.log(attributes);
    });
});

出力した結果は以下となり、lambda で連携した内容と一致していることが確認できます。

amazon connect streams データ取得
amazon connect streams データ取得

(おまけ) Amazon Connect Service API編

Amazon Connect では、Web API が用意されていて、データ取得/操作が行えます。 これらを利用すれば、オペレータに表示する画面から直接 Amazon Connect の機能を使えます。

docs.aws.amazon.com

API を利用するには、IAM のポリシー設定も必要になります。

キューに存在する待ち呼*2の数を取得

例として、キューに存在する待ち呼数の取得を紹介します。 当該情報を取得するには、get_current_metric_data というAPIを利用します。 python を用いて説明します。

def getCurrentQueue():

    import boto3

    # API クライアントを生成します。(IAMのポリシー情報を渡します。)
    client = boto3.client(
        "connect",
        region_name="ap-southeast-2",
        aws_access_key_id="xxxxxxxxxxxxxxxxxx",
        aws_secret_access_key="xxxxxxxxxxxxxxxxxx"
    )

    # キューの現在の情報を取得します。
    current_metoric = client.get_current_metric_data(
        InstanceId="[Amazon Connect インスタンス ID]",
        Filters={
            'Queues': [
                "[キューID]"
            ]
        },
        CurrentMetrics=[
            {
                'Name': 'CONTACTS_IN_QUEUE',
                'Unit': 'COUNT'
            }
        ]
    )

    logger.info(current_metoric["MetricResults"])

get_current_metric_data APIには、引数の CurrentMetrics 配列に別の情報を追加することで様々なデータが取得できます。 詳しくは、API ドキュメントをご覧ください。

docs.aws.amazon.com

以下は出力内容になります。

[
    {
        'Collections': [
            {
                'Metric': {
                    'Name': 'CONTACTS_IN_QUEUE',
                    'Unit': 'COUNT'
                },
                'Value': 0
            }
        ]
    }
]

Amazon Connect を導入してみて

Amazon Connect の導入を行い、技術面、運用面で思うところをあげました。

良かったこと

構築がとても楽

とにかく電話が出来るまでが早くて驚きました。 電話線工事などにかかる時間がなくなり、代わりに自社システム連携などに時間を割くことが出来ました。

拡張性が優秀

AWS のサービスであるため、Lambda など他サービスの連携が楽という利点があります。

また、amazon-connect-streams では電話機の機能だけを切り出してくれているため、それ以外すべてを使いやすくカスタマイズ出来ます。

もはや自社システムに 電話サービスを追加する感覚とも言えるでしょう。

コールフローの設定で柔軟に運用が可能

自社システムの連携を随時アップデートしていく環境では、どうしても本番環境で動作確認したい場合があります。

そんな時もコールフローで特定の電話番号だけ別の処理をさせることも出来るので、動作確認が容易になります。 メンテナンスによる急な営業時間外の設定など、緊急性のある運用もカバーできるところも良い点だと思います。

オペレーター事情に左右されにくい

Amazon Connect ではユーザに対する権限の付与や、人員の増減、権限の設定変更、などが簡単に出来ます。

ベンダーによっては、権限の変更に依頼が必要だったり、料金変更があることもあります。

人員変更が滅多にない組織なら気にならないかもしれませんが、スタートアップや柔軟に変更を行う組織の場合はありがたいでしょう。

課題に感じること

学習コストが高い(技術)

東京リージョンで提供されることが発表されているとは言え、まだ日本で導入している企業は少ないため、ドキュメントは少ないです。 弊社もトライ・アンド・エラーを繰り返して習得しました。

月の無料利用額があるため、まずはその額内でいろいろ試して習得するのが良いかと思います。

学習コストが高い(運用)

技術視点だけではなく、運用面でも学習コストが高いと感じました。

運用に入ると、コールセンターの管理者に Amazon Connect の管理を任せたいものです。 コールフロー、エージェント管理、メトリクスの作成など、習得しなければ使いこなせないものが多い印象です。

技術視点と同様に、ドキュメントが少ないため、こちらもトライ・アンド・エラーで習得しなければならないのが現状です。

一部メトリクス情報に難あり

一部の内容で満足いかない箇所がありました。 例えば、弊社で感じている箇所として以下があります。

  • リアルタイムのキュー内容表示において、自動更新が約15秒間隔(待ち呼に気付くのが遅れる)
  • 問い合わせ検索(発着信の内容を調べられる)では、通話に関するタイムスタンプに秒単位の表示が無い(APIでは取得しているのであくまで表示だけのようです。)

細かい話と捉えることも出来ますが、組織が定めるコールセンター業務のKPIに影響することも有りえます。 特に大規模な組織では気になるところではないでしょうか。

オペレータのステータス管理に不安あり

Amazon Connect ではオペレータに「Available」「offline」など、状態を管理出来ます。

「Available」という状態が電話を受け付けている状態を表しますが、この状態は明示的に変更しない限り、そのまま維持されます。 変更を怠ると、物理的に電話が取れない状況であろうと、ルーティングプロファイルによって電話が割り当てられてしまう危険があります。 これを運用以外の方法で防ぐには、Web APIを利用して自動的に状態を変更する、コールフローとシフトを連動させる、など一工夫必要になります。 導入するにあたり、方針は決めておく必要がありそうです。

また、ACW*3の状態に手動で変更出来ない、ということもあります。 Amazon Connect では、任意のカスタムステータスの作成が可能であり、似たステータスを作ることは可能です。 しかしメトリクスに影響するなど不安は残ります。

オペレータへ繋がる仕組みが今ひとつ

Amazon Connect では、ルーティングプロファイル内のオペレータに優先度設定や、特定のオペレータへ繋ぐ機能はありません。

組織のやり方によりますが、他のオペレータが全て電話で埋まった時に管理者も電話を受ける、という方針があった場合、実現する方法は難しいです。 埋まった時点で状態を変更する運用回避や、ルーティングプロファイルや Lambda を駆使する方法もありますが、難易度は高い印象です。

(18/12/08 追記) 上記取消線の内容は、管理者用のルーティングプロファイルを別途作成し、キューの優先度及び遅延時間を設定することで実現できました。

まとめ

AWS の1サービスとして提供される Amazon Connect ですが、まだ日本のコールセンター業務に適応するには、運用面で課題が残りそうな印象があります。 ここは、日本企業の導入が進むのと同時に、Amazon Connect も進化し、適応していくことを期待しています。

一方、技術面の視点から見ると、AWS との連携やカスタマイズの容易さなど、エンジニアがとっかかりやすい環境が揃っていると感じました。 今回弊社が行った自社システムの連携もほんの一部に過ぎず、コールセンターにおける業務効率化の可能性はとても大きいと思います。

以上、Amazon Connect の導入を検討している会社の参考になれば幸いです。

muraishis

*1:Computer Telephony Integration の略。電話とシステムの連携を行う技術のこと。

*2:オペレータに繋がるのを待っている着信のこと

*3:After Call Workの略。電話を切った後、対応内容の記録など、オペレータが作業をする時間のことをいう。

サードパーティJavaScriptの最適化

本記事は、一休.com Advent Calendar 2018の4日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。

今日は、パフォーマンス改善に取り組むフロントエンドエンジニアの多くが頭を悩ませているであろう、サードパーティスクリプト(3rd Party JavaScript)について書きます。

サードパーティスクリプトとは

サードパーティスクリプトとは、外部のドメインから読み込むJavaScriptのことです。典型的には、Google Analytics等のスクリプトが、サードパーティスクリプトに該当します。

一休.comでは、サードパーティスクリプトを、アクセス解析・広告のリターゲティング・A/Bテスト等、様々な用途に使用しています。これらのスクリプトは、一つ一つは小さなものであるため、画面表示のスピードに対するインパクトは意識しづらいものです。しかし、細かいコストが積み重なることで、サイトの読み込み速度に大きな悪影響を与えることもあります。

一休.comにおけるサードパーティスクリプト

一休.comでも、様々なサードパーティスクリプトを利用しています。たとえば、スマホのホテルページ( https://www.ikyu.com/sd/00001290/ )では、以下の外部ドメインからスクリプトを読み込んでいました。

  • bam.nr-data.net
  • js-agent.newrelic.com
  • b92.yahoo.co.jp
  • www.google-analytics.com
  • www.googleadservices.com
  • s.btstatic.com
  • yjtag.yahoo.co.jp
  • s.yjtag.jp
  • www.googletagmanager.com
  • googleads.g.doubleclick.net
  • static.criteo.net
  • s.yimg.jp

これらを合計した転送量は、ページ内で読み込んでいるJSの3分の1を占めます(300KB中、100KB)。

サードパーティスクリプトがページ読み込み速度に与える影響

↓は、スマホホテルページのパフォーマンス計測結果です。

https://www.webpagetest.org/result/181017_SD_14a6718d17894b5d25bea6ff2c01713d/

f:id:ryo-utsunomiya:20181203112751p:plain
サードパーティスクリプトあり

画面の描画完了まで(Speed Index)が3.142s、ユーザが操作可能になるまで(First Interactive)には8.3秒かかっています。

次に、WebpagetestのBlock機能を利用して、サードパーティスクリプトの読み込みを行わない状態で計測してみます。

https://www.webpagetest.org/result/181017_9Z_9051ce3990f33aadaae363806147fada/

f:id:ryo-utsunomiya:20181203122317p:plain
サードパーティスクリプトなし

画面の描画完了まで(Speed Index)が3.142s => 2.787s、ユーザが操作可能になるまで(First Interactive)は8.3秒 => 5.6秒に改善しました。

サードパーティスクリプトの多くはasyncで読み込むため、レンダリングに与える影響は軽微です。しかし、あるjsが別のjsを連れてくる、という構成になっていることが多く、jsファイルの読み込みがダラダラと続きがちです。このことは計測結果のWaterfallを見るとわかりやすいと思います。これによって、画面の表示が終わってもブラウザはまだローディング中で、画面を操作するともたつく、という状況が生まれます。

このように、サードパーティスクリプトを使用する際は、それがユーザ体験に与える悪影響も考慮する必要があります。

他のサイトは?

一休.comでは、サードパーティスクリプトによって一定のパフォーマンス劣化が見られることがわかりました。

では、他のサイトではどうでしょうか?

一例として、A/B Testing, Ads and Other Third Party Tagsというトークでは、あるWebサイトから1つスクリプトを取り除くことで、ページのロード時間が4秒改善し、サイトの売り上げが26%改善したことが紹介されています(このトークは、他にも面白い事例が満載なので、サードパーティスクリプトの改善に興味のある方は視聴をおすすめします)。

日本の事例としては、日本経済新聞社の宍戸さんが公開されている、『PWA導入の成果と課題』というスライドが、身につまされる内容で参考になります。

PWA導入の成果と課題 / nikkei-pwa-html5conf2018 - Speaker Deck

このスライドでは、インターネット全体で、サードパーティスクリプトのサイズが自前のスクリプトのサイズよりも多くなってきていること(1st party 100KBに対して3rd party 250KB)、日経新聞でも、サイズは明言されていないものの、jsリクエストの半分がサードパーティスクリプトであることなどが紹介されています。

最適化されたサイトの事例

次に、サードパーティスクリプトの配信が最適化されたサイトの事例も紹介しておきます。

宿泊予約サイトのExpediaは、一休.comと同様、様々なサードパーティスクリプトを使用しています。Expediaでは、パフォーマンスを最適化したSPA版を開発しており、スマートフォン向けホテル検索ページはSPAになっています。

Expediaのスマートフォン向けホテル検索ページ(※PCから閲覧する場合はUAをスマホのものにしてください)を開いて、Chrome DevToolsのNetworkタブ等で読み込んでいるjsのhostを確認すると、全て同一ドメイン(https://c.travel-assets.com おそらくExpediaのCDN)から取得していることに気づきます。

推測ですが、サードパーティスクリプトをセルフホストするか、トラッキングやA/Bテストのためのスクリプトを内製しているものと思われます。外部からは一切読み込まないという決断ができるのがすごいところ。

サードパーティスクリプトのパフォーマンス改善

サードパーティスクリプトの最適化について書かれた包括的なドキュメントとしては、Google Web FundamentalsのLoading Third-Party JavaScriptがおすすめです。

抜粋して紹介すると、

  • サードパーティスクリプトはasyncまたはdeferで読み込むこと
  • サードパーティスクリプトの配信サーバが遅い場合、セルフホストを検討すること
  • サイトに明確な価値をもたらさないスクリプトは削除すること
  • preconnect/dns-prefetch等のresource hintsを活用すること

などです。

一休.comでも、サードパーティスクリプトは基本asyncで読み込み、さらに、サードパーティスクリプトのドメインにはpreconnectを行っています。

一番強力な方法は「削除」なのですが、サードパーティスクリプトは多くがビジネスサイド主導で導入されており、開発側の判断だけでは削除まで踏み切れません(宍戸さんのスライドでも、技術の問題だけではないと言及があります)。

サードパーティスクリプトの最適化には、エンジニアリングチームだけでなく、サービスを運営する組織全体が、UXに高い意識をもつ必要があります。

"User First and Challenge" を社是とする一休としては、積極的に取り組んでいきたいところです。

今後の方針

一休.comで、今後行えそうなサードパーティ最適化の施策としては、以下を検討しています。

  • サードパーティスクリプトのアセットバジェットの導入
    • 読み込んでよいJSはトータルでXXKB以内、等
  • Google Tag Manager(gtm.js)の削除
    • 他のスクリプトを連れてくるだけのJSがVue.js 2個分(44KB)は大きすぎ
    • 簡単にスクリプトを追加できる = 簡単にサイトを遅くできる

まとめ

サードパーティスクリプトには、それぞれに独自のメリットがあります。たとえば、Google Analytics等のアクセス解析は、サイトを運営するうえで欠かせない情報を提供してくれます。

一方、サードパーティスクリプトにはコストがかかります。サードパーティスクリプトを読み込むと、サイトは確実に遅くなります。

用法・容量を守って、価値あるWebサービスを提供していきたいですね 💪

参考文献

vimeo.com

speakerdeck.com

developers.google.com

VueコンポーネントのState管理を考える

この記事は一休.comアドベントカレンダー2018の3日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。

一休.comにVue.jsを導入して、約1年が経ちました。スマートフォン版の予約入力画面から始まり、PCとスマートフォン版のホテルページほか、さまざまなUIコンポーネントがVue.jsで実装されるようになってきています。

また、予約入力画面のような複雑な状態管理を伴う画面の実装のため、Vuexを導入しています。

ここ1年ほどVue.js + Vuexというスタックで開発を行ってきて、アプリケーションの設計について色々と思うところがあったので、今回は現状でどういう構成が最適と考えているか、紹介します。

Vue.jsアプリケーションのState

Vueコンポーネントには、親から受け取るpropsと、自分自身で保持するstate(data)という2つの概念があります。また、子コンポーネントから親コンポーネントに何かしらの値を戻したい場合、eventの仕組みを利用することができます。

propsとdataについては説明を省きますが、eventについてはサンプルコードを載せておきます。

KeywordSearch.vueというコンポーネントがあって、このコンポーネントは検索キーワードの入力とバリデーションを受け持つとします。一方、検索処理の実行はAPI経由なのか画面遷移なのかが画面によって変わるため、親コンポーネントに委譲します。

このような場合、以下のようにeventの仕組みを利用することで、子から親へ値を受け渡すことができます。

// KeywordSearch.vue
<template>
  <div>
    <label>
      検索キーワード <input type="text" name="query" v-model="keyword" />
    </label>
    <button type="button" @click="search">検索</button>
  </div>
</template>
<script>
export default {
  name: "keyword-search",
  data() {
    return {
      keyword: "",
    };
  },
  methods: {
    search() {
      // search イベントを発火し、keywordを引数として渡す
      this.$emit("search", this.keyword);
    }
  }
};
</script>

// KeywordSearch.vueの親コンポーネント
<template>
  <div><keyword-search @search="handleSearch" /></div>
</template>
<script>
import KeywordSearch from "./KeywordSearch.vue";
export default {
  name: "App",
  components: {
    KeywordSearch
  },
  methods: {
    // KeywordSearchのsearchイベントのハンドラ
    handleSearch(keyword) {
      // 検索APIに問い合わせたり、画面遷移したり
    }
  }
};
</script>

まとめると、Vueコンポーネントにおいて、データの流れは以下のようになっています。

  • 自分自身で保持: data
  • 親 => 子: props
  • 子 => 親: event

このように、コンポーネント間のデータの流れがpropsとeventによって行われることをとらえて、「Props Down, Event Up」と呼ぶことがあります(もともとはReact.jsのコミュニティ発祥のフレーズだと思います)。

この辺の話はVue.jsのドキュメントでも詳しく説明されています。

jp.vuejs.org

グローバルな状態管理の必要性

ここまで説明してきたのは親子関係のあるコンポーネントの話です。しかし、実際のアプリケーションでは、親子関係のないコンポーネント間でデータを共有したい場合があります。これについても、Vue.jsおよびVuexのドキュメントに、Vuexを使うべき場面についての解説が載っています。

jp.vuejs.org

vuex.vuejs.org

一休.comの画面の例としては、「今すぐポイント割引後料金を表示」の切り替えボタンは、直接の親子関係のないコンポーネントがオン・オフの状態を共有するため、Vuexを使っています。

f:id:ryo-utsunomiya:20181202164300p:plain
「今すぐポイント割引後料金を表示」の切り替えボタン

Vuexのバッドプラクティス

Vuexは便利なのですが、何でもかんでもVuexを使うのは良いやり方とはいえません。Vuexを使っていると、以下のようなコンポーネントを書いてしまうことがあります。

<template>
  <div>{{ someState }}</div>
</template>
<script>
import { mapState } from "vuex";

export default {
  computed: {
    ...mapState(["someState"])
  }
};
</script>

このコンポーネントの作りがイマイチなのは、単に値を受け取って表示するだけのコンポーネントなのに、propsではなくStoreに依存している点です。この設計が良くないことを説明するために、モジュール結合度という概念を紹介します。

モジュール結合度

モジュール結合度とは、あるモジュールと別のモジュールの結合度を表す度合いです。低いほど疎結合、高いほど密結合となります。見た目を制御するコンポーネントの場合、様々な場所での使用が想定されるため、結合度は低い方が望ましいです。

モジュール結合度 説明
1 データ結合 引数で単純なデータを渡すパターン
2 スタンプ結合 引数で構造体などのオブジェクトを渡すパターン
3 制御結合 引数の種類によって、メソッドの内の処理が変わるパターン
4 外部結合 単一のグローバルデータを参照しているパターン
5 共通結合 複数のグローバルデータを参照しているパターン
6 内容結合 他のオブジェクトの内部を参照しているパターン

qiita.com

Vueコンポーネントでいうと、propsは「データ結合」ないし「スタンプ結合」なので、結合度は1〜2です。これに対して、storeは概念としてはグローバルデータの参照に当たり、複数のデータを参照することが多いため、「共通結合」の5、gettersやactionsに依存している場合は「内容結合」の6になります。

つまり、storeを参照するコンポーネントはstoreと密結合し、storeなしでは利用できなくなってしまうのです。

このような密結合が常に問題になるわけではありませんが、コンポーネントツリーの末端に近いようなコンポーネントがstoreを参照しているのは、コンポーネントの設計が歪になっていることのシグナルかもしれません。

Vue + Vuexアプリケーションの状態管理の指針

Vuexがその設計の参考としているFluxアーキテクチャでは、状態が一カ所に集約されていて、Single Source of Truthになると安心できる、という考え方があります。この考え方を適用すると、データは基本的にVuexのstoreから引いてくるのが正しいように思えます。

こうしたSingle Source of Truthの考え方と、疎結合なコンポーネント設計は、対立する概念ではありません。ここで参考になるのは、Presentational ComponentとContainer Componentという考え方です。

medium.com

ざっくりいうと、Presentational Componentは、propsを受け取って、表示をするだけのコンポーネント、Container ComponentはPresentational Componentを管理し、値を受け渡すコンポーネントです。

  • Container Componentはstoreを参照し、必要に応じてPresentational Componentにデータを渡す
  • Presentational Componentはstoreは参照しない

このようにコンポーネントを分類することで、storeと密結合する部分をContainer Componentに限定し、Presentational Componentは疎結合に保つことができます。

また、もう1つの設計指針として、「Vuexはグローバルな状態のストアである」という点を強調しておきたいと思います。modulesに区切ることで細かいstoreを定義することは可能ですが、実装上どこからでも参照できるVuex storeは、概念的にはグローバル変数と同じです。コンポーネントローカルな状態(要素の表示・非表示等)は、Vuex Storeで管理する状態ではありません。一方、ローディングの表示等、画面全体に影響するような状態はVuexで管理すると便利です。

まとめ

以下のような指針で設計を行うことで、Vueコンポーネント設計の基本を守りつつ、Vuexを適材適所で活用できます。

  • 親子関係のあるコンポーネント間のデータの受け渡しは「Event Up, Props down」で
  • 親子関係のないコンポーネント間のデータの共有はVuexで
  • コンポーネントの固有のデータはコンポーネントのローカルステート(data)で
  • グローバルなデータはVuexで

参考文献

jp.vuejs.org jp.vuejs.org vuex.vuejs.org qiita.com medium.com

SVGスプライトアイコンの作り方・使い方

この記事は一休.com アドベントカレンダー 2018の2日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でフロントエンドの開発を行っています。

今回は、最近一休.comに導入した、SVGスプライトによるアイコンの作り方・使い方について紹介します。

f:id:ryo-utsunomiya:20181201135110p:plain
StorybookのSVGスプライトアイコン一覧

アイコンの一般的な使い方

アイコンは、一般的に、以下のような方式で使用されると思います。

  1. ビットマップ画像(gif, png等)
  2. アイコンフォント(Font Awosome等)
  3. SVG

このうち、ビットマップ画像によるアイコンは拡大・縮小に弱いため、様々な解像度の画面に対応する必要のある現代には不向きです。アイコンフォントはベクター画像なので拡大・縮小に強く、豊富なアイコンがライブラリとして提供されているのが魅力です。SVGもアイコンフォントと同様のベクター画像ですが、フォントにはない柔軟性を備えています。

一休.comは歴史のあるサービスのため、これらのアイコンが混在していますが、最近はSVGアイコンを使うことが多いです。

SVGの柔軟性

SVGは、HTMLに直接埋め込んで使用可能です。そのため、CSSによるスタイリングが可能です。アイコンフォントも色の設定やサイズの調整が可能ですが、パーツ毎に色を塗り分けたりするような柔軟なスタイリングは、SVGでしかできません。また、JavaScriptから操作しやすいという特徴もあります。

一休.comにおけるSVGアイコン使用の問題点

一休.comでのSVGアイコンの使用方法は、いくつかの変遷をたどっています。

はじめはimgタグでsvgファイルを読み込む使い方でしたが、これではSVGの柔軟性で挙げた特徴はほとんど活用できません。

<img src="/path/to/icon.svg" />

インラインSVGにすると、柔軟性は得られますが、記述が煩雑になります。

<svg ..(アイコンにもよるが、200バイトくらい).. />

そこで、Vueコンポーネント化が試みられました。以下のように、1つのSVGアイコンに対して、1つのVueコンポーネントを作る設計です。

<template>
  <svg .../>
</template>
<script>
  export default {
    name: 'some-icon',
  };
</script>

これによってSVGのスタイリングの柔軟性は増しましたが、この方式には2つ問題がありました。

1つはパフォーマンスで、アイコン1個あたり1KB(minify+gzip)、アイコン20個で約20KBものJSサイズ増加が発生したことです。本来SVGアイコンは1個200~300byte程度で、gzipするとさらに縮みます。Vueコンポーネント化することで、本来のサイズの10倍ほどに膨らんでしまっています(これはVueコンポーネント設計のまずさに起因しているため、個別にコンポーネントを作るのではなく、汎用的なSVGアイコンコンポーネントを導入するようにしていれば、パフォーマンスへの悪影響は緩和できたと思います)。

もう1つの問題は、VueコンポーネントはVueのコンテキストの中でしか使えないことです。一休.comはフルSPAではないので、サーバサイド(aspx/cshtml)で出力している部分もあります。サーバサイドで出力している部分では、Vueコンポーネントは使えないため、imgタグなどを使う必要があります。

これらの問題を解消し、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. JSフレームワークに依存せず、どこでも使える

という条件を満たすソリューションを検討しました。

SVGスプライトとuse要素

SVGについて調べたところ、use要素というものがあることがわかりました。ページ内の別のSVGに定義されている要素を呼び出して使うことができます。

<!-- アイコン定義 -->
<svg>
  <symbol id="someIcon" ... />
</svg>

<!-- アイコン使用 -->
<svg>
  <use xlink:href="someIcon" />
<svg>

use要素を使う場合、必要なアイコンをSVGスプライトにまとめて、それをインラインSVGとしてHTML内に書き出す必要があります。インラインSVGはネットワークリクエストが発生しないため、パフォーマンス面ではアイコンを個別に読み込むよりも有利です。

この方式を採用すると、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. JSフレームワークに依存せず、どこでも使える

という要件を全て満たせることがわかりました。

残る課題は現行の開発環境への組み込みです。

gulpによるSVGスプライト

SVGスプライト化にはgulpを使いました。webpackオンリーの環境ならwebpackでやっても良いと思います。webpackの役割をあまり増やしたくないのと、SVGスプライト関係のサンプルコードはgulpを使っているものが多かったこともあり、gulpを使用しました。

SVGスプライトのためのgulpタスクは以下のようになっています。

const gulp = require('gulp');
const path = require('path');
const svgmin = require('gulp-svgmin');
const svgstore = require('gulp-svgstore');
const cheerio = require('gulp-cheerio');

const commonDir = 'path/to/common';

gulp.task('svg-sprite',() => {
  gulp
    .src(commonDir + 'icon/*.svg')
    .pipe(
      svgmin(file => {
        const prefix = path.basename(
          file.relative,
          path.extname(file.relative),
        );
        return {
          plugins: [
            {
              cleanupIDs: {
                prefix: prefix + '-',
                minify: true,
              },
            },
          ],
        };
      }),
    )
    .pipe(svgstore({ inlineSvg: true }))
    .pipe(
      cheerio({
        run: function($) {
          $('svg').attr('style', 'display:none');
        },
        parserOptions: { xmlMode: true },
      }),
    )
    .pipe(gulp.dest(commonDir + 'icon-dist'));
});

このタスクは以下の流れで処理を行います。

  1. common/icon 配下にあるsvgファイルを取得
  2. gulp-svgminを使ってSVGを圧縮
  3. gulp-svgstoreを使ってSVGを結合
  4. gulp-cheerioを使って、結合したSVGファイルを非表示に
  5. common/icon-dist にファイル(icon.svg)を書き出し

このようにして、 https://www.ikyu.com/common/icon-dist/icon.svg (実際にサイトで使用しているSVGスプライト)は作成されています。また、このSVGスプライトは、サーバサイドでHTMLのbodyの開始直後に書き出されています。

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

利用側では、以下のように、svg要素の中にuse要素を配置し、use要素のxlink:href属性にSVGのid(#ファイル名)を指定することで、アイコンを参照できます。スタイリングはCSSで行います。

<svg class="l-header-search-icon"><use xlink:href="#search"></use></svg>

まとめ

以上、SVGスプライトを使用したアイコンの作り方について紹介しました。

参考文献

GulpでSVGスプライトとアイコン一覧を一発生成 - Bit Journey's Tech Blog