一休.com Developers Blog

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

ヘルプデスクに Halp を導入して改善した話

f:id:rotom:20210521184904p:plain

社内情報システム部 コーポレートエンジニアの大多和(id:rotom / tawapple)です。 最近はオフィスファシリティと、Jamf Pro や Dialpad や、情シスの採用をやっています。

今回は情シスの業務において外すことのできない、社内のヘルプデスクを改善した話をします。

一休のヘルプデスクについて

これまでのヘルプデスク

2018年の記事でも紹介している通り、一休では営業やコーポレート部門のメンバーを含めた全メンバーで Slack・Google Workspace を導入しています。

user-first.ikyu.co.jp

社内からのヘルプデスクについては、Google フォームに入力してもらった内容が Slack に自動投稿され、Slack のスレッドでやりとりを行い、問題を解決していました。

f:id:rotom:20210426174458p:plain

この方法を導入することで、口頭、電話、Slack など分散していた問い合わせ窓口を1つのチャンネルに集約することができました。

課題だったこと

一方で、この方法を使った運用にはいくつか課題点がありました。

対応状況のステータスが分からない

この問い合わせが対応待ちなのか、調査などの対応中なのか、すでに解決しているのか、忘れられているのか、といったステータスがひと目でわからず、スレッドでのやりとりや、絵文字でのリアクションでしか確認することができない状況でした。

これにより対応の抜け漏れが発生することがあり、改善点として挙げられていました。

スマートフォンから投稿しづらい

一休のメンバーは営業が6割を占めており、ホテル・旅館やレストランなどの取引先や移動中など、外出時に問い合わせを行うことも少なくありません。

Google フォームを使った問い合わせ方法は、情シスにとっては管理がしやすくなった一方で、ユーザーにとってはスマートフォンからの投稿に手間が多い状態でした。 ブログのドメインにもなっていますが、一休は全社を通して「ユーザーファースト」という、ユーザーにとっての価値を追求する文化が根付いています。

www.ikyu.co.jp

情シスにとってのユーザーは社員であり、この状態はユーザーファーストではありませんでした。 また、外出時の問い合わせは緊急を要することも多く、問い合わせから解決までをスピーディーに行う必要があります。

以上のことから、スマートフォンからも投稿しやすく、すばやく問い合わせができる仕組みをつくる必要がありました。

DM で問い合わせがきてしまう

上記の使い勝手の悪さもあり、Slack の DM で情シスメンバーに直接問い合わせがよくありました。

ヘルプデスクを DM で行ってしまうと他者からやりとりが見えないため、ナレッジが貯まらず同じ問い合わせが続いてしまう、対応が属人化し特定のメンバーに負荷がかかってしまう、対象のメンバーが離席していると対応が遅れてしまう、など多くの問題を抱えていました。

qiita.com

これらの課題からヘルプデスクにチケット管理ソリューションの導入を検討しました。

Halp について

ここで本題の Halp の登場です。ハルプと読みます。

www.atlassian.com

アメリカのスタートアップ企業が開発していたヘルプデスクソリューションで、2020年5月に Jira や Confluence などを開発する Atlassian が買収しました。

jp.techcrunch.com

一休では2020年7月から検証・評価を開始し、実用性の確認が取れたことから2020年10月に本導入しました。

Halp で改善できたこと

対応状況の見える化

f:id:rotom:20210513163114p:plain

Halp のコンソールより、チケットごとのリクエスター(ユーザー)、アサインエージェント、対応状況、最終更新日時が一覧で確認できます。 これにより、誰もアサインされていないチケットや、しばらく更新されずオープンのままのチケットなどを確認することができ、抜け漏れを防げるようになりました。

f:id:rotom:20210513163348p:plain

また、Halp のレポート機能により、チケットを拾うまでの応答時間(First Response Times)、解決までにかかった時間(Resolution Times)を表示することができます。 問い合わせの粒度がまばらなため数値は大きめになってはしまうのですが、ここの数値は少しでも小さくなるように意識し対応しています。

また、日ごとのチケット作成数や、アサインエージェントごとの担当チケット数もこちらから確認可能となっています。

Slack ネイティブな問い合わせと対応

Halp ではチケットの発行からクローズまで、Slack 上で完結することができます。

it-helpdesk のようなユーザー対応を行うヘルプデスク用チャンネルと itdept-triage のような情シスメンバー用のトリアージチャンネルの2つを用意します。

f:id:rotom:20210514182758p:plain

ユーザーはチケットについて意識せず、ただ Slack のヘルプデスクチャンネルに問い合わせるだけで、自動でチケットが発行されます。

f:id:rotom:20210514183910p:plain

Bot がチケットを発行した旨をスレッドに投稿します。このあとのユーザー対応はスレッドで行います。 このやりとりはすべてトリアージチャンネルと自動同期するため、情シスメンバーはトリアージチャンネルのみで対応可能です。

f:id:rotom:20210520170442p:plain

情シスメンバー内での相談や依頼などは :lock: 🔒 の絵文字を先頭につけることで、ヘルプデスクチャンネルには自動同期されず、やりとりをすることができます。

f:id:rotom:20210520174109p:plain

f:id:rotom:20210520180623p:plain

ステータスの更新、クローズまで、すべてチケット操作が Slack 上で完結し、他のシステムやページを開く必要もありません。

これにより、ユーザーはヘルプデスクチャンネルだけ、情シスはトリアージチャンネルだけで問い合わせが完結し、 スマートフォンからも操作がしやすいSlack ネイテイブな対応が可能となりました。

DM 問題への対応

Halp は DM に対しても機能します。DM で届いた問い合わせにも :ticket: 🎫 リアクションをつけることでチケットが発行されます。

f:id:rotom:20210521165913p:plain

発行されたチケットはトリアージチャンネルに自動投稿されるため、ナレッジを情シスメンバー内に共有することができます。 また、DM がチケット化されることで対応状況や対応件数も把握できるようになりました。

f:id:rotom:20210521182216p:plain

日頃より DM ではなくチャンネルで問い合わせていただくようにアナウンス・誘導することも大切ですが、 実際に DM で問い合わせが来たときにチャンネルと同じようにチケット化する、というアプローチが取れるようになりました。

自動応答 bot

現在はまだ β ではありますが、「Halp Ansers」という自動応答の機能も開発されています。 現時点(2021/5)では日本語非対応なため、「Zoom」「SmartHR」などアルファベットの SaaS 名などで利用ができます。

