トレタ初のiPhoneアプリ、トレテルをリリースしました

技術部でiOSを担当している高(@y_koh)です。

入社以来予約台帳アプリをメインに見てきたのですが、今回スポットで「トレテル」の開発に携わりました。トレタ初のiPhoneアプリになります。

f:id:y_koh:20181116120247p:plain トレテル | 飲食店のための予約トラブル防止アプリ

今回トレテルのリリースにあたり、どの様に開発していたのかをご紹介できればと思います。

飲食店さまが頭を悩ませるNo Show問題

まずその前に。

「No Show」という言葉を聞いたことがあるでしょうか。

僕はトレタに入って初めて知りました。「No Show」とは飲食業界や宿泊業界で言われる「無断キャンセル」の通称です。予約をしていたにもかかわらず当日連絡もなく現れない。見込んでいた売上が立たないだけでなく、本来受け入れることが出来たお客さまを断っているケースもあります。またそのために用意していた食材が無駄になってしまうこともあります。

売上だけでなく、心情的にもかなりのダメージがあります。

宿泊や航空券のようにシステム的に対応できていないからという問題もあります。もちろん将来的にはそうなるべきだと思うのですが、今すぐに、というのが難しいのが現状です。この様な現状があり、飲食店さまからするとかなり根深い問題になっています。

この様な状況を重く捉え、今回、経済産業省からガイドラインとして下記が発表されました。

No show(飲食店における無断キャンセル)対策レポートが発表! (METI/経済産業省)

無断キャンセル対策推進協議会にはトレタも参加しています。

さらに、トレタとして何か出来ることはないかということを考えて開発されたのが予約トラブル防止アプリ「トレテル」です。

トレテルってどんなアプリ?

f:id:y_koh:20181119150803p:plain

トレテルのコアバリューは、予約していただいたお客さまにキャンセルポリシーを伝え、お客さまの手でその内容に承諾していただくことです。今までの飲食店予約はここが曖昧になっていました。

実際に利用いただいている店舗さまにヒアリングしたところとても良いフィードバックをいただけました。正直アプリとしてはまだまだなのですが、実際にお店の課題を解決できたことはとても嬉しいことです。

f:id:y_koh:20181119151848p:plain

プロジェクトの発足(2018年1月)

さて、ここからはこのプロジェクトをどうやって進めてきたかをご紹介します。

最初にプロジェクトの話を聞いたのは今年の1月くらいです。要件も少ないし、1,2ヶ月くらいで作って出したいね、という話がありました。その時はまだ「No Show」を解決できるための何か、くらいのイメージでした。そこから色々と調整事があり、リリース時期は少し先の夏以降がターゲットとなりました。

プロトタイプでの検証(2018年3月〜4月)

f:id:y_koh:20181116120004p:plain

リリースまで少し時間が出来たので、まずプロトタイプを作って検証してみることにしました。

とはいえリソースはなかなか逼迫していたため、デザインだけ社内で行い、実装は外部の方に手伝っていただく形で進めました。このプロトタイプでの技術検証は後の本開発でとても役に立ちました。

f:id:y_koh:20181116133139p:plain
初期デザイン

プロトタイプで検証したかったことは3つあります。

  • Firebase環境
  • MVP(Minimum Viable Product)フィーチャー
  • ユーザーテスト

今回社内で初めてバックエンドにFirebase環境を使ってみることにしました。これはサーバサイドエンジニアリソースが無かったこともありますが、今回の様にわりと小さめのアプリを作るのであればサーバサイドもアプリエンジニア側で対応出来たほうが早いのではということで採用してみました。

また今後プロトタイプを実装する際に、Firebaseが使えるようになっていればアプリエンジニアだけでさくっと実装することができるのでそれに慣れておくためにも今回のプロジェクトは丁度よいなと思いました。

