astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

GitLab CIを四倍速にした話

f:id:astamuse:20191220191229p:plain

chotaroです。豊洲PITでたくさんの人と頭の中のOMOIDEを共有してから一週間、興奮冷めやらぬ日々です。 astamuse lab年内最後の記事になります!

さて先日、ICPチームで利用しているGitLab CIの実行時間を4分の1にしました。 一度CIを動かすと1時間強待ちが発生する状況になっていたため、改善が急務でした。改善の結果と、どんなことをしたか自分の備忘も兼ねて書いておこうと思います。
悩める人の参考になれば嬉しいです。

before/after

10月に改善を実施したので、9月と11月の比較になります。

pipeline実行回数 合計実行時間(時間[分]) 平均実行時間
9月 164回 114時間[6880分] 0:41:57
11月 106回 15時間[945分] 0:08:55

1回のpipelineにかかる時間がなんと4分の1以下になりました(๑•̀ㅂ•́)و✧
せめて平均30分以内、を目指していたので大成功です!

改善ポイント

以下が主なポイントになります。

  • 無駄なjobは動かさないようにしましょう
  • 一度pullしたdocker imageを再利用するようにしましょう。
  • executorのスペックを見直しましょう。
  • 毎回利用する外部リソース(依存するjarとか)が存在する場合、cacheを活用して使いまわしましょう。