f:id:rotom:20210521183001p:plain

f:id:rotom:20210521183143p:plain

キーワードマッチで自動応答をすることで、適切な問い合わせ窓口へ誘導や、トラブルシューティングの URL やマニュアルを展開することができ、 かんたんな問い合わせであれば、bot で自己解決を促すこともできるようになりました。

終わりに

こうした業務の改善により、ユーザーにとっても使いやすく、情シスにとっても管理がしやすく、素早く問題が解決できる、 従業員体験を向上できるヘルプデスクを引き続き目指していきたいと思います。

さて、ここまで読んでいただいたあなたは、きっと一休の情シスに興味があると思います

一休では組織を IT の面で成長させる、情シス・コーポレートエンジニアを募集しています! 社内インフラ・ネットワーク系の方に限らず、SaaS などのシステムを活用して業務の改善に取り組める方は大歓迎です!

インターネット企業としては比較的歴史の長い成長した組織ではありますが、裁量を持ってシステムの選定・導入に携わることができ、 チーム一丸となって最新の技術・サービスや、エンタープライズ向け製品に触れることができる充実した環境です。

hrmos.co

note.com

ご興味のある方はぜひご応募、ご連絡をお願いします。一度お話しましょう!

WebComponents でログインコンポーネントをつくってサービス横断で使えるようにした話

こんにちは。プロダクト開発部の渥美 id:atsumim です。
今回サービス横断で利用できるログインコンポーネントを WebComponents で実装したのでその紹介をします。

1. 背景

今年の2月に電話番号での会員登録及び認証機能をリリースしました。

これに伴って一休の会員基盤も刷新しました。
一休のサービスは主に、宿泊、レストラン、スパとあるのですが、 歴史的経緯により会員基盤が分散してしまっていたので、ひとつにまとめる狙いもありました。

f:id:atsumim:20210427113250j:plain
会員基盤 Before/After

その一環として、一休のサービスで横断して使えるログインコンポーネントを WebComponents で実装しました。 このコンポーネントにログインや会員登録の処理を集約し、新会員基盤へのインターフェースとするようにしました。
また、電話番号認証や2段階認証設定のモーダルも実装しました。下記が実際の画面です。

ログインモーダル 電話番号認証モーダル 2段階認証モーダル
f:id:atsumim:20210426161345p:plain
f:id:atsumim:20210426161308p:plain
f:id:atsumim:20210426161238p:plain

この記事ではログインモーダルに絞ってお話します。

2. 技術選定

技術選定するにあたって、条件は以下の通りでした。

  • ページ遷移を挟まずにログインができる
  • どのアプリケーションプラットフォームでも利用できる

1つ目の条件からモーダルコンポーネントを提供することはほぼ決まっていました。
予約入力をしている途中でログインページに遷移すると体験を損ねてしまうので、スムーズな予約を実現するためにはモーダルコンポーネントでの提供が必須でした。

2つ目の条件として、一休のサービスは主に Vue.js, Python テンプレート, ASP.NET 等のプラットフォームで 画面描画を行なっているのですが、どのプラットフォームでもログインができるようにする必要がありました。 そのためには Web 標準で使える WebComponents が適任でした。

WebComponents について詳しくはこちらの記事がよくまとまっています。

WebComponents の実装フレームワークには PolymerLitElement がありますが、 Vue CLI が標準で WebComponents をビルドできるのでこれを利用しました。 内部的には vue-web-component-wrapper が使われています。大変助かりました🙏

3. 実装

一部省略していますが、下記のインターフェースになるようにログインモーダルを実装しています。 実装したログインモーダルは <ikyu-login> という CustomElement で定義しました。 HTML に <ikyu-login> と書けば通常の HTML タグ同様に使えるようになります。

Attributes

Attribute Type Default Note
login-only Boolean false ログイン画面のみ表示するか
signup-only Boolean false 会員登録画面のみ表示するか
open Boolean false モーダルを表示するか

Events

Event Type Note
login Boolean ログイン及び会員登録成功
error Error ログイン及び会員登録失敗
close Boolean モーダルを閉じる

HTML への組み込み

実際に HTML への組み込みを見てみましょう。 CustomElement に属性を指定する場合は setAttribute 関数、イベントを取得する場合は addEventListener 関数を使います。

<html>
  <head>
    <meta charset="utf-8">
    <title>ログイン</title>
    <script src="https://unpkg.com/core-js-bundle@3.0.0-alpha.1"></script> // IE11 用
    <script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script> // IE11, Edge 用
    <script src="./ikyu-login.js"></script>
  </head>
  <body>
    <button onclick="openIkyuLogin()">open</button>
    <ikyu-login show-signup></ikyu-login>

    <script>
      const ikyuLogin = document.querySelector('ikyu-login');

      function openIkyuLogin() {
        ikyuLogin.setAttribute('open', true);
      }

      ikyuLogin.addEventListener('close', () => {
        ikyuLogin.setAttribute('open', false);
      });

      ikyuLogin.addEventListener('login', (status) => {
         // リダイレクトしたりする
      });
    </script>
  </body>
</html>

Vue への組み込み

Vue に組み込むときは通常の Vue コンポーネントと同様に propsevent のやりとりができます。 setAttributeprops, addEventListenerevent に置き換わるイメージです。

下記は実際の利用例です。

<template>
 <div>
   <button @click="openIkyuSignupOnly()">モーダルを開く</button>
    <ikyu-login
      :open="ikyuLoggingin"
      :login-only="ikyuLoginOnly"
      :signup-only="ikyuSignupOnly"
      @close="ikyuLoggingin = false"
      @login="reload"
      @error="onError"
    >
 </div>
</template>
<script lang="ts">

export default Vue.extend({
  data() {
    return {
      ikyuLoggingin: false,
      ikyuLoginOnly: false,
      ikyuSignupOnly: false,
    }
  },
  methods: {
    openIkyuSignupOnly() {
      this.ikyuLoginOnly = false;
      this.ikyuSignupOnly = true;
      this.ikyuLoggingin = true;
    },
    reload(status) {
      window.location.reload()
    },
    onError(error: Error) {
      console.log(error);
    },
  }
});
</script>

注意点として、Vue 内で CustomElements を利用するときは Vue コンポーネントとして見なされてしまうため、明示的に Vue コンポーネントではないことを宣言する必要があります。