プロトタイプ検証の結果として、Firebaseが使えることと、MVPフィーチャーの精査はある程度出来ました。ただ、ユーザーテストは当初実際の店舗さまに使っていただこうとしていたのですが、諸般の事情により実現できず、社内だけでのテストとなりました。ここだけ少し心残りです。

この様なプロトタイプ検証を経て、実際の開発へと移りました。

本開発(2018年5月下旬〜7月)

f:id:y_koh:20181116120025p:plain

開発自体は8月頭には終わっていたのですが、リリース時期の調整等あり、しばらく寝かせていました。ちょうどXcode 10がリリースされたのでその対応と若干の調整をして11月1日にリリースとなります。

体制

  • プロダクトオーナー1名
  • iOSエンジニア1名
  • デザイナー1名
  • QA1名(テスト期間のみ)

プロトタイプ後の本開発は、この体制で約2.5ヶ月で行いました。

アプリ構成

アプリはシンプルなMVVMです。今回RxSwiftは使っていません。メインの台帳アプリのリニューアル話が進んでいたのですが、そこでRxSwift使うべきかどうかという議論をしていました。今回のアプリは規模も小さいのでRxSwift使わずにどうなるか試してみようということで使わないことにしました。

ウェブ画面

2画面だけですがウェブ画面もあります。SMSを受け取ったお客さまがキャンセルポリシーを確認して承諾する画面です。この画面はCloud functionsで実装しています。

開発で工夫したこと

アプリ開発に携わったことがあるメンバーが僕以外にいなかったため、全体的な開発プロセス周りやiOSデザインディレクション的なことをしました。

開発プロセスはまずガントチャートを作りました。これはスケジュール管理と言うよりは、見積もりのためです。新しいものを作るときにはどうしても要件が膨らみがちになります。それをちゃんと工数を見える化さするために作りました。

見える化できていると、「これやりたいんだけど…」と言われた時に「でもスケジュール的に余裕ないよね。これを外せば出来るけどどうする?」って話がしやすいです。やりたい気持ちは超わかるんですけど、ちゃんとリリースできること以上に大事なことはないので、気持ちはわかるよーと思いながらバッサバッサ切りました。

デザインに関しては、アプリとしてのコアは何なのか?ということをかなり話し合いました。プロトタイプを作ったとはいえやっぱりまだブレていたんですね。

あとは、アプリとして不自然な部分を一緒に直していきました。

例えば、最初のデザインではアプリを起動してすぐに予約を登録したいからという理由で一番左のタブに予約登録フォームが配置されていました。タブは画面遷移のボタンであると同時にそれぞれの画面の状態を持っているものです。つまり、フォームの入力中にその状態を保持したまま他のタブに遷移できてしまいます。下書きは別ですが、入力中という状態を持つ必要はないのでこれはモーダルにして入力中に他のことがしたくなった場合は、入力を完了するか破棄するかの二択にする方が自然だよ、みたいなことを一つ一つやっていきました。

Firebaseで大変だったこと

  • Cloud Firestore
  • Firebase Authentication
  • DynamicLinks
  • domain

ほとんどは初めての環境だからという部分が多かったです。とはいえ開発中に仕様が変わったものもありますし、消したいのに削除ボタン未実装なんてのもちょくちょくありました。Firebaseを実プロダクトに導入する場合は、ある程度結構余裕を持ってやるのが良さそうです。

Cloud Firestore

そもそもDB設計が久しぶりでした。その上RDBではなくNoSQLドキュメントベースDB。最初はちょっと戸惑いました。RDBで当たり前にできるようなcountやちょっと複雑なクエリも使えません。

DB設計に関してはmonoさんのこの記事がとても役に立ちました。
Cloud Firestoreの勘所 パート1 — 概要 – google-cloud-jp – Medium

ドキュメントベースではテーブル、レコードではなく、コレクションとドキュメントという考え方をします。これは呼び方が変わっただけではなく、考え方自体が変わります。RDBの場合、リレーションはありますが、基本的にすべてのテーブルは階層無くフラットな状態で考えます。