当たり前ですね!(※できてなかった

基本的には公式documentの記述をもとにそれぞれの環境に当てはめていけばいい感じになります。

それでは良いCIライフを!アデュー!

......というのもあんまりなのでかなり泥臭い内容ですが、私の闘いの記録を残します。

改善する前の状況

  • 言語は java / scala
  • マルチプロジェクトな構成のリポジトリ
  ~/pjroot
.
├── .git
├── .gitlab-ci.yml
├── api
│   ├── src
│   └── test
├── batch
│   ├── src
│   └── test
├── commonLibrary
│   ├── src
│   └── test
└── web
    ├── src
    └── test
  • 各PJで自動testを実施
  • どんな小さな変更でもすべてのテストを実施(README変更でもpipelineが実行されてしまう)
  • 一度のpipelineですべてのプロジェクトについて順次テストを実行
  • コミットのたび毎回全部のjobが実行される

->諸々の結果、一回あたり1時間強かかる上、pipelineの実行回数が無駄に多い

GitLab CIについて

利用方法

GitLab上で利用できるCIです。
ドキュメントはこちら
紙面の都合から詳細は省きますm( )m

ざっくり概要

  • ymlでjobや環境の設定を管理することができる
  • gitlab-runnerというCIランナーをgitlab上のGUIで管理して利用する。
  • executorがjobを実際に実行する。
    • executorには様々なものを設定できる。dockerや仮想マシン インスタンスなど。(インスタンスについてはawsやgcpが利用可能。)

我々の利用方法

  • GCPインスタンス上でgitlab-runnerが動いている
  • executorはdocker-machine形式により実行。
    • インスタンスを立ち上げ, dockerコンテナをそれ上で動かしてjobを実行する。

現状を確認してボトルネックを明確にする

jobの実行をgitlab上で観測する

まずどう動いているのかがわからなかったので、ひたすら眺めてみました。

  • gitlab-ciのjobのログをひたすら眺める
    • どこで詰まっているのかを調査するため、timeコマンドを挟むなどして実行時間を確認する
  • gitlab-runnerにモニタリングツールを挿してもらう
    • オートスケールの動き方など、どのようにexecutorがjobを処理していくのかをGUIで観測する

手元でrunnerを立てて観測してみる

リソースの状況などもっと詳細を確認したかったので手元でrunner、およびexecutorを立てて眺めてみました。

  • gitlab-runnerは手元で実行することが可能
    • gitlab-runner registerで対話形式で登録
  • executorをdockerにして実行
    • remote上で動かすときも実体はdockerコンテナ上でtestを実行しているだけなので、それをローカルで再現
    • 手元であればdocker psdocker statsで観測が可能

で、結局何がボトルネックになっていそうなのか

  • pipeline一本あたりに実行されるjobが多すぎる(再確認

複数PJが一つのリポジトリに乗っかっていて、一つのPJ修正でも全部のテストが起動している

  • 一度に並列に実行されるjobが少なく、待ちが発生している

jobが2並列しか実行できないのにpiplineの同じステージに5個jobが積まれている、、、

  • docker imageをpullするのに毎回時間がかかっている

前回実行時に取得したimageなどを使えないのか?

  • 依存するライブラリの取得に時間がかかっている

cacheで共有化できないか?

  • CPUの使用率がコンパイルのタイミングで跳ね上がる

最小のインスタンスを使っている現状ではスペック不足なのでは?

ボトルネックをそれぞれ対処してみる

pipelineで実行されるjobの制御

更新されているものが何か、によって実行JOBを制御します。

  • タイミングの制御:MRを立てている場合だけ実行する
  • 実行JOBの制御:該当directoryを変更している場合のみ実行する

各JOBについて、以下のようにonlyを記載することで制御します。

job:
    only:
        changes:
            - batch/**/*
        refs:
            - merge_request

並列実行数を増やす

これはgitlab-runnerのconcurrent設定で実現できます
ただし、このconcurrentを増やすほどexecutorが並列実行される=リソースを食い合うので、設定には注意しましょう

/[config_directory]>/config.toml

concurrent = 5

docker imageを使い回す

利用しようとしているimageがすでにpullされている場合、新たにpullするのをやめるようにします。
これはgitlab-runnerのpull_policy設定で実現できます

※公式documentに記載有り

/[config_directory]>/config.toml

[[runners]]
  [runners.docker]
    pull_policy = "if-not-present"

cacheの仕組みを利用して、一度取得したjarを別のpipelineでも使い回す

cacheを利用して、scalaの依存解決速度を爆速にします
※cacheについての公式ドキュメントはこちら

cacheの保存場所の設定を行わない場合、実行する環境でしかそのcacheを使い回せません。
そのため、gcs上にcacheを保存するbucketを作成して、そこに保存するようにします。runnerの設定にて指定することができます。

/[config_directory]>/config.toml

  [runners.cache]
    Type = "gcs"
    Path = "cache"
    Shared = true
      [runners.cache.gcs]
        CredentialsFile = "<credential fileを配置したpath>"
        BucketName = "<cacheを保存先のbucketのname>"

cacheの保存先の設定ができたら、次にcacheを利用するようjobの設定を行います。
ivyでの依存解決に時間がかかっていることがわかったので、.ivy2配下のcacheをpipelineを跨いで使い回すように設定します。

# 環境変数でsbtの参照先を上書きします
variables:
    JAVA_TOOL_OPTIONS: "-Dsbt.ivy.home=${CI_PROJECT_DIR}/.ivy2/ -Dsbt.global.base=./.sbtboot -Dsbt.boot.directory=./.boot"
    # coursierを利用する場合はこちらも指定してあげましょう
    COURSIER_CACHE: "${CI_PROJECT_DIR}/.cache"

...

job:
    cache:
        # jobの名前単位でkeyを設定することで、pipelineを跨いで同じjobでcacheを再利用させます
        key: app-dependencies-cache-${CI_JOB_NAME}
        paths:
            - ${CI_PROJECT_DIR}/.ivy2/

設定したら何度かjobを実行してみます。

cacheを利用できている場合、開始時にDownloading cache.zip from ~~, jobの終了時にUploading cache.zip to ~~~と表示されるので確認しましょう。

注意事項

cacheが効いてない?と感じる場合、以下のことを疑いましょう

  • cacheするディレクトリは間違っていませんか?
  • 使いまわしたいリソースを上書きしていませんか?
    • keyを元にpathを作成し、そのpathにzipファイルを保存しにいくため、同じkeyを用いてしまうと、cacheの内容が上書きされてしまいます
    • 改善策:jobごとにcache-keyを作成して、jobごとに保存するようにする(上記のkey設定を参考にしてください)

executorのスペックを見直してコンパイル速度を改善する 金で殴るともいう

ローカルでdockerに4コア使わせたところ、300%ほど利用していることがわかったので、スペックの調整をします。
executorが起動するインスタンスの設定を修正します。

docker-machineのオートスケールはDockerMachineの仕組みに依存するとのことなので、DockerMachineのgce向けのドキュメントを参考にインフラ担当に設定してもらいます(ありがとうございました!)

/[config_directory]>/config.toml

[[runners]]
    [runners.machine]
        MachineOptions = [
        ...

job設定に環境のスペックを確認するコマンドを入れるなどして反映されていることを確認しましょう。

まとめとして

これらの結果が前述の成果になります(再掲

pipeline実行回数 合計実行時間(時間[分]) 平均実行時間
9月 164回 114時間[6880分] 0:41:57
11月 106回 15時間[945分] 0:08:55

とにかく改善しないと開発のスピードがバリヤバいという危機感から、一つ一つ調べたり、仮説を立てて実践を繰り返しました。

一番効果があったのは何かと言われると明確ではないのですが、

  • executorのスペックを見直してコンパイル速度を改善する

が一つの鍵になり、そこ以外の地味な部分がすべて活用されるようになった結果劇的にjob時間が短くなった印象です。

成果だけみると月間100時間短縮して素晴らしい改善効果なんですが、一方で「改善をあとに回した結果、数十ないし数百時間を無駄にしてしまっていた」という事実も浮き彫りに。。。

もっと早く問題意識を持って改善すべきだった事案です。反省。

gitlab-ciはまだ不慣れな面も多々あるのですが、今回の件でだいたいお友達になれたような気がします。 来年はDeployフローの改善に着手したいところです。

次回更新は年明け予定です。それでは皆様、良いお年を!

AI社員のつくり方

フロントエンドエンジニアをしているkitoです。
今回は、Slackにいると便利なAI社員ことチャットボットについて書きたいと思います。
事の発端は、メルカリさんの「AI社員」が入社! 翻訳から日々の業務サポートまで大活躍中というブログのエントリーを読んだことです。Dialogflowを知らなかったこともあり、非常に興味深い内容で、弊社にもAI社員がいたら便利なのでは? と漠然と思うようになりました。
そして実際、HISASHIくんにインスパイアされたAI社員をつくってみたので、その概要を書きたいと思います。

Dialogflow

Dialogflowは、Googleが運用開発している自然言語解析プラットフォームです。自然言語処理を丸投げできるので、対話型botをつくる際には本当に有り難いサービスです。これなしで気軽にチャットボットはつくれないでしょう。しかも、Standard Editionの範囲なら無料で利用できます。今回、こちらのサービスを利用します。

さて、社内のヘルプデスク業務などを行うAI社員に必要な機能として最低限下記が必要かなと思います。

  • 気軽に話かけられる
  • メンションつきの質問に答えられる
  • 正しい答えを学習させられる

まず、気軽に話しかけるか否かですが、あまり難しく考えても仕方がないので、HISASHIくん同様いらすとやのイラストをアイコンにして、mizekoという名前にしました。(弊社、アスタミューゼなので。。)

f:id:astamuse:20191212111053p:plain

DialogflowとSlackの連携

メンションつきの質問に答えられるようにするのは、予想外に骨が折れました。
Dialogflowには、Slack、Facebookなどと簡単に連携できるintegrationsという機能があり、当初これを利用すれば簡単にAI社員ができるのではと思っていました。

しかし、実際には、@mizekoのようなメンションつきのテキストに反応するだけではなくて、あらゆるテキストに反応してしまい、チャットの邪魔になってしまいました。通常のチャットボットのユースケースならば発言する人が一人いて、それに随時反応すれば良いのかもしれませんが、会社のSlackで使う場合、様々な人がチャットに参加しているので、メンションつきもしくは特定のテキストにだけ反応して応答するようにしなければなりません。
これを解決するために、integrationsを使うのではなく、すべてAPIを通じて受け答えするように変更しました。
SlackのAPIでパブリックなチャンネルのテキストを取得し、@mizekoのメンションがついていた場合、DialogflowのAPIにテキストを投げ、解析の結果をSlackのAPIを通じて該当チャンネルに投稿するという流れです。

SlackのAPI

まず、SlackのEvent APIでパブリックなチャンネルのメッセージを待ち受けて、メッセージに@mizekoのメンションがあれば、DialogflowのAPIを叩く必要があるので、Request URLを待ち受けるサーバーが必要になります。
開発中や手元で試してみるなら、ngrokを使うのが簡単でしょう。本格的に試すならfirebaseが第一選択として考えられます。ただし、外部APIを呼ぶ場合は、有料プランのFlameかプランBlazeプランにする必要がある点注意です。Blazeプランの無料枠があるので、使い方によっては良い選択肢だと思います。私は、重量課金が少し気になったので他で用意しました。

さて、肝心のSlackのEvent APIですが、まずこちらでCreate New APPから新しいAPPを作成します。
OAuth & PermissionsにあるOAuth Access Tokenを控えておきましょう。 f:id:astamuse:20191212111130p:plain 次に、Event Subscriptionsの「Subscribe to workspace events」でSubscribeするEventからmessage.channelsを選びます。
f:id:astamuse:20191212111154p:plain 同じページの一番上にあるEnable EventのスイッチをONにして、Event Subscriptionsから、Request URLを指定します。
私の場合、Express.jsで下記のようなコードでRequest URLをSubscribeしました。

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
app.use(bodyParser.json());

app.post("/api/channels.history", function(req, res) {
    res.setHeader("Content-Type", "text/plain");
    res.send(req.body.challenge);

    //ここでDialogflowの処理

});

app.listen(process.env.PORT || 8080);

channels.historyのレスポンスは、公式のSlack APIを参考にしてください。

DialogflowのAPI

Dialogflowは、先にも書いたように自然言語解析プラットフォームでIntentというユーザーのリクエスト単位で管理されています。「今日の東京の天気教えて?」というユーザリクエストに、「東京は今日晴れです」と答えるのがひとつのIntentになります。
今回のAI社員には、新規Intentを覚えさせ、またIntentをUpdateして再学習させなければなりません。
要は、Slackのメッセージに応じて、DialogflowのAPIでIntentをCreateしたりUpdateしたりすることになるのですが、例えばAI社員が間違った回答をしたとき何を基準にIntentをUpdateすれば良いのでしょうか?

メルカリさんのHISASHIくん場合、ブログのスクリーンショットから察すると、ある特定のチャンネルでの質問に対して、HISASHIくんの回答にスレッド形式で:hisashi:という絵文字をつけると正しい答えとしてみなして、後で?Updateしているようでした。

このスレッド形式での正しい答えを絵文字つきで教える方式は、クレバーなやり方だと思ったので、弊社のmizekoでも採用しました。
ただ、特定のチャンネルではなくすべてのチャンネルで、かつリアルタイムで学習して欲しかったので、APIでIntentをCreateもしくはUpdateするようにしました。SlackのAPIの設定で、Event Subscriptionsを「Subscribe to workspace events」で設定したのは、すべてのパブリックなチャンネルで、AI社員mizekoに質問でき学習させるためです。

DialogflowのAPIに関しては、公式のリファレンスが参考になります。

公式のサンプルをそのまま使えるので、最初の設定さえ間違えなければそれほど迷わないと思います。 ポイントとしては、detectTextIntentでintentの検出を行い、帰って来た結果をSlackで返信するところです。

function detectTextIntent(projectId, sessionId, queries, languageCode) {
  const dialogflow = require('dialogflow');
  const sessionClient = new dialogflow.SessionsClient();

  async function detectIntent(
    projectId,
    sessionId,
    query,
    contexts,
    languageCode
  ) {
    //省略  
    const responses = await sessionClient.detectIntent(request);
    return responses[0];
  }

  async function executeQueries(projectId, sessionId, queries, languageCode) {
       //省略

       //slackのチャンネルに返信
       await slack.chat.postMessage({
                    token: botToken,
                    channel: _channel,
                    text: intentResponse.queryResult.fulfillmentText
                })

  }
  executeQueries(projectId, sessionId, queries, languageCode);

}

また、Slackのメッセージがスレッドであるかは、channels.historyレスポンスのメッセージにthread_tsが含まれているかどうかで判断できます。 スレッド上でのメッセージの場合は、:mizeko:という絵文字があれば、それは正しい答えを教えようとしているのだと判断できます。 どのthreadが該当のthreadであるかは、conversations.repliesから導きだせます。

f:id:astamuse:20191212111217p:plain

Threadで正解を教え、もう一度同じ質問をすると正しく答えてくれます。(IntentのUpdateで上書きしているので、後から教えた方を覚えます) f:id:astamuse:20191212124201p:plain

まとめ

メルカリのHISASHIくんにインスパイアされたAI社員をつくり、APIを通じて簡単な学習機能をつけました。社内で広く使われていくためには、 Dialogflowコンソールからの手入力による柔軟な運用が必要になるのではないかという気もしますが、自動化できる部分は自動化して勝手に育ってくれると嬉しいですね。
これからのオフィスでは、生産性を上げるためにBotがさらに活用されていくことでしょう。Botとうまく付きあって生産性を上げていきたいですよね? f:id:astamuse:20191212120422p:plain

アスタミューゼでは、エンジニア・デザイナーを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。お待ちしています。

しくじり リーンキャンバス

Gyopi です。
みかんが美味しい季節になりました。
風邪の予防にも、みかんいいですよ。

さて、今年を振り返る意味でも
今年取り組んだ取り組みの1つを振り返ります。

構成

  • リーンキャンバス のやってみたきっかけ
  • CPF/PSF/PMF を理解・整理する
  • 機能がフィットするかの検証が停滞した原因
  • 次につなげる

リーンキャンバス のやってみたきっかけ

当時、プロダクト開発を進めるにあたり開発側から「顧客のイメージが分からない」という声が多かったこともあり、
事業サイドが考えているターゲットを具体的に絞ってプロダクト開発を行うことを狙いました。

前提として私たちが開発しているInnovationCapital Pathfinder (通称 ICP)は、

新規事業を起こさなければならない企業の中の人に提供していた
新規事業コンサルティングをWebで広くサービス提供しよう!

と始まったもので、事業サイドはもともとコンサルティングを行なっていたチームが担っています。

参考までに新規事業を生み出すノウハウは本題ではないですが、下記エントリなどで少し紹介されています。 lab.astamuse.co.jp

コンサルティングノウハウは社内に存在しますが、

「コンサルティング」という形式では顕在化していたニーズを「Webサービス」という形でも引き続きニーズと定義できるのか

というのが大きな課題でした。というより、今でも常に課題です。

CPF/PSF/PMF を理解・整理する

置かれている立場を踏まえてフェーズを分解し、CPF/PSF/PMFを意識して
ブループリント作成や顧客インタビューなどに取り組みました。

CPF/PSF/PMFを意識すると言っても難解なのですが、それぞれのフェーズの役割というものがあります。

少し長くなりますが、以下の項目を確認していくことで Problem Solution Fit までの確認を行いました。

Customer Problem Fit の確認

  • 誰の何を解決するサービスなのか
    • 課題仮説の構築
    • 何の代替なのか(同業他社ではなく、時間を消費する相手として)
    • ジョブスペックの作成
  • 想定したカスタマーの課題は本当に存在するのか
    • 前提条件を洗い出す
    • 課題〜前提の検証(プロブレムインタビュー)
  • 終了条件
    • 課題が存在する前提条件をしっかり検証し、課題が存在することが確認できたか?
    • 課題を持っている顧客イメージを明確化にできたか?

Problem Solution Fit の確認

  • 課題を解決するベストな方法は何なのか
    • その際に、プロダクト側に最低限持たせる機能(MVP)は何なのか
    • UXブループリントの作成
    • プロトタイプの構築
      • ジョブスペックに対応する体験を一貫して提供できるようにする
    • プロトタイプインタビュー
  • 終了条件
    • 顧客がそのソリューションを利用する理由を明確にできるか?
    • ソリューション仮説の磨き込みを通じてカスタマーが持つ課題の理解がさらに深まったか?
    • その課題を解決できる必要最小限の機能を持つソリューションの洗い出しができているか?
    • カスタマーが期待すること全体を言語化できているか?

Product Market Fit の確認

  • Lean Canvas の Fix
  • スプリントの実行
    • 新しいユーザーストーリーをMVPに実装し、定量分析とカスタマーインタビューによる定性分析
    • カスタマーが継続的に欲しがるプロダクトの実現
    • AARRR(AARRR)Acquisition / Activation / Retention / Referral / Revenue
      • 特に Retention / Referral / Revenue を注意することを想定
  • 終了条件
    • PMF 到達先行指標
      • 各項目の NPS スコアがプラス(仮置き。9・10を付けた%から0〜6を付けた%を引いた数字)
      • Product Market Fit Survey の「非常に残念に思う」が40%以上
    • ユーザーの高いリテンションを保てているか?
    • カスタマー獲得から売上を確保するまでは確立できているか?
    • リーンキャンバスの項目全体を見て成立しているか?

各フェーズにおいて着目すべきリーンキャンバス の項目は異なります。
以下の図は理解に役立ちました。

CPF/PSF/PMF とリーンキャンバス の着目点
CPF/PSF/PMF とリーンキャンバス の着目点

業務のフローやキーワードをベースに「顧客層の具体化・条件化」「解決する課題の定義」を言語化しました。
そして、Adobe XD で作成したモックを使ったインタビューを実施し、最初の仮説の当たっていた部分も考慮不足の点もあぶり出されました。

コンサルティングでのニーズがあるのだから、Customer Problem Fit は飛ばしてもいいのかな、
という話も出ましたが最初から考えることで順序立てて捉えて議論に臨めたので良かったです。

今は Product Market Fit における最適解を模索するための企画と開発をしているということになります。

機能がフィットするかの検証が停滞した原因

今回の反省のメインとなるのですが、結果として機能単位での追加と仮説検証があまり進まなかったです。
ビジネス仮説の検証内容が大きくなり、機能開発とのバランスが良くなかったことが原因と考えています。

また、検証の対象の絞り方にも改善点がありました。
後付けの説明にはなりますが、複数の業界、複数の職種、複数の目的、というところを同時に検証しにいきました。
これは視野を最初の時点でむやみに狭めないという点では有用でしたが、機能開発面では要件を絞れず開発側に迷惑をかけました。

開発陣の頑張りの甲斐もあってリリースにはこぎつけましたが、スムーズだったとは言い難いです。
一度インタビューは行なったものの、その後のフィードバックを継続的に得ることも課題の1つです。

下記のようなフローを見据えると、上記で上がった課題は改善していかないといけません。

PMF到達までのフロー図
PMF到達までのフロー図

取り組むべき課題を3つまとめると

  • 仮説粒度を小さく定義する
  • 検証対象を絞る
  • 顧客からのフィードバックを集めることを仕組み化する

次につなげる

一番幸いなことは、コンサル→サービス、になっても課題解決ニーズ自体は観測できておりユーザとなってくださっていることです。 全体的な課題を色々と分解しながら、頓挫しないように小さな検証の繰り返しを進めたいです。

f:id:astamuse:20191204113159j:plain

仮説の粒度が大きく検証対象が絞れなかったという課題に関しては、具体的な顧客像を定め、その利用定着を具体的に目指しています。 顧客からのフィードバックも、利用していただくユーザーとの複合的なワークショップの取り組みや、使い勝手に関するやりとりを通じて、フィードバックを集められそうです。

当初にあった「顧客のイメージが分からない」に関して、事業サイドだけでなく開発面も含めて共通言語としてのペルソナを制定しています。 その取り組みに関しては主導してくれているデザイナーが今度書いてくれると思います!

さいごに

いくらでも長くかけるので、はしょり過ぎたかもしれません。

見ていただいた方の役に立つ内容だったのか怪しいですが、 未来の自分達にとってこの経験も乗り越えて上手くいったな、と語れるように頑張ります!

最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です! 興味のある方はぜひ下記バナーからご応募ください!!

Copyright © astamuse company, ltd. all rights reserved.