Vue.config.ignoredElements = ['ikyu-login'];

InternetExplorer の対応

案の定 InternetExplorer では WebComponents が動作しないので、憎しみと愛を持って対応します。
pollyfill の読み込み、スタイル崩れなどなどありましたが結果なんとかなってよかったです。IE 許すまじ。

まずは IE および Edge 用に core-jswebcomponents-loader を読み込みます。

<script src="https://unpkg.com/core-js-bundle@3.0.0-alpha.1"></script> // IE11 用
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script> // IE11, Edge 用

vue-web-component-wrapper 内ではES6で実装しているのでそのままでは IE で動きません。許さない。 これに対処するため babel で pollyfill してやる必要があります。また、ES6 をトランスパイルするための prebuild を記述します。

package.json は以下のようになりました。

{
  "scripts": {
    "prebuild": "npm-run-all babel node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js -o node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js",
    "build": "cross-env vue-cli-service build --no-clean --target wc --inline-vue --name ikyu-login 'src/components/IkyuLoginAndSignup.vue'",
  },
  "dependencies": {
    "babel-loader": "8.1.0",
    "babel-plugin-transform-es2015-arrow-functions": "6.22.0",
    "core-js": "3",
  },
  "devDependencies": {
    "@babel/cli": "7.11.6",
    "@babel/core": "7.11.6",
    "@babel/plugin-syntax-async-generators": "7.8.4",
    "@babel/plugin-transform-arrow-functions": "7.10.4",
    "@babel/plugin-transform-regenerator": "7.10.4",
    "@babel/preset-env": "7.11.5",
    "babel-plugin-transform-async-to-generator": "6.24.1",
    "babel-plugin-transform-custom-element-classes": "0.1.0",
    "babel-plugin-transform-es2015-shorthand-properties": "6.24.1",
    "babel-plugin-transform-es2015-template-literals": "6.22.0",
  }
}

これで InternetExplorer でも WebComponents が使えるようになりました🎉
IE 特有のデザイン崩れ等も対応しつつ、モダンブラウザと遜色なく動作するようになっています。


余談ですが IE に悪態をつきながら対応していたら同僚から実績解除の称号を得ました😇

f:id:atsumim:20210427123507p:plain
Legendary Hate Speech...

4. 所感

WebComponents を使ってみてよかった点と改善点を挙げます。

よかった点

コンポーネント指向であること

ログインモーダルの他にも認証モーダルなどを実装したのですが、共通コンポーネントを使い回せたので実装コストがかなり減りました。

Vue.js との親和性が高い

一休のアプリケーションプラットフォームは Vue.js が多いので、 Vue コンポーネントと同様のコンテキストスイッチで実装できたのはよかったです。

改善点

そのまま配信しようとすると重くなってしまった

当然ですが、実装を進めていくとどんどんファイルサイズが大きくなってしまいます。 そのまま配信するとコンポーネントを読み込んでいるページパフォーマンスが下がってしまう懸念があるので gzip での圧縮やブラウザキャッシュを付けて改善することが必要となってきます。


結果的に各サービスでのログイン実装が簡潔になり、ログイン処理が新会員基盤に集約できました。
Web 標準でお手軽に再利用できるコンポーネントが必要になった場合は、是非 WebComponents の選択肢を考えてみてください。

ヤフーのInternal Hack Dayに一休も参加しました

こんにちは。
宿泊事業本部のいがにんこと山口です。id:igatea

ヤフー社内で毎年開催されているハッカソンイベント「Internal Hack Day」が先日6/27~6/29に開催されました。
そのハッカソンにZ Holdingsのアスクル、一休、PayPay、ZOZOテクノロジーズが一緒に参加出来る運びになり、一休からも参加させていただきました。
この記事ではInternal Hack Dayに参加してきたレポートを書きます。

Internal Hack Day

Internal Hack Dayはヤフー社内で毎年行われている社内向けのハッカソンイベントです。
チームを組んでテーマに沿った新しい機能やサービスのアイデアを出し合い、短い期間で作り上げて競い合うイベントとなっています。
チームは自社だけで組んでもいいですし、他社の方と組むことも可能です。

Internal Hack Dayのルールは以下の通りです。

  • 開発時間は24時間、9:00~21:00の2日間
  • プレゼン時間は90秒

通常ルールは上記のみなのですが、新型コロナウイルスの流行に伴い、今回は上記ルールに加えて以下のルールも追加されました。

  • 「新しい生活様式での課題解決」をテーマに
  • 開発、発表は原則オンラインで行う

自分はハッカソンには初参加だったのですが、ハッカソンというと開催会場でみんなで集まって開発、開催会場で発表、というのが当たり前だと思っていたのですが、それが全てオンラインで行われるということで新しい試みでおもしろかったです。

開発

ハッカソン中はずっとオンラインのビデオ通話を繋げながらやっていました。
24時間なので、ずっと集中して出来るわけではないのでオンオフ切り替えるためにもご飯の時なんかは通話を切ってゆっくり過ごしたりしていました。

やっぱりオンラインコミュニケーションは大変だったりします。
オンラインで通話をしていると熱量とか空気感が伝わりづらいし感じにくい。
自分のチームは2人チームだったのでまだ問題ないのですが、これが人数が増えてくると収集つかなそうな印象を受けました。

ハッカソン中はずっと議論をしていて、手よりも口を動かすことのほうが多かったです。
最初の3時間は新しい生活様式の課題って何かをずっと議論していました。
仮説を建てて検証、さらに深堀して課題として正しいのか、課題にアプローチできているのか、今の自分たちに24時間で行えることか(発表時に成果物を見せなければいけないのでここは重要)をしっかり行ってから開発を始めました。

オンラインでの開催なので他チームの状況が全く分からなかったのもちょっとドキドキしました。
自分たちはまだ全然形に出来ていないけど他のチームはどんな感じなんだろう?と思いながら開発していました。
ここらへんはオンライン開催の課題ですね。

オンラインで複数人の声が混ざっても聞き取れるように

最終的に僕たちはオンラインでの会議や飲み会での会話がぎこちなくなりがち、というところに目をつけました。
原因の一つに複数人の声が混ざった時に聞き分けづらいことがあると考え、そんな問題を解決するためにオンラインで複数人の声が混ざっても聞き取れるように、そんなツールを作りました。

f:id:igatea:20200713002829p:plain