ドキュメントベースの場合はここに階層の概念が加わります。

今回のトレテルで言うと、予約のテーブルを作るのではなく、店舗ドキュメントの下に予約コレクションを持たせる形になります。こうすることでアクセス権をこの店舗の配下だけにして、店舗に必要な情報をまとめて取得することが出来るのですが、逆に予約情報を全て引っ張ってきたいときに1つのテーブルを見るということができません。

もし、調査目的で予約情報を全て見たいケースがある場合には、Cloud Functionsを使い、予約の追加をトリガーとして別の調査用コレクションにコピーするということが必要になります。

この辺はすこし考え方の変換が必要になるところです。

Firebase Authentication

Auth自体はそんなに難しいことはないです。ただ、認証に使う情報はAuthenticationとして扱うのですが、ここにメールアドレス以外のユーザー情報をもたせることが出来ません。

ユーザー情報を持たせる場合はFirestoreにusersコレクションを作って、そこに持たせることになります。ちょっと手間ですが、複数の認証プロバイダに対応したりするために切り出しているのかなと思います。

Firebase Dynamic Links

個人的に一番ハマりました。結局は完全に僕の勘違いだったのですが。

sendEmailVerificationを使ったメールアドレス確認をする際に、Dynamic Linksのドメインが必要になります。これを、Dynamic Links自体が必要と勘違いし、そのリンクURLをcontinueUrlに入れてしまっていました。

正しくは、ドメインだけあればよいのです。ドキュメントにもちゃんと書いてありました。 https://firebase.google.com/docs/auth/web/passing-state-in-email-actions?hl=ja#configuring_firebase_dynamic_links

ハマったのは、これでもなんとなく動いてたということです。ただ人によっては正常な挙動をしていなかったりして、調査した結果間違いに気づきました。

またさらに問題をややこしくしていたのが複数ドメインです。Dynamic Linksには複数ドメインを追加できるのですが、メールアドレス確認で送られてくるメールのリンクには、Dynamic Linksに登録された1番目のdomainが使われるみたいなのです。

Dynamic Linksの1番目は開発時に追加した不要なdomainで、これが使われてる理由がわからずハマりました。

Firebase所感

慣れるまで難しい部分がありましたが、総合的には良いと思います。実際にこれをFirebase無しで自分でサーバサイド組んでと考えると、、とてもじゃないですが終わらなかったと思います。

ステージングと本番環境を分けたいときにプロジェクトを完全に別にする必要があるのは少し手間です。ステージング環境で作ったものを本番環境にコピペしたいことはよくあるのですが、今の所一つ一つ手作業でコピペするしかないです。反映漏れが出てしまう可能性があるのが少し怖いですね。

またCloud FirestoreがまだBetaなのもあって自動バックアップが無いのもちょっと辛いところです。自動バックアップとBigQueryへの自動エクスポートが出来るとかなり使い勝手は良くなると思います。

終わりに

今回のプロジェクトで一番時間を取られたのはUIの部分です。正直ちょっと見積もり甘かったなと思っていてここは反省点です。プロトタイプ検証もしているし、基本的にきれいに整えていけばいいだろうと思っていたんですね。それがやっぱりこのサービスのコアって何だろうね、と考えるたびにUIも変わっていきました。

正直機能としては予約を登録して、SMSで送って、お客さまが承諾して、それを確認できるだけ、という感じでかなり絞っています。しかし、それでも考える量はこれだけ膨大なんなだというのは改めて痛感しました。特に軸となる部分が変わってしまうと全体を変える必要が出てきてしまってつらいですね。

トレタは基本的に台帳アプリを主軸にしているのでiOSのプロダクトはこれしかありませんでした。不用意にプロダクトを増やすのはメンテの観点から考えてもあまりやるべきではないと思うのですが、やっぱり新しいサービス開発は学ぶものが多いです。特にトレタアプリは10万行規模になってきていて何かをためそうにも気軽にできない状況があります。

