astamuse Lab

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

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

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

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

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

さいごに

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

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

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

Sangriaで始めるGraphQL入門

f:id:astamuse:20191120111334p:plain
sangria
お久しぶりでございます。Scalaでバックエンドを開発しているaxtstar(@axtstart)です。

今回は、WebAPI開発時にSangria(GraphQL)を使ってみて便利だなと感じたので紹介します。

GraphQLとは?

GraphQL(グラフQL)は、APIのために作られた、データクエリとデータ操作のための言語と、保存されたデータに対してクエリを実行するランタイムである
~~
GraphQLは、2012年にFacebookの内部で開発され、2015年に公開された
(抜粋: Wikipedia)

GitHubやNetflix、SpotifyでGraphQLを利用しているというニュースも記憶に新しいかと思います。

REST vs GraphQL

上記、Wikipediaにもあるように、RESTと比較して、 ウェブAPIの開発に、その他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供することを目的としているようです。

また、機能としては、読み込み、書き込み(ミューテーション)、データのサブスクリプション(リアルタイム更新機能)を持っています。

RESTとGraphQLの比較表

-- REST GraphQL
主導 クライアントドリブン サーバドリブン
エンドポイント インターフェース数分 基本的には一つ
通信 JSON JSON

ただ、以下の点では注意が必要とのこと

  • 比較的新しい技術のため、サーバライブラリは、言語によって差がある
  • クライアントでどんなクエリでもかけるのでパフォーマンスに注意
  • キャッシュの機構はRESTより複雑になる
  • ファイルアップロードのような機能は無い

Sangriaとは?

Sangriaとは、Scala版GraphQLの実装です。

github.com

play-frameworkやakka-http上で動作するようです。

今回はakka-http上で動作する公式サンプルに基づいて説明していきます。

依存関係

akka-http、JSONの変換にcirceを用いたバージョンのbuild.sbtは以下な感じです。

libraryDependencies ++= Seq(
  "org.sangria-graphql" %% "sangria" % "1.4.2",
  "org.sangria-graphql" %% "sangria-slowlog" % "0.1.8",
  "org.sangria-graphql" %% "sangria-circe" % "1.2.1",

  "com.typesafe.akka" %% "akka-http" % "10.1.10",
  "de.heikoseeberger" %% "akka-http-circe" % "1.29.1",

  "io.circe" %% "circe-core" % "0.12.1",
  "io.circe" %% "circe-parser" % "0.12.1",
  "io.circe" %% "circe-optics" % "0.9.3"
)

公式のakka-httpサンプルとはほんの少し変えています。

起動

sbt run で起動すると、playgroundが下記でアクセスできます。

http://localhost:8080/graphql

こんな画面

f:id:astamuse:20191119231758p:plain
playground

こちらはある種のIDE*1になっていて、補完やサジェッスションを提供してくれるため、意外なほど簡単に目的のクエリを記述することができます。

クエリ例

query{
  humans{
    id
    name
    homePlanet
    current_time
  }
}

補完の例

f:id:astamuse:20191120095436p:plain
completion

また右側のDOCS、SCHEMAというところに存在するクエリの仕様が表示されます。

スキーマ表示

f:id:astamuse:20191120095549p:plain
schema

スキーマ定義

Scalaで記載するスキーマ定義はsangria.schema._ で提供される、DSL*2で記述します。

EnumType、InterfaceType、ObjectType、Argument、ListType、OptionTypeなどを定義でき、最終的にObjectTypeとしてクエリのスキーマとして公開します。 StringやIntなども組み込みのTypeが存在するようですが、LocalDateTimeのような、文字列化の法則が決まっていないようなTypeは自分でParserを記述する必要があるようです。

スキーマ定義の例

Enum

  val EpisodeEnum = EnumType(
    "Episode",
    Some("One of the films in the Star Wars Trilogy"),
    List(
      EnumValue("NEWHOPE",
        value = Episode.NEWHOPE,
        description = Some("Released in 1977.")),
      EnumValue("EMPIRE",
        value = Episode.EMPIRE,
        description = Some("Released in 1980.")),
      EnumValue("JEDI",
        value = Episode.JEDI,
        description = Some("Released in 1983."))))