ZoomのURLを入れると、同じURLを入力した人同士を自動で音が被らないように音が聞こえる方向を調整してくれます。

課題の目の付け所、アプローチなどが評価されて、元々の賞にはなかった特別賞が急遽作られて表彰していただけたのはとても嬉しかったです。

結び

グループ内で他会社と一緒に何かイベントをやるというのは初めてだったので、別の会社のカルチャーに触れることが出来てとても刺激的でした。 また会社をまたいで何かやりたいですね。

他の受賞作品などはヤフーのテックブログにて。 https://techblog.yahoo.co.jp/entry/2020071430011124/

GraphQLのN+1問題を解決する DataLoaderの使い方

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

こんにちは。宿泊事業本部の宇都宮です。この記事では、GraphQLサーバ実装時に遭遇するN+1問題と、その解決のために使えるライブラリを紹介します。

フィールド単位でresolverを用意する

GraphQLでは、クライアントのクエリに応じてオンデマンドに結果を取得できます。

たとえば、以下のクエリを投げると…

{
  accommodation(accommodationId: "00001050") {
    name
  }
}

以下のようなレスポンスが取得できます。

{
  "data": {
    "accommodation": {
      "name": "マンダリン オリエンタル 東京"
    }
  }
}

ここで、施設のクチコミレーティングを取得したい場合、以下のようなクエリを投げることができます。

{
  accommodation(accommodationId: "00001050") {
    name
    rating
  }
}

このとき、サーバサイドではクエリによって必要なカラム(場合によっては、JOINするテーブル)が動的に変わります。バックエンドで動的にSQLを組み立てるのも1つの方法でしょう。しかし、SQLの組み立てロジックが複雑になったり、生成されるSQLが巨大でパフォーマンスの悪いものになったりするといった懸念点があります。

別のアプローチとして、追加のJOINが必要になるフィールドには GraphQL resolverを別に用意して、GraphQLサーバにレスポンスの組み立てを任せる、というものもあります。このようにすると、各resolverの実装をシンプルに保ちつつ、複雑なクエリに応答することができます。

一休.comでも使用している gqlgen というGoのGraphQLライブラリでは、以下の手順でフィールド単位のresolverを用意できます。

(1) GraphQLのスキーマと、gqlgenの設定ファイルを用意する

# schema.graphql

type Accommodation {
    name: String!
    rating: Float!
}
# gqlgen.yml

models:
  Accommodation:
    fields:
      rating:
        resolver: true # この設定がキモ

(2) go generate して、インタフェースを満たす

Resolverのインタフェースは以下のようになります。

// generated.go
type AccommodationResolver interface {
    Rating(ctx context.Context, obj *Accommodation) (float64, error)
}

これを満たす実装は以下のように書けます。

// resolver.go

func (r *accommodationResolver) Rating(ctx context.Context, obj *Accommodation) (float64, error) {
    summary, err := appcontext.From(ctx).Loader.ReviewSummary.LoadByAccommodationID(ctx, obj.AccommodationID)
    if err != nil {
        return 0, err
    }
    return summary.Rating, nil
}

N+1問題

このようにすると、無駄なデータの取得を避けつつ、resolverの実装がシンプルに保つことができます。しかし、以下のようなクエリを処理する際には問題が発生します。

{
  accommodation(accommodationId: "00001050") {
    name
    rating
    neighborhoods {
      name
      rating
    }
  }
}

ここでは、ある施設の近隣施設を取得して、それらのratingを取得しています。仮に、クチコミのレーティング取得が select rating from review_summary where accommodation_id = ? のようなクエリで実装されていると、このクエリが近隣施設の数だけ実行されることになります。このように、関連レコードの件数の分、追加データ取得用のクエリが発生する状態をN+1問題と呼びます。

このときのSQLの流れは以下のようになります。

-- 親の accommodation と rating を取得
select name from accommodation where accommodation_id = ?;
select rating from review_summary where accommodation_id = ?;

-- 近隣施設を取得
select accommodation_id, name from neighborhood_accommodation where accommodation_id = ?;

-- 近隣施設の数だけ rating を取得するクエリが発行される。。。
select rating from review_summary where accommodation_id = ?;
select rating from review_summary where accommodation_id = ?;
select rating from review_summary where accommodation_id = ?;
select rating from review_summary where accommodation_id = ?;
select rating from review_summary where accommodation_id = ?;

-- ↑ではなく、↓のように一括で取ってほしい
select rating, accommodation_id from review_summary where accommodation_id in (?, ?, ?, ?, ?);

このような場合、RailsなどではORMの 一括読み込み 機能を利用します。

一方、gqlgenでは、各resolverは平行に実行されるので、ORMによる一括読み込みは利用できません。このような場合に利用可能な、データ取得をバッチ化する仕組みが DataLoader です。DataLoaderのオリジナルはJavaScript実装の graphql/dataloader ですが、様々な言語のDataLoader実装が公開されています。また、DataLoaderはGraphQLサーバで使うために作られたライブラリですが、GraphQLとは関係なく、REST APIなどでも利用できます。

GoのDataLoaderライブラリ

Go製の有力なDataLoaderライブラリは、私が把握している範囲では以下の2つです。

前者は graph-gophers/graphql-go 、後者は gqlgen の作者によるライブラリです。

一休.comではgqlgenを使っているため、当初は dataloaden の方を試しました。dataloadenはgqlgenと同じくgo generateによるコード生成ライブラリとなっており、型安全なDataLoaderを生成できるという特長があります。しかし、モデルの配置方法などに制約が強く、私たちの用途には合いませんでした。

そこで、今は graph-gophers/dataloader を使っています。

DataLoaderの仕組み

サンプルコードに入る前に、DataLoaderの仕組みについて解説します。DataLoaderは前述したようにデータ取得をバッチ化するためのライブラリですが、そのための仕組みとしては「一定時間待って、その間に実行されたデータ取得リクエストをバッチ化する」というアプローチを取っています。

「一定時間」は、1msや16msなどといった値になります。この値が大きくなるとバッチ化できる範囲が広がりますが、その分レスポンスタイムが遅くなるおそれがあります。

graph-gophers/dataloader では、dataloader.Loader の Load() メソッドを呼び出すと、 Thunk という型の関数が返ってきます。この関数はJavaScriptのPromiseのようなもので、一定時間待った後で値が取得できます。

thunk := dataloader.Load(ctx, key)