もちろん、ビジネスに繋がらないものを出してもしょうがないのですが、こんなものがあればお客さまのこの部分は解決できるのでは?みたいなのはエンジニア側からもどんどん提案していきたいなと思っています。とりあえず今ひとつはアイディアがあって、さてこれをどうビジネスと繋げようかなというところです。

トレタはRubyKaigi2018にスポンサーとして参加します & 【仙台版】トレタMapのご紹介

f:id:m_nakamura145:20180525145955p:plainサーバーサイドエンジニアの中村です。

私たちトレタは今年のRubyKaigi2018にスポンサーとして参加します!

rubykaigi.org

5/31(金)〜6/2(日)の3日間、会場となる宮城県仙台国際センターにてブース出展しています。是非お気軽にお立ち寄りください!

【仙台版】トレタMapを作りました!

https://rubykaigi2018.toreta.in/

杜の都仙台市はトレタ導入店も多いこともあり、会場で出会った方達が飲みにいかれる際のお店探しに役立つ「【仙台版】トレタMap」というグルメMapを作りました!

トレタMapって何?

私たちトレタは、飲食店向けの予約/顧客管理に特化したクラウド型サービスを提供しています。 「トレタMap」とは、仕事上もプライベートも、急な飲み会や2次会探しが多いトレタメンバーからの「今いる場所から近いお店が知りたい。そしてせっかくならトレタ導入店に行きたい」そんなニーズから作られた、トレタ導入店で構成されるグルメMapのことです。

この「トレタMap」はこれまで社内のみで公開していたのですが、今回スポンサーとして参加するRubyKaigiの会場が仙台で開催されることとあって、会場にお越しになる皆さんに、少しでもお店探しのお役に立てればと「【仙台版】トレタMap」として一部カスタマイズして限定公開することにしました。

トレタMapの特徴

  • GoogleMap上から直接お店情報が確認できる
  • お店情報からウェブ予約ページに遷移してそのまま予約できる

f:id:m_nakamura145:20180523104542p:plain

そう、この「トレタMap」の一番の特徴は、そのまま空席検索&予約ができてしまうこと!24時間予約可能なので「予約したいのに営業時間外」だったり「移動中で電話できない」時などにとっても便利です!(「トレタかんたんウェブ予約」を設定している店舗に限ります)

配布場所

当日会場のブースにてQRコード付きのチラシを配布いたします。

※ブースに来てくれた方限定の配布と思っていましたが、事前にお店を知りたい方も多いと思いこちらでもURLを配布します(ブースにも是非遊びに来てください!)

▼「【仙台版】トレタMap」

https://rubykaigi2018.toreta.in/

掲載店舗

今回Map上に掲載される店舗一覧です。お店探しの参考してください。

docs.google.com

※ブログ作成時の最新情報を記載しています。ご利用時に情報が変更になっている場合は何卒ご了承ください。(掲載期間は6/3まで)

使ってくれた皆さんへのお願い

今回はもともと社内ツールだったものをお試し限定公開しています。実際に使っていただいた感想を是非伺いたいと思っています。ツイッター等で #rubykaigi #toretamap のハッシュタグでご意見、ご感想をお願いします!

グルメMap仲間のご紹介

私たち以外にも参加者の皆様に向けたグルメMapをご紹介されているスポンサーさんがいらっしゃったのでご紹介いたします。

SmartHRさん

tech.smarthr.jp

2年連続「自腹で作るグルメマップ」を公開されています。素敵です。 「【仙台版】トレタMap」と合わせれば3日間のお店探しはもう困りません。

私たちは仲間を募集しています

私たちトレタはまだまだたくさんの「やりたいこと」「叶えたい世界」があります。 日本が誇る食の世界をITの力で一緒に幸せにしてくれる仲間を募集しています。

ぜひ仙台でお会いしましょう!