ObjectTypeの例

  val Human =
    ObjectType(
      "Human",
      "A humanoid creature in the Star Wars universe.",
      interfaces[CharacterRepo, Human](Character),
      fields[CharacterRepo, Human](
        Field("id", StringType,
          Some("The id of the human."),
          resolve = _.value.id),
        Field("name", OptionType(StringType),
          Some("The name of the human."),
          resolve = _.value.name),
        Field("friends", ListType(Character),
          Some("The friends of the human, or an empty list if they have none."),
          resolve = ctx ⇒ characters.deferSeqOpt(ctx.value.friends)),
        Field("appearsIn", OptionType(ListType(OptionType(EpisodeEnum))),
          Some("Which movies they appear in."),
          resolve = _.value.appearsIn map (e ⇒ Some(e))),
        Field("homePlanet", OptionType(StringType),
          Some("The home planet of the human, or null if unknown."),
          resolve = _.value.homePlanet),
        /* 試しに追加した型 */
        Field("current_time", Types.LocalDateTimeType,
          Some("current time."),
          resolve = _.value.current_time),
      ))

引数定義

  val LimitArg = Argument("limit", OptionInputType(IntType), defaultValue = 20)

LimitArg(最大取得件数)をクエリ定義の部分で利用する

      Field("humans", ListType(Human),
        arguments = LimitArg :: OffsetArg :: Nil,
        resolve = ctx ⇒ ctx.ctx.getHumans(ctx arg LimitArg, ctx arg OffsetArg))

上記、Types.LocalDateTimeType(LocalDateTime)のパーサー例*3

object Types {

  case object DateCoercionViolation extends ValueCoercionViolation("Date value expected")

  def parseDate(s: String) = Try(LocalDateTime.parse(s)) match {
    case Success(date) => Right(date)
    case Failure(_) => Left(DateCoercionViolation)
  }

  implicit val LocalDateTimeType = ScalarType[LocalDateTime](
    "DateTime",
    coerceOutput = (dt, _) => dt.toString,
    coerceUserInput = {
      case s: String => parseDate(s)
      case _ => Left(DateCoercionViolation)
    },
    coerceInput = {
      case StringValue(s, _, _, _, _) => parseDate(s)
      case _ => Left(DateCoercionViolation)
    })

}

パフォーマンス計測

GraphQLは複雑なクエリやネストの深いクエリをクライアントから自由に書けるようになっているためか、パフォーマンス測定を有効にできるようにサンプルではなっていました。

HTTP Headersに以下の設定を行うと有効になるよう*4です。

{
  "X-Apollo-Tracing":true
}

f:id:astamuse:20191119232453p:plain
tracing

クエリ(サーバサイド)

サーバサイド側でDSLライクにクエリを書くこともできます。 RESTのグルー(のり)として利用する*5のもありかなと思いました。

      path("query"){
        get {
          import sangria.ast._
          import sangria.macros._
          val queryAst: Document =
            graphql"""
            {
              humans {
                 name
              }
            }
          """
          executeGraphQL(queryAst, None, Json.obj(), false)
        }
      }

結果

$ curl http://localhost:8080/query
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   139  100   139    0     0    158      0 --:--:-- --:--:-- --:--:--   158
{"data":{"humans":[{"name":"Luke Skywalker"},{"name":"Darth Vader"},{"name":"Han Solo"},{"name":"Leia Organa"},{"name":"Wilhuff Tarkin"}]}}

クエリ

ここからはクライアント側でどのように呼び出すかを決めるクエリの記述法に関して見ていきます。

複数のスキーマを同時に呼び出す

query{
  humans{
    name
  }
  droids{
    name
  }
}

結果

f:id:astamuse:20191120101351p:plain
double schema

引数を取る場合

query{
  human(id:"1000"){
    name
  }
  droid(id:"2000"){
    id
    name
  }
}

結果

f:id:astamuse:20191120101835p:plain
argument

また、記述時に型エラーなどを教えてくれます(Validation)

以下は文字列型に数値を適用しようとしてエラー

f:id:astamuse:20191120102032p:plain
validation

クライアント側でクエリを自由に記述できるため、ある意味奇妙なクエリもかけるようです。

友達の友達の友達を検索

query  {
  humans{
    name
    friends{
      name
      friends{
        name
        friends{
          name
        }
      }
    }
  }
}

結果:

f:id:astamuse:20191120000357p:plain

まとめ

RESTでAPIを作ろうとした場合、丁寧につくれば作るほど、Endpointが細分化していき、またそれらを繋げるためにエッジ-サーバ間のラウンドトリップが増加する、もしくは統合的な項目を持った、メンテナンス性の低いEndpointが増加する問題があると感じていますが、GraphQLの場合はクエリに複数のスキーマ呼び出しを行ったりといったことが柔軟なので、エッジ-サーバ間の通信量をおさえつつもtidyな開発ができるのでは無いかと期待しています。

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

*1:graphql-playground-reactというものを利用している