実際のサーバでは、 Load() は平行して呼ばれるため、各goroutineが Thunk を受け取ります。

// goroutine A
thunk := dataloader.Load(ctx, key)

// goroutine B
thunk := dataloader.Load(ctx, key)

// goroutine C
thunk := dataloader.Load(ctx, key)

このthunkを呼び出すと、結果を取得できます。

thunk := dataloader.Load(ctx, key)
result, err := thunk()

一定の待ち時間の間に呼び出された Load() のkeyを覚えておいて、一括でデータ取得を行うのがDataLoaderの仕組みです。

// ここで 1ms のタイマースタート
s := loader.ReviewSummary.LoadByAccommodationID(ctx, "00000001")

// 0.5ms経過…

// この呼び出しは↑と一緒にバッチ化される
s := loader.ReviewSummary.LoadByAccommodationID(ctx, "00000002")

// 1ms 経過:↑の2件をまとめて、以下のクエリを発行し、結果を返す
// select accommodation_id, rating from review_summary where accommodation_id in ('00000001', '00000002')

// この呼び出しは別のバッチになる
s := loader.ReviewSummary.LoadByAccommodationID(ctx, "00000003")

DataLoaderのサンプルコード

完全な形のサンプルコードとしては、 hatena/go-Intern-Bookmark がオススメです。ここでは、このサンプルコードを題材に graph-gophers/dataloader の使い方を解説します。

DataLoaderを使うには、まず以下のようにバッチでデータ取得を行う関数を用意します(コードは簡略化しています)。

// loader/entry.go
func newEntryLoader(app service.BookmarkApp) dataloader.BatchFunc {
    return func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
        entryIDs := keysToEntryIDs(keys)
        entrys, _ := app.ListEntriesByIDs(entryIDs) // ここがキモ。 select * from entry where id in (...) を投げる
        return entrysToResults(entrys)
    }
}

次に、この関数を context に保持させます。なぜ context に保持させるのかというと、DataLoaderのキャッシュ機能はリクエスト単位のデータのキャッシュを意図しているからです。リクエスト毎に内容が空になる context は、DataLoaderを保持させる場所にぴったりです。これによって、バッチ化の対象は同一リクエスト内の Load() の呼び出しに限定されます。

contextへの追加はミドルウェアで行います。

// web/server.go

func (s *server) attatchLoaderMiddleware(next http.Handler) http.Handler {
    loaders := loader.New(s.app)
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        next.ServeHTTP(w, r.WithContext(loaders.Attach(r.Context())))
    })
}

このようにしてcontextに登録したDataLoaderは、以下のようにして呼び出せます。

// resolver/bookmark_resolver.go

// hatena/go-Intern-Bookmark は graph-gophers/graphql-go を使っているため、
// resolverの書き方がgqlgenとは異なる
func (b *bookmarkResolver) Entry(ctx context.Context) (*entryResolver, error) {
    // LoadEntry は context から DataLoader を取得し、Load() を呼び出して、結果を Entry 構造体にして返す
    entry, err := loader.LoadEntry(ctx, b.bookmark.EntryID)
    if err != nil {
        return nil, err
    }
    return &entryResolver{entry: entry}, nil
}

DataLoaderとDataDog APM

一休で使っているDataDogのAPM(Application Performance Monitoring)だと、以下のようなトレースが見えます。resolverが平行に実行されている様子が分かりやすいです。

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

紫色がDB問い合わせで、Ratingの取得は1回のDB問い合わせにまとめられていることがわかります。また、APMを活用すると、「待ち時間が短すぎて、複数のバッチに分かれてしまっていないか?」といった調査も簡単にできます 👍

むすび

今回はGoのDataLoaderライブラリの使い方を紹介しました。DataLoaderはややトリッキーですが、ハイパフォーマンスなGraphQLサーバの実装には欠かせないライブラリだと思います。

採用情報

一休では、GoやGraphQLに強みのあるエンジニアを募集しています! 一休.comのバックエンドは .NET Framework から Go への移行を進めていて、バックエンドでGoを書く割合が少しずつ増えているところです。

hrmos.co

イベント告知: 1/29に「一休.comのプロダクト改善事例と開発の裏側」を開催します

来週1/29(水)にエンジニア向けの採用PRイベントとして一休.comのプロダクト改善事例と開発の裏側を開催します。

一休では、主力サービスである 一休.com、一休.comレストランのプロダクト開発に関わるエンジニア職種の方を積極採用中です。

本イベントでは約2年に渡る一休.comのプロダクト改善の歴史を振り返りながら、実際に取り組んだ課題と改善に対するアプローチについてエンジニアリングマネージャーの田中(id:kentana20)がお話します。

トークセッションの後は、CTOの伊藤 (id:naoya) と2人でパネルディスカッションをしながら参加者のみなさまからの質問にもお答えします。

イベントの詳細、参加方法については以下のconnpassイベントページをご覧ください。皆様のご参加をお待ちしています!

ikyu.connpass.com

Datadog Log Management でアプリケーション稼働モニタリング

こんにちは。 システム本部CTO室のakasakasです。

今回は、Datadog Log Management を使ってアプリケーション稼働モニタリングをしている話をしたいと思います。

一休のモニタリング周りの話

Datadog Log Management とアプリケーション稼働モニタリングの話をする前に、一休でどのような監視をしているのか?という話を簡単にします。

一休ではDatadogをモニタリングツールとして使っています。 主な用途は2つあります。

  • インフラのリソースモニタリング
  • 外形監視

インフラのリソースモニタリング

インフラメトリクスのダッシュボードとアラートの設定は運用として乗っています。 具体的には、サービス(宿泊・レストランetc)毎のアプリケーションサーバやDBサーバのモニタリングをしています。

CPUで閾値を超えたら、Slack通知が飛び、エンジニアが対応するという形をとっています。

f:id:akasakas:20200111192017p:plain
インフラメトリクスのダッシュボード

f:id:akasakas:20200111192054p:plain:w500

外形監視

Datadog Synthetics API Tests を使って、外形監視をしています。 こちらも同様に、外形監視で異常が起きたら、Slackに通知が飛び、エンジニアが対応します。

f:id:akasakas:20200111192141p:plain
Synthetics API Tests

f:id:akasakas:20200111192211p:plain:w500

モニタリング観点で一休が抱えていた課題