git pushでサーバを構築する

2018年あけましておめでとうございます。

新年早々ひどい肩こりに悩まされていましたが、最近ようやく回復してきましたインフラの山田です。

去年の振り返り

f:id:tyamada112:20180125110027p:plain

さて、2017年の振り返りになりますが、おかげさまで昨年は99.996%のシステム稼働率を記録することが出来ました。 今年も飲食店様を支えるインフラとして、この稼働率をキープしていきます。

git push起点でインフラをつくる

トレタではInfrastructure as Codeに積極的に取り組んでいます。 全てのインフラリソースをコード化出来ているわけではありませんが、 git pushでインフラができあがる の考えのもと仕組みを作っています。 さて、今回はサーバの構築に関して紹介します。

使っているツール

以前のブログでも紹介させて頂いていますが、サーバ周りを運用を回すのには以下のツールを使っています。

これらのツールをCircle CIを起点として実行しています。 処理の流れについて順を追って書いてみます。

1. AMIをつくる。

最初にPakcerでサーバの種となるAMIを作ります。

f:id:tyamada112:20180125110241p:plain

絵の通り、git pushをして、あとは放っておけば単体テストまでが完了したAMIが自動で出来上がってきます。いわゆるゴールデンイメージです。 AMIのImage nameにはtimestampを付与しているので、「あ。MW周りの設定を3日前の状態に戻したいなー」とかあれば戻しは楽です。 当然、AMIを作るだけというのはアレなので、以下の条件を満たすAMIは、削除をする仕組みを入れています。

  • 保存日数を超過している。
  • 保存世代数を超えている。

さて、実際のtemplateの中身はこんな感じです。