*2:ここに記載

*3:https://github.com/axtstar/sangria-akka-http-example/blob/forBlog/src/main/scala/Types.scala

*4:ここで指定しているので変更は可能

*5:ここに

GitLab Pagesで複数SwaggerUIを自動で公開する

こんにちは、開発部のomiです。
前回の投稿から7ヶ月が経っていてびっくりしました。絶対背が伸びたと思います。

今回は、現在携わっている新しいプロジェクトで導入した GitLab Pages について書きたいと思います。

経緯

今のプロジェクトではAPI設計にSwaggerを使用しました。

Swagger Specで書いた仕様は、最初はSwagger Codegenでhtml化し、毎回サーバにアップして公開していました。

修正の都度ローカルでhtml化し、サーバにアップする作業は面倒ですし、アップを忘れて仕様書と実装のずれが出てくる恐れもあります。

そこで、我がチームの頼れるリーダーイケマスこと池田さん lab.astamuse.co.jp に提案していただいた、GitLab Pagesでのhtml公開を行ってみることにしました。

↓GitLab Pages 公式 Document
GitLab Pages

手順

今回、この方の方法を真似してみることにしました。

gitlab.com

私たちのプロジェクトのSwagger Specは用途ごとに分かれており、それぞれを別のURLで同時に公開したかったので、少し工夫する必要がありました。
Swagger Specが一つだよ、という方は↑の方の設定のままで大丈夫です。

Projectの任意のフォルダにSwagger Specを配置する

project
.
├── README.md
├── api-docs
│   ├── api01
│   │   └── api01.yaml
│   ├── api02
│   │   └── api02.yaml
│   ├── api03
│   │   └── api03.yaml
│   ├── api04
│   │   └── api04.yaml
│   └── api05
│       └── api05.yaml

今回は api-docs というフォルダの下にyamlを配置しました。
先述の通りこのプロジェクトのSwagger Specは用途ごとに分かれていたのでそれぞれサブディレクトリに入れてあげる必要があります。

.gitlab-ci.yml に設定を記述する

.gitlab-ci.yml に以下の通り設定を記述します。

image: hseeberger/scala-sbt:8u181_2.12.6_1.2.3

variables:
    DOCS_FOLDER: "api-docs"

cache:
    paths:
        - ./node_modules

stages:
    - deploy

pages:
    image: node:10-alpine
    stage: deploy
    before_script:
        - npm install swagger-ui-dist@3.22.1
    script:
        - mkdir -p public
        - cp -rp $DOCS_FOLDER public/
        - cd public/$DOCS_FOLDER/
        - find . -mindepth 1 -type d -exec cp -p ../../node_modules/swagger-ui-dist/* {} \;
        - ls -1 | while read FILE ; do sed -i "s#https://petstore\.swagger\.io/v2/swagger\.json#${FILE}.yaml#g" ${FILE}/index.html ; done
    artifacts:
        paths:
            - public
    only:
        - develop

variables
Swagger Specを入れたフォルダを指定します。
pages:stage
html公開を走らせたいタイミング
pages:script
GitLabではpublicという名前のフォルダが公開対象と決まっているため、public配下にhtmlを配置してあげる必要があります。
DOCS_FOLDERで指定したSwagger Specを入れたフォルダをpublicフォルダにコピーし、各サブディレクトリ配下にswagger-ui-distフォルダをコピーします。
デフォルトでは公開するhtmlのurlがhttps://petstore.swagger.io/v2/swagger.json になっているので、ここをまるっとそれぞれのyaml / jsonの名前に変換します。
pages:artifacts
path:publicで生成されたジョブの成果物をジョブが成功した後にGitLabに送信します。

これが、developブランチのdeploy時に毎回走ることになります。

Swagger Specから生成されたhtmlが公開される

上記設定をしておくと、developブランチがdeployされたタイミングで

http://[Project].[GitLabのホスト]/[api-docsまでのパス]/api-docs/api01/

というようなURLでAPI仕様書が公開されます。

※イメージ

f:id:astamuse:20191109183606p:plain
swagger-ui-image

最後に

今までの方法だとサーバ上のhtmlとプロジェクトにpushされているyamlの内容が食い違うというようなことが起こり得ていましたが、
今回の対応でyamlと公開されるhtmlが連動するようになり、yamlの管理も徹底されるようになったのでとても良い効果だと思いました。

弊社ではエンジニア・デザイナーを募集中です!興味を持っていただけましたらバナーからよろしくお願いします!
以上です。読んでいただきありがとうございました。

Copyright © astamuse company, ltd. all rights reserved.