インフラレイヤーでのモニタリングはできているが、アプリケーションレイヤーでのモニタリングはできていないというのが課題感としてありました。

ここでいうアプリケーションレイヤーでのモニタリングとは

  • 予約が正常にできているかどうか
    • エラーが多発してないか?
  • 予約通知メールが正常に送られているかどうか
    • メール送信件数が適切か?異常に多い、少ないということはないか?
  • 検索導線でのリクエスト数がどの程度あるのか?エラーがどの程度あるのか?

というサービスの状態がヘルシーかどうかという観点です。

※レイテンシーやエラーレートといったAPMとは異なります。Datadog APMは一部のサービスで運用しています。

これらを時系列で監視し(e.g. 10分毎の予約件数/1日ごとのメール送信件数) 異変があれば、アラートを飛ばすという仕組みがあれば、いち早く障害に気づけると考えました。

Datadog Log Management

このアプリケーション観点の監視をするために、Datadog Log Managementが有効だと考えました。

Datadog Log Management は Datadog 上でログを管理するサービスです。

一休では昨年ログ管理サービスをLogentriesからDatadog Log Management に完全移行しました。

導入方法や詳細な使い方は割愛します。

docs.datadoghq.com

Datadog Log Management を使って、アプリケーションログ・アクセスログをベースに時系列の予約状況・検索数の推移・メール送信件数etcを集計&ダッシュボードでグラフ化&アラートの設定ができれば、アプリケーション稼働モニタリングが実現できると考えました。

Datadog Log Management からダッシュボード作成

実際にDatadog Log Management から作成したアプリケーションモニタリングのダッシュボードがこちらです。

f:id:akasakas:20200111194244p:plain
宿泊スマートフォン予約状況

f:id:akasakas:20200111194316p:plain
宿泊PC・スマホ検索導線のアクセス推移とエラー状況

グラフの作成方法は

  • LogEvents を選択
  • タグで絞り込み

のみで、簡単です。

f:id:akasakas:20200111202201p:plain:w500

Datadog Log Management からアラート作成

予約状況の監視もアラートで検知することもできます。

f:id:akasakas:20200111200618p:plain:w500

New Monitor から Logs を選択し、検索クエリを指定すれば、Monitorが作成できます。

f:id:akasakas:20200111203513p:plain:w500

必要なメトリクスはカスタムメトリクスを作る

Datadog Log Management では取得できないメトリクスもあると思います。 その場合は、Datadog API を使って、カスタムメトリクスを作ります。

メトリクス API については下記をご覧ください。 docs.datadoghq.com

Datadog API を扱う際はRubyとPythonでそれぞれ API Clientがあるので、そちらを使うのがいいと思います。

GitHub - DataDog/datadogpy: The Datadog Python library

GitHub - DataDog/dogapi-rb: Ruby client for Datadog's API

カスタムメトリクスを作る例として、一休では検索にSolrを使っています。 SolrのIndex数を監視したいという場合は、SolrからIndex数を取得し、APIを使ってカスタムメトリクスを作成しDatadogに送信します。

具体的には下記のようなスクリプトをLambdaで定期実行するイメージです。

from datadog import initialize, api
import time
import requests

options = {
    'api_key': '<DATADOG_API_KEY>'
}

initialize(**options)

# Solrにリクエスト
r = requests.get('<Solr Endpoint>')

# Index数取得
index_count = r.json()['index_count']

now = time.time()

# Solrのindex数をカスタムメトリクスにして、Datadogに送信
api.Metric.send(metric="solr.index.count", points=(now, index_count), type="count")

カスタムメトリクスが作成できれば、Datadog上でダッシュボードとアラートが設定できます。

f:id:akasakas:20200114132046p:plain:w500
カスタムメトリクスから作成したSorのインデックス数

Datadog Log Management から取得できないが、監視したい項目については カスタムメトリクスを作るのもアリだと思います。

graph_snapshot API を使って、デイリーレポート

ただ、単純に

  • ダッシュボード作りました
  • アラート作りました

だけだと、せっかく作ったダッシュボードやアラートがエンジニアから忘れ去られそうという懸念がありました。

なので、「アプリケーションちゃんと動いているよ!エラーちょっと多いよ!」というのを伝える意味も込めて、デイリーレポートをslackに投稿するようにしました。

下記のようなイメージです。

f:id:akasakas:20200111193048p:plain:w500
アプリケーション稼働モニタリングのデイリーレポート

デイリーレポートをすることで、「エラーちょっと多いから確認した方がよくない?」みたいなことになり、調査&対応するという方向でエンジニアが動いてくれます。

f:id:akasakas:20200114155125p:plain:w500

これは graph_snapshot API を使って、キャプチャを作り、Slackに投稿するスクリプトをLambdaで日時で動かしています。

graph_snapshot API については下記をご覧ください。

docs.datadoghq.com

graph_snapshot API については細かいところを含めて、いくつか注意点があるので書いときます。

1.デフォルトの Rate Limitiing がけっこう厳しい

https://docs.datadoghq.com/ja/api/?lang=bash#rate-limiting に記載がある通り、

graph_snapshot API 呼び出しのレート制限値は、60/時間/Organization です。これは、オンデマンドで増やすことができます。

とあるので、無邪気にAPIを叩いていると、すぐに引っかかります。

2. graph_snapshot API のタイムゾーンがUTC固定

graph_snapshot API のタイムゾーンはUTCになっていて、任意のタイムゾーンに変更できません。

3. API リクエストで渡すパラメータがちょっと複雑

graph_snapshot API でグラフを作成する場合のAPIリクエストでJSONを扱う場合があるので、ちょっと面倒です。

DashBoardと同様のグラフを作りたい場合は、該当するグラフのJSONをリクエストにつめる必要があります。

f:id:akasakas:20200111232123p:plain

GitHub - DataDog/datadogpy: The Datadog Python library を使ったサンプル例が以下になりますが、JSONが長くなってしまうのが少し煩わしく感じるかもしれません。

from datadog import initialize, api
import time

options = {
    'api_key': '<DATADOG_API_KEY>',
    'app_key': '<DATADOG_APPLICATION_KEY>'
}

initialize(**options)