{
  "variables": {
    "sample1_aws_access_key": "xxxxxxxxxxxxxxxxxxxx",
    "sample1_aws_secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxx",
    "sample2_aws_access_key": "xxxxxxxxxxxxxxxxxxxx",
    "sample2_aws_secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "builders": [{
    "name": "sample1",
    "access_key": "xxxxxxxxxxxxxxxx",
    "secret_key": "xxxxxxxxxxxxxxxx",
    "region": "ap-northeast-1",
    "source_ami": "xxxxxx",
    "vpc_id": "xxxxxx",
    "subnet_id": "xxxxxx",
    "associate_public_ip_address": xxxxxx,
    "type": "amazon-ebs",
    "instance_type": "t2.medium",
    "ssh_username": "ubuntu",
    "ami_name": "toreta-sample1 / sample_service / {timestamp}}"
  },{
    "name": "sample2",
    "access_key": "xxxxxxxxxxxxxxxx",
    "secret_key": "xxxxxxxxxxxxxxxx",
    "region": "ap-northeast-1",
    "source_ami": "xxxxxx",
    "vpc_id": "xxxxxx",
    "subnet_id": "xxxxxx",
    "associate_public_ip_address": xxxxxx,
    "type": "amazon-ebs",
    "instance_type": "t2.medium",
    "ssh_username": "ubuntu",
    "ami_name": "toreta-sample2 / sample_service / {{timestamp}}"
  },

~~
~~

  }],
  "provisioners": [
    {
      "type": "shell",
      "scripts": ["base_scripts/base.sh"]
    },
    {
      "type": "ansible",
      "playbook_file": "ansible/initialize.yml"
    },
    {
      "type": "file",
      "source": "serverspec",
      "destination": "/tmp/serverspec"
    },
    {
      "type": "ansible",
      "playbook_file": "sample1_inventory.yml",
      "extra_arguments": [ "--extra-vars", "/xxxxx/xxxxx/xxxxx/xxxxx" ],
      "only": ["sample1"]
    },
    {
      "type": "shell",
      "scripts": ["scripts/sample1_spec.sh"],
      "execute_command": "{{ .Vars }} sudo -E bash '{{ .Path }}'",
      "only": ["sample1"]
    },
~~
~~

privisionersでの処理について、全部書くと大変な量になってしまうので1role分のみ書きます。

①パッケージ周りの更新

ここでは主にpackegeのupdate/upgradeの処理を実行します。 全roleのサーバで共通して流します。

sudo sed -i".bak" -e 's/\/\/archive.ubuntu.com/\/\/ftp.jaist.ac.jp/g' /etc/apt/sources.list
sudo apt-get -y update
sudo apt-get -y upgrade

② Ansible

playbookは汎用/専用の2種類を用意しています。

まずは全サーバに同じ処理を施すための汎用playbookを実行します。 packageのインストールや、rbenv, pyenvなどのインストール、ユーザ周りの整備などの処理を行います。 尚、ここで実行する内容は、全roleで共通して流す内容であることと、管理の二重化を避けるためにansible-galaxyで共通化しています。

次にrole毎に処理内容の異なる専用playbookを流して、Ansibleの処理は完了です。

③Serverspec

仕上げに、Serverspecでテストを行います。 事前に/tmpにserverspecをまるごと送り込んであるので、該当するspecファイルをシェルスクリプト経由で実行します。

#!/bin/sh

TARGET="xxxxx"
SPEC_ENV="xxxxxxxxxx"

cd /tmp/serverspec && /usr/local/rbenv/shims/bundle install --path .bundle
cd /tmp/serverspec && export TARGET=${TARGET} && export SPEC_ENV=${SPEC_ENV} && /usr/local/rbenv/shims/bundle exec rake spec

全てのspecを通過したら、AMIが完成します。 尚、ここで出来上がったAMIにはアプリケーションコード(Rails)は含ませていません。(Packerの中に特定時点のコードを含めること事故につながるため。)

2. サーバをつくる。

次にTerraformでサーバをつくります。

f:id:tyamada112:20180125110252p:plain

絵の通り、ここもgit pushをするだけです。 CircleCI経由でTerraform applyまで完了し、サーバが出来上がります。事前にPackerの中でテストも完了しているので、即サービスインできます。

resource "aws_launch_configuration" "sample-lc-blue" {
  name            = "sample-lc-blue"
  image_id        = "${var.sample1_blue}"
  instance_type   = "t2.medium"
  key_name        = "${var.samplekey}"
  security_groups = [
    "${aws_vpc.xxxxxxxxxxxxx}",
    "${aws_security_group.xxxxxxxxxxxxxx.id}",
~~
~~

変数周りはtfvarsファイルで別管理しています。 なので、AMI-IDをvar.sample1_blueに埋め込んで、terraform applyしてやるだけです。 (尚、トレタではAuto Scaling を使用してインスタンスを動的にスケールをさせているため、aws_launch_configurationを定義しています。)

EC2が起動してきたら、Packerの中に含むことができなかったRailsアプリケーションを流し込みます。 この流し込みには、userdataを利用しており、リポジトリをローカルにcloneし、local deployします。

deployが正常に完了したことを確認したら、Mackerelの監視が有効化されます。 Railsアプリケーションを流し込んでいない状態で監視を有効化すると、Rackサーバが未起動、workerが未起動状態など不完全な状態となるため、重大障害扱いとなってしまいます。そのためPacker buildの中では、Mackerel起動時の初期監視パラメータを on_start = "standby" としています。

サービスインが完了すると、slackに成功メッセージがpostされ、サーバ構築は完了です。

まとめ

今回はインフラのサーバの構築について紹介させて頂きました。 git pushでインフラができあがる の考えのもと仕組みを作っているので、シンプルなオペレーションができています。

また、今回は運用に関しては紹介をしていないんですが、運用についてもできるだけシンプルな運用なできるような仕組を作り心かげています。また次回ご紹介させて頂ければと思います。

それでは今年も一年、トレタの開発者ブログをよろしくお願いします。

© Toreta, Inc.

Powered by Hatena Blog