# Take a graph snapshot
end = int(time.time())
start = end - (60 * 60)
resp = api.Graph.create(
    graph_def='{\
        "viz": "timeseries", \
        "requests": [ \
            { \
                "q": "xxxxxxxxxxx", \
                "type": "bars", \
                "style": { \
                    "palette": "dog_classic", \
                    "type": "solid", \
                    "width": "normal" \
                } \
            } \
        ], \
        "yaxis": { \
            "scale": "linear", \
            "min": "auto", \
            "max": "auto", \
            "includeZero": true, \
            "label": "" \
        }, \
        "markers": [] \
    }',
    start=start,
    end=end
)

print(resp["snapshot_url"])

まとめ

今回は、Datadog Log Management を使って、アプリケーション稼働モニタリングを実現した話をしました。

単純なログ管理ツールとして使うだけでも、Datadog Log Management は便利ですが、 ダッシュボードやアラートなどを組み合わせることで、アプリケーションの状態が一目でわかるというのはいいと思いました。

最後に

Datadogのサポートの皆様にはいつも助けられています。 どんな問い合わせに対しても、いつも丁寧にサポート頂いているDatadogの皆様に御礼申し上げます。

Amazon EKS でWindowsコンテナを動かす

Amazon EKS Windows Container を使ってみる。

今年の10月に、Amazon EKSがWindows ワーカーノードのサポートを開始しました。

aws.amazon.com

一休では、今年の初めから、既存アプリケーションのEKS移行を行っており、夏には、ほぼすべてのLinux系アプリケーションをEKSへ移行することができました。

user-first.ikyu.co.jp

これを踏まえ、Windows系のウェブアプリケーションもEKSへ移行できないか、技術検証を行っています。具体的な検証ポイントは以下のふたつです。

  1. Amazon EKS で、Linuxコンテナ同様、Windows コンテナが動作するか。
  2. 既存のWindowsのWebアプリケーション(ASP.NETアプリケーションをDockerコンテナ化できるか。

2については、公開されている各種チュートリアルやサンプルなどを参考に、動作させることができました。

この記事では、主に、1.の検証でわかったことや注意点を紹介したいと思います。

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

Amazon EKS でWindowsのワーカーノードを作ってみる。

まず、eksctlを使って新しいeksクラスタを作成し、Windowsのワーカーノードを作ってみます。 作成のコマンドは以下の通りです。

eksctl create cluster -f cluster.yaml --install-vpc-controllers

--install-vpc-controllers という引数を渡すことで、Windowsノードグループに必要なリソースを追加します。

cluster.yamlの内容は以下の通りです。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: dev-win
  region: ap-northeast-1
  version: "1.14"
vpc:
  id: "vpc-123456789"
  cidr: "10.0.0.0/16"
  subnets:
    private:
      ap-northeast-1a:
        id: "subnet-xxxxxxxx"
        cidr: "10.0.144.0/24"
      ap-northeast-1c:
        id: "subnet-yyyyyyyy"
        cidr: "10.0.145.0/24"
      ap-northeast-1d:
        id: "subnet-zzzzzzzz"
        cidr: "10.0.146.0/24"
iam:
  withOIDC: true
nodeGroups:
  - name: ng-control-1
    labels: {role: workers }
    tags: {Stack: development, Site: ikyucom, Role: eks-node, k8s.io/cluster-autoscaler/wincluster: owned, k8s.io/cluster-autoscaler/enabled: "true"}
    instanceType: t3.medium
    desiredCapacity: 2
    maxSize: 2
    ebsOptimized: true
    privateNetworking: true
    securityGroups:
      attachIDs: [sg-xxxxx]
      withShared: true
    ssh:
      allow: true
      publicKeyPath: xxxx-key
  - name: ng-win-1
    amiFamily: WindowsServer2019FullContainer
    labels: {role: workers }
    tags: {Stack: development, Site: ikyucom, Role: eks-node, k8s.io/cluster-autoscaler/wincluster: owned, k8s.io/cluster-autoscaler/enabled: "true"}
    instanceType: t3.medium
    desiredCapacity: 3
    maxSize: 3
    ebsOptimized: true
    privateNetworking: true
    securityGroups:
      attachIDs: [sg-xxxxx]
      withShared: true
    ssh:
      allow: true
      publicKeyPath: xxxx-key

注目点はふたつです。

  • withOIDC: true を設定しています。IAMのアクセス許可をOIDC(OpenID Connect)を使って、Kubernetes サービスアカウントに割り当てるために必要な設定です。詳細は後述します。
  • ノードグループをふたつ作成しています。LinuxのノードグループとWindowsのノードグループです。これは、一部のシステム系のPodが、Linuxでしか動かないためです。

作成したクラスタにポッドをデプロイする。

システム系のポッドをデプロイする。

一休では、aws-alb-ingress-controllerexternal-dnsを使ってWebアプリケーションを提供しています。Windowsのコンテナアプリケーションでもこのふたつを使っていきたいです。 ただし、このふたつは、Windowsノードグループ上では動作しません。Linux上で動作させる必要があります。 これを実現するために、node selectorを使います。Linux ノードにはデフォルトで、 kubernetes.io/os: linux というラベルがついています(Windowsの場合は、kubernetes.io/os: windows がつきます)。 これを踏まえると、aws-alb-ingress-controllerのyamlは以下の通りになります。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: alb-ingress-controller
  template:
    metadata:
      labels:
        app.kubernetes.io/name: alb-ingress-controller
    spec:
      containers:
        - name: alb-ingress-controller
          args:
            - --ingress-class=alb
            - --cluster-name=dev-win
            - --aws-vpc-id=vpc-123456789
            - --aws-region=ap-northeast-1
          image: docker.io/amazon/aws-alb-ingress-controller:v1.1.3
          ports:
          - containerPort: 10254
            name: health
            protocol: TCP
      serviceAccountName: aws-alb-ingress-controller
      nodeSelector:
        kubernetes.io/os: linux

external-dns は以下のようになります。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:v0.5.17
        args:
        - --log-level=info
        - --domain-filter=dev.com
        - --policy=upsert-only
        - --provider=aws
        - --registry=txt
        - --interval=1m
        - --source=service
        - --source=ingress
        ports:
        - containerPort: 7979
          protocol: TCP
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 7979
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
      nodeSelector:
        kubernetes.io/os: linux

このふたつのyamlをapplyすることで、両方ともLinuxノードで動かすことができます。 ※ サービスアカウントの設定については後述します。

Webアプリケーションをデプロイする。

こちらもnode selectorを使って、windowsノードグループにデプロイさせるようにします。 事前にデプロイした、alb-ingress-controllerとexternal-dnsを使って、クラスタ外からアクセスできるウェブアプリケーションとして、デプロイするには以下のyamlになります。

## Deploymentの定義
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-web-app
spec:
  selector:
    matchLabels:
      app: test-web-app
  replicas: 1
  template:
    metadata:
      labels:
        app: test-web-app
    spec:
      containers:
      - name: test-web-app
        image: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test-web-app:latest
        ports:
        - name: http
          containerPort: 80
        imagePullPolicy: IfNotPresent
        livenessProbe:
          httpGet:
            port: 80
            path: /prob
          failureThreshold: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            port: 80
            path: /prob
          failureThreshold: 5
          periodSeconds: 5
      dnsConfig:
        options:
          - name: ndots
            value: '1'
      serviceAccountName: test-web-app
      nodeSelector:
        kubernetes.io/os: windows
---
## Serviceの定義
apiVersion: v1
kind: Service
metadata:
  name: test-web-app
  namespace: default
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: test-web-app
  type: NodePort
---
## Ingressの定義
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  labels:
    app: test-web-app
  name: test-web-app
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
    alb.ingress.kubernetes.io/healthcheck-path: /health
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/security-groups: security-group-name
    alb.ingress.kubernetes.io/subnets: subnet-xxxxxxxx,subnet-yyyyyyyy,subnet-zzzzzzzz
    kubernetes.io/ingress.class: alb
    external-dns.alpha.kubernetes.io/hostname: testapp.dev.com
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName:  test-web-app
              servicePort: 80

これをapplyすることで、無事、Windowsノードにウェブアプリケーションをポッドを配置できました。

AWSリソースへのアクセス権限はどうするか

ここまでの説明で大事な点を割愛しています。alb-ingress-controllerやexternal-dnsのAWSのリソースへのアクセス権限についてです。また、デプロイするウェブアプリケーションも要件によってはAWSのリソースにアクセスするでしょう。 このような場合、kube2iamkiamを使うことでポッド単位でAWSリソースに対するアクセス権限の制御が行えます。 実際、一休では、すでにサービスインしているクラスタではkube2iamを使っています。 しかし、kube2iamは、Deamonsetであり、Windowsノードでは動作しません。したがって、別の方法で、同じことを実現する必要があります。

kube2iamなしでIAM ロールを Kubernetes サービスアカウントに関連付ける

すでにいくつかの記事で紹介されていますが、今年の9月に、EKSがネイティブで IAM ロールを Kubernetes サービスアカウントに関連付ける仕組みを提供し始めました。

aws.amazon.com

dev.classmethod.jp

この仕組みを使えば、kube2iamを使わずに、podのAWSリソースに対するアクセス権限の制御ができそうです。 実際にうまくいくかどうか試してみます。ここでは、aws-alb-ingress-controllerがきちんとapplication load balancerを作成できるかどうか確認してみます。

まず、上述したクラスタ定義を使って、eksctlでクラスタを作ります。すると、AWSコンソールのIAMの画面のIDプロバイダーに、下記のように、OpenID Connectのプロバイダが作成されます。 withOIDC: true を設定したのはこのためです。

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

また、AWSコンソールで新しく作成されたEKSクラスタの設定を見ると、上述のOpenID ConnectプロバイダのURLが、表示されます。

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

次に、alb-ingress-controller用ロールに信頼関係を設定を設定します。

公式ブログのチュートリアル では、eksctl create iamserviceaccount コマンドを使って、新規にKubernetesサービスアカウントと対応するIAMロールを作成しています。このコマンドを実行するだけで、以下の3つの必要な設定が、一発で完了します。 - IAMロールの新規作成 - Kubernetesサービスアカウントの作成 - KubernetesサービスアカウントがIAMロールを引き受けるようにする信頼ポリシーの設定

一方、わたしたちのケースでは、新規にIAMロールを作るらずに、既存のEKSクラスタで使っているaws-alb-ingress-controller用のロール alb-ingress-controller-role を使いまわしたいです。 このため、eksctl create iamserviceaccount コマンドは使わずに、手動で設定してみます。 といっても設定は簡単です。↓の通り、対象のロールの信頼関係編集ボタンクリックします。

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

そして、次のようなポリシーを設定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.ap-northeast-1.amazonaws.com/id/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:sub": "system:serviceaccount:kube-system:aws-alb-ingress-controller",
          "oidc.eks.ap-northeast-1.amazonaws.com/id/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx には、先ほど作成された OpenID Connectプロバイダのarnの識別子が入ります。

そして、Kubernetes側にaws-alb-ingress-controller用のサービスアカウントを作成します。↑のポリシーに書いた通り、サービスアカウント名は、 aws-alb-ingress-controller で作成します。 yamlは以下の通り。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: aws-alb-ingress-controller
  namespace: kube-system 
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::xxxxxxxxxxxx:role/alb-ingress-controller-role

# ロールやロールのバインディングも必要ですが、ここでは割愛します。

annotationsに、 eks.amazonaws.com/role-arn というキーで、ロールのarnを指定します。 あと、はこのサービスアカウントをapplyします。

AWSコンソールのロードバランサの画面で、クラスタ名のタグで検索してみると、↓の通り、albが作成されています。

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

これで、windowsノードにデプロイしたPodが外部からのリクエストを受けれるようになりました。

WIndowsノードの課題

これで、Windowsのウェブアプリケーションも、プロダクション環境で、EKSで動かせる、と思いきや、ひとつ大きなハードルがありました。 Datadogが、Windowsノードが含まれたKubernetesクラスタの監視をサポートしていないのです。 Linuxノードだけであれば、Datadogのhelmチャートを入れるだけで、必要なメトリクスをほぼすべて収集できます。 一休は監視/ログ管理/APMをすべてDatadogに集約しているので、WindowsノードだけDatadog以外の方法を採用する、というのは合理的ではありません。 こちらはDatadog側にリクエストを出している状況です。

まとめ

EKSでWIndowsワーカーノードを扱う方法を書きました。クラスタの準備、とアプリケーションの動作確認自体は比較的簡単に行うことができました。 今後は、DatadogのWindows対応を待ちつつ、EKSへの移行を引き金にして、なかなか着手しにくいWindowsウェブアプリケーションのコンテナ化を推進し、必要なリファクタリングを行ったり、細かな技術検証をするフェーズになりそうです。


この記事の筆者について

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